diff --git a/.eslintrc.json b/.eslintrc.json index 7b537000..3e27a3b7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -106,7 +106,6 @@ "files": ["*.spec.ts"], "extends": ["plugin:jest/recommended"], "rules": { - "jest/no-done-callback": "off", "jest/expect-expect": "off" } }, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e097f79..e85298ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: install - run: npm install --force + run: npm install - name: build run: npm run build -- --skip-nx-cache - name: test diff --git a/angular.json b/angular.json index c4792405..45bf7eca 100644 --- a/angular.json +++ b/angular.json @@ -87,8 +87,7 @@ "test": { "builder": "@nrwl/jest:jest", "options": { - "jestConfig": "apps/example-app/jest.config.js", - "setupFile": "apps/example-app/src/test-setup.ts" + "jestConfig": "apps/example-app/jest.config.js" }, "outputs": ["coverage/"] } @@ -232,8 +231,7 @@ "test": { "builder": "@nrwl/jest:jest", "options": { - "jestConfig": "projects/testing-library/jest.config.js", - "setupFile": "projects/testing-library/test-setup.ts" + "jestConfig": "projects/testing-library/jest.config.js" }, "outputs": ["coverage/projects/testing-library"] } @@ -284,8 +282,7 @@ "test": { "builder": "@nrwl/jest:jest", "options": { - "jestConfig": "projects/jest-utils/jest.config.js", - "setupFile": "projects/jest-utils/test-setup.ts" + "jestConfig": "projects/jest-utils/jest.config.js" }, "outputs": ["coverage/projects/jest-utils"] } diff --git a/apps/example-app/src/app/examples/14-async-component.spec.ts b/apps/example-app/src/app/examples/14-async-component.spec.ts index 6a2ca2e5..ba72ce70 100644 --- a/apps/example-app/src/app/examples/14-async-component.spec.ts +++ b/apps/example-app/src/app/examples/14-async-component.spec.ts @@ -14,3 +14,16 @@ test('can use fakeAsync utilities', fakeAsync(async () => { const hello = await screen.findByText('Hello world'); expect(hello).toBeInTheDocument(); })); + +test('can use fakeTimer utilities', async () => { + jest.useFakeTimers(); + await render(AsyncComponent); + + const load = await screen.findByRole('button', { name: /load/i }); + fireEvent.click(load); + + jest.advanceTimersByTime(10_000); + + const hello = await screen.findByText('Hello world'); + expect(hello).toBeInTheDocument(); +}); diff --git a/apps/example-app/src/app/examples/15-dialog.component.spec.ts b/apps/example-app/src/app/examples/15-dialog.component.spec.ts index bcb41875..694af3a7 100644 --- a/apps/example-app/src/app/examples/15-dialog.component.spec.ts +++ b/apps/example-app/src/app/examples/15-dialog.component.spec.ts @@ -1,5 +1,6 @@ import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { render, screen, fireEvent, waitForElementToBeRemoved } from '@testing-library/angular'; +import { render, screen, waitForElementToBeRemoved, fireEvent } from '@testing-library/angular'; +import userEvent from '@testing-library/user-event'; import { DialogComponent, DialogContentComponent, DialogContentComponentModule } from './15-dialog.component'; @@ -18,24 +19,25 @@ test('dialog closes', async () => { }); const cancelButton = await screen.findByRole('button', { name: /cancel/i }); - fireEvent.click(cancelButton); + userEvent.click(cancelButton); expect(closeFn).toHaveBeenCalledTimes(1); }); -test('opens and closes the dialog with buttons', async () => { +test('closes the dialog via the backdrop', async () => { await render(DialogComponent, { imports: [MatDialogModule, DialogContentComponentModule], }); const openDialogButton = await screen.findByRole('button', { name: /open dialog/i }); - fireEvent.click(openDialogButton); + userEvent.click(openDialogButton); await screen.findByRole('dialog'); await screen.findByRole('heading', { name: /dialog title/i }); - const cancelButton = await screen.findByRole('button', { name: /cancel/i }); - fireEvent.click(cancelButton); + // using fireEvent because of: + // unable to click element as it has or inherits pointer-events set to "none" + fireEvent.click(document.querySelector('.cdk-overlay-backdrop')); await waitForElementToBeRemoved(() => screen.getByRole('dialog')); @@ -43,19 +45,19 @@ test('opens and closes the dialog with buttons', async () => { expect(dialogTitle).not.toBeInTheDocument(); }); -test('closes the dialog via the backdrop', async () => { +test('opens and closes the dialog with buttons', async () => { await render(DialogComponent, { imports: [MatDialogModule, DialogContentComponentModule], }); const openDialogButton = await screen.findByRole('button', { name: /open dialog/i }); - fireEvent.click(openDialogButton); + userEvent.click(openDialogButton); await screen.findByRole('dialog'); await screen.findByRole('heading', { name: /dialog title/i }); - // eslint-disable-next-line testing-library/no-node-access - fireEvent.click(document.querySelector('.cdk-overlay-backdrop')); + const cancelButton = await screen.findByRole('button', { name: /cancel/i }); + userEvent.click(cancelButton); await waitForElementToBeRemoved(() => screen.getByRole('dialog')); diff --git a/apps/example-app/src/app/issues/issue-106.spec.ts b/apps/example-app/src/app/issues/issue-106.spec.ts index 0ac749a8..b1f6cc2c 100644 --- a/apps/example-app/src/app/issues/issue-106.spec.ts +++ b/apps/example-app/src/app/issues/issue-106.spec.ts @@ -15,7 +15,7 @@ class TestSelectComponent { } } -it('https://github.com/testing-library/angular-testing-library/issues/106', async () => { +test('https://github.com/testing-library/angular-testing-library/issues/106', async () => { await render(TestSelectComponent); const toggle = screen.getByTestId('toggle'); const hiddenText = screen.queryByTestId('getme'); @@ -30,7 +30,7 @@ it('https://github.com/testing-library/angular-testing-library/issues/106', asyn await waitFor(() => expect(screen.queryByTestId('getme')).toBeInTheDocument()); }); -it('better https://github.com/testing-library/angular-testing-library/issues/106', async () => { +test('better https://github.com/testing-library/angular-testing-library/issues/106', async () => { await render(TestSelectComponent); const toggle = screen.getByTestId('toggle'); const hiddenText = screen.queryByTestId('getme'); diff --git a/jest.preset.js b/jest.preset.js index 21c2ab11..3f434cfc 100644 --- a/jest.preset.js +++ b/jest.preset.js @@ -7,22 +7,10 @@ module.exports = { }, resolver: '@nrwl/jest/plugins/resolver', moduleFileExtensions: ['ts', 'js', 'html'], - coverageReporters: ['html'], - snapshotSerializers: [ - 'jest-preset-angular/build/serializers/no-ng-attributes', - 'jest-preset-angular/build/serializers/ng-snapshot', - 'jest-preset-angular/build/serializers/html-comment', - ], globals: { 'ts-jest': { tsconfig: '/tsconfig.spec.json', stringifyContentPathRegex: '\\.(html|svg)$', - astTransformers: { - before: [ - 'jest-preset-angular/build/InlineFilesTransformer', - 'jest-preset-angular/build/StripStylesTransformer', - ], - }, }, }, }; diff --git a/package.json b/package.json index aa4e8a86..af3e7ed8 100644 --- a/package.json +++ b/package.json @@ -28,18 +28,18 @@ "prepare": "husky install" }, "dependencies": { - "@angular/animations": "12.0.0", - "@angular/cdk": "12.0.0", - "@angular/common": "12.0.0", - "@angular/compiler": "12.0.0", - "@angular/core": "12.0.0", - "@angular/forms": "12.0.0", - "@angular/material": "12.0.0", - "@angular/platform-browser": "12.0.0", - "@angular/platform-browser-dynamic": "12.0.0", - "@angular/router": "12.0.0", - "@ngrx/store": "12.0.0", - "@nrwl/angular": "12.0.3", + "@angular/animations": "12.1.1", + "@angular/cdk": "12.1.1", + "@angular/common": "12.1.1", + "@angular/compiler": "12.1.1", + "@angular/core": "12.1.1", + "@angular/forms": "12.1.1", + "@angular/material": "12.1.1", + "@angular/platform-browser": "12.1.1", + "@angular/platform-browser-dynamic": "12.1.1", + "@angular/router": "12.1.1", + "@ngrx/store": "12.2.0", + "@nrwl/angular": "12.5.1", "@nrwl/nx-cloud": "11.2.0", "@testing-library/dom": "^8.0.0", "@testing-library/user-event": "^13.1.9", @@ -49,23 +49,23 @@ "zone.js": "~0.11.4" }, "devDependencies": { - "@angular-devkit/build-angular": "12.0.0", + "@angular-devkit/build-angular": "12.1.0", "@angular-eslint/eslint-plugin": "~12.0.0", "@angular-eslint/eslint-plugin-template": "~12.0.0", "@angular-eslint/template-parser": "~12.0.0", - "@angular/cli": "12.0.0", - "@angular/compiler-cli": "12.0.0", - "@angular/language-service": "12.0.0", - "@nrwl/cli": "12.0.3", - "@nrwl/eslint-plugin-nx": "12.0.3", - "@nrwl/jest": "12.0.3", - "@nrwl/linter": "12.0.3", - "@nrwl/node": "12.0.3", - "@nrwl/nx-plugin": "12.0.3", - "@nrwl/workspace": "12.3.1", + "@angular/cli": "12.1.0", + "@angular/compiler-cli": "12.1.1", + "@angular/language-service": "12.1.1", + "@nrwl/cli": "12.5.1", + "@nrwl/eslint-plugin-nx": "12.5.1", + "@nrwl/jest": "12.5.1", + "@nrwl/linter": "12.5.1", + "@nrwl/node": "12.5.1", + "@nrwl/nx-plugin": "12.5.1", + "@nrwl/workspace": "12.5.1", "@testing-library/jest-dom": "^5.11.10", "@types/jasmine": "~3.5.0", - "@types/jest": "~26.0.3", + "@types/jest": "^26.0.23", "@types/node": "14.14.37", "@typescript-eslint/eslint-plugin": "4.22.0", "@typescript-eslint/parser": "4.22.0", @@ -77,11 +77,11 @@ "eslint-plugin-jest-dom": "3.8.0", "eslint-plugin-testing-library": "^4.0.1", "husky": "^6.0.0", - "jasmine-core": "~3.6.0", + "jasmine-core": "~3.7.0", "jasmine-spec-reporter": "~5.0.0", - "jest": "^26.1.0", - "jest-preset-angular": "8.4.0", - "karma": "~5.0.0", + "jest": "^27.0.6", + "jest-preset-angular": "9.0.4", + "karma": "~6.3.4", "karma-chrome-launcher": "~3.1.0", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", @@ -90,7 +90,7 @@ "prettier": "^2.3.0", "rimraf": "^3.0.2", "semantic-release": "^17.1.1", - "ts-jest": "26.5.4", + "ts-jest": "^27.0.3", "ts-node": "9.1.1", "typescript": "4.2.4" } diff --git a/projects/jest-utils/jest.config.js b/projects/jest-utils/jest.config.js index efed0e8c..0d80b628 100644 --- a/projects/jest-utils/jest.config.js +++ b/projects/jest-utils/jest.config.js @@ -5,4 +5,5 @@ module.exports = { color: 'magenta', }, preset: '../../jest.preset.js', + setupFilesAfterEnv: ['/test-setup.ts'], }; diff --git a/projects/jest-utils/tests/create-mock.spec.ts b/projects/jest-utils/tests/create-mock.spec.ts index 8b814fe6..98b94314 100644 --- a/projects/jest-utils/tests/create-mock.spec.ts +++ b/projects/jest-utils/tests/create-mock.spec.ts @@ -24,12 +24,12 @@ class FixtureComponent { } } -it('mocks all functions', () => { +test('mocks all functions', () => { const mock = createMock(FixtureService); expect(mock.print.mock).toBeDefined(); }); -it('provides a mock service', async () => { +test('provides a mock service', async () => { const { getByText } = await render(FixtureComponent, { providers: [provideMock(FixtureService)], }); @@ -39,14 +39,13 @@ it('provides a mock service', async () => { expect(service.print).toHaveBeenCalledTimes(1); }); -// eslint-disable-next-line jest/expect-expect -it('is possible to write a mock implementation', async (done) => { +test('is possible to write a mock implementation', async () => { const { getByText } = await render(FixtureComponent, { providers: [provideMock(FixtureService)], }); const service = TestBed.inject(FixtureService) as Mock; - service.print.mockImplementation(() => done()); fireEvent.click(getByText('Print')); + expect(service.print).toHaveBeenCalled(); }); diff --git a/projects/testing-library/jest.config.js b/projects/testing-library/jest.config.js index 379e4581..216a1f4e 100644 --- a/projects/testing-library/jest.config.js +++ b/projects/testing-library/jest.config.js @@ -5,4 +5,5 @@ module.exports = { color: 'magenta', }, preset: '../../jest.preset.js', + setupFilesAfterEnv: ['/test-setup.ts'], }; diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 168bd0d7..b521ab5d 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -8,7 +8,7 @@ import { SimpleChanges, ApplicationInitStatus, } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NavigationExtras, Router } from '@angular/router'; @@ -310,14 +310,21 @@ async function waitForWrapper( callback: () => T extends Promise ? never : T, options?: dtlWaitForOptions, ): Promise { + let inFakeAsync = true; + try { + tick(0); + } catch (err) { + inFakeAsync = false; + } + detectChanges(); + return await dtlWaitFor(() => { - try { - return callback(); - } catch (error) { - setImmediate(() => detectChanges()); - throw error; + setTimeout(() => detectChanges(), 0); + if (inFakeAsync) { + tick(0); } + return callback(); }, options); } @@ -345,10 +352,8 @@ async function waitForElementToBeRemovedWrapper( } return await dtlWaitForElementToBeRemoved(() => { - const result = cb(); detectChanges(); - setImmediate(() => detectChanges()); - return result; + return cb(); }, options); } diff --git a/projects/testing-library/tests/issues/issue-188.spec.ts b/projects/testing-library/tests/issues/issue-188.spec.ts index cb430bb4..7b3d8263 100644 --- a/projects/testing-library/tests/issues/issue-188.spec.ts +++ b/projects/testing-library/tests/issues/issue-188.spec.ts @@ -17,7 +17,7 @@ class BugOnChangeComponent implements OnChanges { } } -it('should output formatted name after rendering', async () => { +test('should output formatted name after rendering', async () => { const { getByText } = await render(BugOnChangeComponent, { componentProperties: { name: 'name' } }); getByText('Hello NAME'); diff --git a/projects/testing-library/tests/issues/issue-67.spec.ts b/projects/testing-library/tests/issues/issue-67.spec.ts index 243dbf4d..642f394d 100644 --- a/projects/testing-library/tests/issues/issue-67.spec.ts +++ b/projects/testing-library/tests/issues/issue-67.spec.ts @@ -15,11 +15,11 @@ import { render } from '../../src/public_api'; }) class BugGetByLabelTextComponent {} -it('first step to reproduce the bug: skip this test to avoid the error or remove the for attribute of label', async () => { +test('first step to reproduce the bug: skip this test to avoid the error or remove the for attribute of label', async () => { expect(await render(BugGetByLabelTextComponent)).toBeDefined(); }); -it('second step: bug happens :`(', async () => { +test('second step: bug happens :`(', async () => { const { getByLabelText, getByTestId } = await render(BugGetByLabelTextComponent); const checkboxByTestId = getByTestId('checkbox'); diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index aeac9789..fe2ae565 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -126,28 +126,19 @@ describe('Angular component life-cycle hooks', () => { }); }); -test('Waits for angular app initialization before rendering components', (done) => { - let resolve; +test('waits for angular app initialization before rendering components', async () => { + const mock = jest.fn(); - const promise = new Promise((res) => { - resolve = res; - }); - - render(FixtureComponent, { + await render(FixtureComponent, { providers: [ { provide: APP_INITIALIZER, - useFactory: () => () => promise, + useFactory: () => mock, multi: true, }, ], - }) - .then(() => { - expect(TestBed.inject(ApplicationInitStatus).done).toEqual(true); - done(); - }) - .catch(done); - - // Wait a bit so the test will fail if render completes without us resolving the promise - setTimeout(resolve, 1000); + }); + + expect(TestBed.inject(ApplicationInitStatus).done).toEqual(true); + expect(mock).toHaveBeenCalled(); });