Skip to content

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 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. scale is 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 too
s.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.

A clip written from an app’s source reaches for controls that are conditional, renamed, or not rendered at all. Ask the page instead:

Terminal window
npx versioncam inspect /shipments # what is on the page and what it is called
npx versioncam measure / '.marker' # where the matches are, as fractions

inspect 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.