Write a clip
Source README.md · versioncam 0.5.3 from npm
A clip is a file that default-exports clip(id, options, body). The body
drives the app through s, its session.
import { clip } from "versioncam";
export default clip("02-find-a-shipment", { title: "Find a shipment" }, async (s) => { await s.open("/"); s.caption("Find a shipment"); await s.hold(500);
await s.camera.zoom(s.byTestId("shipments"), 1.35); await s.typeInto(s.byTestId("filter"), "berg"); await s.settle({ label: "filtered" });
await s.highlight(s.byTestId("list-status"), "ring", { for: 1500 }); await s.hold(1600);});Every await advances the clip’s time and captures frames as it goes.
s.camera.*, s.highlight and s.caption write to tracks at the current
instant and cost no time. s.offCamera(page => …) runs raw Playwright with
nothing captured, for setup that should not appear, such as mocking an API.
Target elements by test id or role, never by coordinates. Strokes (s.drag
for a straight one, s.lasso for a closed loop) are written in fractions of a
target’s box for the same reason. A clip built from locators survives a layout
change.
npx versioncam dsl prints every method with its options and defaults, and a
test holds that list to the session’s code. The same reference is
The clip DSL.
| Option | Default | What it sets |
|---|---|---|
title |
required | The clip’s name, kept in its timeline. It is not drawn: write a caption with s.caption. |
seed |
1 |
Seeds the jitter in cursor paths, dwells and typing. A new seed is a new take of the same script. |
viewport, dpr, fps |
the config’s | For a clip that needs a different shape. |
capture |
the config’s | A clip that films a canvas sets { forceCaptureEvery: 1 } here, and one whose app animates in ways better left out sets { transitions: "zero" }. |
The camera
Section titled “The camera”The camera crops the recorded page as the video plays. There are two ways to point it.
s.camera.zoom(target, scale)frames one thing and stays there: a form being filled, an answer to read.scaleis a ceiling, so a small target is framed with room around it.s.camera.follow()follows the cursor at one scale until the next camera line. Use it when a clip moves around the page, in place of a zoom before each thing the cursor uses, which zooms in and out and pans in fits and starts.
s.camera.follow({ scale: 1.35 });await s.typeInto(s.byTestId("recipient"), "Mira Halloran");await s.click(s.byTestId("create-shipment"));await s.click(s.byTestId("cancel-3")); // across the page, and the camera goes toos.camera.reset();await s.hold(1200);A camera that follows keeps the cursor inside a dead zone in the middle of
the frame, and moves only when the cursor leaves it: a move to the next field
of a form does not move the picture. It never shows beyond the page’s edge,
and it moves smoothly, without overshooting. Its path is worked out from the
whole cursor track once the clip has run, so it sets off with the cursor
instead of trailing behind it. s.camera.release() stops it where it is.
Every camera line starts from wherever the camera is, still or moving, and
eases into its new framing over its duration. A zoom written while the last
one is still moving takes over from it without a stop. s.camera.reset()
goes back to the whole page.
versioncam review warns about a click that lands outside what the camera
shows. The clip DSL has every option and its default.
Look before you write
Section titled “Look before you write”A clip written from an app’s source reaches for controls that are conditional, renamed, or not rendered at all. Ask the page instead:
npx versioncam inspect /shipments # what is on the page and what it is callednpx versioncam measure / '.marker' # where the matches are, as fractionsinspect says where each name came from: [aria-label], [label],
[placeholder], [text] or [name]. That decides the locator. byLabel
finds the first two but not a placeholder; byPlaceholder finds that.
measure prints what each match says, so a beat’s expect can quote the
status line it waits for instead of guessing at it.