@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +27 -0
  3. package/adapters/browser/playwright/README.md +2 -0
  4. package/adapters/browser/playwright/src/page-frame-capture.js +10 -2
  5. package/adapters/chain/papi/README.md +3 -1
  6. package/adapters/environment/engine/README.md +3 -3
  7. package/adapters/environment/engine/src/engine/fork-refresh.js +72 -1
  8. package/adapters/features/cucumber/README.md +4 -0
  9. package/adapters/features/cucumber/src/steps/register-feature-steps.js +15 -1
  10. package/adapters/system/node/README.md +5 -5
  11. package/adapters/video/ffmpeg/README.md +4 -0
  12. package/adapters/video/ffmpeg/src/ffmpeg-video-editor.d.ts +1 -1
  13. package/adapters/video/ffmpeg/src/ffmpeg-video-editor.js +19 -6
  14. package/core/application/README.md +30 -1
  15. package/core/application/src/features/direct-feature-steps.d.ts +23 -0
  16. package/core/application/src/features/direct-feature-steps.js +25 -0
  17. package/core/application/src/features/feature-recording.d.ts +6 -0
  18. package/core/application/src/features/feature-recording.js +3 -0
  19. package/core/application/src/features/feature-scenario.d.ts +10 -8
  20. package/core/application/src/features/feature-scenario.js +11 -15
  21. package/core/application/src/features/feature-step-times.d.ts +11 -1
  22. package/core/application/src/features/feature-step-times.js +20 -6
  23. package/core/application/src/features/lab-feature-session.d.ts +3 -1
  24. package/core/application/src/features/lab-feature-session.js +5 -2
  25. package/core/application/src/features/run-feature-steps.d.ts +3 -1
  26. package/core/application/src/features/run-feature-steps.js +4 -2
  27. package/core/application/src/lab-runtime-use-cases.js +5 -1
  28. package/core/application/src/recordings/demo-markup.d.ts +8 -0
  29. package/core/application/src/recordings/demo-markup.js +11 -1
  30. package/core/application/src/recordings/demo-recording.d.ts +0 -2
  31. package/core/application/src/recordings/demo-recording.js +40 -23
  32. package/core/application/src/recordings/frame-sources.d.ts +20 -1
  33. package/core/application/src/recordings/frame-sources.js +89 -0
  34. package/core/application/src/recordings/record-runs.d.ts +7 -1
  35. package/core/application/src/recordings/record-runs.js +66 -11
  36. package/core/application/src/recordings/recording-draft.d.ts +4 -0
  37. package/core/application/src/recordings/recording-draft.js +13 -4
  38. package/core/domain/README.md +14 -0
  39. package/core/domain/src/recordings/demo-cut.js +6 -4
  40. package/core/domain/src/recordings/demo-holds.d.ts +12 -0
  41. package/core/domain/src/recordings/demo-holds.js +31 -0
  42. package/core/domain/src/recordings/index.d.ts +1 -0
  43. package/core/domain/src/recordings/index.js +1 -0
  44. package/core/domain/src/recordings/recording-data.d.ts +9 -1
  45. package/core/domain/src/recordings/recording-settings.d.ts +5 -1
  46. package/core/domain/src/recordings/recording-settings.js +7 -0
  47. package/core/domain/src/recordings/run-recording.d.ts +2 -0
  48. package/core/domain/src/recordings/run-recording.js +8 -1
  49. package/core/ports/README.md +6 -0
  50. package/core/ports/src/driven/recording/demo-ports.d.ts +2 -0
  51. package/core/ports/src/driven/recording/frame-capture.d.ts +4 -0
  52. package/core/ports/src/driving/demo.d.ts +2 -0
  53. package/core/ports/src/driving/feature-session.d.ts +7 -0
  54. package/entrypoints/cli/README.md +6 -3
  55. package/entrypoints/cli/ai-kit/skills/product-lab-integration/references/notes-lab.md +1 -1
  56. package/entrypoints/cli/ai-kit/skills/product-lab-setup/SKILL.md +1 -0
  57. package/entrypoints/cli/ai-kit/skills/product-lab-setup/references/minimal-lab.md +1 -1
  58. package/entrypoints/cli/src/demo-command.d.ts +1 -1
  59. package/entrypoints/cli/src/demo-command.js +10 -5
  60. package/entrypoints/mcp/README.md +3 -0
  61. package/entrypoints/panel/README.md +6 -3
  62. package/entrypoints/panel/src/visible/detached-identities.js +5 -1
  63. package/entrypoints/panel/src/visible/window-frames.js +14 -1
  64. 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 video cut to about a minute, every action at real speed with its result\nheld, waits sped up and labelled (\u00D78 waiting for finality), a caption per Gherkin 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 longest the cut should be (default 60); over it the waits speed up first\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.";
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 video cut to about a minute, every action at real speed with its result
9
- held, waits sped up and labelled (×8 waiting for finality), a caption per Gherkin step, plus demo.vtt (the captions),
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 longest the cut should be (default 60); over it the waits speed up first
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; a normal tab and the lab window via `openLabWindow`, including real
421
- `framenavigated` reports and the acting guard). Optional `ADDRESS_BAR_SCREENS=<directory>` writes review screenshots
422
- (unset: none).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parity/product-lab",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Run and test Polkadot products against real local chains, hosts and browsers: the framework and the product-lab CLI.",
6
6
  "license": "Apache-2.0",