Skip to the solution
MediumFrontend9 min read

How do you test signal components, defer blocks, and new control flow in 2025?

353 views

The problem

Question presented to candidate: "A dashboard uses signal inputs, a @defer (on viewport) chart with placeholder, loading and error blocks, and @if/@for/@switch in its templates. How do you unit test all of that with TestBed today?"

What a strong answer should cover:

  • Signal components: setInput(), subscribe to output(), await fixture.whenStable() in the zoneless default.
  • @defer: configure deferBlockBehavior: DeferBlockBehavior.Manual, get blocks with fixture.getDeferBlocks(), then block.render(DeferBlockState.Loading | Complete | Error) and assert each state.
  • Know the default: Playthrough, which tries real triggers; on viewport needs IntersectionObserver, which jsdom does not have.
  • States move forward: after Error, rendering Complete is ignored; after Complete, Placeholder is ignored.
  • Control flow needs no special API: drive the signals, render, assert the DOM; @for with track reuses DOM nodes, which you can assert by identity.
  • @defer dependencies are loaded lazily in the app, but in a unit test the point is the state machine, not the network.

Clarifying questions expected:

  • "Do we want to test each defer state, or just that the main content works once loaded?"
  • "Unit tests in jsdom, or browser tests where viewport triggers are real?"

Code / implementation expected: Yes β€” a spec rendering each defer state manually and asserting @if/@for/@switch output.

  • testing
  • angular
  • signals

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 10 sections

Read the code

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); // ignored after Error
    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); // same element, moved and re-numbered
  });
});

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 66 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextMedium

How do signal forms and ControlValueAccessor evolve in modern Angular?

Continue