From 6a64bbe4fd71aa819282b80670e2f763b58ef926 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:32:20 +0200 Subject: [PATCH 1/4] feat: update to Angular 20 (#530) BREAKING CHANGE: The angular minimum version has changed. BEFORE: Angular 17,18,19 were supported. AFTER: Angular 20 is supported: - TestBed.get is removed --- .github/workflows/ci.yml | 2 +- .gitignore | 1 + .../src/app/issues/issue-491.spec.ts | 4 +- package.json | 38 +++++++++---------- projects/testing-library/package.json | 10 ++--- .../src/lib/testing-library.ts | 7 ++-- .../tests/defer-blocks.spec.ts | 1 - 7 files changed, 30 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 215ef7d..b35c5ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: - node-version: ${{ fromJSON((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') && '[22]' || '[18, 20, 22]') }} + node-version: ${{ fromJSON((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') && '[22]' || '[20, 22, 24]') }} os: ${{ fromJSON((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') && '["ubuntu-latest"]' || '["ubuntu-latest", "windows-latest"]') }} runs-on: ${{ matrix.os }} diff --git a/.gitignore b/.gitignore index 1204690..22faaca 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ yarn.lock Thumbs.db .cursor/rules/nx-rules.mdc .github/instructions/nx.instructions.md +.history diff --git a/apps/example-app-karma/src/app/issues/issue-491.spec.ts b/apps/example-app-karma/src/app/issues/issue-491.spec.ts index 7da4d6d..9320251 100644 --- a/apps/example-app-karma/src/app/issues/issue-491.spec.ts +++ b/apps/example-app-karma/src/app/issues/issue-491.spec.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { Router } from '@angular/router'; -import { render, screen, waitForElementToBeRemoved } from '@testing-library/angular'; +import { render, screen } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; it('test click event with router.navigate', async () => { @@ -31,8 +31,6 @@ it('test click event with router.navigate', async () => { await user.click(screen.getByRole('button', { name: 'submit' })); - await waitForElementToBeRemoved(() => screen.queryByRole('heading', { name: 'Login' })); - expect(await screen.findByRole('heading', { name: 'Logged In' })).toBeVisible(); }); diff --git a/package.json b/package.json index b3f540b..3888395 100644 --- a/package.json +++ b/package.json @@ -27,15 +27,15 @@ "prepare": "git config core.hookspath .githooks" }, "dependencies": { - "@angular/animations": "19.2.14", - "@angular/cdk": "19.2.18", - "@angular/common": "19.2.14", - "@angular/compiler": "19.2.14", - "@angular/core": "19.2.14", - "@angular/material": "19.2.18", - "@angular/platform-browser": "19.2.14", - "@angular/platform-browser-dynamic": "19.2.14", - "@angular/router": "19.2.14", + "@angular/animations": "20.0.0", + "@angular/cdk": "20.0.0", + "@angular/common": "20.0.0", + "@angular/compiler": "20.0.0", + "@angular/core": "20.0.0", + "@angular/material": "20.0.0", + "@angular/platform-browser": "20.0.0", + "@angular/platform-browser-dynamic": "20.0.0", + "@angular/router": "20.0.0", "@ngrx/store": "19.0.0", "@nx/angular": "21.1.2", "@testing-library/dom": "^10.4.0", @@ -44,18 +44,18 @@ "zone.js": "^0.15.0" }, "devDependencies": { - "@angular-devkit/build-angular": "19.2.9", - "@angular-devkit/core": "19.2.9", - "@angular-devkit/schematics": "19.2.9", + "@angular-devkit/build-angular": "20.0.0", + "@angular-devkit/core": "20.0.0", + "@angular-devkit/schematics": "20.0.0", "@angular-eslint/builder": "19.2.0", "@angular-eslint/eslint-plugin": "19.2.0", "@angular-eslint/eslint-plugin-template": "19.2.0", "@angular-eslint/schematics": "19.2.0", "@angular-eslint/template-parser": "19.2.0", - "@angular/cli": "~19.2.0", - "@angular/compiler-cli": "19.2.14", - "@angular/forms": "19.2.14", - "@angular/language-service": "19.2.14", + "@angular/cli": "~20.0.0", + "@angular/compiler-cli": "20.0.0", + "@angular/forms": "20.0.0", + "@angular/language-service": "20.0.0", "@eslint/eslintrc": "^2.1.1", "@nx/eslint": "21.1.2", "@nx/eslint-plugin": "21.1.2", @@ -63,7 +63,7 @@ "@nx/node": "21.1.2", "@nx/plugin": "21.1.2", "@nx/workspace": "21.1.2", - "@schematics/angular": "19.2.9", + "@schematics/angular": "20.0.0", "@testing-library/jasmine-dom": "^1.3.3", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.5.2", @@ -91,7 +91,7 @@ "karma-jasmine-html-reporter": "2.0.0", "lint-staged": "^15.3.0", "ng-mocks": "^14.13.1", - "ng-packagr": "19.2.2", + "ng-packagr": "20.0.0", "nx": "21.1.2", "postcss": "^8.4.49", "postcss-import": "14.1.0", @@ -102,7 +102,7 @@ "semantic-release": "^24.2.1", "ts-jest": "29.1.0", "ts-node": "10.9.1", - "typescript": "5.7.3", + "typescript": "5.8.2", "typescript-eslint": "^8.19.0" } } diff --git a/projects/testing-library/package.json b/projects/testing-library/package.json index 0c3abd6..79ad49b 100644 --- a/projects/testing-library/package.json +++ b/projects/testing-library/package.json @@ -29,11 +29,11 @@ "migrations": "./schematics/migrations/migrations.json" }, "peerDependencies": { - "@angular/animations": ">= 17.0.0", - "@angular/common": ">= 17.0.0", - "@angular/platform-browser": ">= 17.0.0", - "@angular/router": ">= 17.0.0", - "@angular/core": ">= 17.0.0", + "@angular/animations": ">= 20.0.0", + "@angular/common": ">= 20.0.0", + "@angular/platform-browser": ">= 20.0.0", + "@angular/router": ">= 20.0.0", + "@angular/core": ">= 20.0.0", "@testing-library/dom": "^10.0.0" }, "dependencies": { diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index f498a89..9c9cabf 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -40,7 +40,6 @@ import { type SubscribedOutput = readonly [key: keyof T, callback: (v: any) => void, subscription: OutputRefSubscription]; const mountedFixtures = new Set>(); -const safeInject = TestBed.inject || TestBed.get; export async function render( component: Type, @@ -126,8 +125,8 @@ export async function render( const componentContainer = createComponentFixture(sut, wrapper); - const zone = safeInject(NgZone); - const router = safeInject(Router); + const zone = TestBed.inject(NgZone); + const router = TestBed.inject(Router); const _navigate = async (elementOrPath: Element | string, basePath = ''): Promise => { const href = typeof elementOrPath === 'string' ? elementOrPath : elementOrPath.getAttribute('href'); const [path, params] = (basePath + href).split('?'); @@ -338,7 +337,7 @@ export async function render( async function createComponent(component: Type): Promise> { /* Make sure angular application is initialized before creating component */ - await safeInject(ApplicationInitStatus).donePromise; + await TestBed.inject(ApplicationInitStatus).donePromise; return TestBed.createComponent(component); } diff --git a/projects/testing-library/tests/defer-blocks.spec.ts b/projects/testing-library/tests/defer-blocks.spec.ts index 7405a4d..ffd5e95 100644 --- a/projects/testing-library/tests/defer-blocks.spec.ts +++ b/projects/testing-library/tests/defer-blocks.spec.ts @@ -33,7 +33,6 @@ test('renders a defer block in different states using DeferBlockBehavior.Playthr deferBlockBehavior: DeferBlockBehavior.Playthrough, }); - expect(await screen.findByText(/loading/i)).toBeInTheDocument(); expect(await screen.findByText(/Defer block content/i)).toBeInTheDocument(); }); From 59f73ae8e8b67e0d47c36bcabc1df0cf6755eb47 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Sun, 8 Jun 2025 19:39:43 +0200 Subject: [PATCH 2/4] feat: remove animations dependency (#531) BREAKING CHANGE: Angular recommends using CSS animations, https://angular.dev/guide/animations/migration Because of the removal of the animations dependency, the `NoopAnimationsModule` is no longer automatically imported in the render function. BEFORE: The `NoopAnimationsModule` is always imported to the render the component. AFTER: Import the `NoopAnimationsModule` in your render configuration where needed: ```ts await render(SutComponent, { imports: [NoopAnimationsModule], }); ``` --- .../app/examples/15-dialog.component.spec.ts | 10 ++++++++-- projects/testing-library/package.json | 1 - projects/testing-library/src/lib/models.ts | 3 +-- .../src/lib/testing-library.ts | 9 +-------- projects/testing-library/tests/render.spec.ts | 20 ------------------- 5 files changed, 10 insertions(+), 33 deletions(-) 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 017afdc..df172be 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,4 +1,5 @@ import { MatDialogRef } from '@angular/material/dialog'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { render, screen } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; @@ -9,6 +10,7 @@ test('dialog closes', async () => { const closeFn = jest.fn(); await render(DialogContentComponent, { + imports: [NoopAnimationsModule], providers: [ { provide: MatDialogRef, @@ -28,7 +30,9 @@ test('dialog closes', async () => { test('closes the dialog via the backdrop', async () => { const user = userEvent.setup(); - await render(DialogComponent); + await render(DialogComponent, { + imports: [NoopAnimationsModule], + }); const openDialogButton = await screen.findByRole('button', { name: /open dialog/i }); await user.click(openDialogButton); @@ -50,7 +54,9 @@ test('closes the dialog via the backdrop', async () => { test('opens and closes the dialog with buttons', async () => { const user = userEvent.setup(); - await render(DialogComponent); + await render(DialogComponent, { + imports: [NoopAnimationsModule], + }); const openDialogButton = await screen.findByRole('button', { name: /open dialog/i }); await user.click(openDialogButton); diff --git a/projects/testing-library/package.json b/projects/testing-library/package.json index 79ad49b..6ea1a38 100644 --- a/projects/testing-library/package.json +++ b/projects/testing-library/package.json @@ -29,7 +29,6 @@ "migrations": "./schematics/migrations/migrations.json" }, "peerDependencies": { - "@angular/animations": ">= 20.0.0", "@angular/common": ">= 20.0.0", "@angular/platform-browser": ">= 20.0.0", "@angular/router": ">= 20.0.0", diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 47ea5bb..33cd71a 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -178,12 +178,11 @@ export interface RenderComponentOptions( sut: Type | string, { imports = [], routes }: Pick, 'imports' | 'routes'>, ) { - const animations = () => { - const animationIsDefined = - imports.indexOf(NoopAnimationsModule) > -1 || imports.indexOf(BrowserAnimationsModule) > -1; - return animationIsDefined ? [] : [NoopAnimationsModule]; - }; - const routing = () => (routes ? [RouterTestingModule.withRoutes(routes)] : []); const components = () => (typeof sut !== 'string' && isStandalone(sut) ? [sut] : []); - return [...imports, ...components(), ...animations(), ...routing()]; + return [...imports, ...components(), ...routing()]; } async function renderDeferBlock( diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index dc54ac5..a93da90 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -17,7 +17,6 @@ import { model, } from '@angular/core'; import { outputFromObservable } from '@angular/core/rxjs-interop'; -import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TestBed } from '@angular/core/testing'; import { render, fireEvent, screen, OutputRefKeysWithCallback, aliasedInput } from '../src/public_api'; import { ActivatedRoute, Resolve, RouterModule } from '@angular/router'; @@ -331,25 +330,6 @@ describe('excludeComponentDeclaration', () => { }); }); -describe('animationModule', () => { - it('adds NoopAnimationsModule by default', async () => { - await render(FixtureComponent); - const noopAnimationsModule = TestBed.inject(NoopAnimationsModule); - expect(noopAnimationsModule).toBeDefined(); - }); - - it('does not add NoopAnimationsModule if BrowserAnimationsModule is an import', async () => { - await render(FixtureComponent, { - imports: [BrowserAnimationsModule], - }); - - const browserAnimationsModule = TestBed.inject(BrowserAnimationsModule); - expect(browserAnimationsModule).toBeDefined(); - - expect(() => TestBed.inject(NoopAnimationsModule)).toThrow(); - }); -}); - describe('Angular component life-cycle hooks', () => { @Component({ selector: 'atl-fixture', From a5ad0c847014419035ddfbff67611c163e338681 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Sat, 14 Jun 2025 16:13:07 +0200 Subject: [PATCH 3/4] feat: add config for testing with zoneless Angular (#532) --- projects/testing-library/src/lib/config.ts | 4 +--- projects/testing-library/src/lib/models.ts | 7 +++++++ projects/testing-library/src/lib/testing-library.ts | 12 ++++++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/projects/testing-library/src/lib/config.ts b/projects/testing-library/src/lib/config.ts index bd8ee9b..cafa7b0 100644 --- a/projects/testing-library/src/lib/config.ts +++ b/projects/testing-library/src/lib/config.ts @@ -3,16 +3,14 @@ import { Config } from './models'; let config: Config = { dom: {}, defaultImports: [], + zoneless: false, }; export function configure(newConfig: Partial | ((config: Partial) => Partial)) { if (typeof newConfig === 'function') { - // Pass the existing config out to the provided function - // and accept a delta in return newConfig = newConfig(config); } - // Merge the incoming config delta config = { ...config, ...newConfig, diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 33cd71a..c095a5e 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -497,4 +497,11 @@ export interface Config extends Pick, 'excludeCompon * Imports that are added to the imports */ defaultImports: any[]; + /** + * Set to `true` to use zoneless change detection. + * This automatically adds `provideZonelessChangeDetection` to the default imports. + * + * @default false + */ + zoneless?: boolean; } diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index b7888d2..5035220 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -10,6 +10,7 @@ import { SimpleChanges, Type, isStandalone, + provideZonelessChangeDetection, } from '@angular/core'; import { ComponentFixture, DeferBlockBehavior, DeferBlockState, TestBed, tick } from '@angular/core/testing'; import { NavigationExtras, Router } from '@angular/router'; @@ -78,6 +79,7 @@ export async function render( initialRoute = '', deferBlockStates = undefined, deferBlockBehavior = undefined, + zoneless = false, configureTestBed = () => { /* noop*/ }, @@ -105,6 +107,7 @@ export async function render( imports: addAutoImports(sut, { imports: imports.concat(defaultImports), routes, + zoneless, }), providers: [...providers], schemas: [...schemas], @@ -510,11 +513,16 @@ function addAutoDeclarations( function addAutoImports( sut: Type | string, - { imports = [], routes }: Pick, 'imports' | 'routes'>, + { + imports = [], + routes, + zoneless, + }: Pick, 'imports' | 'routes'> & Pick, ) { const routing = () => (routes ? [RouterTestingModule.withRoutes(routes)] : []); const components = () => (typeof sut !== 'string' && isStandalone(sut) ? [sut] : []); - return [...imports, ...components(), ...routing()]; + const provideZoneless = () => (zoneless ? [provideZonelessChangeDetection()] : []); + return [...imports, ...components(), ...routing(), ...provideZoneless()]; } async function renderDeferBlock( From e2295c6a10e59e7a92f435ba039c4ecf5ef5b974 Mon Sep 17 00:00:00 2001 From: Christian24 Date: Sat, 28 Jun 2025 20:43:00 +0200 Subject: [PATCH 4/4] feat: add stronger types (#413) --- projects/testing-library/src/lib/models.ts | 25 +++++++++++++------ .../src/lib/testing-library.ts | 5 ++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index c095a5e..656b266 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -1,4 +1,13 @@ -import { Type, DebugElement, EventEmitter, Signal, InputSignalWithTransform } from '@angular/core'; +import { + Type, + DebugElement, + ModuleWithProviders, + EventEmitter, + EnvironmentProviders, + Provider, + Signal, + InputSignalWithTransform, +} from '@angular/core'; import { ComponentFixture, DeferBlockBehavior, DeferBlockState, TestBed } from '@angular/core/testing'; import { Routes } from '@angular/router'; import { BoundFunction, Queries, queries, Config as dtlConfig, PrettyDOMOptions } from '@testing-library/dom'; @@ -153,7 +162,7 @@ export interface RenderComponentOptions | unknown[])[]; /** * @description * A collection of providers needed to render the component via Dependency Injection, for example, injectable services or tokens. @@ -174,7 +183,7 @@ export interface RenderComponentOptions | ModuleWithProviders)[]; /** * @description * A collection of schemas needed to render the component. @@ -314,7 +323,7 @@ export interface RenderComponentOptions | any[])[]; + componentImports?: (Type | unknown[])[]; /** * @description * Queries to bind. Overrides the default set from DOM Testing Library unless merged. @@ -462,7 +471,7 @@ export interface RenderComponentOptions { component: Type; - providers: any[]; + providers: Provider[]; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -496,7 +505,7 @@ export interface Config extends Pick, 'excludeCompon /** * Imports that are added to the imports */ - defaultImports: any[]; + defaultImports?: (Type | ModuleWithProviders)[]; /** * Set to `true` to use zoneless change detection. * This automatically adds `provideZonelessChangeDetection` to the default imports. diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 5035220..535ef39 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -6,6 +6,7 @@ import { OnChanges, OutputRef, OutputRefSubscription, + Provider, SimpleChange, SimpleChanges, Type, @@ -436,7 +437,7 @@ function overrideComponentImports(sut: Type | string, imports: function overrideChildComponentProviders(componentOverrides: ComponentOverride[]) { if (componentOverrides) { for (const { component, providers } of componentOverrides) { - TestBed.overrideComponent(component, { set: { providers } }); + TestBed.overrideComponent(component, { set: { providers: providers as Provider[] } }); } } } @@ -499,7 +500,7 @@ function addAutoDeclarations( wrapper, }: Pick, 'declarations' | 'excludeComponentDeclaration' | 'wrapper'>, ) { - const nonStandaloneDeclarations = declarations?.filter((d) => !isStandalone(d)); + const nonStandaloneDeclarations = declarations.filter((d) => !isStandalone(d as Type)); if (typeof sut === 'string') { if (wrapper && isStandalone(wrapper)) { return nonStandaloneDeclarations;