From 198b75efd8a32717ff892e0556b525a3e8f9bed7 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Sat, 19 Nov 2022 15:04:09 +0100 Subject: [PATCH 01/13] feat: update to Angular 15 --- .github/workflows/ci.yml | 2 +- apps/example-app-karma/.browserslistrc | 17 ----- apps/example-app-karma/project.json | 1 + apps/example-app-karma/src/test.ts | 6 -- apps/example-app-karma/tsconfig.app.json | 4 +- apps/example-app-karma/tsconfig.spec.json | 4 +- apps/example-app/.browserslistrc | 17 ----- apps/example-app/project.json | 3 +- .../example-app/src/app/app-routing.module.ts | 2 +- apps/example-app/src/app/app.module.ts | 2 +- apps/example-app/src/app/material.module.ts | 4 +- apps/example-app/tsconfig.app.json | 3 +- nx.json | 29 +++++--- package.json | 68 +++++++++---------- projects/testing-library/project.json | 7 +- .../src/lib/testing-library.ts | 9 +-- .../tests/render-template.spec.ts | 2 +- projects/testing-library/tsconfig.lib.json | 4 +- .../testing-library/tsconfig.lib.prod.json | 4 +- 19 files changed, 85 insertions(+), 103 deletions(-) delete mode 100644 apps/example-app-karma/.browserslistrc delete mode 100644 apps/example-app/.browserslistrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcb1859f..de3b8eeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: install - run: npm install + run: npm install --legacy-peer-deps - name: build run: npm run build -- --skip-nx-cache - name: test diff --git a/apps/example-app-karma/.browserslistrc b/apps/example-app-karma/.browserslistrc deleted file mode 100644 index 427441dc..00000000 --- a/apps/example-app-karma/.browserslistrc +++ /dev/null @@ -1,17 +0,0 @@ -# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries - -# For the full list of supported browsers by the Angular framework, please see: -# https://angular.io/guide/browser-support - -# You can see what browsers were selected by your queries by running: -# npx browserslist - -last 1 Chrome version -last 1 Firefox version -last 2 Edge major versions -last 2 Safari major versions -last 2 iOS major versions -Firefox ESR -not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. diff --git a/apps/example-app-karma/project.json b/apps/example-app-karma/project.json index 75e38aab..eb966a8c 100644 --- a/apps/example-app-karma/project.json +++ b/apps/example-app-karma/project.json @@ -1,4 +1,5 @@ { + "name": "example-app-karma", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "application", "sourceRoot": "apps/example-app-karma/src", diff --git a/apps/example-app-karma/src/test.ts b/apps/example-app-karma/src/test.ts index c2ab726d..60544d54 100644 --- a/apps/example-app-karma/src/test.ts +++ b/apps/example-app-karma/src/test.ts @@ -9,11 +9,5 @@ beforeEach(() => { jasmine.addMatchers(JasmineDOM); }); -declare const require: any; - // First, initialize the Angular testing environment. getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {}); -// Then we find all the tests. -const context = require.context('./', true, /\.spec\.ts$/); -// And load the modules. -context.keys().map(context); diff --git a/apps/example-app-karma/tsconfig.app.json b/apps/example-app-karma/tsconfig.app.json index 4de7101b..79a77d1b 100644 --- a/apps/example-app-karma/tsconfig.app.json +++ b/apps/example-app-karma/tsconfig.app.json @@ -3,7 +3,9 @@ "compilerOptions": { "outDir": "../../dist/out-tsc", "types": [], - "allowJs": true + "allowJs": true, + "target": "ES2022", + "useDefineForClassFields": false }, "files": ["src/main.ts", "src/polyfills.ts"], "include": ["src/**/*.d.ts"], diff --git a/apps/example-app-karma/tsconfig.spec.json b/apps/example-app-karma/tsconfig.spec.json index f4b0d715..ff71a7ee 100644 --- a/apps/example-app-karma/tsconfig.spec.json +++ b/apps/example-app-karma/tsconfig.spec.json @@ -2,7 +2,9 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", - "types": ["jasmine", "node", "@testing-library/jasmine-dom"] + "types": ["jasmine", "node", "@testing-library/jasmine-dom"], + "target": "ES2022", + "useDefineForClassFields": false }, "files": ["src/test.ts", "src/polyfills.ts"], "include": ["**/*.spec.ts", "**/*.d.ts"] diff --git a/apps/example-app/.browserslistrc b/apps/example-app/.browserslistrc deleted file mode 100644 index 427441dc..00000000 --- a/apps/example-app/.browserslistrc +++ /dev/null @@ -1,17 +0,0 @@ -# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries - -# For the full list of supported browsers by the Angular framework, please see: -# https://angular.io/guide/browser-support - -# You can see what browsers were selected by your queries by running: -# npx browserslist - -last 1 Chrome version -last 1 Firefox version -last 2 Edge major versions -last 2 Safari major versions -last 2 iOS major versions -Firefox ESR -not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. diff --git a/apps/example-app/project.json b/apps/example-app/project.json index dcc8b341..b31a7efd 100644 --- a/apps/example-app/project.json +++ b/apps/example-app/project.json @@ -1,4 +1,5 @@ { + "name": "example-app", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "application", "sourceRoot": "apps/example-app/src", @@ -75,7 +76,7 @@ "options": { "jestConfig": "apps/example-app/jest.config.ts" }, - "outputs": ["coverage/"] + "outputs": ["{workspaceRoot}/coverage/"] } }, "tags": [] diff --git a/apps/example-app/src/app/app-routing.module.ts b/apps/example-app/src/app/app-routing.module.ts index a553ba61..6a9f0b9e 100644 --- a/apps/example-app/src/app/app-routing.module.ts +++ b/apps/example-app/src/app/app-routing.module.ts @@ -95,7 +95,7 @@ export const routes: Routes = [ ]; @NgModule({ - imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })], + imports: [RouterModule.forRoot(routes, {})], exports: [RouterModule], }) export class AppRoutingModule {} diff --git a/apps/example-app/src/app/app.module.ts b/apps/example-app/src/app/app.module.ts index cb95434f..e0ab6e3a 100644 --- a/apps/example-app/src/app/app.module.ts +++ b/apps/example-app/src/app/app.module.ts @@ -7,7 +7,7 @@ import { StoreModule } from '@ngrx/store'; import { AppRoutingModule } from './app-routing.module'; import { MaterialModule } from './material.module'; import { MatIconModule } from '@angular/material/icon'; -import { MatListModule } from '@angular/material/list'; +import { MatLegacyListModule as MatListModule } from '@angular/material/legacy-list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; diff --git a/apps/example-app/src/app/material.module.ts b/apps/example-app/src/app/material.module.ts index 297c9d74..68797007 100644 --- a/apps/example-app/src/app/material.module.ts +++ b/apps/example-app/src/app/material.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; -import { MatInputModule } from '@angular/material/input'; -import { MatSelectModule } from '@angular/material/select'; +import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input'; +import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatNativeDateModule } from '@angular/material/core'; diff --git a/apps/example-app/tsconfig.app.json b/apps/example-app/tsconfig.app.json index ab030646..6feb8d6e 100644 --- a/apps/example-app/tsconfig.app.json +++ b/apps/example-app/tsconfig.app.json @@ -4,7 +4,8 @@ "outDir": "../../dist/out-tsc", "types": [], "allowJs": true, - "target": "ES2020" + "target": "ES2022", + "useDefineForClassFields": false }, "files": ["src/main.ts", "src/polyfills.ts"], "include": ["src/**/*.d.ts"], diff --git a/nx.json b/nx.json index e7ffe758..3529497e 100644 --- a/nx.json +++ b/nx.json @@ -15,13 +15,6 @@ "environment": "all" } }, - "implicitDependencies": { - "package.json": { - "dependencies": "*", - "devDependencies": "*" - }, - ".eslintrc.json": "*" - }, "tasksRunnerOptions": { "default": { "runner": "@nrwl/nx-cloud", @@ -75,7 +68,27 @@ "$schema": "./node_modules/nx/schemas/nx-schema.json", "targetDefaults": { "build": { - "dependsOn": ["^build"] + "dependsOn": ["^build"], + "inputs": ["production", "^production"] + }, + "test": { + "inputs": ["default", "^production"] + }, + "lint": { + "inputs": ["default", "{workspaceRoot}/.eslintrc.json"] } + }, + "namedInputs": { + "default": ["{projectRoot}/**/*", "sharedGlobals"], + "sharedGlobals": [], + "production": [ + "default", + "!{projectRoot}/**/*.spec.[jt]s", + "!{projectRoot}/tsconfig.spec.json", + "!{projectRoot}/karma.conf.js", + "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", + "!{projectRoot}/jest.config.[jt]s", + "!{projectRoot}/.eslintrc.json" + ] } } diff --git a/package.json b/package.json index 447d18ef..3d70b880 100644 --- a/package.json +++ b/package.json @@ -28,51 +28,51 @@ "prepare": "git config core.hookspath .githooks" }, "dependencies": { - "@angular/animations": "14.1.3", - "@angular/cdk": "14.1.3", - "@angular/common": "14.1.3", - "@angular/compiler": "14.1.3", - "@angular/core": "14.1.3", - "@angular/material": "14.1.3", - "@angular/platform-browser": "14.1.3", - "@angular/platform-browser-dynamic": "14.1.3", - "@angular/router": "14.1.3", + "@angular/animations": "15.0.0", + "@angular/cdk": "15.0.0", + "@angular/common": "15.0.0", + "@angular/compiler": "15.0.0", + "@angular/core": "15.0.0", + "@angular/material": "15.0.0", + "@angular/platform-browser": "15.0.0", + "@angular/platform-browser-dynamic": "15.0.0", + "@angular/router": "15.0.0", "@ngrx/store": "14.0.2", - "@nrwl/angular": "14.5.8", - "@nrwl/nx-cloud": "14.4.1", + "@nrwl/angular": "15.2.1", + "@nrwl/nx-cloud": "15.0.2", "@testing-library/dom": "^8.11.1", "rxjs": "7.5.6", "tslib": "~2.3.1", "zone.js": "~0.11.4" }, "devDependencies": { - "@angular-devkit/build-angular": "14.1.3", + "@angular-devkit/build-angular": "15.0.0", "@angular-eslint/builder": "14.0.2", - "@angular-eslint/eslint-plugin": "14.0.2", - "@angular-eslint/eslint-plugin-template": "14.0.2", + "@angular-eslint/eslint-plugin": "14.0.4", + "@angular-eslint/eslint-plugin-template": "14.0.4", "@angular-eslint/schematics": "14.0.2", - "@angular-eslint/template-parser": "14.0.2", - "@angular/cli": "~14.1.0", - "@angular/compiler-cli": "14.1.3", - "@angular/forms": "14.1.3", - "@angular/language-service": "14.1.3", - "@nrwl/cli": "14.5.8", - "@nrwl/eslint-plugin-nx": "14.5.8", - "@nrwl/jest": "14.5.8", - "@nrwl/linter": "14.5.8", - "@nrwl/node": "14.5.8", - "@nrwl/nx-plugin": "14.5.8", - "@nrwl/workspace": "14.5.8", + "@angular-eslint/template-parser": "14.0.4", + "@angular/cli": "~15.0.0", + "@angular/compiler-cli": "15.0.0", + "@angular/forms": "15.0.0", + "@angular/language-service": "15.0.0", + "@nrwl/cli": "15.2.1", + "@nrwl/eslint-plugin-nx": "15.2.1", + "@nrwl/jest": "15.2.1", + "@nrwl/linter": "15.2.1", + "@nrwl/node": "15.2.1", + "@nrwl/nx-plugin": "15.2.1", + "@nrwl/workspace": "15.2.1", "@swc-node/register": "^1.4.2", "@swc/core": "^1.2.173", "@testing-library/jasmine-dom": "^1.2.0", "@testing-library/jest-dom": "^5.15.1", "@testing-library/user-event": "^13.5.0", "@types/jasmine": "4.0.3", - "@types/jest": "28.1.7", + "@types/jest": "28.1.8", "@types/node": "18.7.1", - "@typescript-eslint/eslint-plugin": "5.29.0", - "@typescript-eslint/parser": "5.29.0", + "@typescript-eslint/eslint-plugin": "5.36.1", + "@typescript-eslint/parser": "5.36.1", "cpy-cli": "^3.1.1", "eslint": "8.15.0", "eslint-config-prettier": "8.3.0", @@ -84,15 +84,15 @@ "jasmine-core": "4.2.0", "jasmine-spec-reporter": "7.0.0", "jest": "28.1.3", - "jest-environment-jsdom": "^28.1.1", - "jest-preset-angular": "12.1.0", + "jest-environment-jsdom": "28.1.3", + "jest-preset-angular": "12.2.2", "karma": "6.4.0", "karma-chrome-launcher": "^3.1.0", "karma-jasmine": "5.1.0", "karma-jasmine-html-reporter": "2.0.0", "lint-staged": "^12.1.6", - "ng-packagr": "14.1.0", - "nx": "14.5.8", + "ng-packagr": "15.0.0", + "nx": "15.2.1", "postcss": "^8.4.5", "postcss-import": "14.1.0", "postcss-preset-env": "7.5.0", @@ -102,6 +102,6 @@ "semantic-release": "^18.0.0", "ts-jest": "28.0.8", "ts-node": "10.9.1", - "typescript": "4.7.4" + "typescript": "4.8.4" } } diff --git a/projects/testing-library/project.json b/projects/testing-library/project.json index f9b45ed8..b460aa28 100644 --- a/projects/testing-library/project.json +++ b/projects/testing-library/project.json @@ -1,4 +1,5 @@ { + "name": "testing-library", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "projects/testing-library/src", @@ -6,7 +7,7 @@ "targets": { "build-package": { "executor": "@nrwl/angular:package", - "outputs": ["dist/@testing-library/angular"], + "outputs": ["{workspaceRoot}/dist/@testing-library/angular"], "options": { "project": "projects/testing-library/ng-package.json", "updateBuildableProjectDepsInPackageJson": false @@ -29,7 +30,7 @@ "outputs": ["{options.outputFile}"] }, "build": { - "executor": "@nrwl/workspace:run-commands", + "executor": "nx:run-commands", "options": { "parallel": false, "commands": [ @@ -50,7 +51,7 @@ "options": { "jestConfig": "projects/testing-library/jest.config.ts" }, - "outputs": ["coverage/projects/testing-library"] + "outputs": ["{workspaceRoot}/coverage/projects/testing-library"] } }, "tags": [] diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 24e20855..66ffb092 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -109,7 +109,7 @@ export async function render( }; const change = (changedProperties: Partial) => { - const changes = getChangesObj(fixture.componentInstance, changedProperties); + const changes = getChangesObj(fixture.componentInstance as Record, changedProperties); setComponentProperties(fixture, { componentProperties: changedProperties }); @@ -296,17 +296,14 @@ function hasOnChangesHook(componentInstance: SutType): componentInstanc ); } -function getChangesObj>( - oldProps: Partial | null, - newProps: Partial, -) { +function getChangesObj(oldProps: Record | null, newProps: Record) { const isFirstChange = oldProps === null; return Object.keys(newProps).reduce( (changes, key) => ({ ...changes, [key]: new SimpleChange(isFirstChange ? null : oldProps[key], newProps[key], isFirstChange), }), - {} as SutType, + {} as Record, ); } diff --git a/projects/testing-library/tests/render-template.spec.ts b/projects/testing-library/tests/render-template.spec.ts index 9dd20d01..a6892dbc 100644 --- a/projects/testing-library/tests/render-template.spec.ts +++ b/projects/testing-library/tests/render-template.spec.ts @@ -135,7 +135,7 @@ describe('removeAngularAttributes', () => { }); test('updates properties and invokes change detection', async () => { - const view = await render('
', { + const view = await render<{ value: string }>('
', { declarations: [UpdateInputDirective], componentProperties: { value: 'value1', diff --git a/projects/testing-library/tsconfig.lib.json b/projects/testing-library/tsconfig.lib.json index 8d9b29fc..0938741e 100644 --- a/projects/testing-library/tsconfig.lib.json +++ b/projects/testing-library/tsconfig.lib.json @@ -5,7 +5,9 @@ "declaration": true, "declarationMap": true, "inlineSources": true, - "types": ["node", "jest"] + "types": ["node", "jest"], + "target": "ES2022", + "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts", "jest.config.ts"], "include": ["**/*.ts"] diff --git a/projects/testing-library/tsconfig.lib.prod.json b/projects/testing-library/tsconfig.lib.prod.json index bf43f045..1f041c94 100644 --- a/projects/testing-library/tsconfig.lib.prod.json +++ b/projects/testing-library/tsconfig.lib.prod.json @@ -1,7 +1,9 @@ { "extends": "./tsconfig.lib.json", "compilerOptions": { - "declarationMap": false + "declarationMap": false, + "target": "ES2022", + "useDefineForClassFields": false }, "angularCompilerOptions": { "compilationMode": "partial" From c6be9bf97ae635fc38345a160ae59cdee7b04339 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Sat, 19 Nov 2022 15:17:59 +0100 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20rename=20=C9=B5componentImports?= =?UTF-8?q?=20to=20componentImports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: The render property ɵcomponentImports is not experimental anymore, and is renamed to componentImports BEFORE: render(ParentComponent, { ɵcomponentImports: [ChildComponent], }); AFTER: render(ParentComponent, { componentImports: [ChildComponent], }); --- projects/testing-library/src/lib/models.ts | 6 ++---- projects/testing-library/src/lib/testing-library.ts | 2 +- projects/testing-library/tests/render.spec.ts | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 1f33192f..c1d931ce 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -215,14 +215,12 @@ export interface RenderComponentOptions | any[])[]; + componentImports?: (Type | any[])[]; /** * @description * Queries to bind. Overrides the default set from DOM Testing Library unless merged. diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 66ffb092..1f71a9e7 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -56,7 +56,7 @@ export async function render( componentProperties = {}, componentProviders = [], childComponentOverrides = [], - ɵcomponentImports: componentImports, + componentImports: componentImports, excludeComponentDeclaration = false, routes = [], removeAngularAttributes = false, diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index 4200bc94..68110a43 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -79,8 +79,8 @@ describe('standalone with child', () => { expect(screen.getByText('A child fixture')).toBeInTheDocument(); }); - it('renders the standalone component with child given ɵcomponentImports', async () => { - await render(ParentFixtureComponent, { ɵcomponentImports: [MockChildFixtureComponent] }); + it('renders the standalone component with a mocked child', async () => { + await render(ParentFixtureComponent, { componentImports: [MockChildFixtureComponent] }); expect(screen.getByText('Parent fixture')).toBeInTheDocument(); expect(screen.getByText('A mock child fixture')).toBeInTheDocument(); }); @@ -88,7 +88,7 @@ describe('standalone with child', () => { it('rejects render of template with componentImports set', () => { const view = render(`
`, { imports: [ParentFixtureComponent], - ɵcomponentImports: [MockChildFixtureComponent], + componentImports: [MockChildFixtureComponent], }); return expect(view).rejects.toMatchObject({ message: /Error while rendering/ }); }); From 9146d63c4f877bc61a2b5cefe76373202486ff1e Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Mon, 21 Nov 2022 18:51:11 +0100 Subject: [PATCH 03/13] docs: update material inputs example (#324) --- apps/example-app/src/app/app.module.ts | 2 +- apps/example-app/src/app/examples/04-forms-with-material.ts | 3 +++ apps/example-app/src/app/material.module.ts | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/example-app/src/app/app.module.ts b/apps/example-app/src/app/app.module.ts index e0ab6e3a..cb95434f 100644 --- a/apps/example-app/src/app/app.module.ts +++ b/apps/example-app/src/app/app.module.ts @@ -7,7 +7,7 @@ import { StoreModule } from '@ngrx/store'; import { AppRoutingModule } from './app-routing.module'; import { MaterialModule } from './material.module'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyListModule as MatListModule } from '@angular/material/legacy-list'; +import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; diff --git a/apps/example-app/src/app/examples/04-forms-with-material.ts b/apps/example-app/src/app/examples/04-forms-with-material.ts index 69ea4636..ed510d1f 100644 --- a/apps/example-app/src/app/examples/04-forms-with-material.ts +++ b/apps/example-app/src/app/examples/04-forms-with-material.ts @@ -6,10 +6,12 @@ import { UntypedFormBuilder, Validators } from '@angular/forms'; template: `
+ Name + Score + Color {{ colorControlDisplayValue }} diff --git a/apps/example-app/src/app/material.module.ts b/apps/example-app/src/app/material.module.ts index 68797007..297c9d74 100644 --- a/apps/example-app/src/app/material.module.ts +++ b/apps/example-app/src/app/material.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; -import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatNativeDateModule } from '@angular/material/core'; From 02dcdb156aee008b3e16b44bdeee73288c2d8dbf Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Mon, 21 Nov 2022 19:53:34 +0100 Subject: [PATCH 04/13] fix: don't fire router events on render (#325) Closes #318 --- .../src/lib/testing-library.ts | 26 +++++------ .../tests/issues/issue-318.spec.ts | 43 +++++++++++++++++++ 2 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 projects/testing-library/tests/issues/issue-318.spec.ts diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 1f71a9e7..002b2f2a 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -29,7 +29,7 @@ import { RenderComponentOptions, RenderTemplateOptions, RenderResult, ComponentO import { getConfig } from './config'; const mountedFixtures = new Set>(); -const inject = TestBed.inject || TestBed.get; +const safeInject = TestBed.inject || TestBed.get; export async function render( component: Type, @@ -99,6 +99,17 @@ export async function render( const componentContainer = createComponentFixture(sut, wrapper); + const zone = safeInject(NgZone); + const router = safeInject(Router); + + if (typeof router?.initialNavigation === 'function') { + if (zone) { + zone.run(() => router?.initialNavigation()); + } else { + router?.initialNavigation(); + } + } + let fixture: ComponentFixture; let detectChanges: () => void; @@ -120,17 +131,6 @@ export async function render( fixture.componentRef.injector.get(ChangeDetectorRef).detectChanges(); }; - const zone = inject(NgZone); - - const router = inject(Router); - if (typeof router?.initialNavigation === 'function') { - if (zone) { - zone.run(() => router?.initialNavigation()); - } else { - router?.initialNavigation(); - } - } - const navigate = async (elementOrPath: Element | string, basePath = ''): Promise => { const href = typeof elementOrPath === 'string' ? elementOrPath : elementOrPath.getAttribute('href'); const [path, params] = (basePath + href).split('?'); @@ -229,7 +229,7 @@ export async function render( async function createComponent(component: Type): Promise> { /* Make sure angular application is initialized before creating component */ - await inject(ApplicationInitStatus).donePromise; + await safeInject(ApplicationInitStatus).donePromise; return TestBed.createComponent(component); } diff --git a/projects/testing-library/tests/issues/issue-318.spec.ts b/projects/testing-library/tests/issues/issue-318.spec.ts new file mode 100644 index 00000000..3f1430e8 --- /dev/null +++ b/projects/testing-library/tests/issues/issue-318.spec.ts @@ -0,0 +1,43 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {Router} from '@angular/router'; +import {RouterTestingModule} from '@angular/router/testing'; +import {Subject, takeUntil} from 'rxjs'; +import {render} from "@testing-library/angular"; + +@Component({ + selector: 'atl-app-fixture', + template: '', +}) +class FixtureComponent implements OnInit, OnDestroy { + unsubscribe$ = new Subject(); + + constructor(private router: Router) {} + + ngOnInit(): void { + this.router.events.pipe(takeUntil(this.unsubscribe$)).subscribe((evt) => { + this.eventReceived(evt) + }); + } + + ngOnDestroy(): void { + this.unsubscribe$.next(); + this.unsubscribe$.complete(); + } + + eventReceived(evt: any) { + console.log(evt); + } +} + + +test('it does not invoke router events on init', async () => { + const eventReceived = jest.fn(); + await render(FixtureComponent, { + imports: [RouterTestingModule], + componentProperties: { + eventReceived + } + }); + expect(eventReceived).not.toHaveBeenCalled(); +}); + From 9384230eef741ddfb3d9b6f3c02201136fc70886 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Tue, 22 Nov 2022 19:28:58 +0100 Subject: [PATCH 05/13] fix: don't invoke ngOnChanges when no properties are provided (#326) Closes #323 BREAKING CHANGE: This change is made to have the same behavior as the run time behavior. BEFORE: The `ngOnChanges` lifecycle is always invoked when a component is rendered. AFTER: The `ngOnChanges` lifecycle is only invoked if a component is rendered with `componentProperties`. --- .../src/lib/testing-library.ts | 2 +- projects/testing-library/tests/render.spec.ts | 39 ++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 002b2f2a..dde9751b 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -210,7 +210,7 @@ export async function render( let isAlive = true; fixture.componentRef.onDestroy(() => (isAlive = false)); - if (hasOnChangesHook(fixture.componentInstance)) { + if (hasOnChangesHook(fixture.componentInstance) && Object.keys(properties).length > 0) { const changes = getChangesObj(null, componentProperties); fixture.componentInstance.ngOnChanges(changes); } diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index 68110a43..b40d70c9 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -22,16 +22,18 @@ import { render, fireEvent, screen } from '../src/public_api'; }) class FixtureComponent {} -test('creates queries and events', async () => { - const view = await render(FixtureComponent); +describe('DTL functionality', () => { + it('creates queries and events', async () => { + const view = await render(FixtureComponent); - /// We wish to test the utility function from `render` here. - // eslint-disable-next-line testing-library/prefer-screen-queries - fireEvent.input(view.getByTestId('input'), { target: { value: 'a super awesome input' } }); - // eslint-disable-next-line testing-library/prefer-screen-queries - expect(view.getByDisplayValue('a super awesome input')).toBeInTheDocument(); - // eslint-disable-next-line testing-library/prefer-screen-queries - fireEvent.click(view.getByText('button')); + /// We wish to test the utility function from `render` here. + // eslint-disable-next-line testing-library/prefer-screen-queries + fireEvent.input(view.getByTestId('input'), { target: { value: 'a super awesome input' } }); + // eslint-disable-next-line testing-library/prefer-screen-queries + expect(view.getByDisplayValue('a super awesome input')).toBeInTheDocument(); + // eslint-disable-next-line testing-library/prefer-screen-queries + fireEvent.click(view.getByText('button')); + }); }); describe('standalone', () => { @@ -201,13 +203,13 @@ describe('Angular component life-cycle hooks', () => { } ngOnChanges(changes: SimpleChanges) { - if (changes.name && this.nameChanged) { - this.nameChanged(changes.name.currentValue, changes.name.isFirstChange()); + if (this.nameChanged) { + this.nameChanged(changes.name?.currentValue, changes.name?.isFirstChange()); } } } - it('will call ngOnInit on initial render', async () => { + it('invokes ngOnInit on initial render', async () => { const nameInitialized = jest.fn(); const componentProperties = { nameInitialized }; const view = await render(FixtureWithNgOnChangesComponent, { componentProperties }); @@ -218,7 +220,7 @@ describe('Angular component life-cycle hooks', () => { expect(nameInitialized).toHaveBeenCalledWith('Initial'); }); - it('will call ngOnChanges on initial render before ngOnInit', async () => { + it('invokes ngOnChanges on initial render before ngOnInit', async () => { const nameInitialized = jest.fn(); const nameChanged = jest.fn(); const componentProperties = { nameInitialized, nameChanged, name: 'Sarah' }; @@ -232,6 +234,17 @@ describe('Angular component life-cycle hooks', () => { /// expect `nameChanged` to be called before `nameInitialized` expect(nameChanged.mock.invocationCallOrder[0]).toBeLessThan(nameInitialized.mock.invocationCallOrder[0]); }); + + it('does not invoke ngOnChanges when no properties are provided', async () => { + @Component({ template: `` }) + class TestFixtureComponent implements OnChanges { + ngOnChanges() { + throw new Error('should not be called'); + } + } + + await render(TestFixtureComponent); + }); }); test('waits for angular app initialization before rendering components', async () => { From 881cc83a003e298e4ae391f8d9bf832d10c3f153 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Fri, 25 Nov 2022 12:42:44 +0100 Subject: [PATCH 06/13] feat: add more fine-grained control over inputs and outputs (#328) BREAKING CHANGE: `rerender` expects properties to be wrapped in an object containing `componentProperties` (or `componentInputs` and `componentOutputs` to have a more fine-grained control). BEFORE: ```ts await render(PersonComponent, { componentProperties: { name: 'Sarah' } }); await rerender({ name: 'Sarah 2' }); ``` AFTER: ```ts await render(PersonComponent, { componentProperties: { name: 'Sarah' } }); await rerender({ componentProperties: { name: 'Sarah 2' } }); ``` --- .../src/app/issues/issue-222.spec.ts | 2 +- .../examples/16-input-getter-setter.spec.ts | 2 +- projects/testing-library/src/lib/models.ts | 45 +++++++++- .../src/lib/testing-library.ts | 68 +++++++++++++-- .../tests/changeInputs.spec.ts | 85 +++++++++++++++++++ .../testing-library/tests/rerender.spec.ts | 25 +++++- 6 files changed, 210 insertions(+), 17 deletions(-) create mode 100644 projects/testing-library/tests/changeInputs.spec.ts diff --git a/apps/example-app-karma/src/app/issues/issue-222.spec.ts b/apps/example-app-karma/src/app/issues/issue-222.spec.ts index b6ac5204..17e9a029 100644 --- a/apps/example-app-karma/src/app/issues/issue-222.spec.ts +++ b/apps/example-app-karma/src/app/issues/issue-222.spec.ts @@ -9,7 +9,7 @@ it('https://github.com/testing-library/angular-testing-library/issues/222 with r expect(screen.getByText('Hello Sarah')).toBeTruthy(); - await rerender({ name: 'Mark' }); + await rerender({ componentProperties: { name: 'Mark' } }); expect(screen.getByText('Hello Mark')).toBeTruthy(); }); diff --git a/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts b/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts index 1d87edf9..53dee01f 100644 --- a/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts +++ b/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts @@ -30,7 +30,7 @@ test('should run logic in the input setter and getter while re-rendering', async expect(screen.getByTestId('value')).toHaveTextContent('I am value from setter Angular'); expect(screen.getByTestId('value-getter')).toHaveTextContent('I am value from getter Angular'); - await rerender({ value: 'React' }); + await rerender({ componentProperties: { value: 'React' } }); // note we have to re-query because the elements are not the same anymore expect(screen.getByTestId('value')).toHaveTextContent('I am value from setter React'); diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index c1d931ce..d9106f63 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -57,13 +57,22 @@ export interface RenderResult extend * Re-render the same component with different properties. * This creates a new instance of the component. */ - rerender: (rerenderedProperties: Partial) => Promise; - + rerender: ( + properties?: Pick< + RenderTemplateOptions, + 'componentProperties' | 'componentInputs' | 'componentOutputs' + >, + ) => Promise; /** * @description * Keeps the current fixture intact and invokes ngOnChanges with the updated properties. */ change: (changedProperties: Partial) => void; + /** + * @description + * Keeps the current fixture intact, update the @Input properties and invoke ngOnChanges with the updated properties. + */ + changeInput: (changedInputProperties: Partial) => void; } export interface RenderComponentOptions { @@ -155,7 +164,7 @@ export interface RenderComponentOptions; + /** + * @description + * An object to set `@Input` properties of the component + * + * @default + * {} + * + * @example + * const component = await render(AppComponent, { + * componentInputs: { + * counterValue: 10 + * } + * }) + */ + componentInputs?: Partial; + /** + * @description + * An object to set `@Output` properties of the component + * + * @default + * {} + * + * @example + * const component = await render(AppComponent, { + * componentOutputs: { + * send: (value) => { ... } + * } + * }) + */ + componentOutputs?: Partial; /** * @description * A collection of providers to inject dependencies of the component. diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index dde9751b..9ca1f619 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -54,6 +54,8 @@ export async function render( queries, wrapper = WrapperComponent as Type, componentProperties = {}, + componentInputs = {}, + componentOutputs = {}, componentProviders = [], childComponentOverrides = [], componentImports: componentImports, @@ -104,25 +106,51 @@ export async function render( if (typeof router?.initialNavigation === 'function') { if (zone) { - zone.run(() => router?.initialNavigation()); + zone.run(() => router.initialNavigation()); } else { - router?.initialNavigation(); + router.initialNavigation(); } } let fixture: ComponentFixture; let detectChanges: () => void; - await renderFixture(componentProperties); + await renderFixture(componentProperties, componentInputs, componentOutputs); - const rerender = async (rerenderedProperties: Partial) => { - await renderFixture(rerenderedProperties); + const rerender = async ( + properties?: Pick, 'componentProperties' | 'componentInputs' | 'componentOutputs'>, + ) => { + await renderFixture( + properties?.componentProperties ?? {}, + properties?.componentInputs ?? {}, + properties?.componentOutputs ?? {}, + ); + }; + + const changeInput = (changedInputProperties: Partial) => { + if (Object.keys(changedInputProperties).length === 0) { + return; + } + + const changes = getChangesObj(fixture.componentInstance as Record, changedInputProperties); + + setComponentInputs(fixture, changedInputProperties); + + if (hasOnChangesHook(fixture.componentInstance)) { + fixture.componentInstance.ngOnChanges(changes); + } + + fixture.componentRef.injector.get(ChangeDetectorRef).detectChanges(); }; const change = (changedProperties: Partial) => { + if (Object.keys(changedProperties).length === 0) { + return; + } + const changes = getChangesObj(fixture.componentInstance as Record, changedProperties); - setComponentProperties(fixture, { componentProperties: changedProperties }); + setComponentProperties(fixture, changedProperties); if (hasOnChangesHook(fixture.componentInstance)) { fixture.componentInstance.ngOnChanges(changes); @@ -178,6 +206,7 @@ export async function render( navigate, rerender, change, + changeInput, // @ts-ignore: fixture assigned debugElement: fixture.debugElement, // @ts-ignore: fixture assigned @@ -190,13 +219,16 @@ export async function render( ...replaceFindWithFindAndDetectChanges(dtlGetQueriesForElement(fixture.nativeElement, queries)), }; - async function renderFixture(properties: Partial) { + async function renderFixture(properties: Partial, inputs: Partial, outputs: Partial) { if (fixture) { cleanupAtFixture(fixture); } fixture = await createComponent(componentContainer); - setComponentProperties(fixture, { componentProperties: properties }); + + setComponentProperties(fixture, properties); + setComponentInputs(fixture, inputs); + setComponentOutputs(fixture, outputs); if (removeAngularAttributes) { fixture.nativeElement.removeAttribute('ng-version'); @@ -246,7 +278,7 @@ function createComponentFixture( function setComponentProperties( fixture: ComponentFixture, - { componentProperties = {} }: Pick, 'componentProperties'>, + componentProperties: RenderTemplateOptions['componentProperties'] = {}, ) { for (const key of Object.keys(componentProperties)) { const descriptor = Object.getOwnPropertyDescriptor((fixture.componentInstance as any).constructor.prototype, key); @@ -272,6 +304,24 @@ function setComponentProperties( return fixture; } +function setComponentOutputs( + fixture: ComponentFixture, + componentOutputs: RenderTemplateOptions['componentOutputs'] = {}, +) { + for (const [name, value] of Object.entries(componentOutputs)) { + (fixture.componentInstance as any)[name] = value; + } +} + +function setComponentInputs( + fixture: ComponentFixture, + componentInputs: RenderTemplateOptions['componentInputs'] = {}, +) { + for (const [name, value] of Object.entries(componentInputs)) { + fixture.componentRef.setInput(name, value); + } +} + function overrideComponentImports(sut: Type | string, imports: (Type | any[])[] | undefined) { if (imports) { if (typeof sut === 'function' && ɵisStandalone(sut)) { diff --git a/projects/testing-library/tests/changeInputs.spec.ts b/projects/testing-library/tests/changeInputs.spec.ts new file mode 100644 index 00000000..962d7ad9 --- /dev/null +++ b/projects/testing-library/tests/changeInputs.spec.ts @@ -0,0 +1,85 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { render, screen } from '../src/public_api'; + +@Component({ + selector: 'atl-fixture', + template: ` {{ firstName }} {{ lastName }} `, +}) +class FixtureComponent { + @Input() firstName = 'Sarah'; + @Input() lastName?: string; +} + +test('changes the component with updated props', async () => { + const { changeInput } = await render(FixtureComponent); + expect(screen.getByText('Sarah')).toBeInTheDocument(); + + const firstName = 'Mark'; + changeInput({ firstName }); + + expect(screen.getByText(firstName)).toBeInTheDocument(); +}); + +test('changes the component with updated props while keeping other props untouched', async () => { + const firstName = 'Mark'; + const lastName = 'Peeters'; + const { changeInput } = await render(FixtureComponent, { + componentInputs: { + firstName, + lastName, + }, + }); + + expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument(); + + const firstName2 = 'Chris'; + changeInput({ firstName: firstName2 }); + + expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument(); +}); + +@Component({ + selector: 'atl-fixture', + template: ` {{ name }} `, +}) +class FixtureWithNgOnChangesComponent implements OnChanges { + @Input() name = 'Sarah'; + @Input() nameChanged?: (name: string, isFirstChange: boolean) => void; + + ngOnChanges(changes: SimpleChanges) { + if (changes.name && this.nameChanged) { + this.nameChanged(changes.name.currentValue, changes.name.isFirstChange()); + } + } +} + +test('will call ngOnChanges on change', async () => { + const nameChanged = jest.fn(); + const componentInputs = { nameChanged }; + const { changeInput } = await render(FixtureWithNgOnChangesComponent, { componentInputs }); + expect(screen.getByText('Sarah')).toBeInTheDocument(); + + const name = 'Mark'; + changeInput({ name }); + + expect(screen.getByText(name)).toBeInTheDocument(); + expect(nameChanged).toHaveBeenCalledWith(name, false); +}); + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'atl-fixture', + template: `
Number
`, +}) +class FixtureWithOnPushComponent { + @Input() activeField = ''; +} + +test('update properties on change', async () => { + const { changeInput } = await render(FixtureWithOnPushComponent); + const numberHtmlElementRef = screen.queryByTestId('number'); + + expect(numberHtmlElementRef).not.toHaveClass('active'); + changeInput({ activeField: 'number' }); + expect(numberHtmlElementRef).toHaveClass('active'); +}); diff --git a/projects/testing-library/tests/rerender.spec.ts b/projects/testing-library/tests/rerender.spec.ts index 0edf69ea..d0ee43b2 100644 --- a/projects/testing-library/tests/rerender.spec.ts +++ b/projects/testing-library/tests/rerender.spec.ts @@ -15,7 +15,26 @@ test('rerenders the component with updated props', async () => { expect(screen.getByText('Sarah')).toBeInTheDocument(); const firstName = 'Mark'; - await rerender({ firstName }); + await rerender({ componentProperties: { firstName } }); + + expect(screen.getByText(firstName)).toBeInTheDocument(); +}); + +test('rerenders without props', async () => { + const { rerender } = await render(FixtureComponent); + expect(screen.getByText('Sarah')).toBeInTheDocument(); + + await rerender(); + + expect(screen.getByText('Sarah')).toBeInTheDocument(); +}); + +test('rerenders the component with updated inputs', async () => { + const { rerender } = await render(FixtureComponent); + expect(screen.getByText('Sarah')).toBeInTheDocument(); + + const firstName = 'Mark'; + await rerender({ componentInputs: { firstName } }); expect(screen.getByText(firstName)).toBeInTheDocument(); }); @@ -33,8 +52,8 @@ test('rerenders the component with updated props and resets other props', async expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument(); const firstName2 = 'Chris'; - rerender({ firstName: firstName2 }); + await rerender({ componentProperties: { firstName: firstName2 } }); expect(screen.queryByText(`${firstName2} ${lastName}`)).not.toBeInTheDocument(); - expect(screen.queryByText(firstName2)).not.toBeInTheDocument(); + expect(screen.queryByText(`${firstName} ${lastName}`)).not.toBeInTheDocument(); }); From 439674f2595feaf9bf1d93550eccd153b3da128b Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Fri, 25 Nov 2022 13:23:23 +0100 Subject: [PATCH 07/13] feat: add ability to turn off auto detect changes (#329) --- projects/testing-library/src/lib/models.ts | 13 +++++ .../src/lib/testing-library.ts | 18 +++---- .../testing-library/tests/fire-event.spec.ts | 51 +++++++++++++++---- 3 files changed, 60 insertions(+), 22 deletions(-) diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index d9106f63..99c8ef99 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -76,6 +76,19 @@ export interface RenderResult extend } export interface RenderComponentOptions { + /** + * @description + * Automatically detect changes as a "real" running component would do. + * + * @default + * true + * + * @example + * const component = await render(AppComponent, { + * autoDetectChanges: false + * }) + */ + autoDetectChanges?: boolean; /** * @description * Will call detectChanges when the component is compiled diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 9ca1f619..7b5bc8e1 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -47,6 +47,7 @@ export async function render( const { dom: domConfig, ...globalConfig } = getConfig(); const { detectChanges: detectChangesOnRender = true, + autoDetectChanges = true, declarations = [], imports = [], providers = [], @@ -65,14 +66,7 @@ export async function render( defaultImports = [], } = { ...globalConfig, ...renderOptions }; - dtlConfigure({ - eventWrapper: (cb) => { - const result = cb(); - detectChangesForMountedFixtures(); - return result; - }, - ...domConfig, - }); + dtlConfigure(domConfig); TestBed.configureTestingModule({ declarations: addAutoDeclarations(sut, { @@ -195,7 +189,6 @@ export async function render( result = doNavigate(); } - detectChanges(); return result ?? false; }; @@ -225,7 +218,6 @@ export async function render( } fixture = await createComponent(componentContainer); - setComponentProperties(fixture, properties); setComponentInputs(fixture, inputs); setComponentOutputs(fixture, outputs); @@ -247,6 +239,10 @@ export async function render( fixture.componentInstance.ngOnChanges(changes); } + if (autoDetectChanges) { + fixture.autoDetectChanges(true); + } + detectChanges = () => { if (isAlive) { fixture.detectChanges(); @@ -403,8 +399,6 @@ async function waitForWrapper( inFakeAsync = false; } - detectChanges(); - return await dtlWaitFor(() => { setTimeout(() => detectChanges(), 0); if (inFakeAsync) { diff --git a/projects/testing-library/tests/fire-event.spec.ts b/projects/testing-library/tests/fire-event.spec.ts index ebb85017..ace4ba82 100644 --- a/projects/testing-library/tests/fire-event.spec.ts +++ b/projects/testing-library/tests/fire-event.spec.ts @@ -1,17 +1,48 @@ import { Component } from '@angular/core'; import { render, fireEvent, screen } from '../src/public_api'; +import { FormsModule } from '@angular/forms'; -@Component({ - selector: 'atl-fixture', - template: ` `, -}) -class FixtureComponent {} +describe('fireEvent', () => { + @Component({ + selector: 'atl-fixture', + template: ` +
Hello {{ name }}
`, + }) + class FixtureComponent { + name = ''; + } -test('does not call detect changes when fixture is destroyed', async () => { - const { fixture } = await render(FixtureComponent); + it('automatically detect changes when event is fired', async () => { + await render(FixtureComponent, { + imports: [FormsModule], + }); - fixture.destroy(); + fireEvent.input(screen.getByTestId('input'), { target: { value: 'Tim' } }); - // should otherwise throw - fireEvent.input(screen.getByTestId('input'), { target: { value: 'Bonjour' } }); + expect(screen.getByText('Hello Tim')).toBeInTheDocument(); + }); + + it('can disable automatic detect changes when event is fired', async () => { + const { detectChanges } = await render(FixtureComponent, { + imports: [FormsModule], + autoDetectChanges: false, + }); + + fireEvent.input(screen.getByTestId('input'), { target: { value: 'Tim' } }); + + expect(screen.queryByText('Hello Tim')).not.toBeInTheDocument(); + + detectChanges(); + + expect(screen.getByText('Hello Tim')).toBeInTheDocument(); + }); + + it('does not call detect changes when fixture is destroyed', async () => { + const { fixture } = await render(FixtureComponent); + + fixture.destroy(); + + // should otherwise throw + fireEvent.input(screen.getByTestId('input'), { target: { value: 'Bonjour' } }); + }); }); From 455b5ef499c8f21d3fa940beac529487fbf8d592 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Fri, 25 Nov 2022 13:33:19 +0100 Subject: [PATCH 08/13] chore: update peer dependencies (#330) BREAKING CHANGE: BEFORE: The minimum version of Angular is v14.0.0 AFTER: The minimum version of Angular is v14.1.0 --- projects/testing-library/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/testing-library/package.json b/projects/testing-library/package.json index 0106b937..c74d252b 100644 --- a/projects/testing-library/package.json +++ b/projects/testing-library/package.json @@ -29,10 +29,10 @@ "migrations": "./schematics/migrations/migration.json" }, "peerDependencies": { - "@angular/common": ">= 14.0.0", - "@angular/platform-browser": ">= 14.0.0", - "@angular/router": ">= 14.0.0", - "@angular/core": ">= 14.0.0" + "@angular/common": ">= 14.1.0", + "@angular/platform-browser": ">= 14.1.0", + "@angular/router": ">= 14.1.0", + "@angular/core": ">= 14.1.0" }, "dependencies": { "@testing-library/dom": "^8.0.0", From d184618805b729152b3987b659e5d26ba29c1a8a Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Sat, 10 Dec 2022 15:29:38 +0100 Subject: [PATCH 09/13] feat: childComponentOverrides is stable (#338) --- projects/testing-library/src/lib/models.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 99c8ef99..63249f0a 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -255,7 +255,6 @@ export interface RenderComponentOptions[]; /** From 6e31db5657be88bd43b8fdfdc751ee55288dd710 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Sat, 10 Dec 2022 17:19:57 +0100 Subject: [PATCH 10/13] fix: invoke changeChanges once on input change (#339) --- .../src/lib/testing-library.ts | 14 ++-- .../tests/changeInputs.spec.ts | 37 +++++------ projects/testing-library/tests/render.spec.ts | 66 +++++++++++++------ 3 files changed, 70 insertions(+), 47 deletions(-) diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 7b5bc8e1..b6b5c32b 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -126,15 +126,9 @@ export async function render( return; } - const changes = getChangesObj(fixture.componentInstance as Record, changedInputProperties); - setComponentInputs(fixture, changedInputProperties); - if (hasOnChangesHook(fixture.componentInstance)) { - fixture.componentInstance.ngOnChanges(changes); - } - - fixture.componentRef.injector.get(ChangeDetectorRef).detectChanges(); + fixture.detectChanges(); }; const change = (changedProperties: Partial) => { @@ -229,6 +223,7 @@ export async function render( fixture.nativeElement.removeAttribute('id'); } } + mountedFixtures.add(fixture); let isAlive = true; @@ -338,7 +333,10 @@ function overrideChildComponentProviders(componentOverrides: ComponentOverride(componentInstance: SutType): componentInstance is SutType & OnChanges { return ( - 'ngOnChanges' in componentInstance && typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function' + componentInstance !== null && + typeof componentInstance === 'object' && + 'ngOnChanges' in componentInstance && + typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function' ); } diff --git a/projects/testing-library/tests/changeInputs.spec.ts b/projects/testing-library/tests/changeInputs.spec.ts index 962d7ad9..e78517d9 100644 --- a/projects/testing-library/tests/changeInputs.spec.ts +++ b/projects/testing-library/tests/changeInputs.spec.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; import { render, screen } from '../src/public_api'; @Component({ @@ -40,30 +40,29 @@ test('changes the component with updated props while keeping other props untouch @Component({ selector: 'atl-fixture', - template: ` {{ name }} `, + template: ` {{ propOne }} {{ propTwo }}`, }) class FixtureWithNgOnChangesComponent implements OnChanges { - @Input() name = 'Sarah'; - @Input() nameChanged?: (name: string, isFirstChange: boolean) => void; - - ngOnChanges(changes: SimpleChanges) { - if (changes.name && this.nameChanged) { - this.nameChanged(changes.name.currentValue, changes.name.isFirstChange()); - } - } + @Input() propOne = 'Init'; + @Input() propTwo = ''; + + // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method, @typescript-eslint/no-empty-function + ngOnChanges() {} } -test('will call ngOnChanges on change', async () => { - const nameChanged = jest.fn(); - const componentInputs = { nameChanged }; - const { changeInput } = await render(FixtureWithNgOnChangesComponent, { componentInputs }); - expect(screen.getByText('Sarah')).toBeInTheDocument(); +test('calls ngOnChanges on change', async () => { + const componentInputs = { propOne: 'One', propTwo: 'Two' }; + const { changeInput, fixture } = await render(FixtureWithNgOnChangesComponent, { componentInputs }); + const spy = jest.spyOn(fixture.componentInstance, 'ngOnChanges'); + + expect(screen.getByText(`${componentInputs.propOne} ${componentInputs.propTwo}`)).toBeInTheDocument(); - const name = 'Mark'; - changeInput({ name }); + const propOne = 'UpdatedOne'; + const propTwo = 'UpdatedTwo'; + changeInput({ propOne, propTwo }); - expect(screen.getByText(name)).toBeInTheDocument(); - expect(nameChanged).toHaveBeenCalledWith(name, false); + expect(spy).toHaveBeenCalledTimes(1); + expect(screen.getByText(`${propOne} ${propTwo}`)).toBeInTheDocument(); }); @Component({ diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index b40d70c9..12a1f628 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -220,7 +220,7 @@ describe('Angular component life-cycle hooks', () => { expect(nameInitialized).toHaveBeenCalledWith('Initial'); }); - it('invokes ngOnChanges on initial render before ngOnInit', async () => { + it('invokes ngOnChanges with componentProperties on initial render before ngOnInit', async () => { const nameInitialized = jest.fn(); const nameChanged = jest.fn(); const componentProperties = { nameInitialized, nameChanged, name: 'Sarah' }; @@ -233,6 +233,23 @@ describe('Angular component life-cycle hooks', () => { expect(nameChanged).toHaveBeenCalledWith('Sarah', true); /// expect `nameChanged` to be called before `nameInitialized` expect(nameChanged.mock.invocationCallOrder[0]).toBeLessThan(nameInitialized.mock.invocationCallOrder[0]); + expect(nameChanged).toHaveBeenCalledTimes(1); + }); + + it('invokes ngOnChanges with componentInputs on initial render before ngOnInit', async () => { + const nameInitialized = jest.fn(); + const nameChanged = jest.fn(); + const componentInput = { nameInitialized, nameChanged, name: 'Sarah' }; + + const view = await render(FixtureWithNgOnChangesComponent, { componentInputs: componentInput }); + + /// We wish to test the utility function from `render` here. + // eslint-disable-next-line testing-library/prefer-screen-queries + expect(view.getByText('Sarah')).toBeInTheDocument(); + expect(nameChanged).toHaveBeenCalledWith('Sarah', true); + /// expect `nameChanged` to be called before `nameInitialized` + expect(nameChanged.mock.invocationCallOrder[0]).toBeLessThan(nameInitialized.mock.invocationCallOrder[0]); + expect(nameChanged).toHaveBeenCalledTimes(1); }); it('does not invoke ngOnChanges when no properties are provided', async () => { @@ -243,30 +260,39 @@ describe('Angular component life-cycle hooks', () => { } } - await render(TestFixtureComponent); + const { fixture, detectChanges } = await render(TestFixtureComponent); + const spy = jest.spyOn(fixture.componentInstance, 'ngOnChanges'); + + detectChanges(); + + expect(spy).not.toHaveBeenCalled(); }); }); -test('waits for angular app initialization before rendering components', async () => { - const mock = jest.fn(); - - await render(FixtureComponent, { - providers: [ - { - provide: APP_INITIALIZER, - useFactory: () => mock, - multi: true, - }, - ], - }); +describe('initializer', () => { + it('waits for angular app initialization before rendering components', async () => { + const mock = jest.fn(); + + await render(FixtureComponent, { + providers: [ + { + provide: APP_INITIALIZER, + useFactory: () => mock, + multi: true, + }, + ], + }); - expect(TestBed.inject(ApplicationInitStatus).done).toBe(true); - expect(mock).toHaveBeenCalled(); + expect(TestBed.inject(ApplicationInitStatus).done).toBe(true); + expect(mock).toHaveBeenCalled(); + }); }); -test('gets the DebugElement', async () => { - const view = await render(FixtureComponent); +describe('DebugElement', () => { + it('gets the DebugElement', async () => { + const view = await render(FixtureComponent); - expect(view.debugElement).not.toBeNull(); - expect(view.debugElement.componentInstance).toBeInstanceOf(FixtureComponent); + expect(view.debugElement).not.toBeNull(); + expect(view.debugElement.componentInstance).toBeInstanceOf(FixtureComponent); + }); }); From c22977888a7a0b74d215450064420cf655fce82d Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Sat, 10 Dec 2022 18:35:56 +0100 Subject: [PATCH 11/13] feat: remove detectChanges in favor of detectChangesOnRender (#341) BREAKING CHANGE: The config property detectChanges is renamed to detectChangesOnRender. BEFORE: ```ts const component = await render(AppComponent, { detectChanges: false }); ``` AFTER: ```ts const component = await render(AppComponent, { detectChangesOnRender: false }); ``` --- projects/testing-library/src/lib/models.ts | 16 ---------------- .../testing-library/src/lib/testing-library.ts | 6 +----- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index d225df80..51fe5f7c 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -89,22 +89,6 @@ export interface RenderComponentOptions( ): Promise> { const { dom: domConfig, ...globalConfig } = getConfig(); const { - detectChanges: detectChangesDeprecated = true, - detectChangesOnRender: detectChangesOnRenderInput, + detectChangesOnRender = true, autoDetectChanges = true, declarations = [], imports = [], @@ -67,9 +66,6 @@ export async function render( defaultImports = [], } = { ...globalConfig, ...renderOptions }; - const detectChangesOnRender = - detectChangesOnRenderInput === undefined ? detectChangesDeprecated : detectChangesOnRenderInput; - dtlConfigure(domConfig); TestBed.configureTestingModule({ From 5e17e9544164b7c40312b52fe2d43ab3ccadbdef Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Sat, 10 Dec 2022 19:08:29 +0100 Subject: [PATCH 12/13] test: extra tests for ngOnChanges (#342) --- projects/testing-library/tests/change.spec.ts | 49 ++++++++++++------- .../tests/changeInputs.spec.ts | 13 +++++ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/projects/testing-library/tests/change.spec.ts b/projects/testing-library/tests/change.spec.ts index 1ba67513..d6d30f46 100644 --- a/projects/testing-library/tests/change.spec.ts +++ b/projects/testing-library/tests/change.spec.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; import { render, screen } from '../src/public_api'; @Component({ @@ -40,32 +40,43 @@ test('changes the component with updated props while keeping other props untouch @Component({ selector: 'atl-fixture', - template: ` {{ name }} `, + template: ` {{ propOne }} {{ propTwo }}`, }) class FixtureWithNgOnChangesComponent implements OnChanges { - @Input() name = 'Sarah'; - @Input() nameChanged?: (name: string, isFirstChange: boolean) => void; - - ngOnChanges(changes: SimpleChanges) { - if (changes.name && this.nameChanged) { - this.nameChanged(changes.name.currentValue, changes.name.isFirstChange()); - } - } + @Input() propOne = 'Init'; + @Input() propTwo = ''; + + // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method, @typescript-eslint/no-empty-function + ngOnChanges() {} } -test('will call ngOnChanges on change', async () => { - const nameChanged = jest.fn(); - const componentProperties = { nameChanged }; - const { change } = await render(FixtureWithNgOnChangesComponent, { componentProperties }); - expect(screen.getByText('Sarah')).toBeInTheDocument(); +test('calls ngOnChanges on change', async () => { + const componentInputs = { propOne: 'One', propTwo: 'Two' }; + const { change, fixture } = await render(FixtureWithNgOnChangesComponent, { componentInputs }); + const spy = jest.spyOn(fixture.componentInstance, 'ngOnChanges'); - const name = 'Mark'; - change({ name }); + expect(screen.getByText(`${componentInputs.propOne} ${componentInputs.propTwo}`)).toBeInTheDocument(); - expect(screen.getByText(name)).toBeInTheDocument(); - expect(nameChanged).toHaveBeenCalledWith(name, false); + const propOne = 'UpdatedOne'; + const propTwo = 'UpdatedTwo'; + change({ propOne, propTwo }); + + expect(spy).toHaveBeenCalledTimes(1); + expect(screen.getByText(`${propOne} ${propTwo}`)).toBeInTheDocument(); }); +test('does not invoke ngOnChanges on change without props', async () => { + const componentInputs = { propOne: 'One', propTwo: 'Two' }; + const { change, fixture } = await render(FixtureWithNgOnChangesComponent, { componentInputs }); + const spy = jest.spyOn(fixture.componentInstance, 'ngOnChanges'); + + expect(screen.getByText(`${componentInputs.propOne} ${componentInputs.propTwo}`)).toBeInTheDocument(); + + change({}); + expect(spy).not.toHaveBeenCalled(); + + expect(screen.getByText(`${componentInputs.propOne} ${componentInputs.propTwo}`)).toBeInTheDocument(); +}); @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'atl-fixture', diff --git a/projects/testing-library/tests/changeInputs.spec.ts b/projects/testing-library/tests/changeInputs.spec.ts index e78517d9..8a970829 100644 --- a/projects/testing-library/tests/changeInputs.spec.ts +++ b/projects/testing-library/tests/changeInputs.spec.ts @@ -65,6 +65,19 @@ test('calls ngOnChanges on change', async () => { expect(screen.getByText(`${propOne} ${propTwo}`)).toBeInTheDocument(); }); +test('does not invoke ngOnChanges on change without props', async () => { + const componentInputs = { propOne: 'One', propTwo: 'Two' }; + const { changeInput, fixture } = await render(FixtureWithNgOnChangesComponent, { componentInputs }); + const spy = jest.spyOn(fixture.componentInstance, 'ngOnChanges'); + + expect(screen.getByText(`${componentInputs.propOne} ${componentInputs.propTwo}`)).toBeInTheDocument(); + + changeInput({}); + expect(spy).not.toHaveBeenCalled(); + + expect(screen.getByText(`${componentInputs.propOne} ${componentInputs.propTwo}`)).toBeInTheDocument(); +}); + @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'atl-fixture', From da1d20958e9acbc8260858ba3a3f7a7abeb0373a Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Mon, 12 Dec 2022 19:53:21 +0100 Subject: [PATCH 13/13] add test example for harness (#343) --- .../src/app/examples/20-test-harness.spec.ts | 33 +++++++++++++++++++ .../src/app/examples/20-test-harness.ts | 19 +++++++++++ 2 files changed, 52 insertions(+) create mode 100644 apps/example-app/src/app/examples/20-test-harness.spec.ts create mode 100644 apps/example-app/src/app/examples/20-test-harness.ts diff --git a/apps/example-app/src/app/examples/20-test-harness.spec.ts b/apps/example-app/src/app/examples/20-test-harness.spec.ts new file mode 100644 index 00000000..eb068002 --- /dev/null +++ b/apps/example-app/src/app/examples/20-test-harness.spec.ts @@ -0,0 +1,33 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatButtonHarness } from '@angular/material/button/testing'; +import { MatSnackBarHarness } from '@angular/material/snack-bar/testing'; +import { render, screen } from '@testing-library/angular'; +import user from '@testing-library/user-event'; + +import { SnackBarComponent } from './20-test-harness'; + +test('can be used with TestHarness', async () => { + const view = await render(``, { + imports: [SnackBarComponent], + }); + const loader = TestbedHarnessEnvironment.documentRootLoader(view.fixture); + + const buttonHarness = await loader.getHarness(MatButtonHarness); + const button = await buttonHarness.host(); + button.click(); + + const snackbarHarness = await loader.getHarness(MatSnackBarHarness); + expect(await snackbarHarness.getMessage()).toMatch(/Pizza Party!!!/i); +}); + +test('can be used in combination with TestHarness', async () => { + const view = await render(SnackBarComponent); + const loader = TestbedHarnessEnvironment.documentRootLoader(view.fixture); + + user.click(screen.getByRole('button')); + + const snackbarHarness = await loader.getHarness(MatSnackBarHarness); + expect(await snackbarHarness.getMessage()).toMatch(/Pizza Party!!!/i); + + expect(screen.getByText(/Pizza Party!!!/i)).toBeInTheDocument(); +}); diff --git a/apps/example-app/src/app/examples/20-test-harness.ts b/apps/example-app/src/app/examples/20-test-harness.ts new file mode 100644 index 00000000..53069075 --- /dev/null +++ b/apps/example-app/src/app/examples/20-test-harness.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; + +@Component({ + selector: 'app-harness', + standalone: true, + imports: [MatButtonModule, MatSnackBarModule], + template: ` + + `, +}) +export class SnackBarComponent { + constructor(private snackBar: MatSnackBar) {} + + openSnackBar() { + return this.snackBar.open('Pizza Party!!!'); + } +}