Angular@defer with DeferBlockBehavior.Manual plus @if/@for/@switch assertions (Vitest, ran with ng test on Angular 22.2.3)
import { Component, signal } from '@angular/core';
import { DeferBlockBehavior, DeferBlockState, TestBed } from '@angular/core/testing';
@Component({ selector: 'app-chart', template: `<b>CHART</b>` })
class Chart {}
@Component({
selector: 'app-dashboard',
imports: [Chart],
template: `
@defer (on viewport) {
<app-chart />
} @placeholder {
<span>placeholder</span>
} @loading {
<span>loading</span>
} @error {
<span>error</span>
}
`,
})
class Dashboard {}
type Todo = { id: number; title: string; done: boolean };
@Component({
selector: 'app-todos',
template: `
@if (todos().length > 0) {
<ul>
@for (t of todos(); track t.id; let i = $index) {
<li [class.done]="t.done">{{ i + 1 }}. {{ t.title }}</li>
}
</ul>
} @else {
<p class="empty">Nothing to do</p>
}
@switch (filter()) {
@case ('done') { <em>showing done</em> }
@default { <em>showing all</em> }
}
`,
})
class Todos {
todos = signal<Todo[]>([]);
filter = signal<'all' | 'done'>('all');
}
const text = (f: { nativeElement: HTMLElement }) => f.nativeElement.textContent.trim();
const all = (f: { nativeElement: HTMLElement }, css: string) =>
[...f.nativeElement.querySelectorAll(css)].map((el) => el.textContent!.trim());
describe('@defer with DeferBlockBehavior.Manual', () => {
beforeEach(() => TestBed.configureTestingModule({ deferBlockBehavior: DeferBlockBehavior.Manual }));
it('renders each state on demand', async () => {
const fixture = TestBed.createComponent(Dashboard);
await fixture.whenStable();
expect(text(fixture)).toBe('placeholder');
const [block] = await fixture.getDeferBlocks();
await block.render(DeferBlockState.Loading);
expect(text(fixture)).toBe('loading');
await block.render(DeferBlockState.Complete);
expect(text(fixture)).toBe('CHART');
});
it('the error state is terminal', async () => {
const fixture = TestBed.createComponent(Dashboard);
await fixture.whenStable();
const [block] = await fixture.getDeferBlocks();
await block.render(DeferBlockState.Error);
expect(text(fixture)).toBe('error');
await block.render(DeferBlockState.Complete);
expect(text(fixture)).toBe('error');
});
});
describe('@if / @for / @switch', () => {
it('follow the signals they read', async () => {
const fixture = TestBed.createComponent(Todos);
await fixture.whenStable();
expect(all(fixture, 'p.empty, em')).toEqual(['Nothing to do', 'showing all']);
fixture.componentInstance.todos.set([
{ id: 1, title: 'Write spec', done: true },
{ id: 2, title: 'Ship', done: false },
]);
fixture.componentInstance.filter.set('done');
await fixture.whenStable();
expect(all(fixture, 'li, em')).toEqual(['1. Write spec', '2. Ship', 'showing done']);
expect(fixture.nativeElement.querySelector('p.empty')).toBeNull();
expect(fixture.nativeElement.querySelectorAll('li.done').length).toBe(1);
});
it('track keeps DOM nodes when items move', async () => {
const fixture = TestBed.createComponent(Todos);
fixture.componentInstance.todos.set([{ id: 1, title: 'Write spec', done: false }]);
await fixture.whenStable();
const original = fixture.nativeElement.querySelector('li');
fixture.componentInstance.todos.update((xs) => [{ id: 0, title: 'Plan', done: false }, ...xs]);
await fixture.whenStable();
const items = [...fixture.nativeElement.querySelectorAll('li')];
expect(items.map((li) => li.textContent)).toEqual(['1. Plan', '2. Write spec']);
expect(items.indexOf(original)).toBe(1);
});
});