From fdcf5fa5c2e30b535439be0766d0b023fce7c35d Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Tue, 3 Dec 2024 10:12:09 +0100 Subject: [PATCH] fix: support Angular 19 (#503) --- .github/workflows/ci.yml | 2 +- package.json | 46 +++++++-------- .../src/lib/testing-library.ts | 15 +++-- projects/testing-library/test-setup.ts | 4 +- projects/testing-library/tests/config.spec.ts | 1 + .../testing-library/tests/find-by.spec.ts | 2 + .../testing-library/tests/integration.spec.ts | 38 +++++++------ .../tests/issues/issue-230.spec.ts | 4 +- .../tests/issues/issue-280.spec.ts | 9 +-- .../tests/render-template.spec.ts | 18 +++--- projects/testing-library/tests/render.spec.ts | 57 ++++++++++--------- .../wait-for-element-to-be-removed.spec.ts | 2 + 12 files changed, 111 insertions(+), 87 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28aac0d0..5820814f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: install - run: npm install + run: npm install --force - name: build run: npm run build -- --skip-nx-cache - name: test diff --git a/package.json b/package.json index e5030d00..0f8aad87 100644 --- a/package.json +++ b/package.json @@ -27,35 +27,35 @@ "prepare": "git config core.hookspath .githooks" }, "dependencies": { - "@angular/animations": "18.2.13", - "@angular/cdk": "18.2.14", - "@angular/common": "18.2.13", - "@angular/compiler": "18.2.13", - "@angular/core": "18.2.13", - "@angular/material": "18.2.14", - "@angular/platform-browser": "18.2.13", - "@angular/platform-browser-dynamic": "18.2.13", - "@angular/router": "18.2.13", - "@ngrx/store": "18.0.2", + "@angular/animations": "19.0.1", + "@angular/cdk": "19.0.1", + "@angular/common": "19.0.1", + "@angular/compiler": "19.0.1", + "@angular/core": "19.0.1", + "@angular/material": "19.0.1", + "@angular/platform-browser": "19.0.1", + "@angular/platform-browser-dynamic": "19.0.1", + "@angular/router": "19.0.1", + "@ngrx/store": "19.0.0-beta.0", "@nx/angular": "20.1.3", - "@testing-library/dom": "^10.0.0", + "@testing-library/dom": "^10.4.0", "rxjs": "7.8.0", "tslib": "~2.3.1", - "zone.js": "0.14.10" + "zone.js": "^0.15.0" }, "devDependencies": { - "@angular-devkit/build-angular": "18.2.9", - "@angular-devkit/core": "18.2.9", - "@angular-devkit/schematics": "18.2.9", + "@angular-devkit/build-angular": "19.0.1", + "@angular-devkit/core": "19.0.1", + "@angular-devkit/schematics": "19.0.1", "@angular-eslint/builder": "18.3.0", "@angular-eslint/eslint-plugin": "18.0.1", "@angular-eslint/eslint-plugin-template": "18.0.1", "@angular-eslint/schematics": "18.3.0", "@angular-eslint/template-parser": "18.0.1", - "@angular/cli": "~18.2.0", - "@angular/compiler-cli": "18.2.13", - "@angular/forms": "18.2.13", - "@angular/language-service": "18.2.13", + "@angular/cli": "19.0.1", + "@angular/compiler-cli": "19.0.1", + "@angular/forms": "19.0.1", + "@angular/language-service": "19.0.1", "@nx/eslint": "20.1.3", "@nx/eslint-plugin": "20.1.3", "@nx/jest": "20.1.3", @@ -68,7 +68,7 @@ "@testing-library/user-event": "^14.4.3", "@types/jasmine": "4.3.1", "@types/jest": "29.5.14", - "@types/node": "18.16.9", + "@types/node": "22.10.1", "@types/testing-library__jasmine-dom": "^1.3.0", "@typescript-eslint/eslint-plugin": "7.16.0", "@typescript-eslint/parser": "7.16.0", @@ -86,7 +86,7 @@ "jasmine-spec-reporter": "7.0.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "jest-preset-angular": "14.1.0", + "jest-preset-angular": "14.4.1", "karma": "6.4.0", "karma-chrome-launcher": "^3.1.0", "karma-coverage": "^2.2.1", @@ -94,7 +94,7 @@ "karma-jasmine-html-reporter": "2.0.0", "lint-staged": "^12.1.6", "ng-mocks": "^14.11.0", - "ng-packagr": "18.2.1", + "ng-packagr": "19.0.1", "nx": "20.1.3", "postcss": "^8.4.5", "postcss-import": "14.1.0", @@ -105,6 +105,6 @@ "semantic-release": "^18.0.0", "ts-jest": "29.1.0", "ts-node": "10.9.1", - "typescript": "5.5.4" + "typescript": "5.6.2" } } diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 8dfa946e..7c7de892 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -34,6 +34,7 @@ import { RenderComponentOptions, RenderResult, RenderTemplateOptions, + Config, } from './models'; type SubscribedOutput = readonly [key: keyof T, callback: (v: any) => void, subscription: OutputRefSubscription]; @@ -82,7 +83,9 @@ export async function render( configureTestBed = () => { /* noop*/ }, - } = { ...globalConfig, ...renderOptions }; + } = { ...globalConfig, ...renderOptions } as RenderComponentOptions & + RenderTemplateOptions & + Config; dtlConfigure({ eventWrapper: (cb) => { @@ -228,7 +231,7 @@ export async function render( return createdFixture; }; - const fixture = await renderFixture(componentProperties, allInputs, componentOutputs, on); + const fixture = await renderFixture(componentProperties, allInputs as any, componentOutputs, on); if (deferBlockStates) { if (Array.isArray(deferBlockStates)) { @@ -494,12 +497,16 @@ function addAutoDeclarations( wrapper, }: Pick, 'declarations' | 'excludeComponentDeclaration' | 'wrapper'>, ) { + const nonStandaloneDeclarations = declarations?.filter((d) => !isStandalone(d)); if (typeof sut === 'string') { - return [...declarations, wrapper]; + if (wrapper && isStandalone(wrapper)) { + return nonStandaloneDeclarations; + } + return [...nonStandaloneDeclarations, wrapper]; } const components = () => (excludeComponentDeclaration || isStandalone(sut) ? [] : [sut]); - return [...declarations, ...components()]; + return [...nonStandaloneDeclarations, ...components()]; } function addAutoImports( diff --git a/projects/testing-library/test-setup.ts b/projects/testing-library/test-setup.ts index 600d0857..8d79c74b 100644 --- a/projects/testing-library/test-setup.ts +++ b/projects/testing-library/test-setup.ts @@ -1,6 +1,8 @@ -import 'jest-preset-angular/setup-jest'; +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; import '@testing-library/jest-dom'; import { TextEncoder, TextDecoder } from 'util'; +setupZoneTestEnv(); + // eslint-disable-next-line @typescript-eslint/naming-convention Object.assign(global, { TextDecoder, TextEncoder }); diff --git a/projects/testing-library/tests/config.spec.ts b/projects/testing-library/tests/config.spec.ts index bb8c61fc..041d991a 100644 --- a/projects/testing-library/tests/config.spec.ts +++ b/projects/testing-library/tests/config.spec.ts @@ -13,6 +13,7 @@ import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; `, + standalone: false, }) class FormsComponent { form = this.formBuilder.group({ diff --git a/projects/testing-library/tests/find-by.spec.ts b/projects/testing-library/tests/find-by.spec.ts index 9d499fda..30f11ee3 100644 --- a/projects/testing-library/tests/find-by.spec.ts +++ b/projects/testing-library/tests/find-by.spec.ts @@ -2,10 +2,12 @@ import { Component } from '@angular/core'; import { timer } from 'rxjs'; import { render, screen } from '../src/public_api'; import { mapTo } from 'rxjs/operators'; +import { AsyncPipe } from '@angular/common'; @Component({ selector: 'atl-fixture', template: `
{{ result | async }}
`, + imports: [AsyncPipe], }) class FixtureComponent { result = timer(30).pipe(mapTo('I am visible')); diff --git a/projects/testing-library/tests/integration.spec.ts b/projects/testing-library/tests/integration.spec.ts index eedec0e9..02ca2902 100644 --- a/projects/testing-library/tests/integration.spec.ts +++ b/projects/testing-library/tests/integration.spec.ts @@ -4,6 +4,7 @@ import { of, BehaviorSubject } from 'rxjs'; import { debounceTime, switchMap, map, startWith } from 'rxjs/operators'; import { render, screen, waitFor, waitForElementToBeRemoved, within } from '../src/lib/testing-library'; import userEvent from '@testing-library/user-event'; +import { AsyncPipe, NgForOf } from '@angular/common'; const DEBOUNCE_TIME = 1_000; @@ -21,6 +22,25 @@ class ModalService { } } +@Component({ + selector: 'atl-table', + template: ` + + + + + +
{{ entity.name }} + +
+ `, + imports: [NgForOf], +}) +class TableComponent { + @Input() entities: any[] = []; + @Output() edit = new EventEmitter(); +} + @Component({ template: `

Entities Title

@@ -31,6 +51,7 @@ class ModalService { `, + imports: [TableComponent, AsyncPipe], }) class EntitiesComponent { query = new BehaviorSubject(''); @@ -55,22 +76,6 @@ class EntitiesComponent { } } -@Component({ - selector: 'atl-table', - template: ` - - - - - -
{{ entity.name }}
- `, -}) -class TableComponent { - @Input() entities: any[] = []; - @Output() edit = new EventEmitter(); -} - const entities = [ { id: 1, @@ -91,7 +96,6 @@ async function setup() { const user = userEvent.setup(); await render(EntitiesComponent, { - declarations: [TableComponent], providers: [ { provide: EntitiesService, diff --git a/projects/testing-library/tests/issues/issue-230.spec.ts b/projects/testing-library/tests/issues/issue-230.spec.ts index fe004b62..8df58f66 100644 --- a/projects/testing-library/tests/issues/issue-230.spec.ts +++ b/projects/testing-library/tests/issues/issue-230.spec.ts @@ -1,8 +1,10 @@ import { Component } from '@angular/core'; import { render, waitFor, screen } from '../../src/public_api'; +import { NgClass } from '@angular/common'; @Component({ template: ` `, + imports: [NgClass], }) class LoopComponent { get classes() { @@ -17,7 +19,7 @@ test('wait does not end up in a loop', async () => { await expect( waitFor(() => { - expect(true).toEqual(false); + expect(true).toBe(false); }), ).rejects.toThrow(); }); diff --git a/projects/testing-library/tests/issues/issue-280.spec.ts b/projects/testing-library/tests/issues/issue-280.spec.ts index 19f644ef..5e59534a 100644 --- a/projects/testing-library/tests/issues/issue-280.spec.ts +++ b/projects/testing-library/tests/issues/issue-280.spec.ts @@ -1,19 +1,21 @@ import { Location } from '@angular/common'; import { Component, NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { RouterLink, RouterModule, RouterOutlet, Routes } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import userEvent from '@testing-library/user-event'; import { render, screen } from '../../src/public_api'; @Component({ - template: `
Navigate
+ template: `
Navigate
`, + imports: [RouterOutlet], }) class MainComponent {} @Component({ - template: `
first page
+ template: `
first page
go to second`, + imports: [RouterLink], }) class FirstComponent {} @@ -35,7 +37,6 @@ const routes: Routes = [ ]; @NgModule({ - declarations: [FirstComponent, SecondComponent], imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) diff --git a/projects/testing-library/tests/render-template.spec.ts b/projects/testing-library/tests/render-template.spec.ts index a6892dbc..e185f702 100644 --- a/projects/testing-library/tests/render-template.spec.ts +++ b/projects/testing-library/tests/render-template.spec.ts @@ -45,7 +45,7 @@ class GreetingComponent { test('the directive renders', async () => { const view = await render('
', { - declarations: [OnOffDirective], + imports: [OnOffDirective], }); // eslint-disable-next-line testing-library/no-container @@ -54,7 +54,7 @@ test('the directive renders', async () => { test('the component renders', async () => { const view = await render('', { - declarations: [GreetingComponent], + imports: [GreetingComponent], }); // eslint-disable-next-line testing-library/no-container @@ -64,7 +64,7 @@ test('the component renders', async () => { test('uses the default props', async () => { await render('
', { - declarations: [OnOffDirective], + imports: [OnOffDirective], }); fireEvent.click(screen.getByText('init')); @@ -74,7 +74,7 @@ test('uses the default props', async () => { test('overrides input properties', async () => { await render('
', { - declarations: [OnOffDirective], + imports: [OnOffDirective], }); fireEvent.click(screen.getByText('init')); @@ -85,7 +85,7 @@ test('overrides input properties', async () => { test('overrides input properties via a wrapper', async () => { // `bar` will be set as a property on the wrapper component, the property will be used to pass to the directive await render('
', { - declarations: [OnOffDirective], + imports: [OnOffDirective], componentProperties: { bar: 'hello', }, @@ -100,7 +100,7 @@ test('overrides output properties', async () => { const clicked = jest.fn(); await render('
', { - declarations: [OnOffDirective], + imports: [OnOffDirective], componentProperties: { clicked, }, @@ -116,7 +116,7 @@ test('overrides output properties', async () => { describe('removeAngularAttributes', () => { it('should remove angular attributes', async () => { await render('
', { - declarations: [OnOffDirective], + imports: [OnOffDirective], removeAngularAttributes: true, }); @@ -126,7 +126,7 @@ describe('removeAngularAttributes', () => { it('is disabled by default', async () => { await render('
', { - declarations: [OnOffDirective], + imports: [OnOffDirective], }); expect(document.querySelector('[ng-version]')).not.toBeNull(); @@ -136,7 +136,7 @@ describe('removeAngularAttributes', () => { test('updates properties and invokes change detection', async () => { const view = await render<{ value: string }>('
', { - declarations: [UpdateInputDirective], + imports: [UpdateInputDirective], componentProperties: { value: 'value1', }, diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index 59e0f75b..52d318ca 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -47,33 +47,31 @@ describe('DTL functionality', () => { }); }); -describe('standalone', () => { +describe('components', () => { @Component({ selector: 'atl-fixture', template: ` {{ name }} `, }) - class StandaloneFixtureComponent { + class FixtureWithInputComponent { @Input() name = ''; } - it('renders standalone component', async () => { - await render(StandaloneFixtureComponent, { componentProperties: { name: 'Bob' } }); + it('renders component', async () => { + await render(FixtureWithInputComponent, { componentProperties: { name: 'Bob' } }); expect(screen.getByText('Bob')).toBeInTheDocument(); }); }); -describe('standalone with child', () => { +describe('component with child', () => { @Component({ selector: 'atl-child-fixture', template: `A child fixture`, - standalone: true, }) class ChildFixtureComponent {} @Component({ selector: 'atl-child-fixture', template: `A mock child fixture`, - standalone: true, // eslint-disable-next-line @angular-eslint/no-host-metadata-property, @typescript-eslint/naming-convention host: { 'collision-id': MockChildFixtureComponent.name }, }) @@ -83,18 +81,17 @@ describe('standalone with child', () => { selector: 'atl-parent-fixture', template: `

Parent fixture

`, - standalone: true, imports: [ChildFixtureComponent], }) class ParentFixtureComponent {} - it('renders the standalone component with a mocked child', async () => { + it('renders the 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(); }); - it('renders the standalone component with child', async () => { + it('renders the component with child', async () => { await render(ParentFixtureComponent); expect(screen.getByText('Parent fixture')).toBeInTheDocument(); expect(screen.getByText('A child fixture')).toBeInTheDocument(); @@ -118,7 +115,6 @@ describe('childComponentOverrides', () => { @Component({ selector: 'atl-child-fixture', template: `{{ simpleService.value }}`, - standalone: true, providers: [MySimpleService], }) class NestedChildFixtureComponent { @@ -128,7 +124,6 @@ describe('childComponentOverrides', () => { @Component({ selector: 'atl-parent-fixture', template: ``, - standalone: true, imports: [NestedChildFixtureComponent], }) class ParentFixtureComponent {} @@ -190,22 +185,22 @@ describe('componentOutputs', () => { }); describe('on', () => { - @Component({ template: ``, standalone: true }) + @Component({ template: `` }) class TestFixtureWithEventEmitterComponent { @Output() readonly event = new EventEmitter(); } - @Component({ template: ``, standalone: true }) + @Component({ template: `` }) class TestFixtureWithDerivedEventComponent { @Output() readonly event = fromEvent(inject(ElementRef).nativeElement, 'click'); } - @Component({ template: ``, standalone: true }) + @Component({ template: `` }) class TestFixtureWithFunctionalOutputComponent { readonly event = output(); } - @Component({ template: ``, standalone: true }) + @Component({ template: `` }) class TestFixtureWithFunctionalDerivedEventComponent { readonly event = outputFromObservable(fromEvent(inject(ElementRef).nativeElement, 'click')); } @@ -313,20 +308,31 @@ describe('on', () => { }); }); -describe('animationModule', () => { +describe('excludeComponentDeclaration', () => { + @Component({ + selector: 'atl-fixture', + template: ` + + + `, + standalone: false, + }) + class NotStandaloneFixtureComponent {} + @NgModule({ - declarations: [FixtureComponent], + declarations: [NotStandaloneFixtureComponent], }) class FixtureModule {} - describe('excludeComponentDeclaration', () => { - it('does not throw if component is declared in an imported module', async () => { - await render(FixtureComponent, { - imports: [FixtureModule], - excludeComponentDeclaration: true, - }); + + it('does not throw if component is declared in an imported module', async () => { + await render(NotStandaloneFixtureComponent, { + imports: [FixtureModule], + excludeComponentDeclaration: true, }); }); +}); +describe('animationModule', () => { it('adds NoopAnimationsModule by default', async () => { await render(FixtureComponent); const noopAnimationsModule = TestBed.inject(NoopAnimationsModule); @@ -458,14 +464,12 @@ describe('DebugElement', () => { describe('initialRoute', () => { @Component({ - standalone: true, selector: 'atl-fixture2', template: ``, }) class SecondaryFixtureComponent {} @Component({ - standalone: true, selector: 'atl-router-fixture', template: ``, imports: [RouterModule], @@ -502,7 +506,6 @@ describe('initialRoute', () => { it('allows initially rendering a specific route with query parameters', async () => { @Component({ - standalone: true, selector: 'atl-query-param-fixture', template: `

paramPresent$: {{ paramPresent$ | async }}

`, imports: [NgIf, AsyncPipe], diff --git a/projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts b/projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts index 5c16a539..64d6c356 100644 --- a/projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts +++ b/projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts @@ -1,10 +1,12 @@ import { Component, OnInit } from '@angular/core'; import { render, screen, waitForElementToBeRemoved } from '../src/public_api'; import { timer } from 'rxjs'; +import { NgIf } from '@angular/common'; @Component({ selector: 'atl-fixture', template: `
👋
`, + imports: [NgIf], }) class FixtureComponent implements OnInit { visible = true;