From 55cf34e61d66e239c751f43172bf231393d2c2d8 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:42:26 +0100 Subject: [PATCH 1/3] feat: add methods to test deferrable views --- .../examples/21-deferable-view.component.ts | 25 ++++++ .../app/examples/21-deferable-view.spec.ts | 23 +++++ projects/testing-library/src/lib/models.ts | 9 +- .../src/lib/testing-library.ts | 83 +++++++++++++------ .../tests/defer-blocks.spec.ts | 67 +++++++++++++++ 5 files changed, 182 insertions(+), 25 deletions(-) create mode 100644 apps/example-app/src/app/examples/21-deferable-view.component.ts create mode 100644 apps/example-app/src/app/examples/21-deferable-view.spec.ts create mode 100644 projects/testing-library/tests/defer-blocks.spec.ts diff --git a/apps/example-app/src/app/examples/21-deferable-view.component.ts b/apps/example-app/src/app/examples/21-deferable-view.component.ts new file mode 100644 index 0000000..ce47a58 --- /dev/null +++ b/apps/example-app/src/app/examples/21-deferable-view.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-deferable-view-child', + template: `

Hello from deferred child component

`, + standalone: true, +}) +export class DeferableViewChildComponent {} + +@Component({ + template: ` + @defer (on timer(2s)) { + + } @placeholder { +

Hello from placeholder

+ } @loading { +

Hello from loading

+ } @error { +

Hello from error

+ } + `, + imports: [DeferableViewChildComponent], + standalone: true, +}) +export class DeferableViewComponent {} diff --git a/apps/example-app/src/app/examples/21-deferable-view.spec.ts b/apps/example-app/src/app/examples/21-deferable-view.spec.ts new file mode 100644 index 0000000..8495387 --- /dev/null +++ b/apps/example-app/src/app/examples/21-deferable-view.spec.ts @@ -0,0 +1,23 @@ +import { render, screen } from '@testing-library/angular'; +import { DeferBlockState } from '@angular/core/testing'; +import { DeferableViewComponent } from './21-deferable-view.component'; + +test('renders deferred views based on state', async () => { + const { renderDeferBlock } = await render(DeferableViewComponent); + + expect(screen.getByText(/Hello from placeholder/i)).toBeInTheDocument(); + + await renderDeferBlock(DeferBlockState.Loading); + expect(screen.getByText(/Hello from loading/i)).toBeInTheDocument(); + + await renderDeferBlock(DeferBlockState.Complete); + expect(screen.getByText(/Hello from deferred child component/i)).toBeInTheDocument(); +}); + +test('initially renders deferred views based on given state', async () => { + await render(DeferableViewComponent, { + deferBlockStates: DeferBlockState.Error, + }); + + expect(screen.getByText(/Hello from error/i)).toBeInTheDocument(); +}); diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 2e198a6..f148ee2 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -1,5 +1,5 @@ import { Type, DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, DeferBlockState, TestBed } from '@angular/core/testing'; import { Routes } from '@angular/router'; import { BoundFunction, Queries, queries, Config as dtlConfig, PrettyDOMOptions } from '@testing-library/dom'; @@ -63,6 +63,11 @@ export interface RenderResult extend 'componentProperties' | 'componentInputs' | 'componentOutputs' | 'detectChangesOnRender' >, ) => Promise; + /** + * @description + * Set the state of a deferrable block. + */ + renderDeferBlock: (deferBlockState: DeferBlockState, deferBlockIndex?: number) => Promise; } export interface RenderComponentOptions { @@ -363,6 +368,8 @@ export interface RenderComponentOptions void; + + deferBlockStates?: DeferBlockState | { deferBlockState: DeferBlockState; deferBlockIndex: number }[]; } export interface ComponentOverride { diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 65932fc..48128cc 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -9,7 +9,7 @@ import { ApplicationInitStatus, isStandalone, } from '@angular/core'; -import { ComponentFixture, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, 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'; @@ -65,6 +65,7 @@ export async function render( removeAngularAttributes = false, defaultImports = [], initialRoute = '', + deferBlockStates = undefined, configureTestBed = () => { /* noop*/ }, @@ -160,10 +161,19 @@ export async function render( } } - let fixture: ComponentFixture; let detectChanges: () => void; - await renderFixture(componentProperties, componentInputs, componentOutputs); + const fixture = await renderFixture(componentProperties, componentInputs, componentOutputs); + + if (deferBlockStates) { + if (Array.isArray(deferBlockStates)) { + for (const deferBlockState of deferBlockStates) { + await renderDeferBlock(fixture, deferBlockState.deferBlockState, deferBlockState.deferBlockIndex); + } + } else { + await renderDeferBlock(fixture, deferBlockStates); + } + } let renderedPropKeys = Object.keys(componentProperties); let renderedInputKeys = Object.keys(componentInputs); @@ -210,60 +220,61 @@ export async function render( }; return { - // @ts-ignore: fixture assigned fixture, detectChanges: () => detectChanges(), navigate, rerender, - // @ts-ignore: fixture assigned + renderDeferBlock: async (deferBlockState: DeferBlockState, deferBlockIndex?: number) => { + await renderDeferBlock(fixture, deferBlockState, deferBlockIndex); + }, debugElement: fixture.debugElement, - // @ts-ignore: fixture assigned container: fixture.nativeElement, debug: (element = fixture.nativeElement, maxLength, options) => Array.isArray(element) ? element.forEach((e) => console.log(dtlPrettyDOM(e, maxLength, options))) : console.log(dtlPrettyDOM(element, maxLength, options)), - // @ts-ignore: fixture assigned ...replaceFindWithFindAndDetectChanges(dtlGetQueriesForElement(fixture.nativeElement, queries)), }; - async function renderFixture(properties: Partial, inputs: Partial, outputs: Partial) { - if (fixture) { - cleanupAtFixture(fixture); - } - - fixture = await createComponent(componentContainer); - setComponentProperties(fixture, properties); - setComponentInputs(fixture, inputs); - setComponentOutputs(fixture, outputs); + async function renderFixture( + properties: Partial, + inputs: Partial, + outputs: Partial, + ): Promise> { + const createdFixture = await createComponent(componentContainer); + setComponentProperties(createdFixture, properties); + setComponentInputs(createdFixture, inputs); + setComponentOutputs(createdFixture, outputs); if (removeAngularAttributes) { - fixture.nativeElement.removeAttribute('ng-version'); - const idAttribute = fixture.nativeElement.getAttribute('id'); + createdFixture.nativeElement.removeAttribute('ng-version'); + const idAttribute = createdFixture.nativeElement.getAttribute('id'); if (idAttribute && idAttribute.startsWith('root')) { - fixture.nativeElement.removeAttribute('id'); + createdFixture.nativeElement.removeAttribute('id'); } } - mountedFixtures.add(fixture); + mountedFixtures.add(createdFixture); let isAlive = true; - fixture.componentRef.onDestroy(() => (isAlive = false)); + createdFixture.componentRef.onDestroy(() => (isAlive = false)); - if (hasOnChangesHook(fixture.componentInstance) && Object.keys(properties).length > 0) { + if (hasOnChangesHook(createdFixture.componentInstance) && Object.keys(properties).length > 0) { const changes = getChangesObj(null, componentProperties); - fixture.componentInstance.ngOnChanges(changes); + createdFixture.componentInstance.ngOnChanges(changes); } detectChanges = () => { if (isAlive) { - fixture.detectChanges(); + createdFixture.detectChanges(); } }; if (detectChangesOnRender) { detectChanges(); } + + return createdFixture; } } @@ -429,6 +440,30 @@ function addAutoImports( return [...imports, ...components(), ...animations(), ...routing()]; } +async function renderDeferBlock( + fixture: ComponentFixture, + deferBlockState: DeferBlockState, + deferBlockIndex?: number, +) { + const deferBlockFixtures = await fixture.getDeferBlocks(); + + if (deferBlockIndex !== undefined) { + if (deferBlockIndex < 0) { + throw new Error('deferBlockIndex must be a positive number'); + } + + const deferBlockFixture = deferBlockFixtures[deferBlockIndex]; + if (!deferBlockFixture) { + throw new Error(`Could not find a deferrable block with index '${deferBlockIndex}'`); + } + await deferBlockFixture.render(deferBlockState); + } else { + for (const deferBlockFixture of deferBlockFixtures) { + await deferBlockFixture.render(deferBlockState); + } + } +} + /** * Wrap waitFor to invoke the Angular change detection cycle before invoking the callback */ diff --git a/projects/testing-library/tests/defer-blocks.spec.ts b/projects/testing-library/tests/defer-blocks.spec.ts new file mode 100644 index 0000000..9c2985b --- /dev/null +++ b/projects/testing-library/tests/defer-blocks.spec.ts @@ -0,0 +1,67 @@ +import { Component } from '@angular/core'; +import { DeferBlockState } from '@angular/core/testing'; +import { render, screen } from '../src/public_api'; + +test('renders a defer block in different states using the official API', async () => { + const { fixture } = await render(FixtureComponent); + + const deferBlockFixture = (await fixture.getDeferBlocks())[0]; + + await deferBlockFixture.render(DeferBlockState.Loading); + expect(screen.getByText(/loading/i)).toBeInTheDocument(); + expect(screen.queryByText(/Defer block content/i)).not.toBeInTheDocument(); + + await deferBlockFixture.render(DeferBlockState.Complete); + expect(screen.getByText(/Defer block content/i)).toBeInTheDocument(); + expect(screen.queryByText(/load/i)).not.toBeInTheDocument(); +}); + +test('renders a defer block in different states using ATL', async () => { + const { renderDeferBlock } = await render(FixtureComponent); + + await renderDeferBlock(DeferBlockState.Loading); + expect(screen.getByText(/loading/i)).toBeInTheDocument(); + expect(screen.queryByText(/Defer block content/i)).not.toBeInTheDocument(); + + await renderDeferBlock(DeferBlockState.Complete, 0); + expect(screen.getByText(/Defer block content/i)).toBeInTheDocument(); + expect(screen.queryByText(/load/i)).not.toBeInTheDocument(); +}); + +test('renders a defer block initially in the loading state', async () => { + await render(FixtureComponent, { + deferBlockStates: DeferBlockState.Loading, + }); + + expect(screen.getByText(/loading/i)).toBeInTheDocument(); + expect(screen.queryByText(/Defer block content/i)).not.toBeInTheDocument(); +}); + +test('renders a defer block initially in the complete state', async () => { + await render(FixtureComponent, { + deferBlockStates: DeferBlockState.Complete, + }); + + expect(screen.getByText(/Defer block content/i)).toBeInTheDocument(); + expect(screen.queryByText(/load/i)).not.toBeInTheDocument(); +}); + +test('renders a defer block in an initial state using the array syntax', async () => { + await render(FixtureComponent, { + deferBlockStates: [{ deferBlockState: DeferBlockState.Complete, deferBlockIndex: 0 }], + }); + + expect(screen.getByText(/Defer block content/i)).toBeInTheDocument(); + expect(screen.queryByText(/load/i)).not.toBeInTheDocument(); +}); + +@Component({ + template: ` + @defer { +

Defer block content

+ } @loading { +

Loading...

+ } + `, +}) +class FixtureComponent {} From ccadc18ec5ef9c8d9109f249dc0c478e760751c0 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Tue, 28 Nov 2023 13:00:34 +0100 Subject: [PATCH 2/3] docs: update table --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index de454b1..3aa070f 100644 --- a/README.md +++ b/README.md @@ -164,11 +164,11 @@ You may also be interested in installing `jest-dom` so you can use | Angular | Angular Testing Library | | ------- | ----------------------- | -| 17.x | 13.x, 14.x | -| 16.x | 13.x, 14.x | -| >= 15.1 | 13.x \|\| 14.x | -| < 15.1 | 11.x \|\| 12.x | -| 14.x | 11.x \|\| 12.x | +| 17.x | 15.x, 14.x, 13.x | +| 16.x | 14.x, 13.x | +| >= 15.1 | 14.x, 13.x | +| < 15.1 | 12.x, 11.x | +| 14.x | 12.x, 11.x | ## Guiding Principles From 97e2d56a37e5f6b199750ad6331ad4a906000fb4 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Tue, 28 Nov 2023 13:02:45 +0100 Subject: [PATCH 3/3] update angular peer dependency version --- 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 ac921f4..7ebb6e7 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": ">= 15.1.0 || >= 16.0.0 || >= 17.0.0", - "@angular/platform-browser": ">= 15.1.0 || >= 16.0.0 || >= 17.0.0", - "@angular/router": ">= 15.1.0 || >= 16.0.0 || >= 17.0.0", - "@angular/core": ">= 15.1.0 || >= 16.0.0 || >= 17.0.0" + "@angular/common": ">= 17.0.0", + "@angular/platform-browser": ">= 17.0.0", + "@angular/router": ">= 17.0.0", + "@angular/core": ">= 17.0.0" }, "dependencies": { "@testing-library/dom": "^9.0.0",