In Angular's TestBed, how do you test each state of a @defer block, and what do DeferBlockBehavior.Playthrough and Manual change?
answer
- test config option
- default behaves like the browser
- Manual starts at placeholder
- getDeferBlocks then render(state)
basics
~10 sAngular TestBed defaults to DeferBlockBehavior.Playthrough, where @defer blocks behave as in a browser. With Manual, blocks stay on their placeholder and you step them with fixture.getDeferBlocks() and render(DeferBlockState.X).
solid answer
~30 s`TestBed.configureTestingModule({ deferBlockBehavior: DeferBlockBehavior.Manual })` switches `@defer` blocks into manual mode: their triggers never fire, the block starts on its placeholder, and the test moves it along. `await fixture.getDeferBlocks()` returns `DeferBlockFixture` objects; `await block.render(DeferBlockState.Loading)`, `Complete` or `Error` renders that state. Rendering `Complete` loads the real dependencies first, and `render` skips the `minimum`/`after` timers so the state appears immediately. Asking for a state the template has no block for, say `Error` with no `@error`, throws. `Playthrough` is the default: blocks run their real triggers, so the test must make them fire and wait. Nested blocks come from `block.getDeferBlocks()`.
code
ts · 31 linesimport { Component } from '@angular/core';
import { TestBed, DeferBlockBehavior, DeferBlockState } from '@angular/core/testing';
import { VideoPlayer } from './video-player';
@Component({
imports: [VideoPlayer],
template: `
@defer (on viewport) {
<app-video-player src="/media/intro.mp4" />
} @placeholder {
<div>Video placeholder</div>
} @loading {
<p>Loading player</p>
}
`,
})
class VideoHost {}
it('renders each defer state', async () => {
TestBed.configureTestingModule({ deferBlockBehavior: DeferBlockBehavior.Manual });
const fixture = TestBed.createComponent(VideoHost);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Video placeholder');
const [block] = await fixture.getDeferBlocks();
await block.render(DeferBlockState.Loading);
expect(fixture.nativeElement.textContent).toContain('Loading player');
await block.render(DeferBlockState.Complete);
expect(fixture.nativeElement.querySelector('app-video-player')).not.toBeNull();
});go deeper
Recall that TestBed has a deferBlockBehavior option and that render(DeferBlockState.X) moves a block between states.
Explain Playthrough versus Manual, why render skips timers, and how to reach nested blocks.
Choose manual mode for state UIs and playthrough for trigger wiring, and keep bundling checks in the build, not the unit test.
Agree a testing convention for deferred sections so suites stay deterministic without mocking Angular internals.
## Why `@defer` needs special test support A **`@defer` block** changes state on its own: a trigger fires, a dynamic import resolves, timers for `minimum` and `after` run. In a unit test that makes it hard to assert "the placeholder shows", then "the loading block shows", then "the content shows", because some of those states may be skipped or pass too fast to observe. Angular's testing package offers two behaviours, chosen per test module: | `DeferBlockBehavior` | What blocks do | Default? | |---|---|---| | `Playthrough` | behave as in a browser: triggers register and fire, chunks load, timers run | **yes** | | `Manual` | triggers never fire (DOM triggers are not even registered); blocks stay on their placeholder until the test renders a state | no | The option is `deferBlockBehavior` in `TestBed.configureTestingModule`. Both values are exported from `@angular/core/testing`, together with `DeferBlockState`. ## Manual mode, step by step 1. Configure: `TestBed.configureTestingModule({ deferBlockBehavior: DeferBlockBehavior.Manual })`. 2. Create the component fixture and run change detection; the placeholder renders. 3. `const [block] = await fixture.getDeferBlocks();` — one `DeferBlockFixture` per defer block currently in the component's view. 4. Call `await block.render(DeferBlockState.Loading)` and assert the loading content. 5. Call `await block.render(DeferBlockState.Complete)`; Angular loads the block's real dependencies, renders the content, and runs change detection. 6. For failure paths, `await block.render(DeferBlockState.Error)` renders the `@error` template. Details worth knowing: - `render()` **skips the `minimum` and `after` timers**, so each requested state appears at once. Test the timing parameters, if you must, in playthrough mode with fake timers. - `render()` **throws** if the template has no block for that state, for example `DeferBlockState.Loading` on a block without `@loading`. - States only move forward: rendering `Placeholder` after `Complete` does not bring the placeholder back. - **Nested blocks**: once a block's content is rendered, `block.getDeferBlocks()` returns the fixtures for blocks inside it. ## `DeferBlockState` values `DeferBlockState.Placeholder`, `Loading`, `Complete` and `Error` — the same states the runtime moves through. ## Playthrough mode Playthrough is right when you want to test **the real trigger**: that clicking the placeholder button loads the panel, or that a `when` condition set by the component's logic loads the content. The test performs the interaction or state change, then waits for the dynamic import and change detection (`await fixture.whenStable()`), then asserts. Element triggers such as `on viewport` depend on browser APIs your test environment may not simulate, which is a common reason to prefer manual mode for those. ## What to test where - **Manual mode**: each sub-block's content and bindings, error UI, nested structure. - **Playthrough mode**: that the chosen trigger actually fires in response to the user action or state. - **Neither**: that the component ends up in a lazy chunk. Bundling is a build output; check it in a production build. ## A caution about provider overrides `TestBed` overrides apply to dependencies loaded by `@defer` blocks as well, so a mocked service used by the deferred component is honoured after `render(DeferBlockState.Complete)`.
- Why does block.render(DeferBlockState.Error) throw on some blocks?`DeferBlockFixture.render` checks that the template defines a block for the requested state. If there is no `@error` block, there is no error template to render, so it throws an error naming the missing block rather than silently doing nothing.
- Why might a playthrough test for an on viewport block never see the content?The viewport trigger relies on IntersectionObserver reporting the placeholder as visible. If the test environment does not implement or simulate it, the trigger never fires. Manual mode sidesteps that by rendering the state directly; keep playthrough for triggers the test can actually drive.
saying these in an interview costs you the question
- Manual is TestBed's default defer behaviour.
- render(DeferBlockState.Complete) skips loading the real dependencies.
- render() honours minimum and after timers.
- getDeferBlocks() also returns blocks nested inside unrendered content.
- In Manual mode the block's own triggers still fire when the test clicks or scrolls.