From 321822d762a52b1ef0a1ee79a6d384178f92594f 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/2] feat: add methods to test deferrable views
---
.../examples/21-deferable-view.component.ts | 23 ++++++
.../app/examples/21-deferable-view.spec.ts | 23 ++++++
projects/testing-library/src/lib/models.ts | 9 ++-
.../src/lib/testing-library.ts | 75 +++++++++++++------
.../tests/defer-blocks.spec.ts | 67 +++++++++++++++++
5 files changed, 172 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..3cb1160
--- /dev/null
+++ b/apps/example-app/src/app/examples/21-deferable-view.component.ts
@@ -0,0 +1,23 @@
+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
+ }
+ `,
+ 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..9a6e3c9
--- /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.Complete,
+ });
+
+ expect(screen.getByText(/Hello from deferred child component/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 48af2d5..b6e727c 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, 0);
+ }
+ }
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 = 0) => {
+ 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,22 @@ function addAutoImports(
return [...imports, ...components(), ...animations(), ...routing()];
}
+async function renderDeferBlock(
+ fixture: ComponentFixture,
+ deferBlockState: DeferBlockState,
+ deferBlockIndex: number,
+) {
+ if (deferBlockIndex < 0) {
+ throw new Error('deferBlockIndex must be a positive number');
+ }
+ const deferBlockFixture = (await fixture.getDeferBlocks())[deferBlockIndex];
+
+ if (!deferBlockFixture) {
+ throw new Error(`Could not find deferrable view with index '${deferBlockIndex}'`);
+ }
+ 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 c03551ceb1ec9472a34368129496044d0f6b3843 Mon Sep 17 00:00:00 2001
From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Wed, 15 Nov 2023 18:40:34 +0100
Subject: [PATCH 2/2] render without index updates all blocks
---
.../examples/21-deferable-view.component.ts | 2 ++
.../app/examples/21-deferable-view.spec.ts | 4 +--
.../src/lib/testing-library.ts | 28 ++++++++++++-------
3 files changed, 22 insertions(+), 12 deletions(-)
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
index 3cb1160..ce47a58 100644
--- a/apps/example-app/src/app/examples/21-deferable-view.component.ts
+++ b/apps/example-app/src/app/examples/21-deferable-view.component.ts
@@ -15,6 +15,8 @@ export class DeferableViewChildComponent {}
Hello from placeholder
} @loading {
Hello from loading
+ } @error {
+ Hello from error
}
`,
imports: [DeferableViewChildComponent],
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
index 9a6e3c9..8495387 100644
--- a/apps/example-app/src/app/examples/21-deferable-view.spec.ts
+++ b/apps/example-app/src/app/examples/21-deferable-view.spec.ts
@@ -16,8 +16,8 @@ test('renders deferred views based on state', async () => {
test('initially renders deferred views based on given state', async () => {
await render(DeferableViewComponent, {
- deferBlockStates: DeferBlockState.Complete,
+ deferBlockStates: DeferBlockState.Error,
});
- expect(screen.getByText(/Hello from deferred child component/i)).toBeInTheDocument();
+ expect(screen.getByText(/Hello from error/i)).toBeInTheDocument();
});
diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts
index b6e727c..733ba0f 100644
--- a/projects/testing-library/src/lib/testing-library.ts
+++ b/projects/testing-library/src/lib/testing-library.ts
@@ -171,7 +171,7 @@ export async function render(
await renderDeferBlock(fixture, deferBlockState.deferBlockState, deferBlockState.deferBlockIndex);
}
} else {
- await renderDeferBlock(fixture, deferBlockStates, 0);
+ await renderDeferBlock(fixture, deferBlockStates);
}
}
@@ -224,7 +224,7 @@ export async function render(
detectChanges: () => detectChanges(),
navigate,
rerender,
- renderDeferBlock: async (deferBlockState: DeferBlockState, deferBlockIndex: number = 0) => {
+ renderDeferBlock: async (deferBlockState: DeferBlockState, deferBlockIndex?: number) => {
await renderDeferBlock(fixture, deferBlockState, deferBlockIndex);
},
debugElement: fixture.debugElement,
@@ -443,17 +443,25 @@ function addAutoImports(
async function renderDeferBlock(
fixture: ComponentFixture,
deferBlockState: DeferBlockState,
- deferBlockIndex: number,
+ deferBlockIndex?: number,
) {
- if (deferBlockIndex < 0) {
- throw new Error('deferBlockIndex must be a positive number');
- }
- const deferBlockFixture = (await fixture.getDeferBlocks())[deferBlockIndex];
+ const deferBlockFixtures = await fixture.getDeferBlocks();
+
+ if (deferBlockIndex !== undefined) {
+ if (deferBlockIndex < 0) {
+ throw new Error('deferBlockIndex must be a positive number');
+ }
- if (!deferBlockFixture) {
- throw new Error(`Could not find deferrable view with index '${deferBlockIndex}'`);
+ 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);
+ }
}
- await deferBlockFixture.render(deferBlockState);
}
/**