Skip to content

Commit 40661ea

Browse files
feat: add API to test deferrable views (#418)
1 parent 523b8fd commit 40661ea

File tree

5 files changed

+182
-25
lines changed

5 files changed

+182
-25
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Component } from '@angular/core';
2+
3+
@Component({
4+
selector: 'app-deferable-view-child',
5+
template: ` <p>Hello from deferred child component</p> `,
6+
standalone: true,
7+
})
8+
export class DeferableViewChildComponent {}
9+
10+
@Component({
11+
template: `
12+
@defer (on timer(2s)) {
13+
<app-deferable-view-child />
14+
} @placeholder {
15+
<p>Hello from placeholder</p>
16+
} @loading {
17+
<p>Hello from loading</p>
18+
} @error {
19+
<p>Hello from error</p>
20+
}
21+
`,
22+
imports: [DeferableViewChildComponent],
23+
standalone: true,
24+
})
25+
export class DeferableViewComponent {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { render, screen } from '@testing-library/angular';
2+
import { DeferBlockState } from '@angular/core/testing';
3+
import { DeferableViewComponent } from './21-deferable-view.component';
4+
5+
test('renders deferred views based on state', async () => {
6+
const { renderDeferBlock } = await render(DeferableViewComponent);
7+
8+
expect(screen.getByText(/Hello from placeholder/i)).toBeInTheDocument();
9+
10+
await renderDeferBlock(DeferBlockState.Loading);
11+
expect(screen.getByText(/Hello from loading/i)).toBeInTheDocument();
12+
13+
await renderDeferBlock(DeferBlockState.Complete);
14+
expect(screen.getByText(/Hello from deferred child component/i)).toBeInTheDocument();
15+
});
16+
17+
test('initially renders deferred views based on given state', async () => {
18+
await render(DeferableViewComponent, {
19+
deferBlockStates: DeferBlockState.Error,
20+
});
21+
22+
expect(screen.getByText(/Hello from error/i)).toBeInTheDocument();
23+
});

projects/testing-library/src/lib/models.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Type, DebugElement } from '@angular/core';
2-
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { ComponentFixture, DeferBlockState, TestBed } from '@angular/core/testing';
33
import { Routes } from '@angular/router';
44
import { BoundFunction, Queries, queries, Config as dtlConfig, PrettyDOMOptions } from '@testing-library/dom';
55

@@ -63,6 +63,11 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType> extend
6363
'componentProperties' | 'componentInputs' | 'componentOutputs' | 'detectChangesOnRender'
6464
>,
6565
) => Promise<void>;
66+
/**
67+
* @description
68+
* Set the state of a deferrable block.
69+
*/
70+
renderDeferBlock: (deferBlockState: DeferBlockState, deferBlockIndex?: number) => Promise<void>;
6671
}
6772

6873
export interface RenderComponentOptions<ComponentType, Q extends Queries = typeof queries> {
@@ -363,6 +368,8 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
363368
* })
364369
*/
365370
configureTestBed?: (testbed: TestBed) => void;
371+
372+
deferBlockStates?: DeferBlockState | { deferBlockState: DeferBlockState; deferBlockIndex: number }[];
366373
}
367374

368375
export interface ComponentOverride<T> {

projects/testing-library/src/lib/testing-library.ts

+59-24
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
ApplicationInitStatus,
1010
isStandalone,
1111
} from '@angular/core';
12-
import { ComponentFixture, TestBed, tick } from '@angular/core/testing';
12+
import { ComponentFixture, DeferBlockState, TestBed, tick } from '@angular/core/testing';
1313
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
1414
import { NavigationExtras, Router } from '@angular/router';
1515
import { RouterTestingModule } from '@angular/router/testing';
@@ -65,6 +65,7 @@ export async function render<SutType, WrapperType = SutType>(
6565
removeAngularAttributes = false,
6666
defaultImports = [],
6767
initialRoute = '',
68+
deferBlockStates = undefined,
6869
configureTestBed = () => {
6970
/* noop*/
7071
},
@@ -160,10 +161,19 @@ export async function render<SutType, WrapperType = SutType>(
160161
}
161162
}
162163

163-
let fixture: ComponentFixture<SutType>;
164164
let detectChanges: () => void;
165165

