waitForNarration
Mark a point where the rendered video should hold until the previous narration's audio finishes.
What it does
waitForNarration() marks a point in the trace where the rendered video should wait for the previous narrate() call's audio to finish before continuing.
It resolves immediately at test time — there is no real-time pause and no setTimeout. The wait is realised only in the rendered video: the preceding narration's subtitle window ends here, and if its TTS audio is longer than that window, the renderer holds the last visible frame for the overflow (the same audio-overflow freeze that narrate() already uses).
Use it when narration would otherwise bleed across a deliberate beat:
- before a click that must not be talked over,
- at the end of a scenario, so the last line is fully heard before the video ends.
Usage
import { narrate, waitForNarration, click } from 'playwright-recast'
await narrate('Click the submit button.')
await click(page.locator('button[type=submit]'))
await waitForNarration() // hold here until the line above is done speaking
await click(page.locator('a.next-step'))It takes no arguments and emits one empty marker step (__recast_wait_for_narration__) into the trace zip. It pushes no voiceover annotations, and no-ops cleanly when setupRecast(test) has not been called.
How it affects subtitle windows
subtitlesFromTrace() normally runs each narrate() line's subtitle until the next narrate() call. When a trace contains waitForNarration() markers, each narration's window ends at the earliest of:
- the next
narrate()marker, - the next
waitForNarration()marker, or - the end of the trace.
So waitForNarration() draws a hard boundary, letting you put clicks and narrations next to each other without the renderer cutting a line short. When a trace has no waitForNarration() markers, subtitle assembly is byte-identical to before.
Pacing narration without autoWait
With a .voiceover() stage you do not need autoWait (or pace()) to give each line enough on-screen time. Run the test at full speed — keep only the small waits you actually want for the visuals — and put a waitForNarration() after each narration's actions. The rendered video then freezes at each boundary for exactly as long as the audio needs, and the line's subtitle is shown for its audio's duration.
// No autoWait, no pace() — the test runs at full speed.
await narrate('First, open the schema editor.')
await click(editorButton)
await waitForNarration() // hold until line 1 finishes speaking
await narrate('Then add a new column.')
await click(addColumnButton)
await waitForNarration() // hold until line 2 finishes speakingThis is the lightest-weight way to keep narration in sync: a short test run, a rendered video that matches the voiceover. It works even when a narrate() is immediately followed by its waitForNarration() with no gap in between — the line is kept and sized from its audio rather than dropped. (autoWait is still the right tool when you render without TTS, where there is no audio to size the pause.)
When the wait actually adds time
The marker only adds time when time is actually needed:
- With
.voiceover(): if the narration's audio overruns its (now narrowed) window, the renderer freezes the last frame for the overflow. The cursor and click ripples keep animating naturally through the freeze — the beat looks deliberate, not like a hang. If the audio fits, the marker is a no-op for that line. - Without voiceover: there is no audio to wait on, so the marker simply bounds the subtitle window. Pad visual time with
pace()ornarrate(text, { autoWait })instead.
Example in a BDD step
import { Then } from './fixtures'
import { narrate, waitForNarration, pace } from 'playwright-recast'
Then('the order is confirmed', async ({ page }, docString?: string) => {
await narrate(docString)
await page.locator('[data-testid="confirmation"]').waitFor()
await pace(page, 1000)
await waitForNarration() // last line of the scenario — let it finish
})