@outerlayer/cli 0.34.0-next.20261026214534 → 0.34.0-next.20261026230708

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 (54) hide show
  1. package/README.md +12 -4
  2. package/dist/{build-BBWAD5LR.js → build-M4UYZAFH.js} +3 -2
  3. package/dist/{build-disk-TGLM5XK7.js → build-disk-ZDVSSHO2.js} +3 -2
  4. package/dist/build-info.json +1 -1
  5. package/dist/{builder-FD53GTWL.js → builder-IORYS76D.js} +2 -1
  6. package/dist/{check-FOWN6M3C.js → check-V66LZDNK.js} +15 -14
  7. package/dist/{chunk-T2HH7IHT.js → chunk-63A6NUEZ.js} +1 -1
  8. package/dist/{chunk-3HJMY3H5.js → chunk-6NXBXWOP.js} +1 -1
  9. package/dist/{chunk-3PUOHONX.js → chunk-DFBY6LX3.js} +6 -5
  10. package/dist/{chunk-RNMCFAYH.js → chunk-F5H3LVHR.js} +2 -2
  11. package/dist/chunk-HDLON3RJ.js +274 -0
  12. package/dist/{chunk-FVF2LMVG.js → chunk-IZVDQXTC.js} +9 -9
  13. package/dist/{chunk-LY62UHRR.js → chunk-JZLMFYFM.js} +1 -1
  14. package/dist/{chunk-XYLHSCLR.js → chunk-KOZ6IBEI.js} +8 -279
  15. package/dist/{chunk-K3ACC2CV.js → chunk-MIRCFK4X.js} +2 -33
  16. package/dist/{chunk-5ZBVKRDK.js → chunk-PN7RMFPP.js} +2 -2
  17. package/dist/{chunk-BQQPDBSF.js → chunk-Q3L5AC75.js} +2 -2
  18. package/dist/{chunk-2GDKM2O4.js → chunk-T6HSNQAT.js} +4 -4
  19. package/dist/{chunk-KR2TIRNQ.js → chunk-T7HUNNJ2.js} +30 -16
  20. package/dist/{chunk-JKLSQOYO.js → chunk-TGBST2QL.js} +11 -7
  21. package/dist/{chunk-3NCWHEGM.js → chunk-TLIZ7FY4.js} +2 -2
  22. package/dist/{chunk-XHZBLIB2.js → chunk-XDJ376OP.js} +7 -1
  23. package/dist/{chunk-ZADRW2V2.js → chunk-XLJXO4QC.js} +4 -4
  24. package/dist/{cli-3RDQSLYI.js → cli-HQWQEHXJ.js} +79 -44
  25. package/dist/{command-C5YKBTK4.js → command-TONUMZ2A.js} +8 -7
  26. package/dist/{config-EBT5VYPX.js → config-4K6OG2SZ.js} +4 -3
  27. package/dist/{container-start-RLX2PL3C.js → container-start-R74IO7CI.js} +12 -11
  28. package/dist/demo-record-cmd-7HJ5C7KM.js +208 -0
  29. package/dist/{demo-voice-cmd-DF6C7EST.js → demo-voice-cmd-MBK3BGHZ.js} +1 -2
  30. package/dist/{docker-CP656XNO.js → docker-XWQM4F7B.js} +2 -1
  31. package/dist/{doctor-EQIAHHZD.js → doctor-SF4GAWAO.js} +9 -8
  32. package/dist/{emit-demo-cmd-YKTL6XWP.js → emit-demo-cmd-6VTA5BSJ.js} +1 -2
  33. package/dist/{find-unit-O6WL6UIW.js → find-unit-I4GWKY4T.js} +6 -5
  34. package/dist/{host-checks-V25YUP4Q.js → host-checks-FKLQST3J.js} +10 -9
  35. package/dist/index.js +1 -1
  36. package/dist/{init-KO3FT2XP.js → init-3MO5WCU3.js} +8 -7
  37. package/dist/{init-YYZ7B4ZD.js → init-TDLUTI6F.js} +8 -7
  38. package/dist/{init-cmd-D3BDS7KY.js → init-cmd-MQ5R35O2.js} +1 -1
  39. package/dist/{loop-UVONXWXB.js → loop-NNNDBFAS.js} +14 -13
  40. package/dist/{machine-2BI76JZO.js → machine-GCW2GGI2.js} +8 -7
  41. package/dist/recorder.cjs +916 -0
  42. package/dist/{runner-env-QN4BLTVZ.js → runner-env-LVVLUHY2.js} +6 -5
  43. package/dist/{runner-identity-53I4UTVY.js → runner-identity-73F2IWBT.js} +2 -1
  44. package/dist/{runner-key-JKXJWGGY.js → runner-key-ASPZ7TQH.js} +8 -7
  45. package/dist/{status-YCQHAY45.js → status-35Z3TW42.js} +10 -9
  46. package/dist/{update-steps-MC3IUHW7.js → update-steps-ZTNIK7LA.js} +11 -10
  47. package/dist/{work-claim-cmd-O54L5DSL.js → work-claim-cmd-QPYCZUMI.js} +4 -3
  48. package/dist/{work-proofs-cmd-LIXV252G.js → work-proofs-cmd-56EY7O45.js} +1 -1
  49. package/dist/{work-proofs-record-MCSA56F5.js → work-proofs-record-SPGI47OS.js} +2 -3
  50. package/package.json +5 -4
  51. package/skill-pack/maintained/outerlayer-amend/SKILL.md +2 -2
  52. package/skill-pack/maintained/outerlayer-build/references/evidence.md +12 -11
  53. package/skill-pack/maintained/outerlayer-evidence/SKILL.md +6 -6
  54. package/skill-pack/maintained/outerlayer-evidence/references/demo.md +12 -11