166-
await renderFixture(componentProperties, componentInputs, componentOutputs);
166+
const fixture = await renderFixture(componentProperties, componentInputs, componentOutputs);
167+
168+
if (deferBlockStates) {
169+
if (Array.isArray(deferBlockStates)) {
170+
for (const deferBlockState of deferBlockStates) {
171+
await renderDeferBlock(fixture, deferBlockState.deferBlockState, deferBlockState.deferBlockIndex);
172+
}
173+
} else {
174+
await renderDeferBlock(fixture, deferBlockStates);
175+
}
176+
}
167177

168178
let renderedPropKeys = Object.keys(componentProperties);
169179
let renderedInputKeys = Object.keys(componentInputs);
@@ -210,60 +220,61 @@ export async function render<SutType, WrapperType = SutType>(
210220
};
211221

212222
return {
213-
// @ts-ignore: fixture assigned
214223
fixture,
215224
detectChanges: () => detectChanges(),
216225
navigate,
217226
rerender,
218-
// @ts-ignore: fixture assigned
227+
renderDeferBlock: async (deferBlockState: DeferBlockState, deferBlockIndex?: number) => {
228+
await renderDeferBlock(fixture, deferBlockState, deferBlockIndex);
229+
},
219230
debugElement: fixture.debugElement,
220-
// @ts-ignore: fixture assigned
221231
container: fixture.nativeElement,
222232
debug: (element = fixture.nativeElement, maxLength, options) =>
223233
Array.isArray(element)
224234
? element.forEach((e) => console.log(dtlPrettyDOM(e, maxLength, options)))
225235
: console.log(dtlPrettyDOM(element, maxLength, options)),
226-
// @ts-ignore: fixture assigned
227236
...replaceFindWithFindAndDetectChanges(dtlGetQueriesForElement(fixture.nativeElement, queries)),
228237
};
229238

