state-scenarios-cli 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +34 -11
- package/dist/cli.js.map +1 -1
- package/dist/{record-QUE3LDEG.js → record-BO2NJ4Q2.js} +3 -25
- package/dist/record-BO2NJ4Q2.js.map +1 -0
- package/dist/record.d.ts +4 -9
- package/package.json +2 -2
- package/skills/state-scenarios/references/media.md +6 -5
- package/dist/record-QUE3LDEG.js.map +0 -1
package/README.md
CHANGED
|
@@ -21,5 +21,5 @@ npx state-scenarios suggest scenarios --src src --schema scenario.schema.json
|
|
|
21
21
|
- `affected` lists the scenarios a branch can affect, and why. `shoot --compare <dir>` writes a before/after/changed image for each state that differs from an earlier run. `coverage` counts the scenario map's states by status. `publish` pushes files to a `scenario-shots` branch and prints Markdown image links for a PR.
|
|
22
22
|
- `doctor` checks the wiring one step at a time and prints each fix. `mcp` lets a coding agent open, drive and save scenarios. `guide` prints the shipped agent skill.
|
|
23
23
|
- Exit codes: `1` a scenario failed, `2` bad arguments or scenarios, `3` something needed is missing or down. Errors print a stable code, e.g. `[DEV_SERVER_DOWN]`.
|
|
24
|
-
- `record` opens each scenario named in `--steps`, runs its steps with a visible cursor (`--cursor-color`, `--cursor <file.svg>`), and saves an MP4, GIF or WebM, optionally cropped to `--element` (or per scenario with `{ element, steps }`). `--fps`, `--width` and `--hold <ms>` tune size and the final frame. Each video gets a `<name>.video.json` manifest (size, length, description, clicks) for editors such as Remotion. MP4, GIF, cropping, `--fps` and `--width` need ffmpeg.
|
|
24
|
+
- `record` opens each scenario named in `--steps`, runs its steps with a visible cursor that reaches each click or field before the action (`--cursor-color`, `--cursor <file.svg>`), and saves an MP4, GIF or WebM, optionally cropped to `--element` (or per scenario with `{ element, steps }`). `--fps`, `--width` and `--hold <ms>` tune size and the final frame. Each video gets a `<name>.video.json` manifest (size, length, description, clicks, focused fields) for editors such as Remotion. MP4, GIF, cropping, `--fps` and `--width` need ffmpeg.
|
|
25
25
|
- `suggest` asks Claude, or another model behind `ANTHROPIC_BASE_URL` via `--model`, which UI states lack scenarios, and drafts them for review.
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
declare global {
|
|
3
3
|
interface Window {
|
|
4
|
-
/** Set by record: reports each pointerdown for the video's manifest. */
|
|
5
|
-
stateScenariosClick(x: number, y: number): void;
|
|
4
|
+
/** Set by record: reports each pointerdown, and each field taking focus, for the video's manifest. */
|
|
5
|
+
stateScenariosClick(x: number, y: number, kind?: 'focus'): void;
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -484,8 +484,8 @@ ${changed} of ${report.length} states changed or new since ${before}`);
|
|
|
484
484
|
const raw = mkdtempSync(join(tmpdir(), "state-scenarios-"));
|
|
485
485
|
await checkServer(baseURL);
|
|
486
486
|
const { openScenario, showCursor } = await import("state-scenarios/playwright");
|
|
487
|
-
const record = await import("./record-
|
|
488
|
-
const { encodeVideo,
|
|
487
|
+
const record = await import("./record-BO2NJ4Q2.js");
|
|
488
|
+
const { encodeVideo, pagePoint, videoManifest, visibleCrop } = record;
|
|
489
489
|
const browser = await launchChromium({ slowMo: 150 });
|
|
490
490
|
let exitCode = 0;
|
|
491
491
|
const cropTo = async (page, selector, viewport) => {
|
|
@@ -517,16 +517,39 @@ ${changed} of ${report.length} states changed or new since ${before}`);
|
|
|
517
517
|
const file = join(out, `${name}.${format}`);
|
|
518
518
|
const clicks = [];
|
|
519
519
|
try {
|
|
520
|
-
await showCursor(page, cursor);
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
520
|
+
await showCursor(page, { ...cursor, glide: true });
|
|
521
|
+
await page.exposeBinding(
|
|
522
|
+
"stateScenariosClick",
|
|
523
|
+
({ frame }, x, y, kind) => {
|
|
524
|
+
const at = Date.now();
|
|
525
|
+
clicks.push(
|
|
526
|
+
pagePoint(frame, x, y).then((point) => ({ at, ...point, kind }), () => void 0)
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
);
|
|
528
530
|
await page.addInitScript(() => {
|
|
529
|
-
|
|
531
|
+
let pressed;
|
|
532
|
+
addEventListener("pointerdown", (e) => {
|
|
533
|
+
pressed = { x: e.clientX, y: e.clientY, t: performance.now() };
|
|
534
|
+
window.stateScenariosClick(e.clientX, e.clientY);
|
|
535
|
+
}, true);
|
|
536
|
+
addEventListener("pointermove", () => pressed = void 0, true);
|
|
537
|
+
addEventListener("focusin", (e) => {
|
|
538
|
+
const field = e.target;
|
|
539
|
+
if (!(field instanceof Element) || !field.matches(
|
|
540
|
+
"textarea, select, [contenteditable]:not([contenteditable=false]), input:not([type=button], [type=submit], [type=reset], [type=checkbox], [type=radio], [type=file], [type=image], [type=range], [type=color])"
|
|
541
|
+
)) return;
|
|
542
|
+
if (pressed && performance.now() - pressed.t < 500) {
|
|
543
|
+
window.stateScenariosClick(pressed.x, pressed.y, "focus");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const box = field.getBoundingClientRect();
|
|
547
|
+
window.stateScenariosClick(
|
|
548
|
+
box.left + Math.min(24, box.width / 2),
|
|
549
|
+
box.top + box.height / 2,
|
|
550
|
+
"focus"
|
|
551
|
+
);
|
|
552
|
+
}, true);
|
|
530
553
|
});
|
|
531
554
|
await openScenario(page, name, { path: resolved.path, strict: !values["no-strict"], panel: false });
|
|
532
555
|
const crop = element ? await cropTo(page, element, options.viewport) : void 0;
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport type { Page } from '@playwright/test';\nimport {\n existsSync,\n mkdirSync,\n mkdtempSync,\n readdirSync,\n readFileSync,\n realpathSync,\n rmSync,\n statSync,\n writeFileSync,\n} from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join, relative, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { parseArgs } from 'node:util';\nimport { type Catalog, createCatalog, scenarioJsonSchema, type ScenarioRule } from 'state-scenarios';\nimport { z } from 'zod';\nimport { checkServer, contextOptions, launchChromium, scenarioLink } from './browser.js';\nimport { CliError } from './errors.js';\nimport type { ChangedFile } from './map.js';\n\nconst HELP = `Usage: state-scenarios <command> <scenarios-dir> [options]\n\n init Create <dir> (default src/scenarios) with default.json, a sibling scenario.schema.json,\n print the wiring snippet, install the agent skills into .agents/skills and .claude/skills,\n and register the MCP server in .mcp.json. --no-skills skips the last two; --base-url sets the\n URL the MCP server opens (default http://localhost:5173, or :3000 for Next.js)\n guide Print the agent skill for this version: guide [topic]. guide alone lists the topics\n doctor Check the wiring one step at a time and print the fix for each problem: scenarios, dev server,\n MSW worker, session start, ready selector, save route, ffmpeg, skills and MCP. --base-url (required)\n mcp Serve an MCP server on stdio for coding agents: list, open and drive scenarios, check ready\n selectors, and save what the app shows as a scenario. --base-url (required)\n validate Check every scenario parses, resolves and passes --rules\n list Print scenarios with links (--base-url) for people and agents; --json for machines\n shoot Open each scenario in Chromium, wait for its ready selector, screenshot it,\n and report requests it didn't mock. --base-url (required), --out, --only a,b, --no-strict,\n --element <selector> to capture one element instead of the full page.\n Each failed scenario gets <out>/<name>.md: what failed and how to fix it.\n --compare <dir> diffs each shot against an earlier run's PNGs and writes\n <out>/<name>.diff.png (before | after | changed) for each state that changed\n affected List the scenarios a branch's changes can affect, and why: changed scenario files,\n map rows whose Source covers a changed file, endpoints and routes the changes touch.\n --base <ref> (default main), --src dir[,dir] to consider only the app's files,\n --map <file|dir>[,…], --json\n coverage Count the scenario map's states by status (covered, missing, can't mock), and report rows\n naming scenarios that don't exist and scenarios no row names. --map, --json.\n Exits 1 when the map names a missing scenario\n publish publish <file…> --folder <name>: commit files to the scenario-shots branch (--branch) without\n touching your checkout, push it (--remote, default origin), and print Markdown image links\n record Open each scenario that has steps, run them, and save a video with a visible cursor.\n --base-url and --steps (required): a module whose default export maps scenario names\n to async (page) => {} steps, or to { element, steps } to crop that one scenario.\n --format mp4 (default), gif or webm; --out, --only, --element, --fps (gif default 12),\n --width <px> to downscale, --hold <ms> on the last frame (default 800).\n --cursor-color <css> (default #ff2d55) or --cursor <file.svg> to draw your own pointer.\n Writes <name>.video.json next to each video: size, length, description and clicks.\n mp4, gif, --element, --fps and --width need ffmpeg on PATH (or FFMPEG_PATH)\n suggest Ask Claude which UI states lack scenarios and draft them into --out (default <dir>/proposed).\n --src dir[,dir] (required), --schema file, --max n, --model (default $STATE_SCENARIOS_MODEL or claude-opus-5-5).\n Needs ANTHROPIC_API_KEY or \\`ant auth login\\`; ANTHROPIC_BASE_URL points it at a compatible proxy.\n --env-file file loads those variables first.\n promote Move <dir>/proposed/*.json into <dir> after validate. --dry-run lists moves only.\n\nCommon: --rules file module exporting \\`rules\\` (ScenarioRule[])\n --json machine-readable output (list, shoot, doctor, affected, coverage, publish)`;\n\ndeclare global {\n interface Window {\n /** Set by record: reports each pointerdown for the video's manifest. */\n stateScenariosClick(x: number, y: number): void;\n }\n}\n\nconst MAX_SOURCE_CHARS = 800_000;\n\nconst { positionals, values } = parseArgs({\n allowPositionals: true,\n options: {\n rules: { type: 'string' },\n 'base-url': { type: 'string' },\n out: { type: 'string' },\n only: { type: 'string' },\n 'no-strict': { type: 'boolean' },\n 'no-panel': { type: 'boolean' },\n viewport: { type: 'string' },\n scale: { type: 'string' },\n 'color-scheme': { type: 'string' },\n element: { type: 'string' },\n locale: { type: 'string' },\n timezone: { type: 'string' },\n steps: { type: 'string' },\n format: { type: 'string' },\n fps: { type: 'string' },\n width: { type: 'string' },\n hold: { type: 'string' },\n cursor: { type: 'string' },\n 'cursor-color': { type: 'string' },\n json: { type: 'boolean' },\n src: { type: 'string' },\n schema: { type: 'string' },\n max: { type: 'string' },\n model: { type: 'string' },\n 'env-file': { type: 'string' },\n 'dry-run': { type: 'boolean' },\n 'no-skills': { type: 'boolean' },\n compare: { type: 'string' },\n base: { type: 'string' },\n map: { type: 'string' },\n folder: { type: 'string' },\n branch: { type: 'string' },\n remote: { type: 'string' },\n help: { type: 'boolean', short: 'h' },\n },\n});\n\nconst Steps = z.function({ input: [z.custom<Page>()], output: z.unknown() });\n\n/** `--steps` module: scenario name to the steps recorded after it opens, or to `{ element, steps }`. */\nconst StepsSchema = z.record(\n z.string(),\n z.union([\n Steps.transform((steps) => ({ steps, element: undefined })),\n z.object({\n element: z.string().optional(),\n steps: Steps,\n }),\n ]),\n);\n\n/** A number flag, undefined when not passed. */\nconst positive = (flag: 'fps' | 'width' | 'hold') => {\n const text = values[flag];\n const n = Number(text);\n\n if (text !== undefined && !(Number.isFinite(n) && n > 0)) {\n throw new CliError('INVALID_ARGUMENT', `--${flag} must be a positive number, not \"${text}\"`);\n }\n\n return text === undefined ? undefined : n;\n};\n\nconst [command, dir = command === 'init' ? 'src/scenarios' : undefined] = positionals;\n\nif (command === 'guide') {\n const { guide } = await import('./skills.js');\n\n try {\n console.log(guide(positionals[1]));\n process.exit(0);\n } catch (err) {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(2);\n }\n}\n\nif (command === 'publish') {\n const { publishFiles } = await import('./publish.js');\n const files = positionals.slice(1);\n\n try {\n if (!files.length || !values.folder) {\n throw new CliError(\n 'INVALID_ARGUMENT',\n 'publish needs files and --folder, e.g. publish shots/*.png --folder pr-42',\n );\n }\n\n const links = publishFiles({\n files,\n folder: values.folder,\n branch: values.branch,\n remote: values.remote,\n });\n\n // Markdown images for a GitHub remote; elsewhere, where each file landed.\n const lines = links.map((\n l,\n ) => (l.url.startsWith('https://') ? `` : `${l.file} ${l.url}`));\n\n console.log(values.json ? JSON.stringify(links, null, 2) : lines.join('\\n'));\n process.exit(0);\n } catch (err) {\n // A failed git command carries git's own message on stderr.\n const detail = z.object({ stderr: z.string() }).safeParse(err).data?.stderr ?? '';\n console.error(\n `state-scenarios publish: ${err instanceof Error ? err.message : String(err)}\\n${detail}`.trim(),\n );\n process.exit(err instanceof CliError ? err.exitCode : 1);\n }\n}\n\nif (values.help || !command || !dir) {\n console.log(HELP);\n process.exit(command ? 2 : 0);\n}\n\n/** Read the scenarios folder; called again by long-running commands to see new files. */\nconst loadCatalog = (rules: ScenarioRule[]): Catalog => {\n try {\n return createCatalog(\n readdirSync(dir).filter((f) => f.endsWith('.json')).map((f) =>\n JSON.parse(readFileSync(join(dir, f), 'utf8'))\n ),\n { rules },\n );\n } catch (err) {\n throw new CliError('INVALID_SCENARIO', err instanceof Error ? err.message : String(err));\n }\n};\n\nconst requireBaseUrl = () => {\n const url = values['base-url'];\n\n if (!url) throw new CliError('INVALID_ARGUMENT', `${command} needs --base-url, e.g. http://localhost:5173`);\n\n return url;\n};\n\ntry {\n if (command === 'init') {\n const { initScenarios } = await import('./init.js');\n const { stack } = initScenarios(dir);\n\n if (!values['no-skills']) {\n const { installSkills, registerMcp } = await import('./skills.js');\n\n const url = values['base-url']\n ?? (stack === 'next' ? 'http://localhost:3000' : 'http://localhost:5173');\n\n const args = ['mcp', dir, '--base-url', url];\n console.log('');\n\n for (const line of [...installSkills(process.cwd()), ...registerMcp(process.cwd(), args)]) {\n console.log(line);\n }\n\n console.log(`\nCodex: codex mcp add state-scenarios -- npx state-scenarios ${args.join(' ')}\nFor agents that need a pointer, add to AGENTS.md (or CLAUDE.md):\n UI states use state-scenarios; read .agents/skills/state-scenarios/SKILL.md before adding or checking one.`);\n }\n\n process.exit(0);\n }\n\n const rules: ScenarioRule[] = values.rules\n ? (await import(pathToFileURL(resolve(values.rules)).href)).rules\n : [];\n\n if (command === 'promote') {\n const { promoteScenarios } = await import('./promote.js');\n const result = promoteScenarios(dir, { rules, dryRun: !!values['dry-run'] });\n process.exit(result.skipped.length ? 1 : 0);\n }\n\n if (command === 'mcp') {\n const { serveMcp } = await import('./mcp.js');\n\n const { version } = z.object({ version: z.string() }).parse(\n JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')),\n );\n\n await serveMcp({\n dir,\n baseURL: requireBaseUrl(),\n loadCatalog: () => loadCatalog(rules),\n flags: values,\n version,\n });\n process.exit(0);\n }\n\n if (command === 'doctor') {\n const { runDoctor } = await import('./doctor.js');\n const { EXIT_CODES, ERROR_CLASSES } = await import('./errors.js');\n\n const checks = await runDoctor({\n dir,\n baseURL: requireBaseUrl(),\n loadCatalog: () => loadCatalog(rules),\n flags: values,\n });\n\n if (values.json) console.log(JSON.stringify(checks, null, 2));\n else {\n for (const c of checks) {\n console.log(\n `${{ ok: '✓', warn: '!', fail: '✗' }[c.status]} ${c.name.padEnd(16)} ${\n c.code ? `[${c.code}] ` : ''\n }${c.detail}`,\n );\n\n if (c.fix && c.status !== 'ok') console.log(` ${' '.repeat(16)} fix: ${c.fix}`);\n }\n }\n\n const failed = checks.filter((c) => c.status === 'fail');\n process.exit(Math.max(0, ...failed.map((c) => (c.code ? EXIT_CODES[ERROR_CLASSES[c.code]] : 1))));\n }\n\n const catalog = loadCatalog(rules);\n\n switch (command) {\n case 'validate': {\n const problems = catalog.check();\n\n for (const p of problems) console.error(`✗ ${p}\\n`);\n console.log(`${catalog.list.length - problems.length}/${catalog.list.length} scenarios valid`);\n process.exit(problems.length ? 1 : 0);\n }\n\n case 'list': {\n const base = values['base-url'];\n\n const rows = catalog.list.map((s) => ({\n name: s.name,\n description: s.description ?? '',\n tags: s.tags ?? [],\n ready: s.ready ?? null,\n url: base\n ? `${base.replace(/\\/$/, '')}${catalog.resolve(s).path ?? '/'}?scenario=${\n encodeURIComponent(s.name)\n }`\n : null,\n }));\n\n if (values.json) console.log(JSON.stringify(rows, null, 2));\n else {\n for (const r of rows) {\n console.log(\n `${r.name.padEnd(32)} ${r.description}${r.url ? `\\n${' '.repeat(33)}${r.url}` : ''}`,\n );\n }\n }\n\n break;\n }\n\n case 'affected': {\n const { affectedScenarios, findMapFiles, readMap } = await import('./map.js');\n const { execFileSync } = await import('node:child_process');\n const base = values.base ?? 'main';\n const git = (args: string[]) => execFileSync('git', args, { encoding: 'utf8' }).trim();\n\n let since: string;\n\n try {\n since = git(['merge-base', base, 'HEAD']);\n } catch {\n throw new CliError(\n 'INVALID_ARGUMENT',\n `can't find --base \"${base}\"; pass a branch or commit, e.g. --base origin/main`,\n );\n }\n\n // Committed, uncommitted and new files, as paths from here.\n const paths = new Set(\n [\n ...git(['diff', '--name-only', '--relative', since]).split('\\n'),\n ...git(['ls-files', '--others', '--exclude-standard']).split('\\n'),\n ].filter(Boolean),\n );\n\n // --src keeps it to the app's folders, so a monorepo's other packages don't show as unmatched.\n const src = values.src?.split(',').map((d) => `${d.replace(/^\\.\\//, '').replace(/\\/$/, '')}/`);\n\n const inScope = (path: string) =>\n !src || path.startsWith(`${dir.replace(/^\\.\\//, '')}/`) || src.some((d) => path.startsWith(d));\n\n const changed = [...paths].flatMap((path): ChangedFile[] => {\n if (!inScope(path)) return [];\n\n try {\n return [{ path, content: readFileSync(path, 'utf8') }];\n } catch {\n return [{ path }]; // deleted\n }\n });\n\n const rows = readMap(findMapFiles(values.map?.split(',')));\n const result = affectedScenarios({ changed, dir, catalog, rows });\n\n if (values.json) {\n console.log(JSON.stringify({ base, since, changed: changed.length, ...result }, null, 2));\n break;\n }\n\n console.log(\n `${changed.length} files changed since ${base} (${since.slice(0, 9)})${\n rows.length ? '' : '; no scenario map found'\n }\\n`,\n );\n\n for (const s of result.scenarios) {\n console.log(s.name);\n\n for (const r of s.reasons) console.log(` ${r.kind}: ${r.detail}`);\n }\n\n if (!result.scenarios.length) console.log('No scenario matched.');\n\n if (result.unmatched.length) {\n console.log('\\nChanged source files no scenario points at (shared ones can change every state):');\n\n const deleted = new Set(changed.flatMap((f) => (f.content === undefined ? [f.path] : [])));\n\n for (const f of result.unmatched) console.log(` ${f}${deleted.has(f) ? ' (deleted)' : ''}`);\n }\n\n if (result.scenarios.length) console.log(`\\n--only ${result.scenarios.map((s) => s.name).join(',')}`);\n break;\n }\n\n case 'coverage': {\n const { coverage, findMapFiles, readMap } = await import('./map.js');\n const files = findMapFiles(values.map?.split(','));\n\n if (!files.length) {\n throw new CliError(\n 'INVALID_ARGUMENT',\n 'no scenario map found in .agents/skills/scenarios-*; run /create-scenario-map, or pass --map',\n );\n }\n\n const result = coverage(readMap(files), catalog);\n\n if (values.json) console.log(JSON.stringify({ maps: files, ...result }, null, 2));\n else {\n const percent = Math.round(result.ratio * 100);\n console.log(\n `${result.covered}/${\n result.covered + result.missing\n } states covered (${percent}%), ${result.cantMock} can't be mocked`,\n );\n\n if (result.missingStates.length) console.log('\\nMissing:');\n\n for (const r of result.missingStates) {\n const state = [r.feature, r.state].filter(Boolean).join(' / ');\n console.log(` ${state.padEnd(48)} ${r.scenario || '-'} ${r.at}`);\n }\n\n if (result.stale.length) console.log('\\nCovered rows naming a scenario that does not exist:');\n\n for (const r of result.stale) console.log(` ${(r.scenario || '(none)').padEnd(48)} ${r.at}`);\n\n if (result.unmapped.length) {\n console.log(`\\nScenarios no map row names: ${result.unmapped.join(', ')}`);\n }\n }\n\n process.exit(result.stale.length ? 1 : 0);\n }\n\n case 'shoot': {\n const baseURL = requireBaseUrl();\n const out = values.out ?? 'scenario-shots';\n const only = values.only?.split(',');\n\n for (const n of only ?? []) {\n if (!catalog.get(n)) throw new CliError('UNKNOWN_SCENARIO', `--only names unknown scenario \"${n}\"`);\n }\n\n // Shooting into the baseline overwrites each PNG before it's compared, so every state reads \"same\".\n const real = (path: string) => (existsSync(path) ? realpathSync(path) : resolve(path));\n\n if (values.compare && real(values.compare) === real(out)) {\n throw new CliError(\n 'INVALID_ARGUMENT',\n `--compare and --out are the same folder (${out}); shoot into a new one, e.g. --out .shots/after`,\n );\n }\n\n mkdirSync(out, { recursive: true });\n await checkServer(baseURL);\n\n const { inspectScenario, failurePage } = await import('./inspect.js');\n const { compareImages } = await import('./compare.js');\n const before = values.compare;\n const browser = await launchChromium();\n const report = [];\n\n for (const s of catalog.list.filter((s) => !only || only.includes(s.name))) {\n const scenario = catalog.resolve(s);\n const context = await browser.newContext({ baseURL, ...contextOptions(scenario, values) });\n const page = await context.newPage();\n page.setDefaultTimeout(10_000);\n\n // Uncaught exceptions and console errors fail the run too.\n const result = await inspectScenario(page, scenario, {\n link: scenarioLink(baseURL, scenario),\n strict: !values['no-strict'],\n panel: !values['no-panel'],\n });\n\n const screenshot = join(out, `${s.name}.png`);\n let captured = false;\n\n if (values.element && !result.error) {\n try {\n await page.locator(values.element).first().screenshot({ path: screenshot });\n captured = true;\n } catch (err) {\n result.codes.push('ELEMENT_NOT_FOUND');\n result.ok = false;\n result.error = `--element: ${err instanceof Error ? err.message.split('\\n')[0] : String(err)}`;\n }\n }\n\n // The full page shows what went wrong when the scenario or the element failed.\n if (!captured) await page.screenshot({ path: screenshot, fullPage: true });\n\n // A page per failure, written for whoever fixes it; a stale one would mislead.\n const details = join(out, `${s.name}.md`);\n rmSync(details, { force: true });\n\n if (!result.ok) {\n writeFileSync(details, failurePage(result, { screenshot, appOrigin: new URL(baseURL).origin }));\n }\n\n await context.close();\n\n // Clear this state's diff from an earlier run into the same folder.\n const diffImage = join(out, `${s.name}.diff.png`);\n rmSync(diffImage, { force: true });\n const previous = before && join(before, `${s.name}.png`);\n let compare: { status: 'same' | 'changed' | 'new'; ratio: number; image: string | null; } | undefined;\n\n if (previous && existsSync(previous)) {\n const { changed, total, image } = await compareImages(\n browser,\n readFileSync(previous),\n readFileSync(screenshot),\n );\n\n if (image) writeFileSync(diffImage, image);\n compare = {\n status: changed ? 'changed' : 'same',\n ratio: changed / total,\n image: image ? diffImage : null,\n };\n } else if (previous) {\n compare = { status: 'new', ratio: 1, image: null };\n }\n\n report.push({ ...result, screenshot, details: result.ok ? null : details, compare });\n }\n\n await browser.close();\n\n // States the earlier run shot that no longer exist (with --only, only the ones asked for).\n const removed = before && existsSync(before)\n ? readdirSync(before)\n .filter((f) => f.endsWith('.png') && !f.endsWith('.diff.png'))\n .map((f) => f.slice(0, -'.png'.length))\n .filter((n) => !catalog.get(n) && (!only || only.includes(n)))\n : [];\n\n if (values.json) console.log(JSON.stringify(before ? { scenarios: report, removed } : report, null, 2));\n else {\n for (const r of report) {\n const change = r.compare && r.compare.status !== 'same'\n ? ` ${r.compare.status}${\n r.compare.image ? ` ${r.compare.image} (${(r.compare.ratio * 100).toFixed(1)}% of pixels)` : ''\n }`\n : '';\n\n console.log(`${r.ok ? '✓' : '✗'} ${r.name.padEnd(32)} ${r.screenshot}${change}`);\n\n if (r.ok) continue;\n console.log(` ${r.codes.join(', ')}${r.error ? `: ${r.error}` : ''}`);\n\n for (const u of r.unhandled) console.log(` unmocked: ${u}`);\n\n for (const e of r.pageErrors) console.log(` uncaught: ${e}`);\n\n for (const e of r.consoleErrors) console.log(` console.error: ${e}`);\n console.log(` ❯ details ${r.details}`);\n }\n\n for (const n of removed) console.log(`- ${n.padEnd(32)} removed since ${before}`);\n\n if (before) {\n const changed = report.filter((r) => r.compare && r.compare.status !== 'same').length;\n console.log(`\\n${changed} of ${report.length} states changed or new since ${before}`);\n }\n }\n\n process.exit(report.every((r) => r.ok) ? 0 : 1);\n }\n\n case 'record': {\n const baseURL = requireBaseUrl();\n\n if (!values.steps) {\n throw new CliError('INVALID_ARGUMENT', 'record needs --steps, e.g. --steps scenarios/steps.ts');\n }\n\n const format = values.format ?? 'mp4';\n\n if (format !== 'mp4' && format !== 'gif' && format !== 'webm') {\n throw new CliError('INVALID_ARGUMENT', `--format must be mp4, gif or webm, not \"${format}\"`);\n }\n\n const steps = StepsSchema.parse((await import(pathToFileURL(resolve(values.steps)).href)).default);\n const only = values.only?.split(',');\n const names = Object.keys(steps).filter((n) => !only || only.includes(n));\n\n for (const n of names) {\n if (!catalog.get(n)) throw new CliError('UNKNOWN_SCENARIO', `--steps names unknown scenario \"${n}\"`);\n }\n\n const fps = positive('fps');\n const width = positive('width');\n const hold = positive('hold') ?? 800;\n\n const cursor = {\n color: values['cursor-color'],\n svg: values.cursor ? readFileSync(values.cursor, 'utf8') : undefined,\n };\n\n const out = values.out ?? 'scenario-videos';\n mkdirSync(out, { recursive: true });\n const raw = mkdtempSync(join(tmpdir(), 'state-scenarios-'));\n\n await checkServer(baseURL);\n const { openScenario, showCursor } = await import('state-scenarios/playwright');\n const record = await import('./record.js');\n const { encodeVideo, glideBeforeClick, pagePoint, videoManifest, visibleCrop } = record;\n // slowMo spaces out steps so viewers can follow them; glideBeforeClick lets the cursor arrive first.\n const browser = await launchChromium({ slowMo: 150 });\n let exitCode = 0;\n\n /**\n * Where the element sits in the video once the scenario is ready. Scrolled into view first:\n * video holds only the viewport. Measured before the steps run, so steps that scroll the page\n * move the element out of the crop.\n */\n const cropTo = async (page: Page, selector: string, viewport: { width: number; height: number; }) => {\n const element = page.locator(selector).first();\n\n const box = await element.scrollIntoViewIfNeeded({ timeout: 5000 }).then(\n () => element.boundingBox(),\n () => null,\n );\n\n const visible = box && visibleCrop(box, viewport);\n\n if (!visible) throw new CliError('ELEMENT_NOT_FOUND', `element \"${selector}\" isn't visible`);\n\n if (visible.clipped) {\n console.log(\n ` element \"${selector}\" (${Math.round(box.width)}x${\n Math.round(box.height)\n }) is larger than the viewport; `\n + `the video shows the visible part. Use a larger --viewport to fit it.`,\n );\n }\n\n return visible.crop;\n };\n\n for (const [name, entry] of Object.entries(steps).filter(([n]) => names.includes(n))) {\n const { element = values.element, steps: run } = entry;\n const resolved = catalog.resolve(catalog.get(name) ?? { name });\n const options = contextOptions(resolved, values);\n\n // Playwright records video at CSS-pixel size whatever the scale, so use a larger --viewport for sharper video.\n const context = await browser.newContext({\n baseURL,\n ...options,\n recordVideo: { dir: raw, size: options.viewport },\n });\n\n const page = await context.newPage();\n const began = Date.now();\n const file = join(out, `${name}.${format}`);\n const clicks: Promise<{ at: number; x: number; y: number; } | undefined>[] = [];\n\n try {\n await showCursor(page, cursor);\n glideBeforeClick(page);\n // Clicks go in the video's manifest, for editors that zoom to them. The listener runs in\n // every frame, so iframe clicks map through the iframe's place and scale; a detached one drops.\n await page.exposeBinding('stateScenariosClick', ({ frame }, x: number, y: number) => {\n const at = Date.now();\n\n clicks.push(\n pagePoint(frame, x, y).then((point) => ({ at, ...point }), () => undefined),\n );\n });\n await page.addInitScript(() => {\n addEventListener('pointerdown', (e) => window.stateScenariosClick(e.clientX, e.clientY), true);\n });\n await openScenario(page, name, { path: resolved.path, strict: !values['no-strict'], panel: false });\n const crop = element ? await cropTo(page, element, options.viewport) : undefined;\n\n const start = (Date.now() - began) / 1000;\n await Promise.resolve(run(page)).catch((err: Error) => {\n throw new CliError('STEPS_FAILED', `steps failed: ${err.message.split('\\n')[0]}`);\n });\n await page.waitForTimeout(hold); // hold the final frame\n const end = Date.now();\n await context.close();\n\n const video = page.video();\n\n if (!video) throw new Error('Playwright recorded no video');\n\n const trimmed = encodeVideo({\n input: await video.path(),\n output: file,\n format,\n start,\n crop,\n fps,\n width,\n });\n\n const manifest = videoManifest({\n name,\n description: resolved.description,\n video: `${name}.${format}`,\n viewport: options.viewport,\n crop,\n width,\n start: began + trimmed.start * 1000,\n end,\n clicks: (await Promise.all(clicks)).filter((c) => c !== undefined),\n });\n\n writeFileSync(join(out, `${name}.video.json`), `${JSON.stringify(manifest, null, 2)}\\n`);\n console.log(`✓ ${name.padEnd(32)} ${file}`);\n } catch (err) {\n exitCode = Math.max(exitCode, err instanceof CliError ? err.exitCode : 1);\n await context.close();\n const code = err instanceof CliError ? `[${err.code}] ` : '';\n console.log(\n `✗ ${name.padEnd(32)} ${code}${err instanceof Error ? err.message.split('\\n')[0] : String(err)}`,\n );\n }\n }\n\n await browser.close();\n rmSync(raw, { recursive: true, force: true });\n process.exit(exitCode);\n }\n\n case 'suggest': {\n if (!values.src) {\n throw new CliError('INVALID_ARGUMENT', 'suggest needs --src, e.g. --src src/pages,src/components');\n }\n\n const sources = values.src.split(',').flatMap((d) => walk(d));\n const size = sources.reduce((n, f) => n + f.content.length, 0);\n\n if (size > MAX_SOURCE_CHARS) {\n throw new Error(\n `--src is ${size.toLocaleString()} characters; narrow it to the screens you want covered`,\n );\n }\n\n const jsonSchema = values.schema\n ? JSON.parse(readFileSync(values.schema, 'utf8'))\n : scenarioJsonSchema();\n\n if (values['env-file']) process.loadEnvFile(values['env-file']);\n const { default: Anthropic } = await import('@anthropic-ai/sdk');\n const model = values.model ?? process.env.STATE_SCENARIOS_MODEL ?? undefined;\n const { suggestScenarios } = await import('./suggest.js');\n console.error(\n `Reading ${sources.length} files (${size.toLocaleString()} chars) and ${catalog.list.length} scenarios with ${\n model ?? 'claude-opus-5-5'\n }…`,\n );\n\n const result = await suggestScenarios(\n { sources, catalog, jsonSchema, max: Number(values.max ?? 8), model },\n {\n client: new Anthropic({\n baseURL: process.env.ANTHROPIC_BASE_URL || undefined,\n // Some Anthropic-compatible proxies require a session id and a user agent.\n defaultHeaders: {\n 'x-opencode-session': `state-scenarios-${Date.now()}`,\n 'User-Agent': 'state-scenarios/0.0 (suggest)',\n },\n }),\n },\n );\n\n console.log('\\nCandidate UI states (not exhaustive)\\n');\n\n for (const s of result.states) {\n const mark = s.covered_by ? '✓' : '✗';\n console.log(\n `${mark} ${`${s.area} / ${s.state}`.padEnd(56)} ${s.covered_by ?? `uncovered (${s.priority})`}`,\n );\n }\n\n const covered = result.states.filter((s) => s.covered_by).length;\n console.log(\n `\\n${result.states.length} states detected, ${covered} have scenarios, ${\n result.states.length - covered\n } uncovered`,\n );\n\n const out = values.out ?? join(dir, 'proposed');\n\n if (result.accepted.length) mkdirSync(out, { recursive: true });\n\n for (const { scenario, covers } of result.accepted) {\n const file = join(out, `${scenario.name}.json`);\n const $schema = values.schema ? relative(out, values.schema).replaceAll('\\\\', '/') : undefined;\n writeFileSync(file, `${JSON.stringify({ $schema, ...scenario }, null, 2)}\\n`);\n console.log(`+ ${file} (${covers})`);\n }\n\n for (const r of result.rejected) console.log(`- rejected proposal for ${r.covers}: ${r.error}`);\n\n if (result.accepted.length) {\n console.log(\n `\\nReview the drafts, then: state-scenarios promote ${dir}`,\n );\n }\n\n break;\n }\n\n default:\n console.error(`Unknown command \"${command}\"\\n\\n${HELP}`);\n process.exit(2);\n }\n} catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const hint = /authentication/i.test(message) ? '\\nSet ANTHROPIC_API_KEY, or run `ant auth login`.' : '';\n const code = err instanceof CliError ? `[${err.code}] ` : '';\n console.error(`state-scenarios ${command}: ${code}${message}${hint}`);\n process.exit(err instanceof CliError ? err.exitCode : 1);\n}\n\nfunction walk(path: string): { path: string; content: string; }[] {\n if (statSync(path).isDirectory()) {\n return readdirSync(path)\n .filter((f) => !['node_modules', 'dist', 'scenarios'].includes(f))\n .flatMap((f) => walk(join(path, f)));\n }\n\n const isSource = /\\.(tsx?|jsx?|vue|svelte)$/.test(path) && !/\\.(d|test|spec)\\.\\w+$/.test(path);\n\n return isSource ? [{ path, content: readFileSync(path, 'utf8') }] : [];\n}\n"],"mappings":"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","names":["s"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport type { Page } from '@playwright/test';\nimport {\n existsSync,\n mkdirSync,\n mkdtempSync,\n readdirSync,\n readFileSync,\n realpathSync,\n rmSync,\n statSync,\n writeFileSync,\n} from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join, relative, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { parseArgs } from 'node:util';\nimport { type Catalog, createCatalog, scenarioJsonSchema, type ScenarioRule } from 'state-scenarios';\nimport { z } from 'zod';\nimport { checkServer, contextOptions, launchChromium, scenarioLink } from './browser.js';\nimport { CliError } from './errors.js';\nimport type { ChangedFile } from './map.js';\n\nconst HELP = `Usage: state-scenarios <command> <scenarios-dir> [options]\n\n init Create <dir> (default src/scenarios) with default.json, a sibling scenario.schema.json,\n print the wiring snippet, install the agent skills into .agents/skills and .claude/skills,\n and register the MCP server in .mcp.json. --no-skills skips the last two; --base-url sets the\n URL the MCP server opens (default http://localhost:5173, or :3000 for Next.js)\n guide Print the agent skill for this version: guide [topic]. guide alone lists the topics\n doctor Check the wiring one step at a time and print the fix for each problem: scenarios, dev server,\n MSW worker, session start, ready selector, save route, ffmpeg, skills and MCP. --base-url (required)\n mcp Serve an MCP server on stdio for coding agents: list, open and drive scenarios, check ready\n selectors, and save what the app shows as a scenario. --base-url (required)\n validate Check every scenario parses, resolves and passes --rules\n list Print scenarios with links (--base-url) for people and agents; --json for machines\n shoot Open each scenario in Chromium, wait for its ready selector, screenshot it,\n and report requests it didn't mock. --base-url (required), --out, --only a,b, --no-strict,\n --element <selector> to capture one element instead of the full page.\n Each failed scenario gets <out>/<name>.md: what failed and how to fix it.\n --compare <dir> diffs each shot against an earlier run's PNGs and writes\n <out>/<name>.diff.png (before | after | changed) for each state that changed\n affected List the scenarios a branch's changes can affect, and why: changed scenario files,\n map rows whose Source covers a changed file, endpoints and routes the changes touch.\n --base <ref> (default main), --src dir[,dir] to consider only the app's files,\n --map <file|dir>[,…], --json\n coverage Count the scenario map's states by status (covered, missing, can't mock), and report rows\n naming scenarios that don't exist and scenarios no row names. --map, --json.\n Exits 1 when the map names a missing scenario\n publish publish <file…> --folder <name>: commit files to the scenario-shots branch (--branch) without\n touching your checkout, push it (--remote, default origin), and print Markdown image links\n record Open each scenario that has steps, run them, and save a video with a visible cursor.\n --base-url and --steps (required): a module whose default export maps scenario names\n to async (page) => {} steps, or to { element, steps } to crop that one scenario.\n --format mp4 (default), gif or webm; --out, --only, --element, --fps (gif default 12),\n --width <px> to downscale, --hold <ms> on the last frame (default 800).\n --cursor-color <css> (default #ff2d55) or --cursor <file.svg> to draw your own pointer.\n Writes <name>.video.json next to each video: size, length, description and clicks.\n mp4, gif, --element, --fps and --width need ffmpeg on PATH (or FFMPEG_PATH)\n suggest Ask Claude which UI states lack scenarios and draft them into --out (default <dir>/proposed).\n --src dir[,dir] (required), --schema file, --max n, --model (default $STATE_SCENARIOS_MODEL or claude-opus-5-5).\n Needs ANTHROPIC_API_KEY or \\`ant auth login\\`; ANTHROPIC_BASE_URL points it at a compatible proxy.\n --env-file file loads those variables first.\n promote Move <dir>/proposed/*.json into <dir> after validate. --dry-run lists moves only.\n\nCommon: --rules file module exporting \\`rules\\` (ScenarioRule[])\n --json machine-readable output (list, shoot, doctor, affected, coverage, publish)`;\n\ndeclare global {\n interface Window {\n /** Set by record: reports each pointerdown, and each field taking focus, for the video's manifest. */\n stateScenariosClick(x: number, y: number, kind?: 'focus'): void;\n }\n}\n\nconst MAX_SOURCE_CHARS = 800_000;\n\nconst { positionals, values } = parseArgs({\n allowPositionals: true,\n options: {\n rules: { type: 'string' },\n 'base-url': { type: 'string' },\n out: { type: 'string' },\n only: { type: 'string' },\n 'no-strict': { type: 'boolean' },\n 'no-panel': { type: 'boolean' },\n viewport: { type: 'string' },\n scale: { type: 'string' },\n 'color-scheme': { type: 'string' },\n element: { type: 'string' },\n locale: { type: 'string' },\n timezone: { type: 'string' },\n steps: { type: 'string' },\n format: { type: 'string' },\n fps: { type: 'string' },\n width: { type: 'string' },\n hold: { type: 'string' },\n cursor: { type: 'string' },\n 'cursor-color': { type: 'string' },\n json: { type: 'boolean' },\n src: { type: 'string' },\n schema: { type: 'string' },\n max: { type: 'string' },\n model: { type: 'string' },\n 'env-file': { type: 'string' },\n 'dry-run': { type: 'boolean' },\n 'no-skills': { type: 'boolean' },\n compare: { type: 'string' },\n base: { type: 'string' },\n map: { type: 'string' },\n folder: { type: 'string' },\n branch: { type: 'string' },\n remote: { type: 'string' },\n help: { type: 'boolean', short: 'h' },\n },\n});\n\nconst Steps = z.function({ input: [z.custom<Page>()], output: z.unknown() });\n\n/** `--steps` module: scenario name to the steps recorded after it opens, or to `{ element, steps }`. */\nconst StepsSchema = z.record(\n z.string(),\n z.union([\n Steps.transform((steps) => ({ steps, element: undefined })),\n z.object({\n element: z.string().optional(),\n steps: Steps,\n }),\n ]),\n);\n\n/** A number flag, undefined when not passed. */\nconst positive = (flag: 'fps' | 'width' | 'hold') => {\n const text = values[flag];\n const n = Number(text);\n\n if (text !== undefined && !(Number.isFinite(n) && n > 0)) {\n throw new CliError('INVALID_ARGUMENT', `--${flag} must be a positive number, not \"${text}\"`);\n }\n\n return text === undefined ? undefined : n;\n};\n\nconst [command, dir = command === 'init' ? 'src/scenarios' : undefined] = positionals;\n\nif (command === 'guide') {\n const { guide } = await import('./skills.js');\n\n try {\n console.log(guide(positionals[1]));\n process.exit(0);\n } catch (err) {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(2);\n }\n}\n\nif (command === 'publish') {\n const { publishFiles } = await import('./publish.js');\n const files = positionals.slice(1);\n\n try {\n if (!files.length || !values.folder) {\n throw new CliError(\n 'INVALID_ARGUMENT',\n 'publish needs files and --folder, e.g. publish shots/*.png --folder pr-42',\n );\n }\n\n const links = publishFiles({\n files,\n folder: values.folder,\n branch: values.branch,\n remote: values.remote,\n });\n\n // Markdown images for a GitHub remote; elsewhere, where each file landed.\n const lines = links.map((\n l,\n ) => (l.url.startsWith('https://') ? `` : `${l.file} ${l.url}`));\n\n console.log(values.json ? JSON.stringify(links, null, 2) : lines.join('\\n'));\n process.exit(0);\n } catch (err) {\n // A failed git command carries git's own message on stderr.\n const detail = z.object({ stderr: z.string() }).safeParse(err).data?.stderr ?? '';\n console.error(\n `state-scenarios publish: ${err instanceof Error ? err.message : String(err)}\\n${detail}`.trim(),\n );\n process.exit(err instanceof CliError ? err.exitCode : 1);\n }\n}\n\nif (values.help || !command || !dir) {\n console.log(HELP);\n process.exit(command ? 2 : 0);\n}\n\n/** Read the scenarios folder; called again by long-running commands to see new files. */\nconst loadCatalog = (rules: ScenarioRule[]): Catalog => {\n try {\n return createCatalog(\n readdirSync(dir).filter((f) => f.endsWith('.json')).map((f) =>\n JSON.parse(readFileSync(join(dir, f), 'utf8'))\n ),\n { rules },\n );\n } catch (err) {\n throw new CliError('INVALID_SCENARIO', err instanceof Error ? err.message : String(err));\n }\n};\n\nconst requireBaseUrl = () => {\n const url = values['base-url'];\n\n if (!url) throw new CliError('INVALID_ARGUMENT', `${command} needs --base-url, e.g. http://localhost:5173`);\n\n return url;\n};\n\ntry {\n if (command === 'init') {\n const { initScenarios } = await import('./init.js');\n const { stack } = initScenarios(dir);\n\n if (!values['no-skills']) {\n const { installSkills, registerMcp } = await import('./skills.js');\n\n const url = values['base-url']\n ?? (stack === 'next' ? 'http://localhost:3000' : 'http://localhost:5173');\n\n const args = ['mcp', dir, '--base-url', url];\n console.log('');\n\n for (const line of [...installSkills(process.cwd()), ...registerMcp(process.cwd(), args)]) {\n console.log(line);\n }\n\n console.log(`\nCodex: codex mcp add state-scenarios -- npx state-scenarios ${args.join(' ')}\nFor agents that need a pointer, add to AGENTS.md (or CLAUDE.md):\n UI states use state-scenarios; read .agents/skills/state-scenarios/SKILL.md before adding or checking one.`);\n }\n\n process.exit(0);\n }\n\n const rules: ScenarioRule[] = values.rules\n ? (await import(pathToFileURL(resolve(values.rules)).href)).rules\n : [];\n\n if (command === 'promote') {\n const { promoteScenarios } = await import('./promote.js');\n const result = promoteScenarios(dir, { rules, dryRun: !!values['dry-run'] });\n process.exit(result.skipped.length ? 1 : 0);\n }\n\n if (command === 'mcp') {\n const { serveMcp } = await import('./mcp.js');\n\n const { version } = z.object({ version: z.string() }).parse(\n JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')),\n );\n\n await serveMcp({\n dir,\n baseURL: requireBaseUrl(),\n loadCatalog: () => loadCatalog(rules),\n flags: values,\n version,\n });\n process.exit(0);\n }\n\n if (command === 'doctor') {\n const { runDoctor } = await import('./doctor.js');\n const { EXIT_CODES, ERROR_CLASSES } = await import('./errors.js');\n\n const checks = await runDoctor({\n dir,\n baseURL: requireBaseUrl(),\n loadCatalog: () => loadCatalog(rules),\n flags: values,\n });\n\n if (values.json) console.log(JSON.stringify(checks, null, 2));\n else {\n for (const c of checks) {\n console.log(\n `${{ ok: '✓', warn: '!', fail: '✗' }[c.status]} ${c.name.padEnd(16)} ${\n c.code ? `[${c.code}] ` : ''\n }${c.detail}`,\n );\n\n if (c.fix && c.status !== 'ok') console.log(` ${' '.repeat(16)} fix: ${c.fix}`);\n }\n }\n\n const failed = checks.filter((c) => c.status === 'fail');\n process.exit(Math.max(0, ...failed.map((c) => (c.code ? EXIT_CODES[ERROR_CLASSES[c.code]] : 1))));\n }\n\n const catalog = loadCatalog(rules);\n\n switch (command) {\n case 'validate': {\n const problems = catalog.check();\n\n for (const p of problems) console.error(`✗ ${p}\\n`);\n console.log(`${catalog.list.length - problems.length}/${catalog.list.length} scenarios valid`);\n process.exit(problems.length ? 1 : 0);\n }\n\n case 'list': {\n const base = values['base-url'];\n\n const rows = catalog.list.map((s) => ({\n name: s.name,\n description: s.description ?? '',\n tags: s.tags ?? [],\n ready: s.ready ?? null,\n url: base\n ? `${base.replace(/\\/$/, '')}${catalog.resolve(s).path ?? '/'}?scenario=${\n encodeURIComponent(s.name)\n }`\n : null,\n }));\n\n if (values.json) console.log(JSON.stringify(rows, null, 2));\n else {\n for (const r of rows) {\n console.log(\n `${r.name.padEnd(32)} ${r.description}${r.url ? `\\n${' '.repeat(33)}${r.url}` : ''}`,\n );\n }\n }\n\n break;\n }\n\n case 'affected': {\n const { affectedScenarios, findMapFiles, readMap } = await import('./map.js');\n const { execFileSync } = await import('node:child_process');\n const base = values.base ?? 'main';\n const git = (args: string[]) => execFileSync('git', args, { encoding: 'utf8' }).trim();\n\n let since: string;\n\n try {\n since = git(['merge-base', base, 'HEAD']);\n } catch {\n throw new CliError(\n 'INVALID_ARGUMENT',\n `can't find --base \"${base}\"; pass a branch or commit, e.g. --base origin/main`,\n );\n }\n\n // Committed, uncommitted and new files, as paths from here.\n const paths = new Set(\n [\n ...git(['diff', '--name-only', '--relative', since]).split('\\n'),\n ...git(['ls-files', '--others', '--exclude-standard']).split('\\n'),\n ].filter(Boolean),\n );\n\n // --src keeps it to the app's folders, so a monorepo's other packages don't show as unmatched.\n const src = values.src?.split(',').map((d) => `${d.replace(/^\\.\\//, '').replace(/\\/$/, '')}/`);\n\n const inScope = (path: string) =>\n !src || path.startsWith(`${dir.replace(/^\\.\\//, '')}/`) || src.some((d) => path.startsWith(d));\n\n const changed = [...paths].flatMap((path): ChangedFile[] => {\n if (!inScope(path)) return [];\n\n try {\n return [{ path, content: readFileSync(path, 'utf8') }];\n } catch {\n return [{ path }]; // deleted\n }\n });\n\n const rows = readMap(findMapFiles(values.map?.split(',')));\n const result = affectedScenarios({ changed, dir, catalog, rows });\n\n if (values.json) {\n console.log(JSON.stringify({ base, since, changed: changed.length, ...result }, null, 2));\n break;\n }\n\n console.log(\n `${changed.length} files changed since ${base} (${since.slice(0, 9)})${\n rows.length ? '' : '; no scenario map found'\n }\\n`,\n );\n\n for (const s of result.scenarios) {\n console.log(s.name);\n\n for (const r of s.reasons) console.log(` ${r.kind}: ${r.detail}`);\n }\n\n if (!result.scenarios.length) console.log('No scenario matched.');\n\n if (result.unmatched.length) {\n console.log('\\nChanged source files no scenario points at (shared ones can change every state):');\n\n const deleted = new Set(changed.flatMap((f) => (f.content === undefined ? [f.path] : [])));\n\n for (const f of result.unmatched) console.log(` ${f}${deleted.has(f) ? ' (deleted)' : ''}`);\n }\n\n if (result.scenarios.length) console.log(`\\n--only ${result.scenarios.map((s) => s.name).join(',')}`);\n break;\n }\n\n case 'coverage': {\n const { coverage, findMapFiles, readMap } = await import('./map.js');\n const files = findMapFiles(values.map?.split(','));\n\n if (!files.length) {\n throw new CliError(\n 'INVALID_ARGUMENT',\n 'no scenario map found in .agents/skills/scenarios-*; run /create-scenario-map, or pass --map',\n );\n }\n\n const result = coverage(readMap(files), catalog);\n\n if (values.json) console.log(JSON.stringify({ maps: files, ...result }, null, 2));\n else {\n const percent = Math.round(result.ratio * 100);\n console.log(\n `${result.covered}/${\n result.covered + result.missing\n } states covered (${percent}%), ${result.cantMock} can't be mocked`,\n );\n\n if (result.missingStates.length) console.log('\\nMissing:');\n\n for (const r of result.missingStates) {\n const state = [r.feature, r.state].filter(Boolean).join(' / ');\n console.log(` ${state.padEnd(48)} ${r.scenario || '-'} ${r.at}`);\n }\n\n if (result.stale.length) console.log('\\nCovered rows naming a scenario that does not exist:');\n\n for (const r of result.stale) console.log(` ${(r.scenario || '(none)').padEnd(48)} ${r.at}`);\n\n if (result.unmapped.length) {\n console.log(`\\nScenarios no map row names: ${result.unmapped.join(', ')}`);\n }\n }\n\n process.exit(result.stale.length ? 1 : 0);\n }\n\n case 'shoot': {\n const baseURL = requireBaseUrl();\n const out = values.out ?? 'scenario-shots';\n const only = values.only?.split(',');\n\n for (const n of only ?? []) {\n if (!catalog.get(n)) throw new CliError('UNKNOWN_SCENARIO', `--only names unknown scenario \"${n}\"`);\n }\n\n // Shooting into the baseline overwrites each PNG before it's compared, so every state reads \"same\".\n const real = (path: string) => (existsSync(path) ? realpathSync(path) : resolve(path));\n\n if (values.compare && real(values.compare) === real(out)) {\n throw new CliError(\n 'INVALID_ARGUMENT',\n `--compare and --out are the same folder (${out}); shoot into a new one, e.g. --out .shots/after`,\n );\n }\n\n mkdirSync(out, { recursive: true });\n await checkServer(baseURL);\n\n const { inspectScenario, failurePage } = await import('./inspect.js');\n const { compareImages } = await import('./compare.js');\n const before = values.compare;\n const browser = await launchChromium();\n const report = [];\n\n for (const s of catalog.list.filter((s) => !only || only.includes(s.name))) {\n const scenario = catalog.resolve(s);\n const context = await browser.newContext({ baseURL, ...contextOptions(scenario, values) });\n const page = await context.newPage();\n page.setDefaultTimeout(10_000);\n\n // Uncaught exceptions and console errors fail the run too.\n const result = await inspectScenario(page, scenario, {\n link: scenarioLink(baseURL, scenario),\n strict: !values['no-strict'],\n panel: !values['no-panel'],\n });\n\n const screenshot = join(out, `${s.name}.png`);\n let captured = false;\n\n if (values.element && !result.error) {\n try {\n await page.locator(values.element).first().screenshot({ path: screenshot });\n captured = true;\n } catch (err) {\n result.codes.push('ELEMENT_NOT_FOUND');\n result.ok = false;\n result.error = `--element: ${err instanceof Error ? err.message.split('\\n')[0] : String(err)}`;\n }\n }\n\n // The full page shows what went wrong when the scenario or the element failed.\n if (!captured) await page.screenshot({ path: screenshot, fullPage: true });\n\n // A page per failure, written for whoever fixes it; a stale one would mislead.\n const details = join(out, `${s.name}.md`);\n rmSync(details, { force: true });\n\n if (!result.ok) {\n writeFileSync(details, failurePage(result, { screenshot, appOrigin: new URL(baseURL).origin }));\n }\n\n await context.close();\n\n // Clear this state's diff from an earlier run into the same folder.\n const diffImage = join(out, `${s.name}.diff.png`);\n rmSync(diffImage, { force: true });\n const previous = before && join(before, `${s.name}.png`);\n let compare: { status: 'same' | 'changed' | 'new'; ratio: number; image: string | null; } | undefined;\n\n if (previous && existsSync(previous)) {\n const { changed, total, image } = await compareImages(\n browser,\n readFileSync(previous),\n readFileSync(screenshot),\n );\n\n if (image) writeFileSync(diffImage, image);\n compare = {\n status: changed ? 'changed' : 'same',\n ratio: changed / total,\n image: image ? diffImage : null,\n };\n } else if (previous) {\n compare = { status: 'new', ratio: 1, image: null };\n }\n\n report.push({ ...result, screenshot, details: result.ok ? null : details, compare });\n }\n\n await browser.close();\n\n // States the earlier run shot that no longer exist (with --only, only the ones asked for).\n const removed = before && existsSync(before)\n ? readdirSync(before)\n .filter((f) => f.endsWith('.png') && !f.endsWith('.diff.png'))\n .map((f) => f.slice(0, -'.png'.length))\n .filter((n) => !catalog.get(n) && (!only || only.includes(n)))\n : [];\n\n if (values.json) console.log(JSON.stringify(before ? { scenarios: report, removed } : report, null, 2));\n else {\n for (const r of report) {\n const change = r.compare && r.compare.status !== 'same'\n ? ` ${r.compare.status}${\n r.compare.image ? ` ${r.compare.image} (${(r.compare.ratio * 100).toFixed(1)}% of pixels)` : ''\n }`\n : '';\n\n console.log(`${r.ok ? '✓' : '✗'} ${r.name.padEnd(32)} ${r.screenshot}${change}`);\n\n if (r.ok) continue;\n console.log(` ${r.codes.join(', ')}${r.error ? `: ${r.error}` : ''}`);\n\n for (const u of r.unhandled) console.log(` unmocked: ${u}`);\n\n for (const e of r.pageErrors) console.log(` uncaught: ${e}`);\n\n for (const e of r.consoleErrors) console.log(` console.error: ${e}`);\n console.log(` ❯ details ${r.details}`);\n }\n\n for (const n of removed) console.log(`- ${n.padEnd(32)} removed since ${before}`);\n\n if (before) {\n const changed = report.filter((r) => r.compare && r.compare.status !== 'same').length;\n console.log(`\\n${changed} of ${report.length} states changed or new since ${before}`);\n }\n }\n\n process.exit(report.every((r) => r.ok) ? 0 : 1);\n }\n\n case 'record': {\n const baseURL = requireBaseUrl();\n\n if (!values.steps) {\n throw new CliError('INVALID_ARGUMENT', 'record needs --steps, e.g. --steps scenarios/steps.ts');\n }\n\n const format = values.format ?? 'mp4';\n\n if (format !== 'mp4' && format !== 'gif' && format !== 'webm') {\n throw new CliError('INVALID_ARGUMENT', `--format must be mp4, gif or webm, not \"${format}\"`);\n }\n\n const steps = StepsSchema.parse((await import(pathToFileURL(resolve(values.steps)).href)).default);\n const only = values.only?.split(',');\n const names = Object.keys(steps).filter((n) => !only || only.includes(n));\n\n for (const n of names) {\n if (!catalog.get(n)) throw new CliError('UNKNOWN_SCENARIO', `--steps names unknown scenario \"${n}\"`);\n }\n\n const fps = positive('fps');\n const width = positive('width');\n const hold = positive('hold') ?? 800;\n\n const cursor = {\n color: values['cursor-color'],\n svg: values.cursor ? readFileSync(values.cursor, 'utf8') : undefined,\n };\n\n const out = values.out ?? 'scenario-videos';\n mkdirSync(out, { recursive: true });\n const raw = mkdtempSync(join(tmpdir(), 'state-scenarios-'));\n\n await checkServer(baseURL);\n const { openScenario, showCursor } = await import('state-scenarios/playwright');\n const record = await import('./record.js');\n const { encodeVideo, pagePoint, videoManifest, visibleCrop } = record;\n // slowMo spaces out steps so viewers can follow them; glide lets the cursor arrive first.\n const browser = await launchChromium({ slowMo: 150 });\n let exitCode = 0;\n\n /**\n * Where the element sits in the video once the scenario is ready. Scrolled into view first:\n * video holds only the viewport. Measured before the steps run, so steps that scroll the page\n * move the element out of the crop.\n */\n const cropTo = async (page: Page, selector: string, viewport: { width: number; height: number; }) => {\n const element = page.locator(selector).first();\n\n const box = await element.scrollIntoViewIfNeeded({ timeout: 5000 }).then(\n () => element.boundingBox(),\n () => null,\n );\n\n const visible = box && visibleCrop(box, viewport);\n\n if (!visible) throw new CliError('ELEMENT_NOT_FOUND', `element \"${selector}\" isn't visible`);\n\n if (visible.clipped) {\n console.log(\n ` element \"${selector}\" (${Math.round(box.width)}x${\n Math.round(box.height)\n }) is larger than the viewport; `\n + `the video shows the visible part. Use a larger --viewport to fit it.`,\n );\n }\n\n return visible.crop;\n };\n\n for (const [name, entry] of Object.entries(steps).filter(([n]) => names.includes(n))) {\n const { element = values.element, steps: run } = entry;\n const resolved = catalog.resolve(catalog.get(name) ?? { name });\n const options = contextOptions(resolved, values);\n\n // Playwright records video at CSS-pixel size whatever the scale, so use a larger --viewport for sharper video.\n const context = await browser.newContext({\n baseURL,\n ...options,\n recordVideo: { dir: raw, size: options.viewport },\n });\n\n const page = await context.newPage();\n const began = Date.now();\n const file = join(out, `${name}.${format}`);\n const clicks: Promise<{ at: number; x: number; y: number; kind?: 'focus'; } | undefined>[] = [];\n\n try {\n await showCursor(page, { ...cursor, glide: true });\n // Clicks and focused fields go in the video's manifest, for editors that zoom to them. The\n // listener runs in every frame, so iframe points map through the iframe's place and scale;\n // a detached one drops.\n await page.exposeBinding(\n 'stateScenariosClick',\n ({ frame }, x: number, y: number, kind?: 'focus') => {\n const at = Date.now();\n\n clicks.push(\n pagePoint(frame, x, y).then((point) => ({ at, ...point, kind }), () => undefined),\n );\n },\n );\n await page.addInitScript(() => {\n let pressed: { x: number; y: number; t: number; } | undefined;\n\n addEventListener('pointerdown', (e) => {\n pressed = { x: e.clientX, y: e.clientY, t: performance.now() };\n window.stateScenariosClick(e.clientX, e.clientY);\n }, true);\n // The glide before fill() moves the mouse, so the field's own point applies.\n addEventListener('pointermove', () => pressed = undefined, true);\n // A field taking focus is about to be typed into, so editors hold the camera on it.\n // Focus from a click keeps the click's point; fill() focuses where the glide put the mouse.\n addEventListener('focusin', (e) => {\n const field = e.target;\n\n if (\n !(field instanceof Element)\n || !field.matches(\n 'textarea, select, [contenteditable]:not([contenteditable=false]), input:not([type=button], [type=submit], [type=reset], [type=checkbox], [type=radio], [type=file], [type=image], [type=range], [type=color])',\n )\n ) return;\n\n if (pressed && performance.now() - pressed.t < 500) {\n window.stateScenariosClick(pressed.x, pressed.y, 'focus');\n\n return;\n }\n\n const box = field.getBoundingClientRect();\n window.stateScenariosClick(\n box.left + Math.min(24, box.width / 2),\n box.top + box.height / 2,\n 'focus',\n );\n }, true);\n });\n await openScenario(page, name, { path: resolved.path, strict: !values['no-strict'], panel: false });\n const crop = element ? await cropTo(page, element, options.viewport) : undefined;\n\n const start = (Date.now() - began) / 1000;\n await Promise.resolve(run(page)).catch((err: Error) => {\n throw new CliError('STEPS_FAILED', `steps failed: ${err.message.split('\\n')[0]}`);\n });\n await page.waitForTimeout(hold); // hold the final frame\n const end = Date.now();\n await context.close();\n\n const video = page.video();\n\n if (!video) throw new Error('Playwright recorded no video');\n\n const trimmed = encodeVideo({\n input: await video.path(),\n output: file,\n format,\n start,\n crop,\n fps,\n width,\n });\n\n const manifest = videoManifest({\n name,\n description: resolved.description,\n video: `${name}.${format}`,\n viewport: options.viewport,\n crop,\n width,\n start: began + trimmed.start * 1000,\n end,\n clicks: (await Promise.all(clicks)).filter((c) => c !== undefined),\n });\n\n writeFileSync(join(out, `${name}.video.json`), `${JSON.stringify(manifest, null, 2)}\\n`);\n console.log(`✓ ${name.padEnd(32)} ${file}`);\n } catch (err) {\n exitCode = Math.max(exitCode, err instanceof CliError ? err.exitCode : 1);\n await context.close();\n const code = err instanceof CliError ? `[${err.code}] ` : '';\n console.log(\n `✗ ${name.padEnd(32)} ${code}${err instanceof Error ? err.message.split('\\n')[0] : String(err)}`,\n );\n }\n }\n\n await browser.close();\n rmSync(raw, { recursive: true, force: true });\n process.exit(exitCode);\n }\n\n case 'suggest': {\n if (!values.src) {\n throw new CliError('INVALID_ARGUMENT', 'suggest needs --src, e.g. --src src/pages,src/components');\n }\n\n const sources = values.src.split(',').flatMap((d) => walk(d));\n const size = sources.reduce((n, f) => n + f.content.length, 0);\n\n if (size > MAX_SOURCE_CHARS) {\n throw new Error(\n `--src is ${size.toLocaleString()} characters; narrow it to the screens you want covered`,\n );\n }\n\n const jsonSchema = values.schema\n ? JSON.parse(readFileSync(values.schema, 'utf8'))\n : scenarioJsonSchema();\n\n if (values['env-file']) process.loadEnvFile(values['env-file']);\n const { default: Anthropic } = await import('@anthropic-ai/sdk');\n const model = values.model ?? process.env.STATE_SCENARIOS_MODEL ?? undefined;\n const { suggestScenarios } = await import('./suggest.js');\n console.error(\n `Reading ${sources.length} files (${size.toLocaleString()} chars) and ${catalog.list.length} scenarios with ${\n model ?? 'claude-opus-5-5'\n }…`,\n );\n\n const result = await suggestScenarios(\n { sources, catalog, jsonSchema, max: Number(values.max ?? 8), model },\n {\n client: new Anthropic({\n baseURL: process.env.ANTHROPIC_BASE_URL || undefined,\n // Some Anthropic-compatible proxies require a session id and a user agent.\n defaultHeaders: {\n 'x-opencode-session': `state-scenarios-${Date.now()}`,\n 'User-Agent': 'state-scenarios/0.0 (suggest)',\n },\n }),\n },\n );\n\n console.log('\\nCandidate UI states (not exhaustive)\\n');\n\n for (const s of result.states) {\n const mark = s.covered_by ? '✓' : '✗';\n console.log(\n `${mark} ${`${s.area} / ${s.state}`.padEnd(56)} ${s.covered_by ?? `uncovered (${s.priority})`}`,\n );\n }\n\n const covered = result.states.filter((s) => s.covered_by).length;\n console.log(\n `\\n${result.states.length} states detected, ${covered} have scenarios, ${\n result.states.length - covered\n } uncovered`,\n );\n\n const out = values.out ?? join(dir, 'proposed');\n\n if (result.accepted.length) mkdirSync(out, { recursive: true });\n\n for (const { scenario, covers } of result.accepted) {\n const file = join(out, `${scenario.name}.json`);\n const $schema = values.schema ? relative(out, values.schema).replaceAll('\\\\', '/') : undefined;\n writeFileSync(file, `${JSON.stringify({ $schema, ...scenario }, null, 2)}\\n`);\n console.log(`+ ${file} (${covers})`);\n }\n\n for (const r of result.rejected) console.log(`- rejected proposal for ${r.covers}: ${r.error}`);\n\n if (result.accepted.length) {\n console.log(\n `\\nReview the drafts, then: state-scenarios promote ${dir}`,\n );\n }\n\n break;\n }\n\n default:\n console.error(`Unknown command \"${command}\"\\n\\n${HELP}`);\n process.exit(2);\n }\n} catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const hint = /authentication/i.test(message) ? '\\nSet ANTHROPIC_API_KEY, or run `ant auth login`.' : '';\n const code = err instanceof CliError ? `[${err.code}] ` : '';\n console.error(`state-scenarios ${command}: ${code}${message}${hint}`);\n process.exit(err instanceof CliError ? err.exitCode : 1);\n}\n\nfunction walk(path: string): { path: string; content: string; }[] {\n if (statSync(path).isDirectory()) {\n return readdirSync(path)\n .filter((f) => !['node_modules', 'dist', 'scenarios'].includes(f))\n .flatMap((f) => walk(join(path, f)));\n }\n\n const isSource = /\\.(tsx?|jsx?|vue|svelte)$/.test(path) && !/\\.(d|test|spec)\\.\\w+$/.test(path);\n\n return isSource ? [{ path, content: readFileSync(path, 'utf8') }] : [];\n}\n"],"mappings":"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","names":["s"]}
|
|
@@ -77,7 +77,8 @@ function videoManifest({ name, description, video, viewport, crop, width, start,
|
|
|
77
77
|
clicks: clicks.map((c) => ({
|
|
78
78
|
t: Math.round(c.at - start),
|
|
79
79
|
x: Math.round((c.x - Math.round(source.x)) * ratio),
|
|
80
|
-
y: Math.round((c.y - Math.round(source.y)) * ratio)
|
|
80
|
+
y: Math.round((c.y - Math.round(source.y)) * ratio),
|
|
81
|
+
...c.kind && { kind: c.kind }
|
|
81
82
|
})).filter((c) => c.t >= 0 && c.x >= 0 && c.y >= 0 && c.x < size.width && c.y < size.height)
|
|
82
83
|
};
|
|
83
84
|
}
|
|
@@ -102,35 +103,12 @@ async function pagePoint(frame, x, y) {
|
|
|
102
103
|
const scaleY = box.height / layout.height;
|
|
103
104
|
return { x: box.x + (layout.left + x) * scaleX, y: box.y + (layout.top + y) * scaleY };
|
|
104
105
|
}
|
|
105
|
-
var gliding = /* @__PURE__ */ new WeakMap();
|
|
106
|
-
function glideBeforeClick(page, ms = 400) {
|
|
107
|
-
gliding.set(page, ms);
|
|
108
|
-
const proto = Object.getPrototypeOf(page.locator("html"));
|
|
109
|
-
if (proto.glides) return;
|
|
110
|
-
proto.glides = true;
|
|
111
|
-
const click = Object.getOwnPropertyDescriptor(proto, "click")?.value;
|
|
112
|
-
proto.click = async function(options) {
|
|
113
|
-
const wait = gliding.get(this.page());
|
|
114
|
-
const hover = { position: options?.position, force: options?.force, timeout: options?.timeout };
|
|
115
|
-
if (wait !== void 0) {
|
|
116
|
-
for (let tries = 0; tries < 3; tries++) {
|
|
117
|
-
await this.hover(hover);
|
|
118
|
-
const before = await this.boundingBox();
|
|
119
|
-
await this.page().waitForTimeout(wait);
|
|
120
|
-
const after = await this.boundingBox();
|
|
121
|
-
if (JSON.stringify(before) === JSON.stringify(after)) break;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return click.call(this, options);
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
106
|
export {
|
|
128
107
|
encodeVideo,
|
|
129
108
|
ffmpegArgs,
|
|
130
|
-
glideBeforeClick,
|
|
131
109
|
outputSize,
|
|
132
110
|
pagePoint,
|
|
133
111
|
videoManifest,
|
|
134
112
|
visibleCrop
|
|
135
113
|
};
|
|
136
|
-
//# sourceMappingURL=record-
|
|
114
|
+
//# sourceMappingURL=record-BO2NJ4Q2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/record.ts"],"sourcesContent":["import type { Frame } from '@playwright/test';\nimport { spawnSync } from 'node:child_process';\nimport { copyFileSync } from 'node:fs';\nimport { CliError } from './errors.js';\n\nexport type VideoFormat = 'mp4' | 'gif' | 'webm';\n\nexport interface Crop {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * The part of an element's box the video shows. Video holds only the viewport, so a crop must\n * stay inside it; `clipped` says the element was larger than the visible area.\n */\nexport function visibleCrop(box: Crop, viewport: { width: number; height: number; }) {\n const x = Math.max(0, box.x);\n const y = Math.max(0, box.y);\n const width = Math.min(viewport.width, box.x + box.width) - x;\n const height = Math.min(viewport.height, box.y + box.height) - y;\n\n if (width < 2 || height < 2) return undefined;\n\n return { crop: { x, y, width, height }, clipped: width < box.width || height < box.height };\n}\n\nconst even = (n: number) => Math.max(2, Math.floor(n / 2) * 2);\n\n/**\n * ffmpeg arguments that trim the page load off the start, crop to an element, and encode.\n * MP4 is H.264 in yuv420p so it plays everywhere, including Safari and phones; GIF uses a\n * generated palette, which keeps it sharp at a fraction of the naive size.\n */\nexport function ffmpegArgs(\n { input, output, format, start, crop, fps, width }: {\n input: string;\n output: string;\n format: VideoFormat;\n /** Seconds to skip, e.g. the page loading before the steps begin. */\n start: number;\n crop?: Crop;\n /** Frames per second; GIFs default to 12. */\n fps?: number;\n /** Maximum output width in pixels, never upscaled; height follows the aspect ratio. */\n width?: number;\n },\n): string[] {\n const rate = fps ?? (format === 'gif' ? 12 : undefined);\n\n // yuv420p needs even sizes.\n const filters = [\n crop && `crop=${even(crop.width)}:${even(crop.height)}:${Math.round(crop.x)}:${Math.round(crop.y)}`,\n rate && `fps=${rate}`,\n // Quoted so the comma in min() isn't read as a filter separator.\n width && `scale='trunc(min(${width},iw)/2)*2':-2:flags=lanczos`,\n ].filter(Boolean);\n\n const codec = {\n mp4: ['-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '23', '-movflags', '+faststart'],\n webm: ['-c:v', 'libvpx-vp9', '-b:v', '0', '-crf', '32'],\n gif: [],\n }[format];\n\n const vf = format === 'gif'\n ? `${filters.join(',')},split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer`\n : filters.join(',');\n\n return [\n '-y',\n '-loglevel',\n 'error',\n '-i',\n input,\n '-ss',\n start.toFixed(2),\n ...(vf ? ['-vf', vf] : []),\n '-an',\n ...codec,\n output,\n ];\n}\n\n/**\n * Convert Playwright's WebM into the requested format. WebM without a crop, --fps or --width is\n * copied as recorded (page load included), so it works without ffmpeg.\n */\nexport function encodeVideo(options: Parameters<typeof ffmpegArgs>[0]) {\n if (options.format === 'webm' && !options.crop && !options.fps && !options.width) {\n copyFileSync(options.input, options.output);\n\n return { start: 0 };\n }\n\n // FFMPEG_PATH is the usual override when ffmpeg isn't on PATH (common on Windows).\n const ffmpeg = process.env.FFMPEG_PATH || 'ffmpeg';\n const result = spawnSync(ffmpeg, ffmpegArgs(options), { encoding: 'utf8' });\n\n if (result.error) {\n throw new CliError(\n 'FFMPEG_MISSING',\n `record needs ffmpeg for ${options.format}${\n options.format === 'webm' ? ' with --element, --fps or --width' : ''\n }: install it (https://ffmpeg.org/download.html) or set FFMPEG_PATH`,\n );\n }\n\n if (result.status !== 0) throw new Error(`ffmpeg failed: ${result.stderr.trim()}`);\n\n return { start: options.start };\n}\n\n/** The video size ffmpeg writes: the crop or viewport, capped at `width` as the scale filter does. */\nexport function outputSize(source: { width: number; height: number; }, width?: number) {\n if (!width) return source;\n\n const w = Math.trunc(Math.min(width, source.width) / 2) * 2;\n\n return { width: w, height: Math.round((w * source.height) / (source.width * 2)) * 2 };\n}\n\n/**\n * What `<name>.video.json` holds next to each video, so video editors such as Remotion can size\n * a composition, title it and zoom to clicks. Times are ms into the saved video; click positions\n * are pixels in it.\n */\nexport function videoManifest(\n { name, description, video, viewport, crop, width, start, end, clicks }: {\n name: string;\n description?: string;\n video: string;\n viewport: { width: number; height: number; };\n crop?: Crop;\n width?: number;\n /** Epoch ms where the saved video begins and ends. */\n start: number;\n end: number;\n /** Epoch ms and page coordinates of each pointerdown, and of each field taking focus. */\n clicks: { at: number; x: number; y: number; kind?: 'focus'; }[];\n },\n) {\n const source = crop\n ? { ...crop, width: even(crop.width), height: even(crop.height) }\n : { x: 0, y: 0, ...viewport };\n\n const size = outputSize({ width: source.width, height: source.height }, width);\n const ratio = size.width / source.width;\n\n return {\n name,\n description,\n video,\n width: size.width,\n height: size.height,\n durationMs: Math.round(end - start),\n clicks: clicks\n .map((c) => ({\n t: Math.round(c.at - start),\n x: Math.round((c.x - Math.round(source.x)) * ratio),\n y: Math.round((c.y - Math.round(source.y)) * ratio),\n ...(c.kind && { kind: c.kind }),\n }))\n .filter((c) => c.t >= 0 && c.x >= 0 && c.y >= 0 && c.x < size.width && c.y < size.height),\n };\n}\n\n/**\n * A point in a frame, in page coordinates, so a click inside an iframe maps to the video.\n * Playwright measures the iframe element on the page, CSS transforms included; comparing that with\n * its layout size gives the scale for the border, padding and the point itself. Scales and\n * translations map exactly; a rotated or skewed iframe maps to its bounding box.\n */\nexport async function pagePoint(frame: Frame, x: number, y: number) {\n if (!frame.parentFrame()) return { x, y };\n\n const element = await frame.frameElement();\n\n const [box, layout] = await Promise.all([\n element.boundingBox(),\n element.evaluate((node) => {\n // frameElement() returns the <iframe> or <frame> element, so this always passes.\n if (!(node instanceof HTMLElement)) return undefined;\n\n const style = getComputedStyle(node);\n\n return {\n left: node.clientLeft + parseFloat(style.paddingLeft),\n top: node.clientTop + parseFloat(style.paddingTop),\n width: node.offsetWidth,\n height: node.offsetHeight,\n };\n }),\n ]);\n\n if (!box || !layout?.width || !layout.height) throw new Error('The iframe is not visible');\n\n const scaleX = box.width / layout.width;\n const scaleY = box.height / layout.height;\n\n return { x: box.x + (layout.left + x) * scaleX, y: box.y + (layout.top + y) * scaleY };\n}\n"],"mappings":"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","names":[]}
|
package/dist/record.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Frame
|
|
1
|
+
import type { Frame } from '@playwright/test';
|
|
2
2
|
export type VideoFormat = 'mp4' | 'gif' | 'webm';
|
|
3
3
|
export interface Crop {
|
|
4
4
|
x: number;
|
|
@@ -72,11 +72,12 @@ export declare function videoManifest({ name, description, video, viewport, crop
|
|
|
72
72
|
/** Epoch ms where the saved video begins and ends. */
|
|
73
73
|
start: number;
|
|
74
74
|
end: number;
|
|
75
|
-
/** Epoch ms and page coordinates of each pointerdown. */
|
|
75
|
+
/** Epoch ms and page coordinates of each pointerdown, and of each field taking focus. */
|
|
76
76
|
clicks: {
|
|
77
77
|
at: number;
|
|
78
78
|
x: number;
|
|
79
79
|
y: number;
|
|
80
|
+
kind?: 'focus';
|
|
80
81
|
}[];
|
|
81
82
|
}): {
|
|
82
83
|
name: string;
|
|
@@ -89,6 +90,7 @@ export declare function videoManifest({ name, description, video, viewport, crop
|
|
|
89
90
|
t: number;
|
|
90
91
|
x: number;
|
|
91
92
|
y: number;
|
|
93
|
+
kind?: "focus" | undefined;
|
|
92
94
|
}[];
|
|
93
95
|
};
|
|
94
96
|
/**
|
|
@@ -101,10 +103,3 @@ export declare function pagePoint(frame: Frame, x: number, y: number): Promise<{
|
|
|
101
103
|
x: number;
|
|
102
104
|
y: number;
|
|
103
105
|
}>;
|
|
104
|
-
/**
|
|
105
|
-
* Make `locator.click()` on this page hover first and wait for the drawn cursor's glide, so videos
|
|
106
|
-
* show the pointer reaching its target before the click. Playwright's click moves and presses in
|
|
107
|
-
* one go, so the page would react before the cursor arrives. The Locator prototype is shared, so
|
|
108
|
-
* it's patched once and other pages click as usual.
|
|
109
|
-
*/
|
|
110
|
-
export declare function glideBeforeClick(page: Page, ms?: number): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "state-scenarios-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "state-scenarios CLI: init, guide, doctor, validate, list, shoot, affected, coverage, publish, record, mcp, suggest and promote scenarios.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sdk": "^0.131.0",
|
|
31
31
|
"ajv": "^8.20.0",
|
|
32
|
-
"state-scenarios": "^0.
|
|
32
|
+
"state-scenarios": "^0.6.0",
|
|
33
33
|
"zod": "4.6.5"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
@@ -41,7 +41,7 @@ npx state-scenarios record src/scenarios --base-url http://localhost:5173 --step
|
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
- The panel is hidden and a bright arrow cursor is drawn (Playwright videos don't show the mouse).
|
|
44
|
-
It pulses, glides to each target before
|
|
44
|
+
It pulses, glides to each target before a click, check or `fill` lands, and sends out a ripple on the
|
|
45
45
|
click. `--cursor-color <css>` (default `#ff2d55`) recolours it; `--cursor <file.svg>` draws your
|
|
46
46
|
own, with its top-left corner at the mouse.
|
|
47
47
|
- The video starts once the scenario is ready and holds the last frame for `--hold` ms (default
|
|
@@ -56,11 +56,12 @@ npx state-scenarios record src/scenarios --base-url http://localhost:5173 --step
|
|
|
56
56
|
- Video records at viewport size, so use a larger `--viewport` for sharper video; `--scale`
|
|
57
57
|
affects screenshots only.
|
|
58
58
|
- MP4, GIF, `--element`, `--fps` and `--width` need ffmpeg on `PATH`, or `FFMPEG_PATH`. WebM without them doesn't.
|
|
59
|
-
- Each video gets a `<name>.video.json` manifest: size, length, description and clicks
|
|
59
|
+
- Each video gets a `<name>.video.json` manifest: size, length, description and clicks; a field
|
|
60
|
+
taking focus is a point with `kind: 'focus'`, so editors can hold on it while it's typed into. See
|
|
60
61
|
[Videos in Remotion](#videos-in-remotion).
|
|
61
|
-
- In your own Playwright scripts, `showCursor(page, { color, svg })` from `state-scenarios/playwright`
|
|
62
|
-
draws the cursor; call it before `openScenario(page, name, { panel: false })`.
|
|
63
|
-
|
|
62
|
+
- In your own Playwright scripts, `showCursor(page, { color, svg, size, halo })` from `state-scenarios/playwright`
|
|
63
|
+
draws the cursor; call it before `openScenario(page, name, { panel: false })`. `{ glide: true }`
|
|
64
|
+
makes actions wait for the cursor to arrive, as `record` does.
|
|
64
65
|
|
|
65
66
|
## Videos in Remotion
|
|
66
67
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/record.ts"],"sourcesContent":["import type { Frame, Locator, Page } from '@playwright/test';\nimport { spawnSync } from 'node:child_process';\nimport { copyFileSync } from 'node:fs';\nimport { CliError } from './errors.js';\n\nexport type VideoFormat = 'mp4' | 'gif' | 'webm';\n\nexport interface Crop {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * The part of an element's box the video shows. Video holds only the viewport, so a crop must\n * stay inside it; `clipped` says the element was larger than the visible area.\n */\nexport function visibleCrop(box: Crop, viewport: { width: number; height: number; }) {\n const x = Math.max(0, box.x);\n const y = Math.max(0, box.y);\n const width = Math.min(viewport.width, box.x + box.width) - x;\n const height = Math.min(viewport.height, box.y + box.height) - y;\n\n if (width < 2 || height < 2) return undefined;\n\n return { crop: { x, y, width, height }, clipped: width < box.width || height < box.height };\n}\n\nconst even = (n: number) => Math.max(2, Math.floor(n / 2) * 2);\n\n/**\n * ffmpeg arguments that trim the page load off the start, crop to an element, and encode.\n * MP4 is H.264 in yuv420p so it plays everywhere, including Safari and phones; GIF uses a\n * generated palette, which keeps it sharp at a fraction of the naive size.\n */\nexport function ffmpegArgs(\n { input, output, format, start, crop, fps, width }: {\n input: string;\n output: string;\n format: VideoFormat;\n /** Seconds to skip, e.g. the page loading before the steps begin. */\n start: number;\n crop?: Crop;\n /** Frames per second; GIFs default to 12. */\n fps?: number;\n /** Maximum output width in pixels, never upscaled; height follows the aspect ratio. */\n width?: number;\n },\n): string[] {\n const rate = fps ?? (format === 'gif' ? 12 : undefined);\n\n // yuv420p needs even sizes.\n const filters = [\n crop && `crop=${even(crop.width)}:${even(crop.height)}:${Math.round(crop.x)}:${Math.round(crop.y)}`,\n rate && `fps=${rate}`,\n // Quoted so the comma in min() isn't read as a filter separator.\n width && `scale='trunc(min(${width},iw)/2)*2':-2:flags=lanczos`,\n ].filter(Boolean);\n\n const codec = {\n mp4: ['-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '23', '-movflags', '+faststart'],\n webm: ['-c:v', 'libvpx-vp9', '-b:v', '0', '-crf', '32'],\n gif: [],\n }[format];\n\n const vf = format === 'gif'\n ? `${filters.join(',')},split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer`\n : filters.join(',');\n\n return [\n '-y',\n '-loglevel',\n 'error',\n '-i',\n input,\n '-ss',\n start.toFixed(2),\n ...(vf ? ['-vf', vf] : []),\n '-an',\n ...codec,\n output,\n ];\n}\n\n/**\n * Convert Playwright's WebM into the requested format. WebM without a crop, --fps or --width is\n * copied as recorded (page load included), so it works without ffmpeg.\n */\nexport function encodeVideo(options: Parameters<typeof ffmpegArgs>[0]) {\n if (options.format === 'webm' && !options.crop && !options.fps && !options.width) {\n copyFileSync(options.input, options.output);\n\n return { start: 0 };\n }\n\n // FFMPEG_PATH is the usual override when ffmpeg isn't on PATH (common on Windows).\n const ffmpeg = process.env.FFMPEG_PATH || 'ffmpeg';\n const result = spawnSync(ffmpeg, ffmpegArgs(options), { encoding: 'utf8' });\n\n if (result.error) {\n throw new CliError(\n 'FFMPEG_MISSING',\n `record needs ffmpeg for ${options.format}${\n options.format === 'webm' ? ' with --element, --fps or --width' : ''\n }: install it (https://ffmpeg.org/download.html) or set FFMPEG_PATH`,\n );\n }\n\n if (result.status !== 0) throw new Error(`ffmpeg failed: ${result.stderr.trim()}`);\n\n return { start: options.start };\n}\n\n/** The video size ffmpeg writes: the crop or viewport, capped at `width` as the scale filter does. */\nexport function outputSize(source: { width: number; height: number; }, width?: number) {\n if (!width) return source;\n\n const w = Math.trunc(Math.min(width, source.width) / 2) * 2;\n\n return { width: w, height: Math.round((w * source.height) / (source.width * 2)) * 2 };\n}\n\n/**\n * What `<name>.video.json` holds next to each video, so video editors such as Remotion can size\n * a composition, title it and zoom to clicks. Times are ms into the saved video; click positions\n * are pixels in it.\n */\nexport function videoManifest(\n { name, description, video, viewport, crop, width, start, end, clicks }: {\n name: string;\n description?: string;\n video: string;\n viewport: { width: number; height: number; };\n crop?: Crop;\n width?: number;\n /** Epoch ms where the saved video begins and ends. */\n start: number;\n end: number;\n /** Epoch ms and page coordinates of each pointerdown. */\n clicks: { at: number; x: number; y: number; }[];\n },\n) {\n const source = crop\n ? { ...crop, width: even(crop.width), height: even(crop.height) }\n : { x: 0, y: 0, ...viewport };\n\n const size = outputSize({ width: source.width, height: source.height }, width);\n const ratio = size.width / source.width;\n\n return {\n name,\n description,\n video,\n width: size.width,\n height: size.height,\n durationMs: Math.round(end - start),\n clicks: clicks\n .map((c) => ({\n t: Math.round(c.at - start),\n x: Math.round((c.x - Math.round(source.x)) * ratio),\n y: Math.round((c.y - Math.round(source.y)) * ratio),\n }))\n .filter((c) => c.t >= 0 && c.x >= 0 && c.y >= 0 && c.x < size.width && c.y < size.height),\n };\n}\n\n/**\n * A point in a frame, in page coordinates, so a click inside an iframe maps to the video.\n * Playwright measures the iframe element on the page, CSS transforms included; comparing that with\n * its layout size gives the scale for the border, padding and the point itself. Scales and\n * translations map exactly; a rotated or skewed iframe maps to its bounding box.\n */\nexport async function pagePoint(frame: Frame, x: number, y: number) {\n if (!frame.parentFrame()) return { x, y };\n\n const element = await frame.frameElement();\n\n const [box, layout] = await Promise.all([\n element.boundingBox(),\n element.evaluate((node) => {\n // frameElement() returns the <iframe> or <frame> element, so this always passes.\n if (!(node instanceof HTMLElement)) return undefined;\n\n const style = getComputedStyle(node);\n\n return {\n left: node.clientLeft + parseFloat(style.paddingLeft),\n top: node.clientTop + parseFloat(style.paddingTop),\n width: node.offsetWidth,\n height: node.offsetHeight,\n };\n }),\n ]);\n\n if (!box || !layout?.width || !layout.height) throw new Error('The iframe is not visible');\n\n const scaleX = box.width / layout.width;\n const scaleY = box.height / layout.height;\n\n return { x: box.x + (layout.left + x) * scaleX, y: box.y + (layout.top + y) * scaleY };\n}\n\nconst gliding = new WeakMap<Page, number>();\n\n/**\n * Make `locator.click()` on this page hover first and wait for the drawn cursor's glide, so videos\n * show the pointer reaching its target before the click. Playwright's click moves and presses in\n * one go, so the page would react before the cursor arrives. The Locator prototype is shared, so\n * it's patched once and other pages click as usual.\n */\nexport function glideBeforeClick(page: Page, ms = 400) {\n gliding.set(page, ms);\n\n const proto: Locator & { glides?: true; } = Object.getPrototypeOf(page.locator('html'));\n\n if (proto.glides) return;\n proto.glides = true;\n\n const click: (this: Locator, options?: Parameters<Locator['click']>[0]) => Promise<void> = Object\n .getOwnPropertyDescriptor(proto, 'click')?.value;\n\n proto.click = async function(this: Locator, options) {\n const wait = gliding.get(this.page());\n const hover = { position: options?.position, force: options?.force, timeout: options?.timeout };\n\n // A target that moves or resizes during the glide (its click point moves either way) gets\n // another one, up to three, so the cursor is there when the click lands.\n if (wait !== undefined) {\n for (let tries = 0; tries < 3; tries++) {\n await this.hover(hover);\n const before = await this.boundingBox();\n await this.page().waitForTimeout(wait);\n const after = await this.boundingBox();\n\n if (JSON.stringify(before) === JSON.stringify(after)) break;\n }\n }\n\n return click.call(this, options);\n };\n}\n"],"mappings":"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","names":[]}
|