@@ -0,0 +1,916 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+ var test$1 = require('@playwright/test');
6
+ var async_hooks = require('async_hooks');
7
+ var crypto = require('crypto');
8
+ var child_process = require('child_process');
9
+ var readline = require('readline');
10
+
11
+ // ../playwright/src/fixture.ts
12
+ function hasGit(directory) {
13
+ return fs.existsSync(path.join(directory, ".git"));
14
+ }
15
+ function repositoryPath(file, isRoot = hasGit) {
16
+ let directory = path.dirname(file);
17
+ for (; ; ) {
18
+ if (isRoot(directory)) return path.relative(directory, file).split(path.sep).join("/");
19
+ const parent = path.dirname(directory);
20
+ if (parent === directory) return void 0;
21
+ directory = parent;
22
+ }
23
+ }
24
+ function buildManifest(params) {
25
+ const test2 = repositoryPath(params.testFile, params.hasGit) ?? path.relative(params.rootDir, params.testFile).split(path.sep).join("/");
26
+ const beats = params.chapters.some((chapter) => chapter.beats !== void 0);
27
+ if (!beats) return { schemaVersion: 4, test: test2, video: params.video, chapters: params.chapters };
28
+ return { schemaVersion: 5, test: test2, video: params.video, ...params.viewport === void 0 ? {} : { viewport: params.viewport }, chapters: params.chapters };
29
+ }
30
+ function smallerPage(page, video) {
31
+ if (page === null || video === void 0) return void 0;
32
+ return page.width < video.width || page.height < video.height ? { width: page.width, height: page.height } : void 0;
33
+ }
34
+ var CHECK_TEXT_MAX = 500;
35
+ function clipCheckText(text) {
36
+ return text.length <= CHECK_TEXT_MAX ? text : `${text.slice(0, CHECK_TEXT_MAX - 1)}\u2026`;
37
+ }
38
+ function seconds(milliseconds) {
39
+ return Math.round(milliseconds) / 1e3;
40
+ }
41
+
42
+ // ../playwright/src/expect.ts
43
+ var running = new async_hooks.AsyncLocalStorage();
44
+ async function recordChecksDuring(checks, body, onPass) {
45
+ const store = { checks, onPass, pending: [] };
46
+ try {
47
+ return await running.run(store, body);
48
+ } finally {
49
+ await Promise.allSettled(store.pending);
50
+ }
51
+ }
52
+ var RECORDED = /* @__PURE__ */ Symbol.for("outerlayer.playwright.recorded");
53
+ function describeValue(value) {
54
+ if (value === null || value === void 0 || typeof value !== "object") return JSON.stringify(value) ?? String(value);
55
+ if (typeof value.toString === "function") {
56
+ const text = String(value);
57
+ if (!text.startsWith("[object ")) return text;
58
+ }
59
+ const kind = value.constructor?.name ?? "value";
60
+ return kind === "Object" ? JSON.stringify(value) : kind.toLowerCase();
61
+ }
62
+ function describeArgs(args) {
63
+ return args.map((arg) => {
64
+ const text = typeof arg === "function" ? "function" : describeValue(arg);
65
+ return text.length > 80 ? `${text.slice(0, 79)}\u2026` : text;
66
+ }).join(", ");
67
+ }
68
+ function isThenable(value) {
69
+ return typeof value?.then === "function";
70
+ }
71
+ function record(check) {
72
+ const store = running.getStore();
73
+ if (store === void 0) return void 0;
74
+ store.checks.push({ text: clipCheckText(check.text), passed: check.passed });
75
+ if (!check.passed || store.onPass === void 0) return void 0;
76
+ const after = store.onPass(store.checks.length - 1);
77
+ store.pending.push(after);
78
+ return after;
79
+ }
80
+ function failed(error) {
81
+ if (typeof error === "object" && error !== null) error[RECORDED] = true;
82
+ return error;
83
+ }
84
+ function wrapMatchers(matchers, context) {
85
+ if (typeof matchers !== "object" && typeof matchers !== "function") return matchers;
86
+ return new Proxy(matchers, {
87
+ get(target, property, receiver) {
88
+ const value = Reflect.get(target, property, receiver);
89
+ if (typeof property === "symbol") return value;
90
+ if (value !== null && typeof value === "object") {
91
+ return wrapMatchers(value, { ...context, modifiers: `${context.modifiers}${property}.` });
92
+ }
93
+ if (typeof value !== "function") return value;
94
+ return (...args) => {
95
+ const text = `${context.subject}.${context.modifiers}${property}(${describeArgs(args)})`;
96
+ const errorsBefore = context.soft ? test$1.test.info().errors.length : 0;
97
+ const settle = (error, hasError) => {
98
+ const softFailure = context.soft && test$1.test.info().errors.length > errorsBefore;
99
+ const after = record({ text, passed: !hasError && !softFailure });
100
+ if (hasError) throw failed(error);
101
+ return after;
102
+ };
103
+ let result;
104
+ try {
105
+ result = value.apply(target, args);
106
+ } catch (error) {
107
+ settle(error, true);
108
+ }
109
+ if (isThenable(result)) {
110
+ return result.then(
111
+ async (resolved) => {
112
+ await settle(void 0, false);
113
+ return resolved;
114
+ },
115
+ (error) => {
116
+ settle(error, true);
117
+ }
118
+ );
119
+ }
120
+ settle(void 0, false);
121
+ return result;
122
+ };
123
+ }
124
+ });
125
+ }
126
+ function wrapEntry(entry, thisArg, args, soft, label) {
127
+ const matchers = Reflect.apply(entry, thisArg, args);
128
+ const [received] = args;
129
+ return wrapMatchers(matchers, { subject: `expect${label}(${describeValue(received)})`, modifiers: "", soft });
130
+ }
131
+ var expect = new Proxy(test$1.expect, {
132
+ apply(target, thisArg, args) {
133
+ return wrapEntry(target, thisArg, args, false, "");
134
+ },
135
+ get(target, property, receiver) {
136
+ const value = Reflect.get(target, property, receiver);
137
+ if (property === "soft" && typeof value === "function") {
138
+ return (...args) => wrapEntry(value, target, args, true, ".soft");
139
+ }
140
+ if (property === "poll" && typeof value === "function") {
141
+ return (...args) => wrapEntry(value, target, args, false, ".poll");
142
+ }
143
+ return value;
144
+ }
145
+ });
146
+ function safeName(parts) {
147
+ return parts.join("-").replace(/[^A-Za-z0-9.-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 100);
148
+ }
149
+ function demoFolderFor(params) {
150
+ if (params.proofOut === void 0 || params.proofOut === "") return params.outputPath(params.folder);
151
+ return path.join(params.proofOut, safeName([params.projectName, params.testId]) || "test", params.folder);
152
+ }
153
+
154
+ // ../playwright/src/human.ts
155
+ function pointerScript() {
156
+ return `(() => {
157
+ if (window.top !== window || window.__outerlayerPointer) return;
158
+ const KEY = "outerlayer-pointer";
159
+ let x = -40, y = -40, shown = false, element;
160
+ try {
161
+ const saved = JSON.parse(sessionStorage.getItem(KEY) || "null");
162
+ if (saved) { x = saved.x; y = saved.y; shown = true; }
163
+ } catch {}
164
+ const place = () => {
165
+ if (!element) return;
166
+ element.style.transform = "translate(" + (x - 3) + "px," + (y - 2) + "px)";
167
+ element.style.opacity = shown ? "1" : "0";
168
+ };
169
+ const mount = () => {
170
+ if (element && element.isConnected) return;
171
+ const root = document.documentElement;
172
+ if (!root) return;
173
+ element = document.createElement("div");
174
+ element.setAttribute("data-outerlayer-pointer", "");
175
+ element.style.cssText = "position:fixed;left:0;top:0;width:24px;height:28px;pointer-events:none;z-index:2147483647;filter:drop-shadow(0 1px 2px rgba(0,0,0,.4));";
176
+ element.innerHTML = '<svg width="24" height="28" viewBox="0 0 24 28" xmlns="http://www.w3.org/2000/svg"><path d="M3 2.5 L3 22 L8.2 17.2 L11.8 25.2 L15.4 23.6 L11.9 15.8 L19 15.8 Z" fill="#111827" stroke="#ffffff" stroke-width="1.8" stroke-linejoin="round"/></svg>';
177
+ root.appendChild(element);
178
+ place();
179
+ };
180
+ const ring = (cx, cy) => {
181
+ const root = document.documentElement;
182
+ if (!root) return;
183
+ const circle = document.createElement("div");
184
+ circle.setAttribute("data-outerlayer-pointer", "");
185
+ circle.style.cssText = "position:fixed;left:" + cx + "px;top:" + cy + "px;width:12px;height:12px;pointer-events:none;z-index:2147483646;border-radius:50%;border:2px solid rgba(59,130,246,.9);background:rgba(59,130,246,.2);transform:translate(-50%,-50%);";
186
+ root.appendChild(circle);
187
+ const began = performance.now();
188
+ const grow = (now) => {
189
+ const p = Math.min(1, (now - began) / 480);
190
+ const size = 12 + 30 * (1 - Math.pow(1 - p, 3));
191
+ circle.style.width = size + "px";
192
+ circle.style.height = size + "px";
193
+ circle.style.opacity = String(1 - p);
194
+ if (p < 1) requestAnimationFrame(grow); else circle.remove();
195
+ };
196
+ requestAnimationFrame(grow);
197
+ };
198
+ addEventListener("mousemove", (event) => {
199
+ x = event.clientX; y = event.clientY; shown = true;
200
+ mount(); place();
201
+ try { sessionStorage.setItem(KEY, JSON.stringify({ x, y })); } catch {}
202
+ }, true);
203
+ addEventListener("mousedown", (event) => ring(event.clientX, event.clientY), true);
204
+ window.__outerlayerPointer = { place: (px, py) => { x = px; y = py; shown = true; mount(); place(); } };
205
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", mount); else mount();
206
+ // A framework that replaces the document's children takes the pointer with it, so put it back.
207
+ setInterval(() => { if (shown && element && !element.isConnected) { element = undefined; mount(); } }, 200);
208
+ })();`;
209
+ }
210
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
211
+ var STEP_TIMEOUT = 5e3;
212
+ function ease(p) {
213
+ return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
214
+ }
215
+ var Hand = class {
216
+ constructor(page, start) {
217
+ this.page = page;
218
+ this.x = start.x;
219
+ this.y = start.y;
220
+ }
221
+ get position() {
222
+ return { x: this.x, y: this.y };
223
+ }
224
+ async moveTo(targetX, targetY) {
225
+ const dx = targetX - this.x;
226
+ const dy = targetY - this.y;
227
+ const distance = Math.hypot(dx, dy);
228
+ if (distance < 2) return;
229
+ const duration = Math.min(950, Math.max(320, 240 + distance * 0.6));
230
+ const steps = Math.max(10, Math.round(duration / 16));
231
+ const bow = Math.min(36, distance * 0.06);
232
+ const normalX = -dy / distance;
233
+ const normalY = dx / distance;
234
+ const fromX = this.x;
235
+ const fromY = this.y;
236
+ const began = Date.now();
237
+ for (let step = 1; step <= steps; step++) {
238
+ const p = step / steps;
239
+ const along = ease(p);
240
+ const side = Math.sin(Math.PI * p) * bow;
241
+ await this.page.mouse.move(fromX + dx * along + normalX * side, fromY + dy * along + normalY * side);
242
+ const wait = began + duration * p - Date.now();
243
+ if (wait > 0) await sleep(wait);
244
+ }
245
+ this.x = targetX;
246
+ this.y = targetY;
247
+ }
248
+ /** Moves to the point Playwright will click: `position` inside the element, or its centre. */
249
+ async pointAt(locator, position) {
250
+ const box = await locator.boundingBox({ timeout: STEP_TIMEOUT }).catch(() => null);
251
+ if (box === null) return;
252
+ const x = box.x + (position?.x ?? box.width / 2);
253
+ const y = box.y + (position?.y ?? box.height / 2);
254
+ await this.moveTo(x, y);
255
+ }
256
+ };
257
+ async function bringIntoView(locator) {
258
+ const inView = await locator.evaluate(
259
+ (element) => {
260
+ const rect = element.getBoundingClientRect();
261
+ return rect.top >= 56 && rect.bottom <= window.innerHeight - 24;
262
+ },
263
+ void 0,
264
+ { timeout: STEP_TIMEOUT }
265
+ ).catch(() => true);
266
+ if (inView) return;
267
+ await locator.evaluate((element) => element.scrollIntoView({ behavior: "smooth", block: "center" }), void 0, { timeout: STEP_TIMEOUT }).catch(() => void 0);
268
+ let previous = Number.NaN;
269
+ let still = 0;
270
+ for (let tries = 0; tries < 90 && still < 4; tries++) {
271
+ await sleep(16);
272
+ const top = await locator.evaluate((element) => element.getBoundingClientRect().top, void 0, { timeout: STEP_TIMEOUT }).catch(() => previous);
273
+ still = Math.abs(top - previous) < 0.5 ? still + 1 : 0;
274
+ previous = top;
275
+ }
276
+ }
277
+ var TEXT_INPUT_TYPES = /* @__PURE__ */ new Set(["", "text", "search", "email", "url", "tel", "password", "number"]);
278
+ function actLikeAPerson(page, hand) {
279
+ const prototype = Object.getPrototypeOf(page.locator("html"));
280
+ const original = {
281
+ click: prototype.click,
282
+ dblclick: prototype.dblclick,
283
+ check: prototype.check,
284
+ uncheck: prototype.uncheck,
285
+ setChecked: prototype.setChecked,
286
+ hover: prototype.hover,
287
+ fill: prototype.fill,
288
+ selectOption: prototype.selectOption
289
+ };
290
+ const approach = async (locator, position) => {
291
+ await locator.waitFor({ state: "visible", timeout: STEP_TIMEOUT }).catch(() => void 0);
292
+ await bringIntoView(locator);
293
+ await hand.pointAt(locator, position);
294
+ await sleep(130);
295
+ };
296
+ const pointing = (name) => async function(options) {
297
+ await approach(this, options?.position);
298
+ return original[name].call(this, options);
299
+ };
300
+ prototype.click = pointing("click");
301
+ prototype.dblclick = pointing("dblclick");
302
+ prototype.check = pointing("check");
303
+ prototype.uncheck = pointing("uncheck");
304
+ prototype.hover = async function(options) {
305
+ await approach(this, options?.position);
306
+ await original.hover.call(this, { ...options, timeout: options?.timeout ?? STEP_TIMEOUT }).catch(() => void 0);
307
+ };
308
+ prototype.setChecked = async function(checked, options) {
309
+ await approach(this, options?.position);
310
+ return original.setChecked.call(this, checked, options);
311
+ };
312
+ prototype.selectOption = async function(values, options) {
313
+ await approach(this);
314
+ const chosen = await original.selectOption.call(this, values, options);
315
+ await sleep(250);
316
+ return chosen;
317
+ };
318
+ prototype.fill = async function(value, options) {
319
+ const field = await this.evaluate(
320
+ (element) => ({
321
+ tag: element.tagName,
322
+ type: element instanceof HTMLInputElement ? element.type : "",
323
+ editable: element.isContentEditable
324
+ }),
325
+ void 0,
326
+ { timeout: options?.timeout ?? 3e4 }
327
+ ).catch(() => void 0);
328
+ const typeable = field !== void 0 && (field.tag === "TEXTAREA" || field.editable || field.tag === "INPUT" && TEXT_INPUT_TYPES.has(field.type) && !value.includes("\n"));
329
+ if (!typeable || value === "") return original.fill.call(this, value, options);
330
+ await approach(this);
331
+ await original.click.call(this);
332
+ await original.fill.call(this, "", options);
333
+ const delay = Math.max(14, Math.min(55, Math.round(1600 / value.length)));
334
+ await this.pressSequentially(value, { delay });
335
+ const typed = await this.inputValue().catch(() => void 0);
336
+ if (typed !== void 0 && typed !== value) await original.fill.call(this, value, options);
337
+ await sleep(120);
338
+ };
339
+ return () => {
340
+ Object.assign(prototype, original);
341
+ };
342
+ }
343
+
344
+ // ../playwright/src/settings.ts
345
+ var FIXED_SETTINGS = {
346
+ /** Each test starts at this time, and its clock then runs normally. */
347
+ startTime: "2026-01-15T10:00:00.000Z",
348
+ timezoneId: "UTC",
349
+ locale: "en-US",
350
+ viewport: { width: 1280, height: 800 },
351
+ deviceScaleFactor: 1
352
+ };
353
+ var MASK_COLOR = "#808080";
354
+ var DEFAULT_DEMO_OPTIONS = { clock: true, random: true, animations: true, caret: true, fonts: true };
355
+ function resolveDemoOptions(options) {
356
+ return { ...DEFAULT_DEMO_OPTIONS, ...options };
357
+ }
358
+ function randomScript() {
359
+ return `(() => {
360
+ const imul = Math.imul;
361
+ let state = 0x2f6e2b1;
362
+ Math.random = () => {
363
+ state = (state + 0x6d2b79f5) >>> 0;
364
+ let t = state;
365
+ t = imul(t ^ (t >>> 15), t | 1);
366
+ t ^= t + imul(t ^ (t >>> 7), t | 61);
367
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
368
+ };
369
+ })();`;
370
+ }
371
+ var ANIMATION_RULES = [
372
+ "animation-duration: 0s !important;",
373
+ "animation-delay: 0s !important;",
374
+ "transition-duration: 0s !important;",
375
+ "transition-delay: 0s !important;",
376
+ "scroll-behavior: auto !important;"
377
+ ];
378
+ var CARET_RULES = ["caret-color: transparent !important;"];
379
+ function stylesheetFor(options) {
380
+ const rules = [...options.animations ? ANIMATION_RULES : [], ...options.caret ? CARET_RULES : []];
381
+ return rules.length === 0 ? "" : `*, *::before, *::after {
382
+ ${rules.join("\n ")}
383
+ }`;
384
+ }
385
+ stylesheetFor(DEFAULT_DEMO_OPTIONS);
386
+ var STYLE_ELEMENT_ID = "outerlayer-demo-style";
387
+ function stylesheetScript(css) {
388
+ return `(() => {
389
+ const add = () => {
390
+ if (document.getElementById(${JSON.stringify(STYLE_ELEMENT_ID)})) return;
391
+ const parent = document.head || document.documentElement;
392
+ if (!parent) return;
393
+ const element = document.createElement("style");
394
+ element.id = ${JSON.stringify(STYLE_ELEMENT_ID)};
395
+ element.textContent = ${JSON.stringify(css)};
396
+ parent.appendChild(element);
397
+ };
398
+ add();
399
+ document.addEventListener("DOMContentLoaded", add);
400
+ })();`;
401
+ }
402
+ var HIDE_OVERLAY = "[data-outerlayer-mask]{visibility:hidden!important}";
403
+ var HIDE_POINTER = "[data-outerlayer-pointer],[data-outerlayer-highlight]{visibility:hidden!important}";
404
+ async function takeFrame(page, params) {
405
+ const { path: path$1, options, masks, fullPage = false, hideOverlay = false } = params;
406
+ if (options.fonts) await page.evaluate(() => document.fonts.ready.then(() => void 0));
407
+ const css = stylesheetFor(options);
408
+ if (css !== "") await page.evaluate(stylesheetScript(css));
409
+ const bytes = await page.screenshot({
410
+ // Motion stops for the frame even when the video keeps it, so two runs give the same frame.
411
+ animations: "disabled",
412
+ caret: options.caret ? "hide" : "initial",
413
+ scale: "css",
414
+ fullPage,
415
+ mask: masks,
416
+ maskColor: MASK_COLOR,
417
+ style: hideOverlay ? `${HIDE_OVERLAY}
418
+ ${HIDE_POINTER}` : HIDE_POINTER
419
+ });
420
+ fs.mkdirSync(path.dirname(path$1), { recursive: true });
421
+ fs.writeFileSync(path$1, bytes);
422
+ return crypto.createHash("sha256").update(bytes).digest("hex");
423
+ }
424
+ function takeStill(page, params) {
425
+ return takeFrame(page, params);
426
+ }
427
+
428
+ // ../playwright/src/highlight.ts
429
+ var HIGHLIGHT_ATTRIBUTE = "data-outerlayer-highlight";
430
+ async function showHighlight(locator) {
431
+ await locator.evaluate(
432
+ (element, attribute) => {
433
+ const box = document.createElement("div");
434
+ box.setAttribute(attribute, "");
435
+ box.style.cssText = "position:fixed;pointer-events:none;z-index:2147483630;border-radius:10px;opacity:0;box-shadow:0 0 0 3px rgba(59,130,246,.95),0 0 0 9999px rgba(15,23,42,.42);";
436
+ document.documentElement.appendChild(box);
437
+ const place = () => {
438
+ if (!box.isConnected) return;
439
+ const rect = element.getBoundingClientRect();
440
+ box.style.left = `${rect.left - 8}px`;
441
+ box.style.top = `${rect.top - 6}px`;
442
+ box.style.width = `${rect.width + 16}px`;
443
+ box.style.height = `${rect.height + 12}px`;
444
+ requestAnimationFrame(place);
445
+ };
446
+ place();
447
+ const began = performance.now();
448
+ const show = (now) => {
449
+ const p = Math.min(1, (now - began) / 260);
450
+ box.style.opacity = String(p);
451
+ if (p < 1) requestAnimationFrame(show);
452
+ };
453
+ requestAnimationFrame(show);
454
+ },
455
+ HIGHLIGHT_ATTRIBUTE,
456
+ { timeout: 1e4 }
457
+ );
458
+ await locator.page().waitForTimeout(300);
459
+ }
460
+ async function clearHighlights(page) {
461
+ await page.evaluate(async (attribute) => {
462
+ const boxes = Array.from(document.querySelectorAll(`[${attribute}]`));
463
+ if (boxes.length === 0) return;
464
+ const began = performance.now();
465
+ await new Promise((resolve) => {
466
+ const hide = (now) => {
467
+ const p = Math.min(1, (now - began) / 220);
468
+ for (const box of boxes) box.style.opacity = String(1 - p);
469
+ if (p < 1) requestAnimationFrame(hide);
470
+ else resolve();
471
+ };
472
+ requestAnimationFrame(hide);
473
+ });
474
+ for (const box of boxes) box.remove();
475
+ }, HIGHLIGHT_ATTRIBUTE).catch(() => void 0);
476
+ }
477
+
478
+ // ../playwright/src/panel.ts
479
+ var PANEL_ATTRIBUTES = { terminal: "data-outerlayer-terminal", http: "data-outerlayer-http" };
480
+ var TITLES = { terminal: "Terminal", http: "HTTP" };
481
+ async function openPanel(page, kind, prompt, typed) {
482
+ await page.evaluate(
483
+ async ({ prompt: prompt2, typed: typed2, title, attributes, attribute }) => {
484
+ for (const name of attributes) document.querySelector(`[${name}]`)?.remove();
485
+ const panel = document.createElement("div");
486
+ panel.setAttribute(attribute, "");
487
+ panel.style.cssText = "position:fixed;left:50%;bottom:28px;width:min(860px,calc(100vw - 64px));transform:translate(-50%,16px);opacity:0;z-index:2147483640;background:#0f172a;color:#e2e8f0;border-radius:12px;box-shadow:0 24px 60px rgba(15,23,42,.45),0 0 0 1px rgba(148,163,184,.25);font:14px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden;";
488
+ const bar = document.createElement("div");
489
+ bar.style.cssText = "display:flex;align-items:center;gap:8px;padding:10px 14px;background:#1e293b;border-bottom:1px solid rgba(148,163,184,.2)";
490
+ for (const colour of ["#f87171", "#fbbf24", "#34d399"]) {
491
+ const dot = document.createElement("span");
492
+ dot.style.cssText = `width:12px;height:12px;border-radius:50%;background:${colour}`;
493
+ bar.appendChild(dot);
494
+ }
495
+ const label = document.createElement("span");
496
+ label.style.cssText = "margin-left:8px;color:#94a3b8;font-size:12px";
497
+ label.textContent = title;
498
+ bar.appendChild(label);
499
+ const body = document.createElement("div");
500
+ body.setAttribute("data-body", "");
501
+ body.style.cssText = "padding:14px 18px 18px;white-space:pre-wrap;word-break:break-word;min-height:72px;max-height:360px;overflow:hidden";
502
+ panel.append(bar, body);
503
+ document.documentElement.appendChild(panel);
504
+ const line = document.createElement("div");
505
+ const promptSpan = document.createElement("span");
506
+ promptSpan.style.color = "#34d399";
507
+ promptSpan.textContent = prompt2;
508
+ const typedSpan = document.createElement("span");
509
+ const caret = document.createElement("span");
510
+ caret.setAttribute("data-caret", "");
511
+ caret.style.cssText = "display:inline-block;width:8px;height:16px;margin-left:1px;vertical-align:-3px;background:#e2e8f0";
512
+ line.append(promptSpan, typedSpan, caret);
513
+ body.appendChild(line);
514
+ const began = performance.now();
515
+ await new Promise((resolve) => {
516
+ const show = (now) => {
517
+ const p = Math.min(1, (now - began) / 260);
518
+ panel.style.opacity = String(p);
519
+ panel.style.transform = `translate(-50%,${16 * (1 - p)}px)`;
520
+ if (p < 1) requestAnimationFrame(show);
521
+ else resolve();
522
+ };
523
+ requestAnimationFrame(show);
524
+ });
525
+ for (const character of typed2) {
526
+ typedSpan.textContent += character;
527
+ await new Promise((resolve) => setTimeout(resolve, 34));
528
+ }
529
+ },
530
+ { prompt, typed, title: TITLES[kind], attributes: Object.values(PANEL_ATTRIBUTES), attribute: PANEL_ATTRIBUTES[kind] }
531
+ );
532
+ }
533
+ async function printToPanel(page, kind, lines) {
534
+ await page.evaluate(
535
+ async ({ lines: lines2, attribute }) => {
536
+ const colours = { plain: "#e2e8f0", good: "#86efac", warn: "#fcd34d", bad: "#fca5a5", muted: "#94a3b8" };
537
+ const body = document.querySelector(`[${attribute}] [data-body]`);
538
+ if (!body) return;
539
+ body.querySelector("[data-caret]")?.remove();
540
+ for (const { text, tone } of lines2) {
541
+ const line = document.createElement("div");
542
+ line.textContent = text;
543
+ line.style.color = colours[tone ?? "plain"] ?? colours.plain;
544
+ body.appendChild(line);
545
+ await new Promise((resolve) => setTimeout(resolve, 60));
546
+ }
547
+ },
548
+ { lines, attribute: PANEL_ATTRIBUTES[kind] }
549
+ );
550
+ }
551
+ async function closePanels(page) {
552
+ await page.evaluate(async (attributes) => {
553
+ const panels = attributes.flatMap((name) => Array.from(document.querySelectorAll(`[${name}]`)));
554
+ if (panels.length === 0) return;
555
+ const began = performance.now();
556
+ await new Promise((resolve) => {
557
+ const hide = (now) => {
558
+ const p = Math.min(1, (now - began) / 220);
559
+ for (const panel of panels) {
560
+ panel.style.opacity = String(1 - p);
561
+ panel.style.transform = `translate(-50%,${12 * p}px)`;
562
+ }
563
+ if (p < 1) requestAnimationFrame(hide);
564
+ else resolve();
565
+ };
566
+ requestAnimationFrame(hide);
567
+ });
568
+ for (const panel of panels) panel.remove();
569
+ }, Object.values(PANEL_ATTRIBUTES)).catch(() => void 0);
570
+ }
571
+ function terminalLines(output) {
572
+ return output.replace(/\u001b\[[0-9;]*m/g, "").replace(/\s+$/, "").split("\n").map((text) => ({ text, tone: text.startsWith("\u2713") ? "good" : text.startsWith("!") ? "warn" : "plain" }));
573
+ }
574
+ var HTTP_BODY_LINES = 14;
575
+ function httpLines(response) {
576
+ const ok = response.status >= 200 && response.status < 400;
577
+ const lines = [{ text: `${response.status} ${STATUS_TEXT[response.status] ?? ""}`.trim(), tone: ok ? "good" : "bad" }];
578
+ if (response.body === void 0) return lines;
579
+ const text = typeof response.body === "string" ? response.body : JSON.stringify(response.body, null, 2) ?? "";
580
+ const bodyLines = text.split("\n");
581
+ for (const line of bodyLines.slice(0, HTTP_BODY_LINES)) lines.push({ text: line });
582
+ if (bodyLines.length > HTTP_BODY_LINES) lines.push({ text: `\u2026 ${bodyLines.length - HTTP_BODY_LINES} more lines`, tone: "muted" });
583
+ return lines;
584
+ }
585
+ var STATUS_TEXT = {
586
+ 200: "OK",
587
+ 201: "Created",
588
+ 202: "Accepted",
589
+ 204: "No Content",
590
+ 400: "Bad Request",
591
+ 401: "Unauthorized",
592
+ 403: "Forbidden",
593
+ 404: "Not Found",
594
+ 409: "Conflict",
595
+ 422: "Unprocessable Content",
596
+ 429: "Too Many Requests",
597
+ 500: "Internal Server Error",
598
+ 503: "Service Unavailable"
599
+ };
600
+ var ESTIMATED_WORDS_PER_SECOND = 2.9;
601
+ function estimateSeconds(text) {
602
+ const words = text.trim().split(/\s+/).filter((word) => word !== "").length;
603
+ return words === 0 ? 0 : words / ESTIMATED_WORDS_PER_SECOND + 0.3;
604
+ }
605
+ function voiceCommand(env) {
606
+ const cli = env.OUTERLAYER_CLI;
607
+ if (cli === void 0 || cli === "") return void 0;
608
+ return /\.[cm]?js$/.test(cli) ? [process.execPath, cli, "demo-voice"] : [cli, "demo-voice"];
609
+ }
610
+ var VoiceClient = class {
611
+ constructor(command) {
612
+ this.command = command;
613
+ this.waiting = /* @__PURE__ */ new Map();
614
+ this.nextId = 1;
615
+ }
616
+ /** Speaks `text` into the WAV file `out`, or estimates its length when no voice can run. */
617
+ async speak(text, out) {
618
+ if (text.trim() === "") return { seconds: 0, written: false };
619
+ if (this.command === void 0 || this.broken !== void 0) return { seconds: estimateSeconds(text), written: false };
620
+ try {
621
+ fs.mkdirSync(path.dirname(out), { recursive: true });
622
+ const seconds2 = await this.request(text, out);
623
+ return { seconds: seconds2, written: true };
624
+ } catch (error) {
625
+ this.broken = error instanceof Error ? error.message : String(error);
626
+ process.stderr.write(`@outerlayer/playwright: the voice failed (${this.broken}); beats use an estimated length
627
+ `);
628
+ return { seconds: estimateSeconds(text), written: false };
629
+ }
630
+ }
631
+ close() {
632
+ this.child?.stdin.end();
633
+ this.child = void 0;
634
+ }
635
+ start() {
636
+ if (this.child !== void 0) return this.child;
637
+ const [program, ...args] = this.command;
638
+ const child = child_process.spawn(program, args, { stdio: ["pipe", "pipe", "pipe"] });
639
+ child.stderr.on("data", (chunk) => process.stderr.write(chunk));
640
+ const fail = (error) => {
641
+ for (const waiting of this.waiting.values()) waiting.reject(error);
642
+ this.waiting.clear();
643
+ this.child = void 0;
644
+ };
645
+ child.on("error", fail);
646
+ child.on("exit", (code) => fail(new Error(`the voice exited with code ${code}`)));
647
+ readline.createInterface({ input: child.stdout }).on("line", (line) => {
648
+ let answer;
649
+ try {
650
+ answer = JSON.parse(line);
651
+ } catch {
652
+ return;
653
+ }
654
+ const waiting = answer.id === void 0 ? void 0 : this.waiting.get(answer.id);
655
+ if (waiting === void 0) return;
656
+ this.waiting.delete(answer.id);
657
+ if (typeof answer.seconds === "number") waiting.resolve(answer.seconds);
658
+ else waiting.reject(new Error(answer.error ?? "the voice gave no length"));
659
+ });
660
+ this.child = child;
661
+ return child;
662
+ }
663
+ request(text, out) {
664
+ const child = this.start();
665
+ const id = this.nextId++;
666
+ return new Promise((resolve, reject) => {
667
+ this.waiting.set(id, { resolve, reject });
668
+ child.stdin.write(`${JSON.stringify({ id, text, out })}
669
+ `);
670
+ });
671
+ }
672
+ };
673
+
674
+ // ../playwright/src/fixture.ts
675
+ async function installScript(page, source) {
676
+ await page.addInitScript(source);
677
+ await page.evaluate(source);
678
+ }
679
+ function drawMask(locator) {
680
+ return locator.evaluate(
681
+ (element, color) => {
682
+ const box = document.createElement("div");
683
+ box.setAttribute("data-outerlayer-mask", "");
684
+ box.style.cssText = `position:fixed;pointer-events:none;z-index:2147483647;background:${color};`;
685
+ const place = () => {
686
+ if (!box.isConnected) return;
687
+ const rect = element.getBoundingClientRect();
688
+ box.style.left = `${rect.left}px`;
689
+ box.style.top = `${rect.top}px`;
690
+ box.style.width = `${rect.width}px`;
691
+ box.style.height = `${rect.height}px`;
692
+ requestAnimationFrame(place);
693
+ };
694
+ document.documentElement.appendChild(box);
695
+ place();
696
+ },
697
+ MASK_COLOR,
698
+ { timeout: 2e3 }
699
+ );
700
+ }
701
+ function paintFrame(page) {
702
+ return page.evaluate(
703
+ () => new Promise((resolve) => {
704
+ const dot = document.createElement("div");
705
+ dot.style.cssText = "position:fixed;left:0;top:0;width:1px;height:1px;pointer-events:none;z-index:2147483647;background:#000;";
706
+ document.documentElement.appendChild(dot);
707
+ requestAnimationFrame(
708
+ () => requestAnimationFrame(() => {
709
+ dot.style.background = "#fff";
710
+ requestAnimationFrame(
711
+ () => requestAnimationFrame(() => {
712
+ dot.remove();
713
+ resolve();
714
+ })
715
+ );
716
+ })
717
+ );
718
+ })
719
+ );
720
+ }
721
+ var pad = (n) => String(n).padStart(2, "0");
722
+ var DEMO_FOLDER = "demo";
723
+ var BEAT_LEAD_IN = 0.15;
724
+ var BEAT_TAIL = 0.45;
725
+ var test = test$1.test.extend({
726
+ // The settings that vary between runs. A config's `use` or a test's `test.use` replaces any of them.
727
+ viewport: [FIXED_SETTINGS.viewport, { option: true }],
728
+ locale: [FIXED_SETTINGS.locale, { option: true }],
729
+ timezoneId: [FIXED_SETTINGS.timezoneId, { option: true }],
730
+ deviceScaleFactor: [FIXED_SETTINGS.deviceScaleFactor, { option: true }],
731
+ demoOptions: [{}, { option: true }],
732
+ // Playwright's own default scales the video down to fit 800 × 800. The demo records at the fixed viewport's size, so
733
+ // the text in a clip reads as it does on the page. The size is set once per worker, so it cannot follow a viewport
734
+ // one test sets: such a test sets `video` with its own size at the top of its file.
735
+ video: { mode: "on", size: FIXED_SETTINGS.viewport },
736
+ demo: async ({ page, demoOptions, video: videoOption }, use, testInfo) => {
737
+ const options = resolveDemoOptions(demoOptions);
738
+ const smaller = smallerPage(page.viewportSize(), typeof videoOption === "object" && videoOption !== null ? videoOption.size : void 0);
739
+ const folder = demoFolderFor({ outputPath: (...segments) => testInfo.outputPath(...segments), proofOut: process.env.OUTERLAYER_PROOF_OUT, testId: testInfo.testId, projectName: testInfo.project.name, folder: DEMO_FOLDER });
740
+ fs.mkdirSync(folder, { recursive: true });
741
+ if (options.clock) await page.clock.install({ time: new Date(FIXED_SETTINGS.startTime) });
742
+ if (options.random) await installScript(page, randomScript());
743
+ if (options.animations) await page.emulateMedia({ reducedMotion: "reduce" });
744
+ const css = stylesheetFor(options);
745
+ if (css !== "") await installScript(page, stylesheetScript(css));
746
+ const startedAt = performance.now();
747
+ await page.evaluate(
748
+ () => new Promise((resolve) => {
749
+ document.documentElement.style.background = "#fafafa";
750
+ requestAnimationFrame(
751
+ () => requestAnimationFrame(() => {
752
+ document.documentElement.style.background = "#fff";
753
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
754
+ })
755
+ );
756
+ })
757
+ );
758
+ const chapters = [];
759
+ const masks = [];
760
+ const clearMasks = async () => {
761
+ masks.length = 0;
762
+ await page.evaluate(() => document.querySelectorAll("[data-outerlayer-mask]").forEach((node) => node.remove())).catch(() => void 0);
763
+ };
764
+ page.on("domcontentloaded", () => {
765
+ for (const masked of masks) void drawMask(masked).catch(() => void 0);
766
+ });
767
+ let chapterCount = 0;
768
+ let running2;
769
+ const voice = new VoiceClient(voiceCommand(process.env));
770
+ let hand;
771
+ const handFor = async () => {
772
+ if (hand !== void 0) return hand;
773
+ await installScript(page, pointerScript());
774
+ hand = new Hand(page, { x: FIXED_SETTINGS.viewport.width / 2, y: FIXED_SETTINGS.viewport.height / 2 });
775
+ return hand;
776
+ };
777
+ let panelOpen = false;
778
+ const demo = {
779
+ async say(narration, steps) {
780
+ const chapter = running2;
781
+ if (chapter === void 0) throw new Error("demo.say() must be called inside demo.chapter()");
782
+ const file = `voice/${pad(chapter.number)}-${pad(chapter.beats.length + 1)}.wav`;
783
+ const spoken = await voice.speak(narration, path.join(folder, file));
784
+ const person = await handFor();
785
+ const startedAtMs = performance.now();
786
+ const start = seconds(startedAtMs - startedAt);
787
+ if (panelOpen) {
788
+ panelOpen = false;
789
+ await closePanels(page);
790
+ }
791
+ const restore = actLikeAPerson(page, person);
792
+ try {
793
+ if (steps !== void 0) await steps();
794
+ } finally {
795
+ restore();
796
+ }
797
+ const due = startedAtMs + (spoken.seconds === 0 ? 0 : (BEAT_LEAD_IN + spoken.seconds + BEAT_TAIL) * 1e3);
798
+ const wait = due - performance.now();
799
+ if (wait > 0) await page.waitForTimeout(wait);
800
+ const end = Math.max(seconds(performance.now() - startedAt), start + 1e-3);
801
+ chapter.beats.push({ start, end, narration, ...spoken.written ? { voice: { file, seconds: spoken.seconds } } : {} });
802
+ await clearHighlights(page);
803
+ },
804
+ async terminal(command, run) {
805
+ await openPanel(page, "terminal", "$ ", command);
806
+ panelOpen = true;
807
+ const output = await run();
808
+ await printToPanel(page, "terminal", terminalLines(output));
809
+ return output;
810
+ },
811
+ async http(request, run) {
812
+ await openPanel(page, "http", "", `${request.method.toUpperCase()} ${request.url}`);
813
+ panelOpen = true;
814
+ if (request.body !== void 0) {
815
+ const body = typeof request.body === "string" ? request.body : JSON.stringify(request.body, null, 2) ?? "";
816
+ await printToPanel(page, "http", body.split("\n").map((text) => ({ text, tone: "muted" })));
817
+ }
818
+ const response = await run();
819
+ await printToPanel(page, "http", httpLines(response));
820
+ return response;
821
+ },
822
+ async highlight(locator) {
823
+ await showHighlight(locator);
824
+ },
825
+ async screenshot(screenshotOptions = {}) {
826
+ const chapter = running2;
827
+ if (chapter === void 0) throw new Error("demo.screenshot() must be called inside demo.chapter()");
828
+ await chapter.idle();
829
+ if (chapter.checks.length === 0) throw new Error("demo.screenshot() must be called after at least one check of the chapter");
830
+ const file = `screenshots/${pad(chapter.number)}.png`;
831
+ const fullPage = screenshotOptions.fullPage === true;
832
+ const masked = screenshotOptions.masks === true && masks.length > 0;
833
+ const sha256 = await takeFrame(page, { path: path.join(folder, file), options, masks: masked ? [...masks] : [], fullPage, hideOverlay: true });
834
+ chapter.screenshot = { check: chapter.checks.length - 1, file, sha256, fullPage, masked };
835
+ },
836
+ async mask(locator) {
837
+ masks.push(locator);
838
+ await drawMask(locator);
839
+ },
840
+ async chapter(criterionId, title, narrationOrBody, maybeBody) {
841
+ const told = typeof narrationOrBody === "function";
842
+ const narration = told ? "" : narrationOrBody;
843
+ const body = told ? narrationOrBody : maybeBody;
844
+ const checks = [];
845
+ const beats = [];
846
+ const stills = [];
847
+ chapterCount += 1;
848
+ const number = chapterCount;
849
+ let queue = Promise.resolve();
850
+ const onPass = (check) => {
851
+ queue = queue.then(async () => {
852
+ const file = `stills/${pad(number)}-${pad(check)}.png`;
853
+ try {
854
+ const sha256 = await takeStill(page, { path: path.join(folder, file), options, masks: [...masks] });
855
+ stills.push({ check, file, sha256 });
856
+ } catch {
857
+ }
858
+ });
859
+ return queue;
860
+ };
861
+ const start = seconds(performance.now() - startedAt);
862
+ const current = { number, checks, beats, idle: () => queue };
863
+ running2 = current;
864
+ const finish = async () => {
865
+ const end = Math.max(seconds(performance.now() - startedAt), start + 1e-3);
866
+ running2 = void 0;
867
+ chapters.push({
868
+ criterionId,
869
+ start,
870
+ end,
871
+ title,
872
+ // A chapter told in beats lists its lines in the narration, for a reader of the record.
873
+ narration: told ? beats.map((beat) => beat.narration).join(" ").slice(0, 2e3) : narration,
874
+ checks,
875
+ stills: stills.sort((a, b) => a.check - b.check),
876
+ ...current.screenshot === void 0 ? {} : { screenshot: current.screenshot },
877
+ ...told ? { beats } : {}
878
+ });
879
+ await clearMasks();
880
+ };
881
+ try {
882
+ const result = await recordChecksDuring(checks, body, onPass);
883
+ await finish();
884
+ return result;
885
+ } catch (error) {
886
+ if (!(typeof error === "object" && error !== null && error[RECORDED] === true)) {
887
+ const message = error instanceof Error ? error.message.split("\n")[0] : String(error);
888
+ checks.push({ text: clipCheckText(`the chapter body threw: ${message}`), passed: false });
889
+ }
890
+ await finish();
891
+ throw error;
892
+ }
893
+ }
894
+ };
895
+ try {
896
+ await use(demo);
897
+ } finally {
898
+ voice.close();
899
+ }
900
+ const video = page.video();
901
+ await paintFrame(page).catch(() => void 0);
902
+ await page.close();
903
+ if (video === null) return;
904
+ const videoPath = path.join(folder, "video.webm");
905
+ await video.saveAs(videoPath);
906
+ const manifest = buildManifest({ testFile: testInfo.file, rootDir: testInfo.config.rootDir, video: "video.webm", chapters, ...smaller === void 0 ? {} : { viewport: smaller } });
907
+ const manifestPath = path.join(folder, "manifest.json");
908
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
909
+ await testInfo.attach("demo-video", { path: videoPath, contentType: "video/webm" });
910
+ await testInfo.attach("demo-manifest", { path: manifestPath, contentType: "application/json" });
911
+ }
912
+ });
913
+
914
+ exports.DEMO_FOLDER = DEMO_FOLDER;
915
+ exports.expect = expect;
916
+ exports.test = test;