@parity/product-lab 0.1.0 → 0.1.2
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/CHANGELOG.md +15 -0
- package/README.md +27 -0
- package/adapters/browser/playwright/README.md +2 -0
- package/adapters/browser/playwright/src/page-frame-capture.js +10 -2
- package/adapters/chain/papi/README.md +3 -1
- package/adapters/environment/engine/README.md +3 -3
- package/adapters/environment/engine/src/engine/fork-refresh.js +72 -1
- package/adapters/features/cucumber/README.md +4 -0
- package/adapters/features/cucumber/src/steps/register-feature-steps.js +15 -1
- package/adapters/system/node/README.md +5 -5
- package/adapters/video/ffmpeg/README.md +4 -0
- package/adapters/video/ffmpeg/src/ffmpeg-video-editor.d.ts +1 -1
- package/adapters/video/ffmpeg/src/ffmpeg-video-editor.js +19 -6
- package/core/application/README.md +30 -1
- package/core/application/src/features/direct-feature-steps.d.ts +23 -0
- package/core/application/src/features/direct-feature-steps.js +25 -0
- package/core/application/src/features/feature-recording.d.ts +6 -0
- package/core/application/src/features/feature-recording.js +3 -0
- package/core/application/src/features/feature-scenario.d.ts +10 -8
- package/core/application/src/features/feature-scenario.js +11 -15
- package/core/application/src/features/feature-step-times.d.ts +11 -1
- package/core/application/src/features/feature-step-times.js +20 -6
- package/core/application/src/features/lab-feature-session.d.ts +3 -1
- package/core/application/src/features/lab-feature-session.js +5 -2
- package/core/application/src/features/run-feature-steps.d.ts +3 -1
- package/core/application/src/features/run-feature-steps.js +4 -2
- package/core/application/src/lab-runtime-use-cases.js +5 -1
- package/core/application/src/recordings/demo-markup.d.ts +8 -0
- package/core/application/src/recordings/demo-markup.js +11 -1
- package/core/application/src/recordings/demo-recording.d.ts +0 -2
- package/core/application/src/recordings/demo-recording.js +40 -23
- package/core/application/src/recordings/frame-sources.d.ts +20 -1
- package/core/application/src/recordings/frame-sources.js +89 -0
- package/core/application/src/recordings/record-runs.d.ts +7 -1
- package/core/application/src/recordings/record-runs.js +66 -11
- package/core/application/src/recordings/recording-draft.d.ts +4 -0
- package/core/application/src/recordings/recording-draft.js +13 -4
- package/core/domain/README.md +14 -0
- package/core/domain/src/recordings/demo-cut.js +6 -4
- package/core/domain/src/recordings/demo-holds.d.ts +12 -0
- package/core/domain/src/recordings/demo-holds.js +31 -0
- package/core/domain/src/recordings/index.d.ts +1 -0
- package/core/domain/src/recordings/index.js +1 -0
- package/core/domain/src/recordings/recording-data.d.ts +9 -1
- package/core/domain/src/recordings/recording-settings.d.ts +5 -1
- package/core/domain/src/recordings/recording-settings.js +7 -0
- package/core/domain/src/recordings/run-recording.d.ts +2 -0
- package/core/domain/src/recordings/run-recording.js +8 -1
- package/core/ports/README.md +6 -0
- package/core/ports/src/driven/recording/demo-ports.d.ts +2 -0
- package/core/ports/src/driven/recording/frame-capture.d.ts +4 -0
- package/core/ports/src/driving/demo.d.ts +2 -0
- package/core/ports/src/driving/feature-session.d.ts +7 -0
- package/entrypoints/cli/README.md +6 -3
- package/entrypoints/cli/ai-kit/skills/product-lab-integration/references/notes-lab.md +1 -1
- package/entrypoints/cli/ai-kit/skills/product-lab-setup/SKILL.md +1 -0
- package/entrypoints/cli/ai-kit/skills/product-lab-setup/references/minimal-lab.md +1 -1
- package/entrypoints/cli/src/demo-command.d.ts +1 -1
- package/entrypoints/cli/src/demo-command.js +10 -5
- package/entrypoints/mcp/README.md +3 -0
- package/entrypoints/panel/README.md +6 -3
- package/entrypoints/panel/src/visible/detached-identities.js +5 -1
- package/entrypoints/panel/src/visible/window-frames.js +14 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { DemoReport, LabRuntime } from "@parity/product-lab/ports/driving";
|
|
2
|
-
export declare const DEMO_USAGE = "Usage: product-lab demo [<recording-id>] [--actor <role> | --window] [--out <dir>] [--max-seconds 60]\n [--format mp4|webm] [--plan | --slide-only] [--failed] [--json] [--config lab.config.ts]\n\nA demo of a recorded feature scenario: its
|
|
2
|
+
export declare const DEMO_USAGE = "Usage: product-lab demo [<recording-id>] [--actor <role> | --window] [--out <dir>] [--max-seconds 60] [--hold-seconds 2]\n [--format mp4|webm] [--plan | --slide-only] [--failed] [--json] [--config lab.config.ts]\n\nA demo of a recorded feature scenario: its real after-step images held for at least 2 seconds each,\nwith captions in a band under the app and step-001.png (one encoded midpoint PNG per step), plus demo.vtt (the captions),\ndemo.json (the edit, segment by segment) and a slide (slide.png, slide.html: the scenario, its steps, who and where,\nthe last screen). Record a run with video first (product-lab test --video, or the lab window's recording).\n <recording-id> default: the newest passed feature recording\n --actor <role> cut that actor's app video (default: the lab window's, else the actor in most steps)\n --window cut the lab window's video\n --out <dir> default: <lab>/.product-lab/demos/<recording-id>\n --max-seconds <n> the target length (default 60); never shortens the step holds\n --hold-seconds <n> hold each after-step image (default 2, minimum 2 seconds)\n --format mp4|webm default mp4 (H.264); webm is VP9\n --plan only demo.json and demo.vtt (needs no ffmpeg)\n --slide-only the plan and the slide, no video (needs no ffmpeg)\n --failed allow a failed run: every frame and the slide say it failed\n --json print the report as JSON\n The video needs ffmpeg and ffprobe on PATH (or PRODUCT_LAB_FFMPEG naming their folder); product-lab never bundles them.\n Exit code: 0 made, 2 refused (no such recording, no video, a failed run without --failed, no ffmpeg) or bad arguments,\n 1 when the lab does not load or the cut fails.";
|
|
3
3
|
export declare function renderDemo(report: DemoReport): string;
|
|
4
4
|
export declare function runDemoCommand(runtime: LabRuntime, argv: string[], signal?: AbortSignal, out?: (text: string) => void, err?: (text: string) => void): Promise<number>;
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { DemoRefusedError } from "@parity/product-lab/domain/recordings";
|
|
5
|
-
export const DEMO_USAGE = `Usage: product-lab demo [<recording-id>] [--actor <role> | --window] [--out <dir>] [--max-seconds 60]
|
|
5
|
+
export const DEMO_USAGE = `Usage: product-lab demo [<recording-id>] [--actor <role> | --window] [--out <dir>] [--max-seconds 60] [--hold-seconds 2]
|
|
6
6
|
[--format mp4|webm] [--plan | --slide-only] [--failed] [--json] [--config lab.config.ts]
|
|
7
7
|
|
|
8
|
-
A demo of a recorded feature scenario: its
|
|
9
|
-
|
|
8
|
+
A demo of a recorded feature scenario: its real after-step images held for at least 2 seconds each,
|
|
9
|
+
with captions in a band under the app and step-001.png (one encoded midpoint PNG per step), plus demo.vtt (the captions),
|
|
10
10
|
demo.json (the edit, segment by segment) and a slide (slide.png, slide.html: the scenario, its steps, who and where,
|
|
11
11
|
the last screen). Record a run with video first (product-lab test --video, or the lab window's recording).
|
|
12
12
|
<recording-id> default: the newest passed feature recording
|
|
13
13
|
--actor <role> cut that actor's app video (default: the lab window's, else the actor in most steps)
|
|
14
14
|
--window cut the lab window's video
|
|
15
15
|
--out <dir> default: <lab>/.product-lab/demos/<recording-id>
|
|
16
|
-
--max-seconds <n> the
|
|
16
|
+
--max-seconds <n> the target length (default 60); never shortens the step holds
|
|
17
|
+
--hold-seconds <n> hold each after-step image (default 2, minimum 2 seconds)
|
|
17
18
|
--format mp4|webm default mp4 (H.264); webm is VP9
|
|
18
19
|
--plan only demo.json and demo.vtt (needs no ffmpeg)
|
|
19
20
|
--slide-only the plan and the slide, no video (needs no ffmpeg)
|
|
@@ -48,6 +49,7 @@ export async function runDemoCommand(runtime, argv, signal, out = (text) => proc
|
|
|
48
49
|
window: { type: "boolean" },
|
|
49
50
|
out: { type: "string" },
|
|
50
51
|
"max-seconds": { type: "string" },
|
|
52
|
+
"hold-seconds": { type: "string" },
|
|
51
53
|
format: { type: "string" },
|
|
52
54
|
plan: { type: "boolean" },
|
|
53
55
|
"slide-only": { type: "boolean" },
|
|
@@ -66,12 +68,14 @@ export async function runDemoCommand(runtime, argv, signal, out = (text) => proc
|
|
|
66
68
|
return 0;
|
|
67
69
|
}
|
|
68
70
|
const maxSeconds = values["max-seconds"] === undefined ? undefined : Number(values["max-seconds"]);
|
|
71
|
+
const holdSeconds = values["hold-seconds"] === undefined ? undefined : Number(values["hold-seconds"]);
|
|
69
72
|
const problem = positionals.length > 1 ? `Unexpected arguments: ${positionals.slice(1).join(" ")}`
|
|
70
73
|
: values.actor !== undefined && values.window ? "--actor and --window cannot be used together"
|
|
71
74
|
: values.plan && values["slide-only"] ? "--plan and --slide-only cannot be used together"
|
|
72
75
|
: values.format !== undefined && values.format !== "mp4" && values.format !== "webm" ? `--format must be mp4 or webm, not ${values.format}`
|
|
73
76
|
: maxSeconds !== undefined && !(Number.isFinite(maxSeconds) && maxSeconds >= 5) ? `--max-seconds must be a number of seconds, at least 5, not ${values["max-seconds"]}`
|
|
74
|
-
: undefined
|
|
77
|
+
: holdSeconds !== undefined && !(Number.isFinite(holdSeconds) && holdSeconds >= 2) ? `--hold-seconds must be a number of seconds, at least 2, not ${values["hold-seconds"]}`
|
|
78
|
+
: undefined;
|
|
75
79
|
if (problem) {
|
|
76
80
|
err(`${problem}\n${DEMO_USAGE}\n`);
|
|
77
81
|
return 2;
|
|
@@ -84,6 +88,7 @@ export async function runDemoCommand(runtime, argv, signal, out = (text) => proc
|
|
|
84
88
|
...(values.window ? { window: true } : {}),
|
|
85
89
|
...(values.out !== undefined ? { out: values.out } : {}),
|
|
86
90
|
...(maxSeconds !== undefined ? { maxSeconds } : {}),
|
|
91
|
+
...(holdSeconds !== undefined ? { holdSeconds } : {}),
|
|
87
92
|
...(values.format ? { format: values.format } : {}),
|
|
88
93
|
...(values.plan ? { planOnly: true } : {}),
|
|
89
94
|
...(values["slide-only"] ? { slideOnly: true } : {}),
|
|
@@ -202,3 +202,6 @@ the file. Neither live script is part of the unit suite.
|
|
|
202
202
|
|
|
203
203
|
The reload test creates its ES-module lab in an OS temporary directory, linking this package's `node_modules`.
|
|
204
204
|
Concurrent lint can no longer discover a generated lab under the repository's `tests/` without a TypeScript project.
|
|
205
|
+
|
|
206
|
+
Design-resource hashes track the current topic text, including the distinction between passive frames-only
|
|
207
|
+
capture and awaited video capture at feature boundaries.
|
|
@@ -417,9 +417,10 @@ Every running actor's app tile has a compact, browser-like address bar in the co
|
|
|
417
417
|
(`data-frame`), so a re-created frame never receives an old frame's address. The bar never shows a guessed location.
|
|
418
418
|
|
|
419
419
|
Tests: `node --import tsx --test entrypoints/panel/tests/address-bar.test.ts` (served shell, headless Chromium,
|
|
420
|
-
loopback fixture apps on ephemeral ports
|
|
421
|
-
|
|
422
|
-
|
|
420
|
+
loopback fixture apps on ephemeral ports that count browser requests by `Sec-Fetch-Dest`, which Chromium sends
|
|
421
|
+
to these loopback origins, ignoring a local port probe without that header;
|
|
422
|
+
a normal tab and the lab window via `openLabWindow`, including real `framenavigated` reports and the acting guard).
|
|
423
|
+
Optional `ADDRESS_BAR_SCREENS=<directory>` writes review screenshots (unset: none).
|
|
423
424
|
|
|
424
425
|
## Visible execution: `@product-lab/shell/visible`
|
|
425
426
|
|
|
@@ -1771,3 +1772,5 @@ Framing reuse (item82): the framing section above retains the original item66 be
|
|
|
1771
1772
|
The drawer puts its existing tabs in a full-width row below the title and Close control. Tabs wrap at narrow widths, keeping Recordings reachable without replacing the live app frame. Watch controls the displayed app in a window opened through `openLabWindow`; the server can keep running in the background while that window remains visible.
|
|
1772
1773
|
|
|
1773
1774
|
Opening a recording displays its captured screenshots over the mounted app, with Play/Pause and previous/next screenshot controls. The viewer labels its fixed1.5s image cadence and absence of video; it cannot recover typing or clicks between captures. Back to live restores the app and run controls. Active runs are checked before and after loading the recording, using the latest panel snapshot. The run audit remains available even with zero recorded steps, and incomplete audits explicitly explain that proof is missing.
|
|
1775
|
+
|
|
1776
|
+
Recording captures of tiles and detached windows honor `CaptureOptions.loadComplete` before taking their image and report `loadComplete: true` when checked. Only video feature after-frame checks request this wait; normal captures retain their existing policy. Tests: `tests/window-frames.test.ts` and `tests/detached-identities.test.ts`.
|
|
@@ -195,6 +195,10 @@ export function detachedFrameCapture(tiles, detached, options = {}) {
|
|
|
195
195
|
const timeoutMs = captureOptions?.timeoutMs ?? options.timeoutMs ?? DETACHED_CAPTURE_TIMEOUT_MS;
|
|
196
196
|
if (signal?.aborted)
|
|
197
197
|
throw new Error(`${window.actor.name}'s detached window was not captured: the recorder stopped waiting`);
|
|
198
|
+
if (captureOptions?.loadComplete) {
|
|
199
|
+
await window.app.waitForFunction(() => document.readyState === "complete", undefined, { timeout: timeoutMs });
|
|
200
|
+
signal?.throwIfAborted();
|
|
201
|
+
}
|
|
198
202
|
const started = Date.now();
|
|
199
203
|
const unsettled = captureOptions?.settled?.selectors.length ? await settle(window.app, captureOptions.settled) : undefined;
|
|
200
204
|
if (unsettled)
|
|
@@ -211,7 +215,7 @@ export function detachedFrameCapture(tiles, detached, options = {}) {
|
|
|
211
215
|
throw new Error(`${window.actor.name}'s detached window gave no frame within ${timeoutMs} ms`, { cause: error });
|
|
212
216
|
throw new Error(`${window.actor.name}'s detached window could not be captured: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
213
217
|
}
|
|
214
|
-
return { status: "captured", mediaType: "image/png", bytes: new Uint8Array(bytes), ...settled };
|
|
218
|
+
return { status: "captured", mediaType: "image/png", bytes: new Uint8Array(bytes), ...settled, ...(captureOptions?.loadComplete ? { loadComplete: true } : {}) };
|
|
215
219
|
},
|
|
216
220
|
};
|
|
217
221
|
}
|
|
@@ -13,6 +13,19 @@ export function windowFrameCapture(page, frameSelector, media, videos, uiCalls)
|
|
|
13
13
|
video: () => windowVideo(page, videos, { source: "lab-window", scope: "window" }),
|
|
14
14
|
async capture(actorId, options) {
|
|
15
15
|
const selector = frameSelector(actorId);
|
|
16
|
+
if (options?.loadComplete) {
|
|
17
|
+
const element = await page.locator(selector).elementHandle({ timeout: options.timeoutMs });
|
|
18
|
+
try {
|
|
19
|
+
const app = await element?.contentFrame();
|
|
20
|
+
if (!app)
|
|
21
|
+
return { status: "unavailable", reason: "The displayed app has no current document" };
|
|
22
|
+
await app.waitForLoadState("load", { timeout: options.timeoutMs });
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await element?.dispose();
|
|
26
|
+
}
|
|
27
|
+
options.signal?.throwIfAborted();
|
|
28
|
+
}
|
|
16
29
|
const started = Date.now();
|
|
17
30
|
const unsettled = options?.settled?.selectors.length ? await settle(page.frameLocator(selector), options.settled) : undefined;
|
|
18
31
|
if (unsettled)
|
|
@@ -45,7 +58,7 @@ export function windowFrameCapture(page, frameSelector, media, videos, uiCalls)
|
|
|
45
58
|
const bytes = await cutInWorker(await shot, view.width, { rect: { x, y, width, height } });
|
|
46
59
|
const after = await page.evaluate(steadyFrameBox, { selector, settleMs: 0 });
|
|
47
60
|
if (!("moving" in after) && sameBox(after, box))
|
|
48
|
-
return { status: "captured", mediaType: "image/png", bytes: new Uint8Array(bytes), ...settled };
|
|
61
|
+
return { status: "captured", mediaType: "image/png", bytes: new Uint8Array(bytes), ...settled, ...(options?.loadComplete ? { loadComplete: true } : {}) };
|
|
49
62
|
}
|
|
50
63
|
finally {
|
|
51
64
|
if (!shot)
|
package/package.json
CHANGED