230-
async function renderFixture(properties: Partial<SutType>, inputs: Partial<SutType>, outputs: Partial<SutType>) {
231-
if (fixture) {
232-
cleanupAtFixture(fixture);
233-
}
234-
235-
fixture = await createComponent(componentContainer);
236-
setComponentProperties(fixture, properties);
237-
setComponentInputs(fixture, inputs);
238-
setComponentOutputs(fixture, outputs);
239+
async function renderFixture(
240+
properties: Partial<SutType>,
241+
inputs: Partial<SutType>,
242+
outputs: Partial<SutType>,
243+
): Promise<ComponentFixture<SutType>> {
244+
const createdFixture = await createComponent(componentContainer);
245+
setComponentProperties(createdFixture, properties);
246+
setComponentInputs(createdFixture, inputs);
247+
setComponentOutputs(createdFixture, outputs);
239248

240249
if (removeAngularAttributes) {
241-
fixture.nativeElement.removeAttribute('ng-version');
242-
const idAttribute = fixture.nativeElement.getAttribute('id');
250+
createdFixture.nativeElement.removeAttribute('ng-version');
251+
const idAttribute = createdFixture.nativeElement.getAttribute('id');
243252
if (idAttribute && idAttribute.startsWith('root')) {
244-
fixture.nativeElement.removeAttribute('id');
253+
createdFixture.nativeElement.removeAttribute('id');
245254
}
246255
}
247256

248-
mountedFixtures.add(fixture);
257+
mountedFixtures.add(createdFixture);
249258

250259
let isAlive = true;
251-
fixture.componentRef.onDestroy(() => (isAlive = false));
260+
createdFixture.componentRef.onDestroy(() => (isAlive = false));
252261

253-
if (hasOnChangesHook(fixture.componentInstance) && Object.keys(properties).length > 0) {
262+
if (hasOnChangesHook(createdFixture.componentInstance) && Object.keys(properties).length > 0) {
254263
const changes = getChangesObj(null, componentProperties);
255-
fixture.componentInstance.ngOnChanges(changes);
264+
createdFixture.componentInstance.ngOnChanges(changes);
256265
}
257266

258267
detectChanges = () => {
259268
if (isAlive) {
260-
fixture.detectChanges();
269+
createdFixture.detectChanges();
261270
}
262271
};
263272

264273
if (detectChangesOnRender) {
265274
detectChanges();
266275
}
276+
277+
return createdFixture;
267278
}
268279
}
269280

@@ -429,6 +440,30 @@ function addAutoImports<SutType>(
429440
return [...imports, ...components(), ...animations(), ...routing()];
430441
}
431442

443+
async function renderDeferBlock<SutType>(
444+
fixture: ComponentFixture<SutType>,
445+
deferBlockState: DeferBlockState,
446+
deferBlockIndex?: number,
447+
) {
448+
const deferBlockFixtures = await fixture.getDeferBlocks();
449+
450+
if (deferBlockIndex !== undefined) {
451+
if (deferBlockIndex < 0) {
452+
throw new Error('deferBlockIndex must be a positive number');
453+
}
454+
455+
const deferBlockFixture = deferBlockFixtures[deferBlockIndex];
456+
if (!deferBlockFixture) {
457+
throw new Error(`Could not find a deferrable block with index '${deferBlockIndex}'`);
458+
}
459+
await deferBlockFixture.render(deferBlockState);
460+
} else {
461+
for (const deferBlockFixture of deferBlockFixtures) {
462+
await deferBlockFixture.render(deferBlockState);
463+
}
464+
}
465+
}
466+
432467
/**
433468
* Wrap waitFor to invoke the Angular change detection cycle before invoking the callback
434469
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Component } from '@angular/core';
2+
import { DeferBlockState } from '@angular/core/testing';
3+
import { render, screen } from '../src/public_api';
4+
5+
test('renders a defer block in different states using the official API', async () => {
6+
const { fixture } = await render(FixtureComponent);
7+
8+
const deferBlockFixture = (await fixture.getDeferBlocks())[0];
9+
10+
await deferBlockFixture.render(DeferBlockState.Loading);
11+
expect(screen.getByText(/loading/i)).toBeInTheDocument();
12+
expect(screen.queryByText(/Defer block content/i)).not.toBeInTheDocument();
13+
14+
await deferBlockFixture.render(DeferBlockState.Complete);
15+
expect(screen.getByText(/Defer block content/i)).toBeInTheDocument();
16+
expect(screen.queryByText(/load/i)).not.toBeInTheDocument();
17+
});
18+
19+
test('renders a defer block in different states using ATL', async () => {
20+
const { renderDeferBlock } = await render(FixtureComponent);
21+
22+
await renderDeferBlock(DeferBlockState.Loading);
23+
expect(screen.getByText(/loading/i)).toBeInTheDocument();
24+
expect(screen.queryByText(/Defer block content/i)).not.toBeInTheDocument();
25+
26+
await renderDeferBlock(DeferBlockState.Complete, 0);
27+
expect(screen.getByText(/Defer block content/i)).toBeInTheDocument();
28+
expect(screen.queryByText(/load/i)).not.toBeInTheDocument();
29+
});
30+
31+
test('renders a defer block initially in the loading state', async () => {
32+
await render(FixtureComponent, {
33+
deferBlockStates: DeferBlockState.Loading,
34+
});
35+
36+
expect(screen.getByText(/loading/i)).toBeInTheDocument();
37+
expect(screen.queryByText(/Defer block content/i)).not.toBeInTheDocument();
38+
});
39+
40+
test('renders a defer block initially in the complete state', async () => {
41+
await render(FixtureComponent, {
42+
deferBlockStates: DeferBlockState.Complete,
43+
});
44+
45+
expect(screen.getByText(/Defer block content/i)).toBeInTheDocument();
46+
expect(screen.queryByText(/load/i)).not.toBeInTheDocument();
47+
});
48+
49+
test('renders a defer block in an initial state using the array syntax', async () => {
50+
await render(FixtureComponent, {
51+
deferBlockStates: [{ deferBlockState: DeferBlockState.Complete, deferBlockIndex: 0 }],
52+
});
53+
54+
expect(screen.getByText(/Defer block content/i)).toBeInTheDocument();
55+
expect(screen.queryByText(/load/i)).not.toBeInTheDocument();
56+
});
57+
58+
@Component({
59+
template: `
60+
@defer {
61+
<p>Defer block content</p>
62+
} @loading {
63+
<p>Loading...</p>
64+
}
65+
`,
66+
})
67+
class FixtureComponent {}

0 commit comments

Comments
 (0)