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
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/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",
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 {}