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/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/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..6ea1a38 100644 --- a/projects/testing-library/package.json +++ b/projects/testing-library/package.json @@ -29,11 +29,10 @@ "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/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/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 47ea5bb..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,16 +183,15 @@ export interface RenderComponentOptions | ModuleWithProviders)[]; /** * @description * A collection of schemas needed to render the component. @@ -315,7 +323,7 @@ export interface RenderComponentOptions | any[])[]; + componentImports?: (Type | unknown[])[]; /** * @description * Queries to bind. Overrides the default set from DOM Testing Library unless merged. @@ -463,7 +471,7 @@ export interface RenderComponentOptions { component: Type; - providers: any[]; + providers: Provider[]; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -497,5 +505,12 @@ 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. + * + * @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 f498a89..535ef39 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -6,13 +6,14 @@ import { OnChanges, OutputRef, OutputRefSubscription, + Provider, SimpleChange, SimpleChanges, Type, isStandalone, + provideZonelessChangeDetection, } from '@angular/core'; import { ComponentFixture, DeferBlockBehavior, DeferBlockState, TestBed, tick } from '@angular/core/testing'; -import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NavigationExtras, Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import type { BoundFunctions, Queries } from '@testing-library/dom'; @@ -40,7 +41,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, @@ -80,6 +80,7 @@ export async function render( initialRoute = '', deferBlockStates = undefined, deferBlockBehavior = undefined, + zoneless = false, configureTestBed = () => { /* noop*/ }, @@ -107,6 +108,7 @@ export async function render( imports: addAutoImports(sut, { imports: imports.concat(defaultImports), routes, + zoneless, }), providers: [...providers], schemas: [...schemas], @@ -126,8 +128,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 +340,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); } @@ -435,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[] } }); } } } @@ -498,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; @@ -512,17 +514,16 @@ function addAutoDeclarations( function addAutoImports( sut: Type | string, - { imports = [], routes }: Pick, 'imports' | 'routes'>, + { + imports = [], + routes, + zoneless, + }: Pick, 'imports' | 'routes'> & Pick, ) { - 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()]; + const provideZoneless = () => (zoneless ? [provideZonelessChangeDetection()] : []); + return [...imports, ...components(), ...routing(), ...provideZoneless()]; } async function renderDeferBlock( 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(); }); 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',