Skip to main content
A demo is one narrated video of your app. It has one chapter for each acceptance criterion. A spoken line describes each step while it happens on screen. The item’s Criteria tab plays the demo, and each chapter is the video proof of its criterion. You record a demo in three steps:
  1. Write a demo script: a Playwright test that imports test and expect from @outerlayer/playwright.
  2. Run outerlayer demo record on the script. It writes a video and a manifest.
  3. Run outerlayer emit demo on the manifest. It builds the narrated video and stores it on the item.
In a runner build, the agent does these steps. The starter evidence skill tells it to record a demo for the criteria whose behavior shows on screen. Use this page to write or change a demo script, or to record a demo on your own machine.

Before you start

Demos cover browser apps. The recorder and its Playwright release ship inside the outerlayer 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:
  1. Install the CLI globally with npm install --global @outerlayer/cli. outerlayer demo record does not run through npx.
  2. Install ffmpeg and espeak-ng. See Install the programs.
  3. Install the browser with npx playwright@1.64.1 install chromium. That version is the Playwright release the CLI runs.
  4. Allow for a one-time download of about 400 MB. See Download the voice pack.
To record with no speech, see Skip the narration. You then need only ffmpeg, and no voice pack.

Install the programs

The commands run ffmpeg and espeak-ng as separate programs. ffprobe comes with ffmpeg.
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 the demo 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.
OuterLayer does not remove secrets from a demo. The video, the screenshots and the narration lines upload as you recorded them. Anything the script types or the app shows is in the video, such as a password, a token or a customer email. Sign in with a test account and seed fake data. See What leaves your machine.
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.goto outside 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 in or not 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.
The refusal names the chapter, the beat, the line and the reason. The check catches words, not tone, so a stiff line in plain words passes. Listen to the rendered demo to find those.

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.
The panel shows only what 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 passed expect 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 beat has one ring, and later checks in the beat draw no other. The ring clears when the pointer moves to another element or the beat ends. These checks draw no ring, and leave the ring to the next check:
  • a check with not, such as expect(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 proof screenshot is a chapter of the demo too. Call demo.screenshot() inside the chapter, after the expect that shows the result.
The call takes one frame of the page, with no ring on it. 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.
The command stores the frame only when the criterion declares 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 with outerlayer demo record.
  • Without --config, the script runs in one Chromium project. --base-url sets where relative page.goto() paths start.
  • With --config, the demo uses that config’s setup, base URL and projects. Pass --project when the config has more than one project. A project that the named project depends on, such as one that signs in, runs as usual.
The command speaks each line with the CLI’s voice while it records, so each beat lasts as long as its spoken line. Each run empties its output folder first. The folder is 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:
Use that path in the next steps. The recording, 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

Run outerlayer emit demo on the manifest, with the item’s number.
The command checks the manifest, the video and the programs it needs before it uploads anything. When it refuses, it stores nothing. See Fix a refused demo. The stored demo shows each chapter’s beats, or the whole chapter when it has one line. It leaves out the setup between beats. It also leaves out each stretch of 1.6 seconds or more with no motion and no speech. When a chapter has a failed check, the command stores the demo, names the chapter and the check, and exits 1. That chapter proves no criterion. Fix the app, record again and run the command again. If an upload fails, run the same command again with the same manifest, video, item and pull request. It stores only what is missing. Inside a recorded session, the demo uploads on the next 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:
Who runs the command depends on who pushed:
  • 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.
An amend that cannot record the demo again posts one comment on the item’s general thread. The comment gives the cause:
  • 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.
To replace a demo by hand, record the script again. Then emit it with the id of the item’s newest demo record, which outerlayer work demo prints:
The new demo retires the old video, its chapters and its screenshots. A screenshot that you emitted with 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. The demo 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 the demoOptions 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:
Playwright sets the video size once for each worker. A viewport that one test sets cannot change it.

Hide a region that varies

Use demo.mask to hide an element that varies for reasons of its own, such as a timestamp from your server.
The mask covers the element with a solid grey box until the chapter ends. The box shows in the video and in every still, and it follows the element when the element moves. It is the same in every run, so it never causes a difference.

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.
The recorder takes a still of the page after each passed check. The command compares each still of the first run with the still at the same step of the second run. It stores the first run’s video, and it does not read the second run’s video. The command output and the stored demo record give a result for each chapter. The item page does not show it. 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 as expect(1).toBe(1), does not wait, so its still can show a later page. Use an async matcher, such as await 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 writes manifest.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.