- Write a demo script: a Playwright test that imports
testandexpectfrom@outerlayer/playwright. - Run
outerlayer demo recordon the script. It writes a video and a manifest. - Run
outerlayer emit demoon the manifest. It builds the narrated video and stores it on the item.
Before you start
Demos cover browser apps. The recorder and its Playwright release ship inside theouterlayer CLI, so your repository installs nothing.
A runner build image holds all that the commands need: the browser, ffmpeg, espeak-ng and the voice pack. A build records a demo with no network. A repository with its own devcontainer file adds nothing to its image.
On your own machine, do these steps first:
- Install the CLI globally with
npm install --global @outerlayer/cli.outerlayer demo recorddoes not run throughnpx. - Install
ffmpegandespeak-ng. See Install the programs. - Install the browser with
npx playwright@1.64.1 install chromium. That version is the Playwright release the CLI runs. - Allow for a one-time download of about 400 MB. See Download the voice pack.
ffmpeg, and no voice pack.
Install the programs
The commands runffmpeg and espeak-ng as separate programs. ffprobe comes with ffmpeg.
- Linux
- macOS
- Windows
Use your package manager.On Fedora,
ffmpeg comes from the RPM Fusion repository. See the Fedora documentation to enable it.Download the voice pack
The first narrated run downloads the voice pack to~/.outerlayer/voice/. The pack is about 400 MB. The command shows progress, and it resumes a broken download. After that run, narration needs no network, no account and no key.
The command checks every file of the pack on each narrated run. If a file is damaged, the command names it. Delete that file, then run the command again.
To download from a server of your own, set OUTERLAYER_VOICE_MIRROR to the address of a server that holds the pack files. The command checks every file the mirror serves in the same way.
Write the demo script
A demo script is a Playwright test that uses thedemo fixture. Name the file *.demo.ts. Playwright’s default test pattern leaves that name out, so your own test runs do not pick it up. Commit the script in your pull request, so a later session can record the demo again.
Import test and expect from @outerlayer/playwright, not from @playwright/test. Your repository does not install that package: outerlayer demo record supplies it. A TypeScript check of the script cannot resolve the import, so leave **/*.demo.ts out of that check.
Add one demo.chapter for each criterion, and tell it in beats with demo.say.
demo.chapter(criterionId, title, body) takes these arguments:
Each
expect that runs inside body is a check of the chapter. A failed expect fails the test, as it does in any Playwright test. A chapter proves its criterion only when it ran at least one check and every check passed.
demo.chapter(criterionId, title, narration, body) also works. It speaks one narration line over the whole chapter, and the demo shows the whole chapter.
Follow these rules for chapters:
- Give each criterion one chapter.
- Put a chapter with no criterion first. Only the first chapter may have none.
- Use the smallest viewport that shows the result. The stored demo is one video of at most 8 MiB. See What the command stores.
Tell a chapter in beats
demo.say(line, steps) is one beat: a spoken line, and the steps on screen that it describes. The steps are ordinary Playwright calls. A beat lasts as long as its line or its steps, whichever is longer.
The demo shows only beats. A step outside a beat is setup, and the demo leaves it out. Use setup to sign in, seed data, wait for a cache or reload a list.
Inside a beat, the video shows a pointer that moves to each element a step clicks or hovers. A fill types its text a key at a time, and the page scrolls smoothly to an element out of view.
Follow these rules for beats:
- Sign in and seed data once, before the first chapter. Do not sign out and in between chapters.
- Write each line about what the viewer sees in the steps of its beat. See Write the lines.
- Go from page to page inside a beat, with the app’s own links. A reload or a
page.gotooutside a beat stays out of the demo.
Write the lines
The viewer hears each line. Write it as you say it to a teammate at your screen.- Say what the viewer sees change, and why it matters to them. The viewer sees a grey number, not “the secondary text color”.
- Name each thing by its label on the screen.
- Talk to the viewer. Use “you”, “we”, “here” and contractions such as “it’s”.
- Say each step once, in one or two short sentences.
- Read each line aloud before you commit it. Write it again when it sounds like a test assertion or an advert.
outerlayer emit demo refuses a line that holds one of these:
- A sales word or a stock phrase, such as
seamless,powerful,effortless,dive inornot just … but. - A dash or an exclamation mark.
- A criterion id, such as
AC-7-02. - More than 30 words, for the line of a beat.
Show a step outside the browser
demo.terminal(command, run) shows a step that runs outside the browser, such as a CLI command. It draws a terminal panel over the page, types command, runs run, and prints what run returns. It returns that output, so the test can check it. Call it inside a beat. The panel closes when the next beat starts.
run returns, so return the command’s real output.
demo.http(request, run) shows a call to an API in the same kind of panel. It types the request’s method and URL, and prints its body when it has one. Then it runs run, and prints the status and body that run returns. It returns that response. The panel prints at most 14 lines of the body, and says how many lines it left out.
Use demo.http when a criterion’s proof is an API response, or when no page shows the step. In this example, request is Playwright’s own fixture. Add it to the test’s arguments, as in async ({ page, demo, request }) => { … }.
Point to an element
Inside a beat, the first passedexpect on a locator dims the page around the element it checks and draws a ring around it. The viewer then sees what the line talks about. So check first the one element that the line names, such as a status in a list:
- a check with
not, such asexpect(locator).not.toBeVisible() - a check on a locator that matches several elements
- a check on a large element, such as a whole card or section
- a check of a computed value, such as a style the test reads with
evaluate
demo.highlight(locator) rings an element that no check looks at. After it, checks in that beat draw no ring.
Mark a screenshot
A criterion that declares the proofscreenshot is a chapter of the demo too. Call demo.screenshot() inside the chapter, after the expect that shows the result.
outerlayer emit demo stores the frame as the criterion’s screenshot.
demo.screenshot(options) takes these options:
Follow these rules:
- Call
demo.screenshot()inside a chapter, after at least one check. The caption of the stored frame quotes the check before the call. - Put the call where the result shows. The command never picks a frame for you.
- Call it once in a chapter. A second call replaces the first frame.
screenshot and every check of the chapter passed. Otherwise it stores no frame and says why.
Record the demo
Make sure the app runs, or that your Playwright config starts it. Then run the script withouterlayer demo record.
- Without
--config, the script runs in one Chromium project.--base-urlsets where relativepage.goto()paths start. - With
--config, the demo uses that config’s setup, base URL and projects. Pass--projectwhen the config has more than one project. A project that the named project depends on, such as one that signs in, runs as usual.
test-results/outerlayer-demo/ unless --output-dir names another. To keep an earlier recording, give each run its own --output-dir.
When the script ends, the command prints the path of the manifest:
video.webm, is in the same folder. If the test fails, the command still writes the manifest, and it exits 1. All the flags are in outerlayer demo record.
Render the demo on your machine
Watch the demo before you store it. Pass--output to render the demo to a file and stop. The command needs no item and uploads nothing.
demo.webm is the demo as a reviewer sees it. Next to it, demo.webm.json lists each chapter’s time range and the time the command left out. The command exits 1 when a check failed, or when the demo holds a frozen, silent stretch over 2 seconds.
Emit the demo
Runouterlayer emit demo on the manifest, with the item’s number.
outerlayer sync. Outside a session it uploads at once. All the flags and refusals are in outerlayer emit demo.
What the command stores
The command stores the demo as one video of at most 8 MiB, with one time range for each chapter. A chapter that ran checks, and passed each one, is the proof of its criterion. The opening chapter, and a chapter with a failed check, prove no criterion. If the video is over 8 MiB, the command encodes it again at a lower quality, up to two times. If it is still too large, the command stores one clip for each chapter and no whole video. The command output says so. The Criteria tab still plays the clips as one video. To keep one video, record a smaller viewport or fewer chapters.Skip the narration
To record and store a demo with no speech, and download nothing, pass a flag to each command:--no-voice times each beat by an estimate of its line. --no-narration stores the demo with no audio. With both flags you need only ffmpeg.
With --no-voice alone, outerlayer emit demo speaks each line later. See Fix a refused demo for a line that then runs too long.
Record the demo again
A push moves the head of the pull request. A demo recorded before the push is then out of date, and its player shows the Out of date tag.outerlayer work proofs --record records the demo again at the new head. It finds the script by the criterion ids in its demo.chapter calls, such as demo.chapter("AC-12", ...). It runs the script and replaces the item’s earlier demo. For this, the repository’s proofs file needs a runner whose glob matches the script:
- After a build or an amend, the runner runs it at the final commit.
- After any other push to an item that a runner built, the built-in automation Record proofs again starts an amend that runs it. See When the proofs are recorded again.
- In a session of your own, run it yourself after each push.
- No test names the criterion. No script at HEAD holds the criterion’s id.
- A chapter fails its checks. The new chapter is stored, but it proves no criterion. The comment names the criterion and the failed check.
- The script cannot run. The comment says why.
outerlayer work demo prints:
outerlayer emit artifact is not part of the demo, so recording the demo again does not replace it.
Fixed settings
Two recordings of an unchanged page can still differ. The clock moves, the timezone changes a date format, and an animation is caught at another point. Thedemo fixture fixes each setting that the browser controls:
The fixture cannot fix what your app’s server controls, such as its clock, its random ids and its data. Compare two runs finds that variation.
Turn a setting off
Every fixed setting is on by default. Turn one off with thedemoOptions option. Each key is true unless you set it to false.
To change the viewport, locale, timezone or pixel ratio, set that Playwright option yourself. Your value replaces the fixed one.
The video records at 1280 × 800. When the page is smaller than the video, the recording fills the rest of each frame in grey. So when you set a viewport, set the video to the same size at the top of the script:
Hide a region that varies
Usedemo.mask to hide an element that varies for reasons of its own, such as a timestamp from your server.
Compare two runs
To find what varies in your app between two recordings, record the script twice and compare the runs. Give each run its own output folder, because a run empties its folder first.
For a
no chapter, fix what varies in your app, or hide it with demo.mask.
- The command refuses when the runs differ in chapters, titles or checks. It names the first difference.
- A still follows an
await expect(...)check at once. A synchronous matcher, such asexpect(1).toBe(1), does not wait, so its still can show a later page. Use an async matcher, such asawait expect(locator).toBeVisible(), for a check whose still must match. - Comparing never changes what a chapter proves. A chapter is the proof of its criterion when its checks pass, whatever the result here.
Fix a refused demo
outerlayer emit demo refuses before it uploads anything, and the message names the chapter. These refusals need a change to the script:
A line runs too long only when the command speaks it after the recording. That happens for a one-line chapter, and for a beat recorded with
--no-voice. The command first speeds the speech up, to at most 1.3 times. It refuses when the line is still too long.
Manifest format
The recorder writesmanifest.json for you. You need this section only when you write a recorder of your own. A recorder that writes the same format gets narration and comparison from outerlayer emit demo.
The recorder writes version 5 when a chapter has beats, and version 4 otherwise.
A beat has these fields:
A still and a screenshot frame have these fields:
A still follows a check that passed. The
check values rise from one still to the next. Do not change a still file after the run: outerlayer emit demo --compare checks each file against its sha256.
See also: Prove the work, outerlayer emit demo.