ptywright 0.4.0 → 0.6.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 +38 -31
- package/dist/agent.mjs +2 -2
- package/dist/bin/ptywright.mjs +1 -1
- package/dist/{cli-C40H_ElC.mjs → cli-IXmvK56U.mjs} +2519 -2472
- package/dist/cli.mjs +1 -1
- package/dist/config.mjs +1 -1
- package/dist/data-sdbf3IDh.mjs +18 -0
- package/dist/env-DPYHo-zH.mjs +36 -0
- package/dist/index.mjs +1 -1
- package/dist/manifest_files-DW80c1H7.mjs +77 -0
- package/dist/mcp.mjs +1 -1
- package/dist/pty-cassette.mjs +2 -1
- package/dist/{pty_like-DqCo7XdB.mjs → pty_like-DWIlWGgA.mjs} +4 -20
- package/dist/{runner-zApMYWZx.mjs → runner-BHXXwxYp.mjs} +1578 -1423
- package/dist/{runner-CembqDgJ.mjs → runner-GIEf0C6d.mjs} +2434 -1134
- package/dist/script.mjs +1 -1
- package/dist/{server-h--2U0Ic.mjs → server-ceZ1-s_J.mjs} +2643 -2527
- package/dist/session.mjs +1 -1
- package/dist/style-BtIUv5H0.mjs +65 -0
- package/dist/{terminal_session-DopC7Xg6.mjs → terminal_session-MX_vWpRG.mjs} +322 -364
- package/package.json +2 -1
- package/dist/{config-B0r-JCFI.mjs → config-bGg636EW.mjs} +1 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as mergeProcessEnv, o as relativeHref, t as envTruthy } from "./env-DPYHo-zH.mjs";
|
|
2
|
+
import { a as styleKey, i as isDefaultStyle, n as extractStyle, r as findMeaningfulEndCol } from "./style-BtIUv5H0.mjs";
|
|
3
|
+
import { a as fnv1a32, c as encodeSgrMouse, i as TraceRecorder, l as encodeKey, n as sleep, o as snapshotGrid, r as applyTextMaskRules, s as snapshotLines, t as TerminalSession } from "./terminal_session-MX_vWpRG.mjs";
|
|
2
4
|
import { createRequire } from "node:module";
|
|
5
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
3
8
|
import { z } from "zod";
|
|
4
|
-
import { spawn } from "bun-pty";
|
|
5
9
|
import { Terminal } from "@xterm/headless";
|
|
6
|
-
import {
|
|
7
|
-
import { pathToFileURL } from "node:url";
|
|
8
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { spawn } from "bun-pty";
|
|
9
11
|
//#region src/pty/bun_pty_adapter.ts
|
|
10
12
|
function toForkOptions(options) {
|
|
11
13
|
return {
|
|
@@ -176,7 +178,7 @@ var SessionManager = class {
|
|
|
176
178
|
const rows = clampInt$2(args.rows ?? 24, 1, 300);
|
|
177
179
|
const cwd = args.cwd ?? process.cwd();
|
|
178
180
|
const term = args.env?.TERM ?? args.name ?? "xterm-256color";
|
|
179
|
-
const env =
|
|
181
|
+
const env = mergeProcessEnv({
|
|
180
182
|
TERM: term,
|
|
181
183
|
COLORTERM: "truecolor"
|
|
182
184
|
}, args.env);
|
|
@@ -226,13 +228,6 @@ function clampInt$2(value, min, max) {
|
|
|
226
228
|
if (int > max) return max;
|
|
227
229
|
return int;
|
|
228
230
|
}
|
|
229
|
-
function mergeEnv(base, override) {
|
|
230
|
-
const env = {};
|
|
231
|
-
for (const [k, v] of Object.entries(process.env)) if (typeof v === "string") env[k] = v;
|
|
232
|
-
for (const [k, v] of Object.entries(base)) env[k] = v;
|
|
233
|
-
if (override) for (const [k, v] of Object.entries(override)) env[k] = v;
|
|
234
|
-
return env;
|
|
235
|
-
}
|
|
236
231
|
function pickTraceEnv(env) {
|
|
237
232
|
const picked = {};
|
|
238
233
|
for (const key of [
|
|
@@ -247,6 +242,232 @@ function pickTraceEnv(env) {
|
|
|
247
242
|
return picked;
|
|
248
243
|
}
|
|
249
244
|
//#endregion
|
|
245
|
+
//#region src/script/text_mask_schema.ts
|
|
246
|
+
const textMaskRuleSchema = z.object({
|
|
247
|
+
regex: z.string().min(1),
|
|
248
|
+
flags: z.string().optional(),
|
|
249
|
+
replacement: z.string().optional(),
|
|
250
|
+
preserveLength: z.boolean().optional()
|
|
251
|
+
});
|
|
252
|
+
const assertionScriptStepSchemas = [
|
|
253
|
+
z.object({
|
|
254
|
+
type: z.literal("expectMeta"),
|
|
255
|
+
bufferType: z.enum(["normal", "alternate"]).optional(),
|
|
256
|
+
cols: z.number().int().positive().optional(),
|
|
257
|
+
rows: z.number().int().positive().optional(),
|
|
258
|
+
cursor: z.object({
|
|
259
|
+
x: z.number().int().positive(),
|
|
260
|
+
y: z.number().int().positive()
|
|
261
|
+
}).optional()
|
|
262
|
+
}).superRefine((value, ctx) => {
|
|
263
|
+
if (value.bufferType === void 0 && value.cols === void 0 && value.rows === void 0 && value.cursor === void 0) ctx.addIssue({
|
|
264
|
+
code: z.ZodIssueCode.custom,
|
|
265
|
+
message: "expectMeta requires at least one assertion (bufferType/cols/rows/cursor)"
|
|
266
|
+
});
|
|
267
|
+
}),
|
|
268
|
+
z.object({
|
|
269
|
+
type: z.literal("snapshot"),
|
|
270
|
+
kind: z.enum([
|
|
271
|
+
"text",
|
|
272
|
+
"view",
|
|
273
|
+
"ansi",
|
|
274
|
+
"view_ansi",
|
|
275
|
+
"grid"
|
|
276
|
+
]),
|
|
277
|
+
scope: z.enum(["visible", "buffer"]).optional(),
|
|
278
|
+
trimRight: z.boolean().optional(),
|
|
279
|
+
trimBottom: z.boolean().optional(),
|
|
280
|
+
maxLines: z.number().int().positive().optional(),
|
|
281
|
+
tailLines: z.number().int().positive().optional(),
|
|
282
|
+
lineNumbers: z.boolean().optional(),
|
|
283
|
+
includeStyles: z.boolean().optional(),
|
|
284
|
+
mask: z.array(textMaskRuleSchema).optional(),
|
|
285
|
+
saveAs: z.string().optional(),
|
|
286
|
+
saveTo: z.string().optional()
|
|
287
|
+
}).superRefine((value, ctx) => {
|
|
288
|
+
if (value.maxLines !== void 0 && value.tailLines !== void 0) ctx.addIssue({
|
|
289
|
+
code: z.ZodIssueCode.custom,
|
|
290
|
+
message: "snapshot: maxLines and tailLines are mutually exclusive"
|
|
291
|
+
});
|
|
292
|
+
}),
|
|
293
|
+
z.object({
|
|
294
|
+
type: z.literal("expect"),
|
|
295
|
+
from: z.string().optional(),
|
|
296
|
+
equals: z.string().optional(),
|
|
297
|
+
contains: z.array(z.string()).optional(),
|
|
298
|
+
notContains: z.array(z.string()).optional(),
|
|
299
|
+
regex: z.string().optional()
|
|
300
|
+
}).superRefine((value, ctx) => {
|
|
301
|
+
if (value.equals === void 0 && !value.contains?.length && !value.notContains?.length && !value.regex) ctx.addIssue({
|
|
302
|
+
code: z.ZodIssueCode.custom,
|
|
303
|
+
message: "expect requires at least one matcher (equals/contains/notContains/regex)"
|
|
304
|
+
});
|
|
305
|
+
}),
|
|
306
|
+
z.object({
|
|
307
|
+
type: z.literal("expectGolden"),
|
|
308
|
+
from: z.string().optional(),
|
|
309
|
+
path: z.string().min(1)
|
|
310
|
+
}),
|
|
311
|
+
z.object({
|
|
312
|
+
type: z.literal("custom"),
|
|
313
|
+
name: z.string().min(1),
|
|
314
|
+
payload: z.unknown().optional()
|
|
315
|
+
}),
|
|
316
|
+
z.object({
|
|
317
|
+
type: z.literal("assert"),
|
|
318
|
+
scope: z.enum(["visible", "buffer"]).optional(),
|
|
319
|
+
text: z.string().optional(),
|
|
320
|
+
regex: z.string().optional(),
|
|
321
|
+
description: z.string().optional()
|
|
322
|
+
}).superRefine((value, ctx) => {
|
|
323
|
+
if (!value.text && !value.regex) ctx.addIssue({
|
|
324
|
+
code: z.ZodIssueCode.custom,
|
|
325
|
+
message: "assert requires text or regex"
|
|
326
|
+
});
|
|
327
|
+
}),
|
|
328
|
+
z.object({
|
|
329
|
+
type: z.literal("assertSemantic"),
|
|
330
|
+
prompt: z.string().min(1),
|
|
331
|
+
description: z.string().optional()
|
|
332
|
+
})
|
|
333
|
+
];
|
|
334
|
+
const inputScriptStepSchemas = [
|
|
335
|
+
z.object({
|
|
336
|
+
type: z.literal("sendText"),
|
|
337
|
+
text: z.string(),
|
|
338
|
+
enter: z.boolean().optional()
|
|
339
|
+
}),
|
|
340
|
+
z.object({
|
|
341
|
+
type: z.literal("pressKey"),
|
|
342
|
+
key: z.string().min(1)
|
|
343
|
+
}),
|
|
344
|
+
z.object({
|
|
345
|
+
type: z.literal("sendMouse"),
|
|
346
|
+
action: z.enum([
|
|
347
|
+
"down",
|
|
348
|
+
"up",
|
|
349
|
+
"move",
|
|
350
|
+
"click",
|
|
351
|
+
"scroll_up",
|
|
352
|
+
"scroll_down"
|
|
353
|
+
]),
|
|
354
|
+
x: z.number().int(),
|
|
355
|
+
y: z.number().int(),
|
|
356
|
+
button: z.enum([
|
|
357
|
+
"left",
|
|
358
|
+
"middle",
|
|
359
|
+
"right"
|
|
360
|
+
]).optional(),
|
|
361
|
+
shift: z.boolean().optional(),
|
|
362
|
+
alt: z.boolean().optional(),
|
|
363
|
+
ctrl: z.boolean().optional()
|
|
364
|
+
}),
|
|
365
|
+
z.object({
|
|
366
|
+
type: z.literal("resize"),
|
|
367
|
+
cols: z.number().int().positive(),
|
|
368
|
+
rows: z.number().int().positive()
|
|
369
|
+
}),
|
|
370
|
+
z.object({
|
|
371
|
+
type: z.literal("mark"),
|
|
372
|
+
label: z.string().optional()
|
|
373
|
+
}),
|
|
374
|
+
z.object({
|
|
375
|
+
type: z.literal("sleep"),
|
|
376
|
+
ms: z.number().int().nonnegative()
|
|
377
|
+
})
|
|
378
|
+
];
|
|
379
|
+
const waitScriptStepSchemas = [
|
|
380
|
+
z.object({
|
|
381
|
+
type: z.literal("waitForText"),
|
|
382
|
+
scope: z.enum(["visible", "buffer"]).optional(),
|
|
383
|
+
text: z.string().optional(),
|
|
384
|
+
regex: z.string().optional(),
|
|
385
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
386
|
+
intervalMs: z.number().int().positive().optional()
|
|
387
|
+
}).superRefine((value, ctx) => {
|
|
388
|
+
if (!value.text && !value.regex) ctx.addIssue({
|
|
389
|
+
code: z.ZodIssueCode.custom,
|
|
390
|
+
message: "waitForText requires text or regex"
|
|
391
|
+
});
|
|
392
|
+
}),
|
|
393
|
+
z.object({
|
|
394
|
+
type: z.literal("waitForStableScreen"),
|
|
395
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
396
|
+
quietMs: z.number().int().positive().optional(),
|
|
397
|
+
intervalMs: z.number().int().positive().optional()
|
|
398
|
+
}),
|
|
399
|
+
z.object({
|
|
400
|
+
type: z.literal("waitForExit"),
|
|
401
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
402
|
+
intervalMs: z.number().int().positive().optional(),
|
|
403
|
+
exitCode: z.number().int().optional(),
|
|
404
|
+
signal: z.union([z.number().int(), z.string()]).optional()
|
|
405
|
+
})
|
|
406
|
+
];
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/script/step_schemas.ts
|
|
409
|
+
const scriptStepSchema = z.union([
|
|
410
|
+
...inputScriptStepSchemas,
|
|
411
|
+
...waitScriptStepSchemas,
|
|
412
|
+
...assertionScriptStepSchemas
|
|
413
|
+
]);
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/script/schema.ts
|
|
416
|
+
const launchConfigSchema = z.object({
|
|
417
|
+
backend: z.enum([
|
|
418
|
+
"pty",
|
|
419
|
+
"frames",
|
|
420
|
+
"ink",
|
|
421
|
+
"ratatui"
|
|
422
|
+
]).optional(),
|
|
423
|
+
command: z.string().min(1).optional(),
|
|
424
|
+
args: z.array(z.string()).optional(),
|
|
425
|
+
cwd: z.string().optional(),
|
|
426
|
+
env: z.record(z.string()).optional(),
|
|
427
|
+
cols: z.number().int().positive().optional(),
|
|
428
|
+
rows: z.number().int().positive().optional(),
|
|
429
|
+
name: z.string().optional(),
|
|
430
|
+
frame: z.string().optional(),
|
|
431
|
+
frames: z.array(z.union([z.string(), z.object({
|
|
432
|
+
name: z.string().optional(),
|
|
433
|
+
text: z.string().optional(),
|
|
434
|
+
frame: z.string().optional(),
|
|
435
|
+
snapshot: z.string().optional(),
|
|
436
|
+
lastFrame: z.string().optional()
|
|
437
|
+
})])).optional(),
|
|
438
|
+
framePath: z.string().optional(),
|
|
439
|
+
frameModule: z.string().optional(),
|
|
440
|
+
advanceOnInput: z.boolean().optional()
|
|
441
|
+
}).superRefine((value, ctx) => {
|
|
442
|
+
if ((value.backend ?? "pty") === "pty") {
|
|
443
|
+
if (!value.command) ctx.addIssue({
|
|
444
|
+
code: z.ZodIssueCode.custom,
|
|
445
|
+
path: ["command"],
|
|
446
|
+
message: "launch.command is required when backend=pty"
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (!value.frame && !value.frames?.length && !value.framePath && !value.frameModule) ctx.addIssue({
|
|
451
|
+
code: z.ZodIssueCode.custom,
|
|
452
|
+
message: "framework launch requires one of frame, frames, framePath, or frameModule when backend is not pty"
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
const scriptTraceSchema = z.object({
|
|
456
|
+
saveCast: z.boolean().optional(),
|
|
457
|
+
saveReport: z.boolean().optional(),
|
|
458
|
+
castPath: z.string().optional(),
|
|
459
|
+
reportPath: z.string().optional(),
|
|
460
|
+
reportScope: z.enum(["visible", "buffer"]).optional(),
|
|
461
|
+
reportMaxFrames: z.number().int().positive().optional()
|
|
462
|
+
});
|
|
463
|
+
const scriptSchema = z.object({
|
|
464
|
+
name: z.string().optional(),
|
|
465
|
+
artifactsDir: z.string().optional(),
|
|
466
|
+
launch: launchConfigSchema,
|
|
467
|
+
trace: scriptTraceSchema.optional(),
|
|
468
|
+
steps: z.array(scriptStepSchema).min(1)
|
|
469
|
+
});
|
|
470
|
+
//#endregion
|
|
250
471
|
//#region src/terminal/view.ts
|
|
251
472
|
function formatSnapshotView(options) {
|
|
252
473
|
const lineNumbers = options.lineNumbers ?? true;
|
|
@@ -307,233 +528,226 @@ function ensureAsciinemaPlayerAssets(reportPath) {
|
|
|
307
528
|
}
|
|
308
529
|
}
|
|
309
530
|
//#endregion
|
|
310
|
-
//#region src/trace/
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const frames = [];
|
|
329
|
-
let previousRowSignatures = null;
|
|
330
|
-
const capture = (args) => {
|
|
331
|
-
if (frames.length >= maxFrames) return;
|
|
332
|
-
let viewHtml;
|
|
333
|
-
let changedCount;
|
|
334
|
-
if (args.overrideViewText) {
|
|
335
|
-
const parsedView = parseSnapshotViewText(args.overrideViewText.text);
|
|
336
|
-
const headerLine = parsedView.headerLine ?? (args.overrideViewText.hash?.trim() ? `hash=${args.overrideViewText.hash.trim()}` : "snapshot");
|
|
337
|
-
const rowSignatures = parsedView.rows.map((r) => r.text);
|
|
338
|
-
const changedLines = diffLineIndices(previousRowSignatures ?? [], rowSignatures);
|
|
339
|
-
previousRowSignatures = rowSignatures;
|
|
340
|
-
changedCount = changedLines.size;
|
|
341
|
-
viewHtml = renderSnapshotViewTextHtml({
|
|
342
|
-
headerLine,
|
|
343
|
-
rows: parsedView.rows,
|
|
344
|
-
changedLines
|
|
345
|
-
});
|
|
346
|
-
} else {
|
|
347
|
-
let lines;
|
|
348
|
-
let hash;
|
|
349
|
-
let changedLines = /* @__PURE__ */ new Set();
|
|
350
|
-
if (scope === "visible") {
|
|
351
|
-
const grid = snapshotGrid(terminal, {
|
|
352
|
-
trimRight: true,
|
|
353
|
-
includeStyles: true
|
|
354
|
-
});
|
|
355
|
-
lines = grid.lines;
|
|
356
|
-
hash = fnv1a32(JSON.stringify(grid));
|
|
357
|
-
const rowSignatures = lines.map((line, idx) => {
|
|
358
|
-
const runs = grid.styleRuns?.[idx] ?? [];
|
|
359
|
-
if (line === "" && runs.length === 0) return "";
|
|
360
|
-
return `${line}\n${JSON.stringify(runs)}`;
|
|
361
|
-
});
|
|
362
|
-
changedLines = diffLineIndices(previousRowSignatures ?? [], rowSignatures);
|
|
363
|
-
previousRowSignatures = rowSignatures;
|
|
364
|
-
} else {
|
|
365
|
-
lines = snapshotLines(terminal, {
|
|
366
|
-
scope,
|
|
367
|
-
trimRight: true
|
|
368
|
-
});
|
|
369
|
-
hash = fnv1a32(lines.join("\n"));
|
|
370
|
-
}
|
|
371
|
-
changedCount = changedLines.size;
|
|
372
|
-
viewHtml = renderSnapshotViewHtml({
|
|
373
|
-
terminal,
|
|
374
|
-
sessionId: "replay",
|
|
375
|
-
scope,
|
|
376
|
-
hash,
|
|
377
|
-
lines,
|
|
378
|
-
meta: getMeta(terminal),
|
|
379
|
-
lineNumbers: true,
|
|
380
|
-
changedLines,
|
|
381
|
-
trimRight: true
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
const previousFrame = frames.at(-1);
|
|
385
|
-
frames.push({
|
|
386
|
-
id: `frame-${frames.length + 1}`,
|
|
387
|
-
atSeconds: args.atSeconds,
|
|
388
|
-
kind: args.kind,
|
|
389
|
-
label: args.label,
|
|
390
|
-
markLabel: args.markLabel,
|
|
391
|
-
viewHtml,
|
|
392
|
-
changedCount,
|
|
393
|
-
stepInfo: args.stepInfo,
|
|
394
|
-
previousViewHtml: previousFrame?.viewHtml
|
|
395
|
-
});
|
|
396
|
-
};
|
|
397
|
-
if (steps && steps.length > 0) for (let i = 0; i < steps.length; i += 1) {
|
|
398
|
-
const stepRec = steps[i];
|
|
399
|
-
if (!stepRec) continue;
|
|
400
|
-
const stepLabel = formatStepLabel$1(stepRec.step);
|
|
401
|
-
const viewText = stepRec.after?.text ?? "";
|
|
402
|
-
const displayIndex = (typeof stepRec.index === "number" ? stepRec.index : i) + 1;
|
|
403
|
-
const stepType = stepRec.step.type;
|
|
404
|
-
const kind = stepType === "mark" ? "mark" : stepType === "resize" ? "resize" : "step";
|
|
405
|
-
const markLabel = kind === "mark" && typeof stepRec.step.label === "string" ? String(stepRec.step.label) : void 0;
|
|
406
|
-
const stepParams = {};
|
|
407
|
-
for (const [key, value] of Object.entries(stepRec.step)) if (key !== "type") stepParams[key] = value;
|
|
408
|
-
capture({
|
|
409
|
-
atSeconds: displayIndex,
|
|
410
|
-
kind,
|
|
411
|
-
label: stepLabel,
|
|
412
|
-
markLabel,
|
|
413
|
-
stepInfo: {
|
|
414
|
-
index: displayIndex,
|
|
415
|
-
type: stepType,
|
|
416
|
-
ok: stepRec.ok,
|
|
417
|
-
error: stepRec.error,
|
|
418
|
-
params: Object.keys(stepParams).length > 0 ? stepParams : void 0,
|
|
419
|
-
durationMs: typeof stepRec.durationMs === "number" ? stepRec.durationMs : void 0
|
|
420
|
-
},
|
|
421
|
-
overrideViewText: {
|
|
422
|
-
text: viewText,
|
|
423
|
-
hash: stepRec.after?.hash
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
if (frames.length >= maxFrames) break;
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
for (const event of parsed.events) {
|
|
430
|
-
const [time, type, data] = event;
|
|
431
|
-
if (type === "o") writeChain = writeChain.then(() => writeTerminal(terminal, data));
|
|
432
|
-
else if (type === "r") writeChain.then(() => {
|
|
433
|
-
const resized = parseResize(data);
|
|
434
|
-
if (resized) terminal.resize(resized.cols, resized.rows);
|
|
435
|
-
capture({
|
|
436
|
-
atSeconds: time,
|
|
437
|
-
kind: "resize",
|
|
438
|
-
label: `resize ${data}`
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
else if (type === "m") writeChain.then(() => {
|
|
442
|
-
const markLabel = (data ?? "").trim();
|
|
443
|
-
capture({
|
|
444
|
-
atSeconds: time,
|
|
445
|
-
kind: "mark",
|
|
446
|
-
label: markLabel ? `mark ${markLabel}` : "mark",
|
|
447
|
-
markLabel
|
|
448
|
-
});
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
await writeChain;
|
|
452
|
-
capture({
|
|
453
|
-
atSeconds: parsed.events.at(-1)?.[0] ?? 0,
|
|
454
|
-
kind: "final",
|
|
455
|
-
label: "final"
|
|
456
|
-
});
|
|
531
|
+
//#region src/trace/asciicast_parse.ts
|
|
532
|
+
function parseAsciicast(cast) {
|
|
533
|
+
const lines = cast.trimEnd().split("\n");
|
|
534
|
+
const header = safeJsonObject(lines[0]);
|
|
535
|
+
const events = [];
|
|
536
|
+
for (const line of lines.slice(1)) {
|
|
537
|
+
if (!line.trim()) continue;
|
|
538
|
+
const value = JSON.parse(line);
|
|
539
|
+
if (!Array.isArray(value) || value.length < 3) continue;
|
|
540
|
+
const time = Number(value[0]);
|
|
541
|
+
const type = String(value[1]);
|
|
542
|
+
const data = String(value[2]);
|
|
543
|
+
if (!Number.isFinite(time)) continue;
|
|
544
|
+
if (type === "o" || type === "i" || type === "r" || type === "m") events.push([
|
|
545
|
+
time,
|
|
546
|
+
type,
|
|
547
|
+
data
|
|
548
|
+
]);
|
|
457
549
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
term: termInfo,
|
|
463
|
-
scope,
|
|
464
|
-
scriptName,
|
|
465
|
-
result,
|
|
466
|
-
artifacts,
|
|
467
|
-
frames,
|
|
468
|
-
eventCount: parsed.events.length
|
|
469
|
-
});
|
|
550
|
+
return {
|
|
551
|
+
header,
|
|
552
|
+
events
|
|
553
|
+
};
|
|
470
554
|
}
|
|
471
|
-
function
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const text = typeof step.text === "string" ? step.text : description;
|
|
479
|
-
if (!text) return enterSuffix ? `sendText (${enterSuffix})` : "sendText";
|
|
480
|
-
if (!showText) return `sendText <redacted> (len=${text.length}${enterSuffix ? `, ${enterSuffix}` : ""})`;
|
|
481
|
-
return `sendText "${truncateInline$1(text)}"${enterSuffix ? ` (${enterSuffix})` : ""}`;
|
|
482
|
-
}
|
|
483
|
-
if (step.type === "waitForText") {
|
|
484
|
-
const text = typeof step.text === "string" ? step.text : void 0;
|
|
485
|
-
const regex = typeof step.regex === "string" ? step.regex : void 0;
|
|
486
|
-
const description = typeof step.description === "string" ? step.description : void 0;
|
|
487
|
-
if (!showText) {
|
|
488
|
-
if (text) return "waitForText (text)";
|
|
489
|
-
if (regex) return "waitForText (regex)";
|
|
490
|
-
return "waitForText";
|
|
491
|
-
}
|
|
492
|
-
if (text) return `waitFor "${truncateInline$1(text)}"`;
|
|
493
|
-
if (regex) return `waitFor /${truncateInline$1(regex)}/`;
|
|
494
|
-
if (description) return `waitForText "${truncateInline$1(description)}"`;
|
|
495
|
-
return "waitForText";
|
|
496
|
-
}
|
|
497
|
-
if (step.type === "assert") {
|
|
498
|
-
const text = typeof step.text === "string" ? step.text : void 0;
|
|
499
|
-
const regex = typeof step.regex === "string" ? step.regex : void 0;
|
|
500
|
-
const description = typeof step.description === "string" ? step.description : void 0;
|
|
501
|
-
if (!showText) {
|
|
502
|
-
if (text) return "assert (text)";
|
|
503
|
-
if (regex) return "assert (regex)";
|
|
504
|
-
return "assert";
|
|
505
|
-
}
|
|
506
|
-
if (text) return `assert "${truncateInline$1(text)}"`;
|
|
507
|
-
if (regex) return `assert /${truncateInline$1(regex)}/`;
|
|
508
|
-
if (description) return `assert "${truncateInline$1(description)}"`;
|
|
509
|
-
return "assert";
|
|
510
|
-
}
|
|
511
|
-
if (step.type === "pressKey" && typeof step.key === "string") return `pressKey ${step.key}`;
|
|
512
|
-
if (step.type === "mark") {
|
|
513
|
-
const label = typeof step.label === "string" ? step.label.trim() : "";
|
|
514
|
-
return label ? `mark ${label}` : "mark";
|
|
555
|
+
function safeJsonObject(line) {
|
|
556
|
+
if (!line) return {};
|
|
557
|
+
try {
|
|
558
|
+
const value = JSON.parse(line);
|
|
559
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
560
|
+
} catch {
|
|
561
|
+
return {};
|
|
515
562
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
563
|
+
}
|
|
564
|
+
//#endregion
|
|
565
|
+
//#region src/trace/report_cli.ts
|
|
566
|
+
async function runTraceReportCli(argv, generateTraceReportHtml) {
|
|
567
|
+
const inputPath = argv[0];
|
|
568
|
+
if (!inputPath) {
|
|
569
|
+
console.error("Usage: bun run src/trace/report.ts <path/to/cast>");
|
|
570
|
+
process.exit(2);
|
|
521
571
|
}
|
|
522
|
-
|
|
572
|
+
const html = await generateTraceReportHtml(await Bun.file(inputPath).text());
|
|
573
|
+
const outPath = join(dirname(inputPath), `${basename(inputPath, extname(inputPath))}.report.html`);
|
|
574
|
+
writeFileSync(outPath, html);
|
|
575
|
+
ensureAsciinemaPlayerAssets(outPath);
|
|
576
|
+
console.log(outPath);
|
|
523
577
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/common/html.ts
|
|
580
|
+
function escapeHtml(text) {
|
|
581
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
528
582
|
}
|
|
529
|
-
function
|
|
530
|
-
|
|
531
|
-
const normalized = value.trim().toLowerCase();
|
|
532
|
-
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
583
|
+
function jsonForHtml(data) {
|
|
584
|
+
return JSON.stringify(data).replaceAll("<", "\\u003c");
|
|
533
585
|
}
|
|
534
|
-
function
|
|
535
|
-
|
|
586
|
+
function coerceDisplayString(value) {
|
|
587
|
+
if (value === null || value === void 0) return "";
|
|
588
|
+
if (typeof value === "string") return value;
|
|
589
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
590
|
+
try {
|
|
591
|
+
return JSON.stringify(value) ?? "";
|
|
592
|
+
} catch {
|
|
593
|
+
return "";
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
//#endregion
|
|
597
|
+
//#region src/trace/term_info.ts
|
|
598
|
+
function getTermInfo(header) {
|
|
599
|
+
if (Number(header.version ?? 2) === 3) {
|
|
600
|
+
const term = header.term;
|
|
601
|
+
return {
|
|
602
|
+
cols: clampInt$1(Number(term?.cols ?? 80), 1, 500),
|
|
603
|
+
rows: clampInt$1(Number(term?.rows ?? 24), 1, 300),
|
|
604
|
+
type: typeof term?.type === "string" ? term.type : "xterm-256color"
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
cols: clampInt$1(Number(header.width ?? 80), 1, 500),
|
|
609
|
+
rows: clampInt$1(Number(header.height ?? 24), 1, 300),
|
|
610
|
+
type: typeof header.term === "string" ? header.term : "xterm-256color"
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
function parseResize(value) {
|
|
614
|
+
const match = /^\s*(\d+)x(\d+)\s*$/.exec(value);
|
|
615
|
+
if (!match) return null;
|
|
616
|
+
return {
|
|
617
|
+
cols: clampInt$1(Number(match[1] ?? 0), 1, 500),
|
|
618
|
+
rows: clampInt$1(Number(match[2] ?? 0), 1, 300)
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function clampInt$1(value, min, max) {
|
|
622
|
+
if (!Number.isFinite(value)) return min;
|
|
623
|
+
const int = Math.trunc(value);
|
|
624
|
+
if (int < min) return min;
|
|
625
|
+
if (int > max) return max;
|
|
626
|
+
return int;
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/trace/report_snapshot_styles.ts
|
|
630
|
+
function renderVisibleRowHtml(terminal, rowIndex, trimRight) {
|
|
631
|
+
const buffer = terminal.buffer.active;
|
|
632
|
+
const nullCell = buffer.getNullCell();
|
|
633
|
+
const startY = buffer.viewportY;
|
|
634
|
+
const line = buffer.getLine(startY + rowIndex);
|
|
635
|
+
const endCol = trimRight ? findMeaningfulEndCol(line, terminal.cols, nullCell) : terminal.cols;
|
|
636
|
+
const segments = [];
|
|
637
|
+
let currentKey = null;
|
|
638
|
+
let currentStyle = null;
|
|
639
|
+
let currentText = "";
|
|
640
|
+
const flush = () => {
|
|
641
|
+
if (!currentStyle) return;
|
|
642
|
+
if (currentText.length === 0) return;
|
|
643
|
+
segments.push({
|
|
644
|
+
key: currentKey ?? styleKey(currentStyle),
|
|
645
|
+
style: currentStyle,
|
|
646
|
+
text: currentText
|
|
647
|
+
});
|
|
648
|
+
currentText = "";
|
|
649
|
+
};
|
|
650
|
+
for (let x = 0; x < endCol; x += 1) {
|
|
651
|
+
const cell = line?.getCell(x, nullCell);
|
|
652
|
+
if (!cell) {
|
|
653
|
+
if (currentStyle) {
|
|
654
|
+
flush();
|
|
655
|
+
currentStyle = null;
|
|
656
|
+
currentKey = null;
|
|
657
|
+
}
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (cell.getWidth() === 0) continue;
|
|
661
|
+
const chars = cell.getChars() || " ";
|
|
662
|
+
const style = extractStyle(cell);
|
|
663
|
+
const key = styleKey(style);
|
|
664
|
+
if (!currentStyle) {
|
|
665
|
+
currentStyle = style;
|
|
666
|
+
currentKey = key;
|
|
667
|
+
currentText = chars;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (key === currentKey) {
|
|
671
|
+
currentText += chars;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
flush();
|
|
675
|
+
currentStyle = style;
|
|
676
|
+
currentKey = key;
|
|
677
|
+
currentText = chars;
|
|
678
|
+
}
|
|
679
|
+
if (currentStyle) flush();
|
|
680
|
+
return segments.map((segment) => renderSegmentHtml(segment.text, segment.style)).join("");
|
|
681
|
+
}
|
|
682
|
+
function renderSegmentHtml(text, style) {
|
|
683
|
+
const safeText = escapeHtml(text);
|
|
684
|
+
if (isDefaultStyle(style)) return safeText;
|
|
685
|
+
const css = styleToCss(style);
|
|
686
|
+
if (!css) return `<span class="seg">${safeText}</span>`;
|
|
687
|
+
return `<span class="seg" style="${css}">${safeText}</span>`;
|
|
688
|
+
}
|
|
689
|
+
function styleToCss(style) {
|
|
690
|
+
let fg = colorToCss(style.fg);
|
|
691
|
+
let bg = colorToCss(style.bg);
|
|
692
|
+
if (style.inverse) {
|
|
693
|
+
const tmp = fg;
|
|
694
|
+
fg = bg;
|
|
695
|
+
bg = tmp;
|
|
696
|
+
}
|
|
697
|
+
const decls = [];
|
|
698
|
+
if (fg) decls.push(`color: ${fg}`);
|
|
699
|
+
if (bg) decls.push(`background-color: ${bg}`);
|
|
700
|
+
if (style.bold) decls.push("font-weight: 600");
|
|
701
|
+
if (style.italic) decls.push("font-style: italic");
|
|
702
|
+
if (style.dim) decls.push("opacity: 0.75");
|
|
703
|
+
const decorations = [];
|
|
704
|
+
if (style.underline) decorations.push("underline");
|
|
705
|
+
if (style.strikethrough) decorations.push("line-through");
|
|
706
|
+
if (decorations.length > 0) decls.push(`text-decoration: ${decorations.join(" ")}`);
|
|
707
|
+
return decls.join("; ");
|
|
708
|
+
}
|
|
709
|
+
function colorToCss(color) {
|
|
710
|
+
if (color.mode === "default") return null;
|
|
711
|
+
if (color.mode === "rgb") return `#${(color.value & 16777215).toString(16).padStart(6, "0")}`;
|
|
712
|
+
return xterm256Color(clampInt$1(color.value, 0, 255));
|
|
713
|
+
}
|
|
714
|
+
function xterm256Color(idx) {
|
|
715
|
+
const table16 = [
|
|
716
|
+
"#000000",
|
|
717
|
+
"#800000",
|
|
718
|
+
"#008000",
|
|
719
|
+
"#808000",
|
|
720
|
+
"#000080",
|
|
721
|
+
"#800080",
|
|
722
|
+
"#008080",
|
|
723
|
+
"#c0c0c0",
|
|
724
|
+
"#808080",
|
|
725
|
+
"#ff0000",
|
|
726
|
+
"#00ff00",
|
|
727
|
+
"#ffff00",
|
|
728
|
+
"#0000ff",
|
|
729
|
+
"#ff00ff",
|
|
730
|
+
"#00ffff",
|
|
731
|
+
"#ffffff"
|
|
732
|
+
];
|
|
733
|
+
if (idx < 16) return table16[idx] ?? "#000000";
|
|
734
|
+
if (idx >= 16 && idx <= 231) {
|
|
735
|
+
const c = [
|
|
736
|
+
0,
|
|
737
|
+
95,
|
|
738
|
+
135,
|
|
739
|
+
175,
|
|
740
|
+
215,
|
|
741
|
+
255
|
|
742
|
+
];
|
|
743
|
+
const n = idx - 16;
|
|
744
|
+
return `rgb(${c[Math.trunc(n / 36) % 6] ?? 0} ${c[Math.trunc(n / 6) % 6] ?? 0} ${c[n % 6] ?? 0})`;
|
|
745
|
+
}
|
|
746
|
+
const v = clampInt$1(8 + (idx - 232) * 10, 0, 255);
|
|
747
|
+
return `rgb(${v} ${v} ${v})`;
|
|
536
748
|
}
|
|
749
|
+
//#endregion
|
|
750
|
+
//#region src/trace/report_snapshot_view.ts
|
|
537
751
|
function parseSnapshotViewText(viewText) {
|
|
538
752
|
const lines = stripAnsi(viewText).replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
539
753
|
const first = lines[0] ?? "";
|
|
@@ -561,77 +775,373 @@ function renderSnapshotViewTextHtml(options) {
|
|
|
561
775
|
}
|
|
562
776
|
return out.join("");
|
|
563
777
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
778
|
+
function renderSnapshotViewHtml(options) {
|
|
779
|
+
const headerLine = formatHeaderLine({
|
|
780
|
+
sessionId: options.sessionId,
|
|
781
|
+
scope: options.scope,
|
|
782
|
+
hash: options.hash,
|
|
783
|
+
meta: options.meta,
|
|
784
|
+
changedCount: options.changedLines.size
|
|
567
785
|
});
|
|
786
|
+
const digits = Math.max(2, String(options.lines.length).length);
|
|
787
|
+
const out = [`<span class="headerblock">${escapeHtml(headerLine)}</span>`];
|
|
788
|
+
if (options.scope === "visible") {
|
|
789
|
+
for (let i = 0; i < options.lines.length; i += 1) {
|
|
790
|
+
const n = i + 1;
|
|
791
|
+
const prefix = options.lineNumbers ? `${String(n).padStart(digits, "0")}│ ` : "";
|
|
792
|
+
const prefixHtml = options.lineNumbers ? `<span class="ln">${escapeHtml(prefix)}</span>` : "";
|
|
793
|
+
const contentHtml = renderVisibleRowHtml(options.terminal, i, options.trimRight);
|
|
794
|
+
const rowClass = options.changedLines.has(i) ? "row changed" : "row";
|
|
795
|
+
out.push(`<span class="${rowClass}">${prefixHtml}${contentHtml}</span>`);
|
|
796
|
+
}
|
|
797
|
+
return out.join("");
|
|
798
|
+
}
|
|
799
|
+
for (let i = 0; i < options.lines.length; i += 1) {
|
|
800
|
+
const n = i + 1;
|
|
801
|
+
const prefix = options.lineNumbers ? `${String(n).padStart(digits, "0")}│ ` : "";
|
|
802
|
+
const prefixHtml = options.lineNumbers ? `<span class="ln">${escapeHtml(prefix)}</span>` : "";
|
|
803
|
+
out.push(`<span class="row">${prefixHtml}${escapeHtml(options.lines[i] ?? "")}</span>`);
|
|
804
|
+
}
|
|
805
|
+
return out.join("");
|
|
568
806
|
}
|
|
569
|
-
function
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
807
|
+
function diffLineIndices(previous, next) {
|
|
808
|
+
const out = /* @__PURE__ */ new Set();
|
|
809
|
+
const max = Math.max(previous.length, next.length);
|
|
810
|
+
for (let i = 0; i < max; i += 1) if ((previous[i] ?? "") !== (next[i] ?? "")) out.add(i);
|
|
811
|
+
return out;
|
|
812
|
+
}
|
|
813
|
+
function getTerminalMeta(terminal) {
|
|
814
|
+
const buffer = terminal.buffer.active;
|
|
815
|
+
return {
|
|
816
|
+
cols: terminal.cols,
|
|
817
|
+
rows: terminal.rows,
|
|
818
|
+
bufferType: buffer.type,
|
|
819
|
+
viewportY: buffer.viewportY,
|
|
820
|
+
baseY: buffer.baseY,
|
|
821
|
+
length: buffer.length,
|
|
822
|
+
cursorX: buffer.cursorX,
|
|
823
|
+
cursorY: buffer.cursorY
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function stripAnsi(str) {
|
|
827
|
+
return str.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
|
|
828
|
+
}
|
|
829
|
+
function formatHeaderLine(input) {
|
|
830
|
+
const cursorViewportRow = input.meta.baseY + input.meta.cursorY - input.meta.viewportY;
|
|
831
|
+
const cursorViewportCol = input.meta.cursorX;
|
|
832
|
+
return [
|
|
833
|
+
`session=${input.sessionId}`,
|
|
834
|
+
`scope=${input.scope}`,
|
|
835
|
+
`size=${input.meta.cols}x${input.meta.rows}`,
|
|
836
|
+
`buffer=${input.meta.bufferType}`,
|
|
837
|
+
`cursor=${cursorViewportCol + 1},${cursorViewportRow + 1}`,
|
|
838
|
+
`hash=${input.hash}`,
|
|
839
|
+
`changed=${input.changedCount}`
|
|
840
|
+
].join(" ");
|
|
841
|
+
}
|
|
842
|
+
//#endregion
|
|
843
|
+
//#region src/trace/report_frame_capture.ts
|
|
844
|
+
function createFrameCapture(args) {
|
|
845
|
+
let previousRowSignatures = null;
|
|
846
|
+
return (captureArgs) => {
|
|
847
|
+
if (args.frames.length >= args.maxFrames) return;
|
|
848
|
+
const view = captureArgs.overrideViewText ? renderOverrideView(captureArgs.overrideViewText, previousRowSignatures) : renderTerminalView(args.terminal, args.scope, previousRowSignatures);
|
|
849
|
+
previousRowSignatures = view.rowSignatures;
|
|
850
|
+
const previousFrame = args.frames.at(-1);
|
|
851
|
+
args.frames.push({
|
|
852
|
+
id: `frame-${args.frames.length + 1}`,
|
|
853
|
+
atSeconds: captureArgs.atSeconds,
|
|
854
|
+
kind: captureArgs.kind,
|
|
855
|
+
label: captureArgs.label,
|
|
856
|
+
markLabel: captureArgs.markLabel,
|
|
857
|
+
viewHtml: view.viewHtml,
|
|
858
|
+
changedCount: view.changedCount,
|
|
859
|
+
stepInfo: captureArgs.stepInfo,
|
|
860
|
+
previousViewHtml: previousFrame?.viewHtml
|
|
861
|
+
});
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
function renderOverrideView(overrideViewText, previousRowSignatures) {
|
|
865
|
+
const parsedView = parseSnapshotViewText(overrideViewText.text);
|
|
866
|
+
const headerLine = parsedView.headerLine ?? (overrideViewText.hash?.trim() ? `hash=${overrideViewText.hash.trim()}` : "snapshot");
|
|
867
|
+
const rowSignatures = parsedView.rows.map((row) => row.text);
|
|
868
|
+
const changedLines = diffLineIndices(previousRowSignatures ?? [], rowSignatures);
|
|
869
|
+
return {
|
|
870
|
+
rowSignatures,
|
|
871
|
+
changedCount: changedLines.size,
|
|
872
|
+
viewHtml: renderSnapshotViewTextHtml({
|
|
873
|
+
headerLine,
|
|
874
|
+
rows: parsedView.rows,
|
|
875
|
+
changedLines
|
|
876
|
+
})
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function renderTerminalView(terminal, scope, previousRowSignatures) {
|
|
880
|
+
let lines;
|
|
881
|
+
let hash;
|
|
882
|
+
let changedLines = /* @__PURE__ */ new Set();
|
|
883
|
+
let rowSignatures;
|
|
884
|
+
if (scope === "visible") {
|
|
885
|
+
const grid = snapshotGrid(terminal, {
|
|
886
|
+
trimRight: true,
|
|
887
|
+
includeStyles: true
|
|
888
|
+
});
|
|
889
|
+
lines = grid.lines;
|
|
890
|
+
hash = fnv1a32(JSON.stringify(grid));
|
|
891
|
+
rowSignatures = lines.map((line, idx) => {
|
|
892
|
+
const runs = grid.styleRuns?.[idx] ?? [];
|
|
893
|
+
if (line === "" && runs.length === 0) return "";
|
|
894
|
+
return `${line}\n${JSON.stringify(runs)}`;
|
|
895
|
+
});
|
|
896
|
+
changedLines = diffLineIndices(previousRowSignatures ?? [], rowSignatures);
|
|
897
|
+
} else {
|
|
898
|
+
lines = snapshotLines(terminal, {
|
|
899
|
+
scope,
|
|
900
|
+
trimRight: true
|
|
901
|
+
});
|
|
902
|
+
hash = fnv1a32(lines.join("\n"));
|
|
903
|
+
rowSignatures = previousRowSignatures ?? [];
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
rowSignatures,
|
|
907
|
+
changedCount: changedLines.size,
|
|
908
|
+
viewHtml: renderSnapshotViewHtml({
|
|
909
|
+
terminal,
|
|
910
|
+
sessionId: "replay",
|
|
911
|
+
scope,
|
|
912
|
+
hash,
|
|
913
|
+
lines,
|
|
914
|
+
meta: getTerminalMeta(terminal),
|
|
915
|
+
lineNumbers: true,
|
|
916
|
+
changedLines,
|
|
917
|
+
trimRight: true
|
|
918
|
+
})
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
//#endregion
|
|
922
|
+
//#region src/trace/report_step_label.ts
|
|
923
|
+
function formatStepLabel$1(step) {
|
|
924
|
+
const showText = envTruthy(process.env.PTYWRIGHT_REPORT_SHOW_STEP_TEXT);
|
|
925
|
+
if (step.type === "custom" && typeof step.name === "string") return `custom(${step.name})`;
|
|
926
|
+
if (step.type === "sendText") {
|
|
927
|
+
const enter = typeof step.enter === "boolean" ? step.enter : void 0;
|
|
928
|
+
const enterSuffix = enter !== void 0 ? `enter=${enter}` : "";
|
|
929
|
+
const description = typeof step.description === "string" ? step.description : "";
|
|
930
|
+
const text = typeof step.text === "string" ? step.text : description;
|
|
931
|
+
if (!text) return enterSuffix ? `sendText (${enterSuffix})` : "sendText";
|
|
932
|
+
if (!showText) return `sendText <redacted> (len=${text.length}${enterSuffix ? `, ${enterSuffix}` : ""})`;
|
|
933
|
+
return `sendText "${truncateInline$1(text)}"${enterSuffix ? ` (${enterSuffix})` : ""}`;
|
|
934
|
+
}
|
|
935
|
+
if (step.type === "waitForText") {
|
|
936
|
+
const text = typeof step.text === "string" ? step.text : void 0;
|
|
937
|
+
const regex = typeof step.regex === "string" ? step.regex : void 0;
|
|
938
|
+
const description = typeof step.description === "string" ? step.description : void 0;
|
|
939
|
+
if (!showText) {
|
|
940
|
+
if (text) return "waitForText (text)";
|
|
941
|
+
if (regex) return "waitForText (regex)";
|
|
942
|
+
return "waitForText";
|
|
943
|
+
}
|
|
944
|
+
if (text) return `waitFor "${truncateInline$1(text)}"`;
|
|
945
|
+
if (regex) return `waitFor /${truncateInline$1(regex)}/`;
|
|
946
|
+
if (description) return `waitForText "${truncateInline$1(description)}"`;
|
|
947
|
+
return "waitForText";
|
|
948
|
+
}
|
|
949
|
+
if (step.type === "assert") {
|
|
950
|
+
const text = typeof step.text === "string" ? step.text : void 0;
|
|
951
|
+
const regex = typeof step.regex === "string" ? step.regex : void 0;
|
|
952
|
+
const description = typeof step.description === "string" ? step.description : void 0;
|
|
953
|
+
if (!showText) {
|
|
954
|
+
if (text) return "assert (text)";
|
|
955
|
+
if (regex) return "assert (regex)";
|
|
956
|
+
return "assert";
|
|
957
|
+
}
|
|
958
|
+
if (text) return `assert "${truncateInline$1(text)}"`;
|
|
959
|
+
if (regex) return `assert /${truncateInline$1(regex)}/`;
|
|
960
|
+
if (description) return `assert "${truncateInline$1(description)}"`;
|
|
961
|
+
return "assert";
|
|
962
|
+
}
|
|
963
|
+
if (step.type === "pressKey" && typeof step.key === "string") return `pressKey ${step.key}`;
|
|
964
|
+
if (step.type === "mark") {
|
|
965
|
+
const label = typeof step.label === "string" ? step.label.trim() : "";
|
|
966
|
+
return label ? `mark ${label}` : "mark";
|
|
967
|
+
}
|
|
968
|
+
if (step.type === "resize") {
|
|
969
|
+
const cols = typeof step.cols === "number" ? step.cols : void 0;
|
|
970
|
+
const rows = typeof step.rows === "number" ? step.rows : void 0;
|
|
971
|
+
if (cols !== void 0 && rows !== void 0) return `resize ${cols}x${rows}`;
|
|
972
|
+
return "resize";
|
|
973
|
+
}
|
|
974
|
+
return step.type;
|
|
975
|
+
}
|
|
976
|
+
function truncateInline$1(text, maxChars = 60) {
|
|
977
|
+
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\\n");
|
|
978
|
+
if (normalized.length <= maxChars) return normalized;
|
|
979
|
+
return `${normalized.slice(0, maxChars)}…(+${normalized.length - maxChars})`;
|
|
980
|
+
}
|
|
981
|
+
//#endregion
|
|
982
|
+
//#region src/trace/report_frames.ts
|
|
983
|
+
async function buildTraceReportFrames(args) {
|
|
984
|
+
const terminal = new Terminal({
|
|
985
|
+
cols: args.term.cols,
|
|
986
|
+
rows: args.term.rows,
|
|
987
|
+
allowProposedApi: true,
|
|
988
|
+
scrollback: 2e3,
|
|
989
|
+
convertEol: true
|
|
990
|
+
});
|
|
991
|
+
const frames = [];
|
|
992
|
+
const capture = createFrameCapture({
|
|
993
|
+
terminal,
|
|
994
|
+
frames,
|
|
995
|
+
scope: args.scope,
|
|
996
|
+
maxFrames: args.maxFrames
|
|
997
|
+
});
|
|
998
|
+
const steps = args.steps;
|
|
999
|
+
if (steps && steps.length > 0) buildStepFrames(steps, frames, args.maxFrames, capture);
|
|
1000
|
+
else await buildCastEventFrames(args.parsed, terminal, capture);
|
|
1001
|
+
terminal.dispose();
|
|
1002
|
+
return frames;
|
|
1003
|
+
}
|
|
1004
|
+
function buildStepFrames(steps, frames, maxFrames, capture) {
|
|
1005
|
+
for (let i = 0; i < steps.length; i += 1) {
|
|
1006
|
+
const stepRec = steps[i];
|
|
1007
|
+
if (!stepRec) continue;
|
|
1008
|
+
const stepLabel = formatStepLabel$1(stepRec.step);
|
|
1009
|
+
const viewText = stepRec.after?.text ?? "";
|
|
1010
|
+
const displayIndex = (typeof stepRec.index === "number" ? stepRec.index : i) + 1;
|
|
1011
|
+
const stepType = stepRec.step.type;
|
|
1012
|
+
const kind = stepType === "mark" ? "mark" : stepType === "resize" ? "resize" : "step";
|
|
1013
|
+
const markLabel = kind === "mark" && typeof stepRec.step.label === "string" ? String(stepRec.step.label) : void 0;
|
|
1014
|
+
const stepParams = collectStepParams(stepRec.step);
|
|
1015
|
+
capture({
|
|
1016
|
+
atSeconds: displayIndex,
|
|
1017
|
+
kind,
|
|
1018
|
+
label: stepLabel,
|
|
1019
|
+
markLabel,
|
|
1020
|
+
stepInfo: {
|
|
1021
|
+
index: displayIndex,
|
|
1022
|
+
type: stepType,
|
|
1023
|
+
ok: stepRec.ok,
|
|
1024
|
+
error: stepRec.error,
|
|
1025
|
+
params: Object.keys(stepParams).length > 0 ? stepParams : void 0,
|
|
1026
|
+
durationMs: typeof stepRec.durationMs === "number" ? stepRec.durationMs : void 0
|
|
1027
|
+
},
|
|
1028
|
+
overrideViewText: {
|
|
1029
|
+
text: viewText,
|
|
1030
|
+
hash: stepRec.after?.hash
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
if (frames.length >= maxFrames) break;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function collectStepParams(step) {
|
|
1037
|
+
const stepParams = {};
|
|
1038
|
+
for (const [key, value] of Object.entries(step)) if (key !== "type") stepParams[key] = value;
|
|
1039
|
+
return stepParams;
|
|
1040
|
+
}
|
|
1041
|
+
async function buildCastEventFrames(parsed, terminal, capture) {
|
|
1042
|
+
let writeChain = Promise.resolve();
|
|
1043
|
+
for (const event of parsed.events) {
|
|
1044
|
+
const [time, type, data] = event;
|
|
1045
|
+
if (type === "o") writeChain = writeChain.then(() => writeTerminal(terminal, data));
|
|
1046
|
+
else if (type === "r") writeChain.then(() => {
|
|
1047
|
+
const resized = parseResize(data);
|
|
1048
|
+
if (resized) terminal.resize(resized.cols, resized.rows);
|
|
1049
|
+
capture({
|
|
1050
|
+
atSeconds: time,
|
|
1051
|
+
kind: "resize",
|
|
1052
|
+
label: `resize ${data}`
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
else if (type === "m") writeChain.then(() => {
|
|
1056
|
+
const markLabel = (data ?? "").trim();
|
|
1057
|
+
capture({
|
|
1058
|
+
atSeconds: time,
|
|
1059
|
+
kind: "mark",
|
|
1060
|
+
label: markLabel ? `mark ${markLabel}` : "mark",
|
|
1061
|
+
markLabel
|
|
1062
|
+
});
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
await writeChain;
|
|
1066
|
+
capture({
|
|
1067
|
+
atSeconds: parsed.events.at(-1)?.[0] ?? 0,
|
|
1068
|
+
kind: "final",
|
|
1069
|
+
label: "final"
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
async function writeTerminal(terminal, data) {
|
|
1073
|
+
await new Promise((resolve) => {
|
|
1074
|
+
terminal.write(data, resolve);
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/trace/report_html.ts
|
|
1079
|
+
function renderTraceReportHtml(input) {
|
|
1080
|
+
const title = input.scriptName || coerceDisplayString(input.header.title) || "ptywright trace report";
|
|
1081
|
+
const command = coerceDisplayString(input.header.command);
|
|
1082
|
+
const timestamp = input.header.timestamp;
|
|
1083
|
+
const headerJson = JSON.stringify(input.header, null, 2);
|
|
1084
|
+
const durationSeconds = input.frames.at(-1)?.atSeconds ?? 0;
|
|
1085
|
+
const markFrames = input.frames.filter((f) => f.kind === "mark");
|
|
1086
|
+
const resultLabel = input.result?.ok === true ? "PASS" : input.result?.ok === false ? "FAIL" : "UNKNOWN";
|
|
1087
|
+
const resultClass = input.result?.ok === true ? "pass" : input.result?.ok === false ? "fail" : "unknown";
|
|
1088
|
+
const artifactsRows = [];
|
|
1089
|
+
if (input.artifacts?.castHref?.trim()) artifactsRows.push({
|
|
1090
|
+
label: "cast",
|
|
1091
|
+
href: input.artifacts.castHref.trim()
|
|
1092
|
+
});
|
|
1093
|
+
if (input.artifacts?.failureErrorHref?.trim()) artifactsRows.push({
|
|
1094
|
+
label: "failure.error.txt",
|
|
1095
|
+
href: input.artifacts.failureErrorHref.trim()
|
|
1096
|
+
});
|
|
1097
|
+
if (input.artifacts?.failureStepHref?.trim()) artifactsRows.push({
|
|
1098
|
+
label: "failure.step.json",
|
|
1099
|
+
href: input.artifacts.failureStepHref.trim()
|
|
1100
|
+
});
|
|
1101
|
+
if (input.artifacts?.failureLastTextHref?.trim()) artifactsRows.push({
|
|
1102
|
+
label: "failure.last.txt",
|
|
1103
|
+
href: input.artifacts.failureLastTextHref.trim()
|
|
1104
|
+
});
|
|
1105
|
+
if (input.artifacts?.failureLastViewHref?.trim()) artifactsRows.push({
|
|
1106
|
+
label: "failure.last.view.txt",
|
|
1107
|
+
href: input.artifacts.failureLastViewHref.trim()
|
|
1108
|
+
});
|
|
1109
|
+
const artifactsHtml = artifactsRows.length === 0 ? `<p class="muted">No artifacts linked.</p>` : `<ul class="artifacts">
|
|
1110
|
+
${artifactsRows.map((a) => `<li><a href="${escapeHtml(a.href)}">${escapeHtml(a.label)}</a><span class="muted"> (${escapeHtml(a.href)})</span></li>`).join("\n")}
|
|
1111
|
+
</ul>`;
|
|
1112
|
+
const castPlayerHtml = `
|
|
1113
|
+
<p class="muted">Render the full recording using <span class="mono">asciinema-player</span>.</p>
|
|
1114
|
+
<div class="cast-controls">
|
|
1115
|
+
<button id="castToggleSize" class="badge chip" type="button">expand</button>
|
|
1116
|
+
<span id="castPlayerStatus" class="muted mono"></span>
|
|
1117
|
+
</div>
|
|
1118
|
+
<div id="castPlayer" class="cast-player"></div>
|
|
1119
|
+
<script id="castData" type="application/json">${jsonForHtml(input.cast)}<\/script>
|
|
1120
|
+
<script>
|
|
1121
|
+
(function () {
|
|
1122
|
+
const statusEl = document.getElementById("castPlayerStatus");
|
|
1123
|
+
const container = document.getElementById("castPlayer");
|
|
1124
|
+
const castEl = document.getElementById("castData");
|
|
1125
|
+
const toggleBtn = document.getElementById("castToggleSize");
|
|
1126
|
+
if (!container || !castEl) return;
|
|
1127
|
+
|
|
1128
|
+
// Load external assets automatically (no extra click).
|
|
1129
|
+
const VERSION = "3.9.0";
|
|
1130
|
+
const LOCAL_CSS = "./asciinema-player.css";
|
|
1131
|
+
const LOCAL_JS = "./asciinema-player.min.js";
|
|
1132
|
+
// Use multiple CDNs to avoid regional blocks (e.g. jsdelivr).
|
|
1133
|
+
const CDN_BASES = [
|
|
1134
|
+
"https://cdn.jsdelivr.net/npm/asciinema-player@" + VERSION + "/dist/bundle/",
|
|
1135
|
+
"https://unpkg.com/asciinema-player@" + VERSION + "/dist/bundle/",
|
|
1136
|
+
];
|
|
1137
|
+
const CSS_URLS = [LOCAL_CSS, ...CDN_BASES.map((b) => b + "asciinema-player.css")];
|
|
1138
|
+
const JS_URLS = [LOCAL_JS, ...CDN_BASES.map((b) => b + "asciinema-player.min.js")];
|
|
1139
|
+
|
|
1140
|
+
function setStatus(text) {
|
|
1141
|
+
if (statusEl) statusEl.textContent = text ? " " + text : "";
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function loadCssOnce() {
|
|
635
1145
|
if (document.getElementById("asciinemaPlayerCss")) return;
|
|
636
1146
|
|
|
637
1147
|
for (const href of CSS_URLS) {
|
|
@@ -1803,275 +2313,387 @@ timestamp=${escapeHtml(coerceDisplayString(timestamp))}</div>
|
|
|
1803
2313
|
</body>
|
|
1804
2314
|
</html>`;
|
|
1805
2315
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
out.push(`<span class="row">${prefixHtml}${escapeHtml(options.lines[i] ?? "")}</span>`);
|
|
1835
|
-
}
|
|
1836
|
-
return out.join("");
|
|
2316
|
+
//#endregion
|
|
2317
|
+
//#region src/trace/report.ts
|
|
2318
|
+
async function generateTraceReportHtml(cast, options) {
|
|
2319
|
+
const parsed = parseAsciicast(cast);
|
|
2320
|
+
const termInfo = getTermInfo(parsed.header);
|
|
2321
|
+
const scope = options?.scope ?? "visible";
|
|
2322
|
+
const maxFrames = options?.maxFrames ?? 200;
|
|
2323
|
+
const scriptName = options?.scriptName?.trim() ? options.scriptName.trim() : "";
|
|
2324
|
+
const result = options?.result;
|
|
2325
|
+
const artifacts = options?.artifacts;
|
|
2326
|
+
const frames = await buildTraceReportFrames({
|
|
2327
|
+
parsed,
|
|
2328
|
+
term: termInfo,
|
|
2329
|
+
scope,
|
|
2330
|
+
maxFrames,
|
|
2331
|
+
steps: options?.steps
|
|
2332
|
+
});
|
|
2333
|
+
return renderTraceReportHtml({
|
|
2334
|
+
cast,
|
|
2335
|
+
header: parsed.header,
|
|
2336
|
+
term: termInfo,
|
|
2337
|
+
scope,
|
|
2338
|
+
scriptName,
|
|
2339
|
+
result,
|
|
2340
|
+
artifacts,
|
|
2341
|
+
frames,
|
|
2342
|
+
eventCount: parsed.events.length
|
|
2343
|
+
});
|
|
1837
2344
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
const
|
|
1843
|
-
const
|
|
1844
|
-
const
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
text: currentText
|
|
1855
|
-
});
|
|
1856
|
-
currentText = "";
|
|
2345
|
+
if (import.meta.main) await runTraceReportCli(process.argv.slice(2), generateTraceReportHtml);
|
|
2346
|
+
//#endregion
|
|
2347
|
+
//#region src/script/failure_artifacts.ts
|
|
2348
|
+
async function writeFailureArtifacts(args) {
|
|
2349
|
+
const { session, artifactsDir, scriptName, stepIndex, step, last, error } = args;
|
|
2350
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2351
|
+
const errorText = err.stack ?? err.message;
|
|
2352
|
+
writeFileSync(join(artifactsDir, "failure.error.txt"), `${errorText}\n`, "utf8");
|
|
2353
|
+
const stepPayload = {
|
|
2354
|
+
script: scriptName,
|
|
2355
|
+
stepIndex: stepIndex >= 0 ? stepIndex + 1 : null,
|
|
2356
|
+
step: step ?? null,
|
|
2357
|
+
last: last ? {
|
|
2358
|
+
kind: last.kind,
|
|
2359
|
+
hash: last.hash
|
|
2360
|
+
} : null
|
|
1857
2361
|
};
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
currentKey = key;
|
|
1885
|
-
currentText = chars;
|
|
2362
|
+
writeFileSync(join(artifactsDir, "failure.step.json"), `${JSON.stringify(stepPayload, null, 2)}\n`, "utf8");
|
|
2363
|
+
let capturedText = void 0;
|
|
2364
|
+
let capturedHash = void 0;
|
|
2365
|
+
try {
|
|
2366
|
+
const captured = await session.snapshotText({
|
|
2367
|
+
scope: "visible",
|
|
2368
|
+
trimRight: true,
|
|
2369
|
+
trimBottom: true,
|
|
2370
|
+
captureFrame: true
|
|
2371
|
+
});
|
|
2372
|
+
capturedText = captured.text;
|
|
2373
|
+
capturedHash = captured.hash;
|
|
2374
|
+
} catch {}
|
|
2375
|
+
const text = capturedText ?? last?.text;
|
|
2376
|
+
const hash = capturedHash ?? last?.hash ?? "unknown";
|
|
2377
|
+
if (text !== void 0) {
|
|
2378
|
+
writeFileSync(join(artifactsDir, "failure.last.txt"), `${text}\n`, "utf8");
|
|
2379
|
+
const view = formatSnapshotView({
|
|
2380
|
+
sessionId: session.id,
|
|
2381
|
+
scope: "visible",
|
|
2382
|
+
hash,
|
|
2383
|
+
lines: text.split("\n"),
|
|
2384
|
+
meta: session.getMeta(),
|
|
2385
|
+
lineNumbers: true
|
|
2386
|
+
});
|
|
2387
|
+
writeFileSync(join(artifactsDir, "failure.last.view.txt"), `${view}\n`, "utf8");
|
|
1886
2388
|
}
|
|
1887
|
-
if (currentStyle) flush();
|
|
1888
|
-
return segments.map((segment) => renderSegmentHtml(segment.text, segment.style)).join("");
|
|
1889
2389
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
if (!css) return `<span class="seg">${safeText}</span>`;
|
|
1895
|
-
return `<span class="seg" style="${css}">${safeText}</span>`;
|
|
2390
|
+
//#endregion
|
|
2391
|
+
//#region src/script/step_label.ts
|
|
2392
|
+
function formatStepLabel(step) {
|
|
2393
|
+
return step.type === "custom" ? `custom(${step.name})` : step.type;
|
|
1896
2394
|
}
|
|
1897
|
-
function
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
if (
|
|
1901
|
-
const
|
|
1902
|
-
|
|
1903
|
-
|
|
2395
|
+
function formatPublicStepLabel(step) {
|
|
2396
|
+
const showText = envTruthy(process.env.PTYWRIGHT_REPORT_SHOW_STEP_TEXT);
|
|
2397
|
+
if (step.type === "custom") return `custom(${step.name})`;
|
|
2398
|
+
if (step.type === "sendText") {
|
|
2399
|
+
const enter = step.enter !== void 0 ? ` enter=${String(step.enter)}` : "";
|
|
2400
|
+
if (!showText) return `sendText <redacted> (len=${step.text.length}${enter})`;
|
|
2401
|
+
return `sendText "${truncateInline(step.text)}"${enter ? ` (${enter.trim()})` : ""}`;
|
|
1904
2402
|
}
|
|
1905
|
-
|
|
1906
|
-
if (
|
|
1907
|
-
if (
|
|
1908
|
-
if (
|
|
1909
|
-
if (
|
|
1910
|
-
if (
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
return decls.join("; ");
|
|
1916
|
-
}
|
|
1917
|
-
function colorToCss(color) {
|
|
1918
|
-
if (color.mode === "default") return null;
|
|
1919
|
-
if (color.mode === "rgb") return `#${(color.value & 16777215).toString(16).padStart(6, "0")}`;
|
|
1920
|
-
return xterm256Color(clampInt$1(color.value, 0, 255));
|
|
1921
|
-
}
|
|
1922
|
-
function xterm256Color(idx) {
|
|
1923
|
-
const table16 = [
|
|
1924
|
-
"#000000",
|
|
1925
|
-
"#800000",
|
|
1926
|
-
"#008000",
|
|
1927
|
-
"#808000",
|
|
1928
|
-
"#000080",
|
|
1929
|
-
"#800080",
|
|
1930
|
-
"#008080",
|
|
1931
|
-
"#c0c0c0",
|
|
1932
|
-
"#808080",
|
|
1933
|
-
"#ff0000",
|
|
1934
|
-
"#00ff00",
|
|
1935
|
-
"#ffff00",
|
|
1936
|
-
"#0000ff",
|
|
1937
|
-
"#ff00ff",
|
|
1938
|
-
"#00ffff",
|
|
1939
|
-
"#ffffff"
|
|
1940
|
-
];
|
|
1941
|
-
if (idx < 16) return table16[idx] ?? "#000000";
|
|
1942
|
-
if (idx >= 16 && idx <= 231) {
|
|
1943
|
-
const c = [
|
|
1944
|
-
0,
|
|
1945
|
-
95,
|
|
1946
|
-
135,
|
|
1947
|
-
175,
|
|
1948
|
-
215,
|
|
1949
|
-
255
|
|
1950
|
-
];
|
|
1951
|
-
const n = idx - 16;
|
|
1952
|
-
return `rgb(${c[Math.trunc(n / 36) % 6] ?? 0} ${c[Math.trunc(n / 6) % 6] ?? 0} ${c[n % 6] ?? 0})`;
|
|
2403
|
+
if (step.type === "pressKey") return `pressKey ${step.key}`;
|
|
2404
|
+
if (step.type === "sendMouse") return `sendMouse ${step.action} (${step.x},${step.y})`;
|
|
2405
|
+
if (step.type === "resize") return `resize ${step.cols}x${step.rows}`;
|
|
2406
|
+
if (step.type === "mark") return step.label ? `mark ${step.label}` : "mark";
|
|
2407
|
+
if (step.type === "sleep") return `sleep ${step.ms}ms`;
|
|
2408
|
+
if (step.type === "waitForText") {
|
|
2409
|
+
if (!showText) return step.text ? "waitForText (text)" : step.regex ? "waitForText (regex)" : "waitForText";
|
|
2410
|
+
if (step.text) return `waitFor "${truncateInline(step.text)}"`;
|
|
2411
|
+
if (step.regex) return `waitFor /${truncateInline(step.regex)}/`;
|
|
2412
|
+
return "waitForText";
|
|
1953
2413
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
2414
|
+
if (step.type === "waitForStableScreen") return "waitForStableScreen";
|
|
2415
|
+
if (step.type === "waitForExit") return "waitForExit";
|
|
2416
|
+
if (step.type === "expectMeta") return "expectMeta";
|
|
2417
|
+
if (step.type === "snapshot") return `snapshot ${step.kind}${step.saveAs ? ` as ${step.saveAs}` : ""}`;
|
|
2418
|
+
if (step.type === "expect") {
|
|
2419
|
+
const parts = [];
|
|
2420
|
+
if (step.equals !== void 0) parts.push("equals");
|
|
2421
|
+
if (step.contains?.length) parts.push(`contains(${step.contains.length})`);
|
|
2422
|
+
if (step.notContains?.length) parts.push(`notContains(${step.notContains.length})`);
|
|
2423
|
+
if (step.regex) parts.push("regex");
|
|
2424
|
+
return parts.length ? `expect ${parts.join(",")}` : "expect";
|
|
2425
|
+
}
|
|
2426
|
+
if (step.type === "expectGolden") return `expectGolden ${step.path}`;
|
|
2427
|
+
if (step.type === "assert") {
|
|
2428
|
+
if (!showText) return step.text ? "assert (text)" : step.regex ? "assert (regex)" : "assert";
|
|
2429
|
+
if (step.text) return `assert "${truncateInline(step.text)}"`;
|
|
2430
|
+
if (step.regex) return `assert /${truncateInline(step.regex)}/`;
|
|
2431
|
+
if (step.description) return `assert "${truncateInline(step.description)}"`;
|
|
2432
|
+
return "assert";
|
|
2433
|
+
}
|
|
2434
|
+
if (step.type === "assertSemantic") return "assertSemantic";
|
|
2435
|
+
return assertUnreachableStep(step);
|
|
1956
2436
|
}
|
|
1957
|
-
function
|
|
1958
|
-
const
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
return out;
|
|
2437
|
+
function truncateInline(text, maxChars = 60) {
|
|
2438
|
+
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\\n");
|
|
2439
|
+
if (normalized.length <= maxChars) return normalized;
|
|
2440
|
+
return `${normalized.slice(0, maxChars)}…(+${normalized.length - maxChars})`;
|
|
1962
2441
|
}
|
|
1963
|
-
function
|
|
1964
|
-
|
|
1965
|
-
const cursorViewportCol = input.meta.cursorX;
|
|
1966
|
-
return [
|
|
1967
|
-
`session=${input.sessionId}`,
|
|
1968
|
-
`scope=${input.scope}`,
|
|
1969
|
-
`size=${input.meta.cols}x${input.meta.rows}`,
|
|
1970
|
-
`buffer=${input.meta.bufferType}`,
|
|
1971
|
-
`cursor=${cursorViewportCol + 1},${cursorViewportRow + 1}`,
|
|
1972
|
-
`hash=${input.hash}`,
|
|
1973
|
-
`changed=${input.changedCount}`
|
|
1974
|
-
].join(" ");
|
|
2442
|
+
function assertUnreachableStep(_step) {
|
|
2443
|
+
return "unknown";
|
|
1975
2444
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2445
|
+
//#endregion
|
|
2446
|
+
//#region src/script/test_data_artifact.ts
|
|
2447
|
+
function writeTestDataArtifact(args) {
|
|
1980
2448
|
try {
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2449
|
+
const testId = basename(args.artifactsDir);
|
|
2450
|
+
const outPath = args.resolveArtifactPath("test.data.js");
|
|
2451
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
2452
|
+
const steps = args.executionSteps.map((s) => ({
|
|
2453
|
+
index: s.index + 1,
|
|
2454
|
+
type: s.step.type,
|
|
2455
|
+
label: formatPublicStepLabel(s.step),
|
|
2456
|
+
ok: s.ok,
|
|
2457
|
+
durationMs: s.durationMs,
|
|
2458
|
+
error: s.ok ? null : s.error ?? null
|
|
2459
|
+
}));
|
|
2460
|
+
const data = {
|
|
2461
|
+
version: 1,
|
|
2462
|
+
testId,
|
|
2463
|
+
scriptName: args.scriptName,
|
|
2464
|
+
ok: args.ok,
|
|
2465
|
+
error: args.ok ? null : args.error ?? null,
|
|
2466
|
+
stepCount: steps.length,
|
|
2467
|
+
steps,
|
|
2468
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2469
|
+
};
|
|
2470
|
+
const json = JSON.stringify(data).replaceAll("<", "\\u003c");
|
|
2471
|
+
writeFileSync(outPath, `globalThis.__ptywright = globalThis.__ptywright || {};
|
|
2472
|
+
globalThis.__ptywright.tests = globalThis.__ptywright.tests || {};
|
|
2473
|
+
globalThis.__ptywright.tests[${JSON.stringify(testId)}] = ${json};\n`, "utf8");
|
|
2474
|
+
} catch {}
|
|
2475
|
+
}
|
|
2476
|
+
//#endregion
|
|
2477
|
+
//#region src/script/trace_artifacts.ts
|
|
2478
|
+
async function writeTraceArtifacts(args) {
|
|
2479
|
+
if (!args.saveCast && !args.saveReport) return;
|
|
2480
|
+
const snapshot = await args.session.snapshotCast();
|
|
2481
|
+
if (args.saveCast) {
|
|
2482
|
+
mkdirSync(dirname(args.castPath), { recursive: true });
|
|
2483
|
+
writeFileSync(args.castPath, snapshot.cast, "utf8");
|
|
2484
|
+
}
|
|
2485
|
+
if (args.saveReport) {
|
|
2486
|
+
const artifactHrefs = buildReportArtifactHrefs({
|
|
2487
|
+
reportPath: args.reportPath,
|
|
2488
|
+
castPath: args.saveCast ? args.castPath : null,
|
|
2489
|
+
artifactsDir: args.artifactsDir,
|
|
2490
|
+
includeFailures: args.result?.ok === false
|
|
2491
|
+
});
|
|
2492
|
+
const html = await generateTraceReportHtml(snapshot.cast, {
|
|
2493
|
+
scope: args.reportScope,
|
|
2494
|
+
maxFrames: args.reportMaxFrames,
|
|
2495
|
+
scriptName: args.scriptName,
|
|
2496
|
+
result: args.result,
|
|
2497
|
+
artifacts: artifactHrefs,
|
|
2498
|
+
steps: args.executionSteps
|
|
2499
|
+
});
|
|
2500
|
+
mkdirSync(dirname(args.reportPath), { recursive: true });
|
|
2501
|
+
writeFileSync(args.reportPath, html, "utf8");
|
|
2502
|
+
ensureAsciinemaPlayerAssets(args.reportPath);
|
|
1984
2503
|
}
|
|
1985
2504
|
}
|
|
1986
|
-
function
|
|
1987
|
-
|
|
2505
|
+
function buildReportArtifactHrefs(args) {
|
|
2506
|
+
const items = {};
|
|
2507
|
+
if (args.castPath) items.castHref = relativeHref(args.reportPath, args.castPath);
|
|
2508
|
+
if (args.includeFailures) {
|
|
2509
|
+
items.failureErrorHref = relativeHref(args.reportPath, join(args.artifactsDir, "failure.error.txt"));
|
|
2510
|
+
items.failureStepHref = relativeHref(args.reportPath, join(args.artifactsDir, "failure.step.json"));
|
|
2511
|
+
items.failureLastTextHref = relativeHref(args.reportPath, join(args.artifactsDir, "failure.last.txt"));
|
|
2512
|
+
items.failureLastViewHref = relativeHref(args.reportPath, join(args.artifactsDir, "failure.last.view.txt"));
|
|
2513
|
+
}
|
|
2514
|
+
return Object.keys(items).length ? items : void 0;
|
|
1988
2515
|
}
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
viewportY: buffer.viewportY,
|
|
1996
|
-
baseY: buffer.baseY,
|
|
1997
|
-
length: buffer.length,
|
|
1998
|
-
cursorX: buffer.cursorX,
|
|
1999
|
-
cursorY: buffer.cursorY
|
|
2000
|
-
};
|
|
2516
|
+
//#endregion
|
|
2517
|
+
//#region src/script/runner_paths.ts
|
|
2518
|
+
function resolveArtifactsDir(script, scriptName, override) {
|
|
2519
|
+
if (override?.trim()) return resolve(override.trim());
|
|
2520
|
+
if (script.artifactsDir?.trim()) return resolve(script.artifactsDir.trim());
|
|
2521
|
+
return resolve(".tmp", "runs", scriptName);
|
|
2001
2522
|
}
|
|
2002
|
-
function
|
|
2003
|
-
const lines = cast.trimEnd().split("\n");
|
|
2004
|
-
const header = safeJsonObject(lines[0]);
|
|
2005
|
-
const events = [];
|
|
2006
|
-
for (const line of lines.slice(1)) {
|
|
2007
|
-
if (!line.trim()) continue;
|
|
2008
|
-
const value = JSON.parse(line);
|
|
2009
|
-
if (!Array.isArray(value) || value.length < 3) continue;
|
|
2010
|
-
const time = Number(value[0]);
|
|
2011
|
-
const type = String(value[1]);
|
|
2012
|
-
const data = String(value[2]);
|
|
2013
|
-
if (!Number.isFinite(time)) continue;
|
|
2014
|
-
if (type === "o" || type === "i" || type === "r" || type === "m") events.push([
|
|
2015
|
-
time,
|
|
2016
|
-
type,
|
|
2017
|
-
data
|
|
2018
|
-
]);
|
|
2019
|
-
}
|
|
2523
|
+
function createScriptPathResolvers(artifactsDir) {
|
|
2020
2524
|
return {
|
|
2021
|
-
|
|
2022
|
-
|
|
2525
|
+
resolveGoldenPath: (path) => isAbsolute(path) ? path : resolve(process.cwd(), path),
|
|
2526
|
+
resolveArtifactPath: (path) => isAbsolute(path) ? path : resolve(artifactsDir, path)
|
|
2023
2527
|
};
|
|
2024
2528
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2529
|
+
//#endregion
|
|
2530
|
+
//#region src/script/runner_load.ts
|
|
2531
|
+
async function loadJsonScriptFileWithDefaultName(scriptPath) {
|
|
2532
|
+
const raw = await Bun.file(scriptPath).text();
|
|
2533
|
+
const parsedJson = JSON.parse(raw);
|
|
2534
|
+
const baseName = basename(scriptPath, extname(scriptPath));
|
|
2535
|
+
return parsedJson && typeof parsedJson === "object" && !Array.isArray(parsedJson) && !("name" in parsedJson) ? {
|
|
2536
|
+
...parsedJson,
|
|
2537
|
+
name: baseName
|
|
2538
|
+
} : parsedJson;
|
|
2033
2539
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2540
|
+
//#endregion
|
|
2541
|
+
//#region src/script/frame_text.ts
|
|
2542
|
+
function normalizeNewlines(text) {
|
|
2543
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
2544
|
+
}
|
|
2545
|
+
function normalizeLineWidth(line, cols, trimRight) {
|
|
2546
|
+
const clipped = line.length > cols ? line.slice(0, cols) : line;
|
|
2547
|
+
return trimRight ? clipped.trimEnd() : clipped.padEnd(cols, " ");
|
|
2548
|
+
}
|
|
2549
|
+
function trimBottomEmptyLines(lines) {
|
|
2550
|
+
let end = lines.length;
|
|
2551
|
+
while (end > 0 && (lines[end - 1] ?? "").trim() === "") end -= 1;
|
|
2552
|
+
return lines.slice(0, end);
|
|
2553
|
+
}
|
|
2554
|
+
function sliceLines(lines, options) {
|
|
2555
|
+
if (options?.maxLines !== void 0) return lines.slice(0, Math.max(0, Math.trunc(options.maxLines)));
|
|
2556
|
+
if (options?.tailLines !== void 0) {
|
|
2557
|
+
const tail = Math.max(0, Math.trunc(options.tailLines));
|
|
2558
|
+
return lines.slice(Math.max(0, lines.length - tail));
|
|
2042
2559
|
}
|
|
2043
|
-
return
|
|
2044
|
-
cols: clampInt$1(Number(header.width ?? 80), 1, 500),
|
|
2045
|
-
rows: clampInt$1(Number(header.height ?? 24), 1, 300),
|
|
2046
|
-
type: typeof header.term === "string" ? header.term : "xterm-256color"
|
|
2047
|
-
};
|
|
2560
|
+
return lines;
|
|
2048
2561
|
}
|
|
2049
|
-
function
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
rows: clampInt$1(Number(match[2] ?? 0), 1, 300)
|
|
2055
|
-
};
|
|
2562
|
+
function inferCols(frames) {
|
|
2563
|
+
return clampInt(frames.reduce((acc, frame) => {
|
|
2564
|
+
const lines = normalizeNewlines(frame).split("\n");
|
|
2565
|
+
return Math.max(acc, ...lines.map((line) => line.length));
|
|
2566
|
+
}, 0) || 80, 1, 500);
|
|
2056
2567
|
}
|
|
2057
|
-
function
|
|
2568
|
+
function inferRows(frames) {
|
|
2569
|
+
return clampInt(frames.reduce((acc, frame) => {
|
|
2570
|
+
return Math.max(acc, normalizeNewlines(frame).split("\n").length);
|
|
2571
|
+
}, 0) || 24, 1, 300);
|
|
2572
|
+
}
|
|
2573
|
+
function clampInt(value, min, max) {
|
|
2058
2574
|
if (!Number.isFinite(value)) return min;
|
|
2059
2575
|
const int = Math.trunc(value);
|
|
2060
2576
|
if (int < min) return min;
|
|
2061
2577
|
if (int > max) return max;
|
|
2062
2578
|
return int;
|
|
2063
2579
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2580
|
+
//#endregion
|
|
2581
|
+
//#region src/script/frame_snapshot.ts
|
|
2582
|
+
function snapshotFrameText(inputLines, options) {
|
|
2583
|
+
if (options?.maxLines !== void 0 && options.tailLines !== void 0) throw new Error("snapshotText: maxLines and tailLines are mutually exclusive");
|
|
2584
|
+
let lines = inputLines;
|
|
2585
|
+
if (options?.trimBottom ?? true) lines = trimBottomEmptyLines(lines);
|
|
2586
|
+
lines = sliceLines(lines, options);
|
|
2587
|
+
lines = applyTextMaskRules(lines, options?.mask);
|
|
2588
|
+
const text = lines.join("\n");
|
|
2589
|
+
return {
|
|
2590
|
+
text,
|
|
2591
|
+
hash: fnv1a32(text)
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
function snapshotFrameAnsiFromText(text, hash) {
|
|
2595
|
+
return {
|
|
2596
|
+
ansi: text,
|
|
2597
|
+
plain: text,
|
|
2598
|
+
hash,
|
|
2599
|
+
lines: text.split("\n").map((line) => ({
|
|
2600
|
+
ansi: line,
|
|
2601
|
+
plain: line
|
|
2602
|
+
}))
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
function snapshotFrameGrid(args) {
|
|
2606
|
+
const grid = {
|
|
2607
|
+
cols: args.cols,
|
|
2608
|
+
rows: args.rows,
|
|
2609
|
+
bufferType: "normal",
|
|
2610
|
+
cursorX: 0,
|
|
2611
|
+
cursorY: args.cursorY,
|
|
2612
|
+
viewportY: 0,
|
|
2613
|
+
lines: args.lines,
|
|
2614
|
+
styleRuns: args.includeStyles ? args.lines.map(() => []) : void 0
|
|
2615
|
+
};
|
|
2616
|
+
return {
|
|
2617
|
+
grid,
|
|
2618
|
+
hash: fnv1a32(JSON.stringify(grid))
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
//#endregion
|
|
2622
|
+
//#region src/script/frame_source.ts
|
|
2623
|
+
async function resolveLaunchFrames(launch, cwd, backend) {
|
|
2624
|
+
const frames = [];
|
|
2625
|
+
if (launch.frames?.length) frames.push(...normalizeFrames(launch.frames));
|
|
2626
|
+
if (launch.frame !== void 0) frames.push(launch.frame);
|
|
2627
|
+
if (launch.framePath) {
|
|
2628
|
+
const path = resolveLaunchPath(cwd, launch.framePath);
|
|
2629
|
+
frames.push(readFileSync(path, "utf8").replace(/\n$/, ""));
|
|
2069
2630
|
}
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2631
|
+
if (launch.frameModule) frames.push(...await loadFrameModule(resolveLaunchPath(cwd, launch.frameModule), backend));
|
|
2632
|
+
if (frames.length === 0) throw new Error(`launch.backend=${backend} requires frame, frames, framePath, or frameModule`);
|
|
2633
|
+
return frames;
|
|
2634
|
+
}
|
|
2635
|
+
async function loadFrameModule(modulePath, backend) {
|
|
2636
|
+
return normalizeFrames(await materializeFrameSource(selectModuleFrameSource(await import(pathToFileURL(modulePath).href), backend)));
|
|
2637
|
+
}
|
|
2638
|
+
function selectModuleFrameSource(mod, backend) {
|
|
2639
|
+
if (mod.frames !== void 0) return mod.frames;
|
|
2640
|
+
if (mod.default !== void 0) return mod.default;
|
|
2641
|
+
if (backend === "ink" && mod.lastFrame !== void 0) return mod.lastFrame;
|
|
2642
|
+
if (backend === "ink" && mod.frame !== void 0) return mod.frame;
|
|
2643
|
+
if (backend === "ratatui" && mod.snapshot !== void 0) return mod.snapshot;
|
|
2644
|
+
if (mod.frame !== void 0) return mod.frame;
|
|
2645
|
+
if (mod.snapshot !== void 0) return mod.snapshot;
|
|
2646
|
+
throw new Error(`frame module did not export frames/default/frame/snapshot/lastFrame`);
|
|
2647
|
+
}
|
|
2648
|
+
async function materializeFrameSource(source) {
|
|
2649
|
+
if (typeof source === "function") return await source();
|
|
2650
|
+
return source;
|
|
2651
|
+
}
|
|
2652
|
+
function normalizeFrames(source) {
|
|
2653
|
+
if (Array.isArray(source)) return source.map((frame) => normalizeFrame(frame));
|
|
2654
|
+
return [normalizeFrame(source)];
|
|
2655
|
+
}
|
|
2656
|
+
function normalizeFrame(frame) {
|
|
2657
|
+
if (typeof frame === "string") return normalizeNewlines(frame).replace(/\n$/, "");
|
|
2658
|
+
if (typeof frame === "object" && frame !== null) {
|
|
2659
|
+
const value = frame;
|
|
2660
|
+
const text = typeof value.text === "string" ? value.text : typeof value.frame === "string" ? value.frame : typeof value.snapshot === "string" ? value.snapshot : typeof value.lastFrame === "string" ? value.lastFrame : void 0;
|
|
2661
|
+
if (text !== void 0) return normalizeNewlines(text).replace(/\n$/, "");
|
|
2662
|
+
}
|
|
2663
|
+
throw new Error("frame entries must be strings or objects with text/frame/snapshot/lastFrame");
|
|
2664
|
+
}
|
|
2665
|
+
function resolveLaunchPath(cwd, path) {
|
|
2666
|
+
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
2667
|
+
}
|
|
2668
|
+
//#endregion
|
|
2669
|
+
//#region src/script/frame_wait.ts
|
|
2670
|
+
async function waitForFrameText(snapshotText, args) {
|
|
2671
|
+
const startedAt = Date.now();
|
|
2672
|
+
while (true) {
|
|
2673
|
+
const snapshot = await snapshotText({
|
|
2674
|
+
scope: args.scope,
|
|
2675
|
+
captureFrame: true
|
|
2676
|
+
});
|
|
2677
|
+
if (args.text && snapshot.text.includes(args.text)) return {
|
|
2678
|
+
found: true,
|
|
2679
|
+
...snapshot
|
|
2680
|
+
};
|
|
2681
|
+
if (args.regex && args.regex.test(snapshot.text)) return {
|
|
2682
|
+
found: true,
|
|
2683
|
+
...snapshot
|
|
2684
|
+
};
|
|
2685
|
+
if (Date.now() - startedAt >= args.timeoutMs) return {
|
|
2686
|
+
found: false,
|
|
2687
|
+
...snapshot
|
|
2688
|
+
};
|
|
2689
|
+
await sleep(Math.max(1, args.intervalMs));
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
async function waitForFrameStableScreen(snapshotText) {
|
|
2693
|
+
return {
|
|
2694
|
+
stable: true,
|
|
2695
|
+
...await snapshotText({ captureFrame: true })
|
|
2696
|
+
};
|
|
2075
2697
|
}
|
|
2076
2698
|
//#endregion
|
|
2077
2699
|
//#region src/script/frame_session.ts
|
|
@@ -2148,17 +2770,11 @@ var FrameSession = class {
|
|
|
2148
2770
|
baseY: 0,
|
|
2149
2771
|
length: this.visibleLines({ trimRight: true }).length,
|
|
2150
2772
|
cursorX: 0,
|
|
2151
|
-
cursorY:
|
|
2773
|
+
cursorY: this.cursorY()
|
|
2152
2774
|
};
|
|
2153
2775
|
}
|
|
2154
2776
|
async snapshotText(options) {
|
|
2155
|
-
|
|
2156
|
-
let lines = options?.scope === "buffer" ? this.currentLines({ trimRight: options?.trimRight }) : this.visibleLines({ trimRight: options?.trimRight });
|
|
2157
|
-
if (options?.trimBottom ?? true) lines = trimBottomEmptyLines(lines);
|
|
2158
|
-
lines = sliceLines(lines, options);
|
|
2159
|
-
lines = applyTextMaskRules(lines, options?.mask);
|
|
2160
|
-
const text = lines.join("\n");
|
|
2161
|
-
const hash = fnv1a32(text);
|
|
2777
|
+
const { text, hash } = snapshotFrameText(options?.scope === "buffer" ? this.currentLines({ trimRight: options?.trimRight }) : this.visibleLines({ trimRight: options?.trimRight }), options);
|
|
2162
2778
|
if (options?.captureFrame ?? true) this.captureFrame(text, hash);
|
|
2163
2779
|
return {
|
|
2164
2780
|
text,
|
|
@@ -2167,29 +2783,17 @@ var FrameSession = class {
|
|
|
2167
2783
|
}
|
|
2168
2784
|
async snapshotAnsi(options) {
|
|
2169
2785
|
const { text, hash } = await this.snapshotText(options);
|
|
2170
|
-
return
|
|
2171
|
-
ansi: text,
|
|
2172
|
-
plain: text,
|
|
2173
|
-
hash,
|
|
2174
|
-
lines: text.split("\n").map((line) => ({
|
|
2175
|
-
ansi: line,
|
|
2176
|
-
plain: line
|
|
2177
|
-
}))
|
|
2178
|
-
};
|
|
2786
|
+
return snapshotFrameAnsiFromText(text, hash);
|
|
2179
2787
|
}
|
|
2180
2788
|
async snapshotGrid(options) {
|
|
2181
2789
|
const lines = this.visibleLines({ trimRight: options?.trimRight });
|
|
2182
|
-
const grid = {
|
|
2790
|
+
const { grid, hash } = snapshotFrameGrid({
|
|
2791
|
+
lines,
|
|
2183
2792
|
cols: this.colsValue,
|
|
2184
2793
|
rows: this.rowsValue,
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
viewportY: 0,
|
|
2189
|
-
lines,
|
|
2190
|
-
styleRuns: options?.includeStyles ? lines.map(() => []) : void 0
|
|
2191
|
-
};
|
|
2192
|
-
const hash = fnv1a32(JSON.stringify(grid));
|
|
2794
|
+
cursorY: this.cursorY(),
|
|
2795
|
+
includeStyles: options?.includeStyles
|
|
2796
|
+
});
|
|
2193
2797
|
if (options?.captureFrame ?? true) this.captureFrame(lines.join("\n"), hash);
|
|
2194
2798
|
return {
|
|
2195
2799
|
grid,
|
|
@@ -2201,32 +2805,10 @@ var FrameSession = class {
|
|
|
2201
2805
|
return this.trace.snapshot({ tailEvents: options?.tailEvents });
|
|
2202
2806
|
}
|
|
2203
2807
|
async waitForText(args) {
|
|
2204
|
-
|
|
2205
|
-
while (true) {
|
|
2206
|
-
const snapshot = await this.snapshotText({
|
|
2207
|
-
scope: args.scope,
|
|
2208
|
-
captureFrame: true
|
|
2209
|
-
});
|
|
2210
|
-
if (args.text && snapshot.text.includes(args.text)) return {
|
|
2211
|
-
found: true,
|
|
2212
|
-
...snapshot
|
|
2213
|
-
};
|
|
2214
|
-
if (args.regex && args.regex.test(snapshot.text)) return {
|
|
2215
|
-
found: true,
|
|
2216
|
-
...snapshot
|
|
2217
|
-
};
|
|
2218
|
-
if (Date.now() - startedAt >= args.timeoutMs) return {
|
|
2219
|
-
found: false,
|
|
2220
|
-
...snapshot
|
|
2221
|
-
};
|
|
2222
|
-
await sleep(Math.max(1, args.intervalMs));
|
|
2223
|
-
}
|
|
2808
|
+
return waitForFrameText((options) => this.snapshotText(options), args);
|
|
2224
2809
|
}
|
|
2225
2810
|
async waitForStableScreen() {
|
|
2226
|
-
return
|
|
2227
|
-
stable: true,
|
|
2228
|
-
...await this.snapshotText({ captureFrame: true })
|
|
2229
|
-
};
|
|
2811
|
+
return waitForFrameStableScreen((options) => this.snapshotText(options));
|
|
2230
2812
|
}
|
|
2231
2813
|
isClosed() {
|
|
2232
2814
|
return this.closed !== null;
|
|
@@ -2261,6 +2843,9 @@ var FrameSession = class {
|
|
|
2261
2843
|
while (lines.length < this.rowsValue) lines.push("");
|
|
2262
2844
|
return lines;
|
|
2263
2845
|
}
|
|
2846
|
+
cursorY() {
|
|
2847
|
+
return Math.max(0, Math.min(this.rowsValue - 1, this.currentLines().length - 1));
|
|
2848
|
+
}
|
|
2264
2849
|
captureFrame(text, hash) {
|
|
2265
2850
|
this.snapshotRing.push({
|
|
2266
2851
|
atMs: Date.now(),
|
|
@@ -2289,374 +2874,424 @@ async function createFrameSessionFromLaunch(args) {
|
|
|
2289
2874
|
advanceOnInput: args.launch.advanceOnInput
|
|
2290
2875
|
});
|
|
2291
2876
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2877
|
+
//#endregion
|
|
2878
|
+
//#region src/script/runner_session.ts
|
|
2879
|
+
async function launchScriptSession(args) {
|
|
2880
|
+
const { launch, scriptName } = args;
|
|
2881
|
+
const cwd = launch.cwd ? resolve(process.cwd(), launch.cwd) : process.cwd();
|
|
2882
|
+
if ((launch.backend ?? "pty") === "pty") {
|
|
2883
|
+
const sessions = new SessionManager({ snapshotRingSize: 50 });
|
|
2884
|
+
if (!launch.command) throw new Error("launch.command is required when backend=pty");
|
|
2885
|
+
return {
|
|
2886
|
+
session: sessions.launchSession({
|
|
2887
|
+
command: launch.command,
|
|
2888
|
+
args: launch.args ?? [],
|
|
2889
|
+
cwd,
|
|
2890
|
+
env: launch.env,
|
|
2891
|
+
cols: launch.cols,
|
|
2892
|
+
rows: launch.rows,
|
|
2893
|
+
name: launch.name
|
|
2894
|
+
}),
|
|
2895
|
+
closeSession: () => sessions.closeAll()
|
|
2896
|
+
};
|
|
2299
2897
|
}
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
return
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
if (mod.default !== void 0) return mod.default;
|
|
2310
|
-
if (backend === "ink" && mod.lastFrame !== void 0) return mod.lastFrame;
|
|
2311
|
-
if (backend === "ink" && mod.frame !== void 0) return mod.frame;
|
|
2312
|
-
if (backend === "ratatui" && mod.snapshot !== void 0) return mod.snapshot;
|
|
2313
|
-
if (mod.frame !== void 0) return mod.frame;
|
|
2314
|
-
if (mod.snapshot !== void 0) return mod.snapshot;
|
|
2315
|
-
throw new Error(`frame module did not export frames/default/frame/snapshot/lastFrame`);
|
|
2898
|
+
const session = await createFrameSessionFromLaunch({
|
|
2899
|
+
launch,
|
|
2900
|
+
cwd,
|
|
2901
|
+
title: scriptName
|
|
2902
|
+
});
|
|
2903
|
+
return {
|
|
2904
|
+
session,
|
|
2905
|
+
closeSession: () => session.close()
|
|
2906
|
+
};
|
|
2316
2907
|
}
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2908
|
+
//#endregion
|
|
2909
|
+
//#region src/script/runner_trace.ts
|
|
2910
|
+
function resolveScriptTraceArtifacts(args) {
|
|
2911
|
+
const trace = args.script.trace ?? {};
|
|
2912
|
+
return {
|
|
2913
|
+
saveCast: trace.saveCast ?? true,
|
|
2914
|
+
saveReport: trace.saveReport ?? true,
|
|
2915
|
+
castPath: args.resolveArtifactPath(trace.castPath ?? `${args.scriptName}.cast`),
|
|
2916
|
+
reportPath: args.resolveArtifactPath(trace.reportPath ?? `${args.scriptName}.report.html`),
|
|
2917
|
+
reportScope: trace.reportScope,
|
|
2918
|
+
reportMaxFrames: trace.reportMaxFrames
|
|
2919
|
+
};
|
|
2320
2920
|
}
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2921
|
+
//#endregion
|
|
2922
|
+
//#region src/script/snapshot.ts
|
|
2923
|
+
function persistSnapshotRecord(args) {
|
|
2924
|
+
const saveAs = args.saveAs?.trim();
|
|
2925
|
+
if (saveAs) args.snapshots.set(saveAs, args.record);
|
|
2926
|
+
const saveTo = args.saveTo?.trim();
|
|
2927
|
+
if (!saveTo) return;
|
|
2928
|
+
const path = args.resolveArtifactPath(saveTo);
|
|
2929
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2930
|
+
writeFileSync(path, `${args.record.text}\n`, "utf8");
|
|
2324
2931
|
}
|
|
2325
|
-
function
|
|
2326
|
-
|
|
2327
|
-
if (
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
if (text !== void 0) return normalizeNewlines(text).replace(/\n$/, "");
|
|
2932
|
+
function selectSnapshot(last, snapshots, from) {
|
|
2933
|
+
const key = from?.trim() ? from.trim() : "last";
|
|
2934
|
+
if (key === "last") {
|
|
2935
|
+
if (!last) throw new Error("expect: no previous snapshot (from=last)");
|
|
2936
|
+
return last;
|
|
2331
2937
|
}
|
|
2332
|
-
|
|
2938
|
+
const found = snapshots.get(key);
|
|
2939
|
+
if (!found) throw new Error(`expect: unknown snapshot reference: ${key}`);
|
|
2940
|
+
return found;
|
|
2333
2941
|
}
|
|
2334
|
-
function
|
|
2335
|
-
|
|
2942
|
+
function assertRecordMatches(record, step, stepIndex) {
|
|
2943
|
+
if (step.equals !== void 0 && record.text !== step.equals) throw new Error(`step ${stepIndex + 1} expect.equals failed`);
|
|
2944
|
+
if (step.contains && step.contains.length > 0) {
|
|
2945
|
+
for (const item of step.contains) if (!record.text.includes(item)) throw new Error(`step ${stepIndex + 1} expect.contains failed: ${JSON.stringify(item)}`);
|
|
2946
|
+
}
|
|
2947
|
+
if (step.notContains && step.notContains.length > 0) {
|
|
2948
|
+
for (const item of step.notContains) if (record.text.includes(item)) throw new Error(`step ${stepIndex + 1} expect.notContains failed: ${JSON.stringify(item)}`);
|
|
2949
|
+
}
|
|
2950
|
+
if (step.regex) {
|
|
2951
|
+
if (!new RegExp(step.regex).test(record.text)) throw new Error(`step ${stepIndex + 1} expect.regex failed: ${JSON.stringify(step.regex)}`);
|
|
2952
|
+
}
|
|
2336
2953
|
}
|
|
2337
|
-
function
|
|
2338
|
-
|
|
2954
|
+
function assertGoldenText(path, text, update) {
|
|
2955
|
+
if (update) {
|
|
2956
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2957
|
+
writeFileSync(path, text, "utf8");
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
if (text !== readFileSync(path, "utf8")) throw new Error(`golden mismatch: ${path}`);
|
|
2339
2961
|
}
|
|
2340
|
-
function
|
|
2341
|
-
|
|
2342
|
-
|
|
2962
|
+
async function snapshotAfterStep(session) {
|
|
2963
|
+
try {
|
|
2964
|
+
const captured = await session.snapshotText({
|
|
2965
|
+
scope: "visible",
|
|
2966
|
+
trimRight: true,
|
|
2967
|
+
trimBottom: true,
|
|
2968
|
+
captureFrame: true
|
|
2969
|
+
});
|
|
2970
|
+
const lines = captured.text.split("\n");
|
|
2971
|
+
const view = formatSnapshotView({
|
|
2972
|
+
sessionId: session.id,
|
|
2973
|
+
scope: "visible",
|
|
2974
|
+
hash: captured.hash,
|
|
2975
|
+
lines,
|
|
2976
|
+
meta: session.getMeta(),
|
|
2977
|
+
lineNumbers: true
|
|
2978
|
+
});
|
|
2979
|
+
return {
|
|
2980
|
+
kind: "view",
|
|
2981
|
+
hash: captured.hash,
|
|
2982
|
+
text: view
|
|
2983
|
+
};
|
|
2984
|
+
} catch {
|
|
2985
|
+
return null;
|
|
2986
|
+
}
|
|
2343
2987
|
}
|
|
2344
|
-
function
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2988
|
+
async function snapshotStep(session, step) {
|
|
2989
|
+
if (step.kind === "grid") {
|
|
2990
|
+
if (step.mask && step.mask.length > 0) throw new Error("snapshot.kind=grid does not support mask (use text/view instead)");
|
|
2991
|
+
const { grid, hash } = await session.snapshotGrid({
|
|
2992
|
+
trimRight: step.trimRight,
|
|
2993
|
+
includeStyles: step.includeStyles,
|
|
2994
|
+
captureFrame: true
|
|
2995
|
+
});
|
|
2996
|
+
return {
|
|
2997
|
+
kind: step.kind,
|
|
2998
|
+
hash,
|
|
2999
|
+
text: JSON.stringify(grid, null, 2)
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
if (step.kind === "ansi" || step.kind === "view_ansi") {
|
|
3003
|
+
const { ansi, hash } = await session.snapshotAnsi({
|
|
3004
|
+
scope: step.scope,
|
|
3005
|
+
trimRight: step.trimRight,
|
|
3006
|
+
trimBottom: step.trimBottom ?? true,
|
|
3007
|
+
maxLines: step.maxLines,
|
|
3008
|
+
tailLines: step.tailLines,
|
|
3009
|
+
mask: step.mask
|
|
3010
|
+
});
|
|
3011
|
+
if (step.kind === "ansi") return {
|
|
3012
|
+
kind: step.kind,
|
|
3013
|
+
hash,
|
|
3014
|
+
text: ansi
|
|
3015
|
+
};
|
|
3016
|
+
const lines = ansi.split("\n");
|
|
3017
|
+
const view = formatSnapshotView({
|
|
3018
|
+
sessionId: session.id,
|
|
3019
|
+
scope: step.scope ?? "visible",
|
|
3020
|
+
hash,
|
|
3021
|
+
lines,
|
|
3022
|
+
meta: session.getMeta(),
|
|
3023
|
+
lineNumbers: step.lineNumbers
|
|
3024
|
+
});
|
|
3025
|
+
return {
|
|
3026
|
+
kind: step.kind,
|
|
3027
|
+
hash,
|
|
3028
|
+
text: view
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
const { text, hash } = await session.snapshotText({
|
|
3032
|
+
scope: step.scope,
|
|
3033
|
+
trimRight: step.trimRight,
|
|
3034
|
+
trimBottom: step.trimBottom ?? true,
|
|
3035
|
+
maxLines: step.maxLines,
|
|
3036
|
+
tailLines: step.tailLines,
|
|
3037
|
+
captureFrame: true,
|
|
3038
|
+
mask: step.mask
|
|
3039
|
+
});
|
|
3040
|
+
if (step.kind === "text") return {
|
|
3041
|
+
kind: step.kind,
|
|
3042
|
+
hash,
|
|
3043
|
+
text
|
|
3044
|
+
};
|
|
3045
|
+
const lines = text.split("\n");
|
|
3046
|
+
const view = formatSnapshotView({
|
|
3047
|
+
sessionId: session.id,
|
|
3048
|
+
scope: step.scope ?? "visible",
|
|
3049
|
+
hash,
|
|
3050
|
+
lines,
|
|
3051
|
+
meta: session.getMeta(),
|
|
3052
|
+
lineNumbers: step.lineNumbers
|
|
3053
|
+
});
|
|
3054
|
+
return {
|
|
3055
|
+
kind: step.kind,
|
|
3056
|
+
hash,
|
|
3057
|
+
text: view
|
|
3058
|
+
};
|
|
2348
3059
|
}
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
3060
|
+
//#endregion
|
|
3061
|
+
//#region src/script/assertion_step_runner_helpers.ts
|
|
3062
|
+
async function expectSessionMeta(session, step) {
|
|
3063
|
+
await session.flush();
|
|
3064
|
+
const meta = session.getMeta();
|
|
3065
|
+
if (step.bufferType !== void 0 && meta.bufferType !== step.bufferType) throw new Error(`expectMeta.bufferType mismatch (got ${meta.bufferType})`);
|
|
3066
|
+
if (step.cols !== void 0 && meta.cols !== step.cols) throw new Error(`expectMeta.cols mismatch (got ${meta.cols})`);
|
|
3067
|
+
if (step.rows !== void 0 && meta.rows !== step.rows) throw new Error(`expectMeta.rows mismatch (got ${meta.rows})`);
|
|
3068
|
+
if (step.cursor) {
|
|
3069
|
+
const cursorViewportRow = meta.baseY + meta.cursorY - meta.viewportY;
|
|
3070
|
+
const actual = {
|
|
3071
|
+
x: meta.cursorX + 1,
|
|
3072
|
+
y: cursorViewportRow + 1
|
|
3073
|
+
};
|
|
3074
|
+
if (actual.x !== step.cursor.x || actual.y !== step.cursor.y) throw new Error(`expectMeta.cursor mismatch (got ${actual.x},${actual.y})`);
|
|
2354
3075
|
}
|
|
2355
|
-
return lines;
|
|
2356
3076
|
}
|
|
2357
|
-
function
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
3077
|
+
async function runAssertStep(session, step, stepIndex) {
|
|
3078
|
+
const regex = step.regex ? new RegExp(step.regex) : void 0;
|
|
3079
|
+
if (!(await session.waitForText({
|
|
3080
|
+
scope: step.scope,
|
|
3081
|
+
text: step.text,
|
|
3082
|
+
regex,
|
|
3083
|
+
timeoutMs: 0,
|
|
3084
|
+
intervalMs: 0
|
|
3085
|
+
})).found) throw new Error(`step ${stepIndex + 1} assert failed: ${step.description || step.text || step.regex || "pattern mismatch"}`);
|
|
2362
3086
|
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
3087
|
+
//#endregion
|
|
3088
|
+
//#region src/script/custom_step_runner.ts
|
|
3089
|
+
async function runCustomStep(args) {
|
|
3090
|
+
const handler = args.stepHandlers?.[args.step.name];
|
|
3091
|
+
if (!handler) throw new Error(`custom handler not found: ${args.step.name}`);
|
|
3092
|
+
return await handler({
|
|
3093
|
+
session: args.session,
|
|
3094
|
+
stepIndex: args.stepIndex,
|
|
3095
|
+
last: args.last,
|
|
3096
|
+
snapshots: args.snapshots,
|
|
3097
|
+
artifactsDir: args.artifactsDir,
|
|
3098
|
+
resolveArtifactPath: args.resolveArtifactPath,
|
|
3099
|
+
resolveGoldenPath: args.resolveGoldenPath,
|
|
3100
|
+
updateGoldens: args.updateGoldens,
|
|
3101
|
+
captureSnapshot: async (snapshotConfig) => {
|
|
3102
|
+
const record = await snapshotStep(args.session, {
|
|
3103
|
+
type: "snapshot",
|
|
3104
|
+
...snapshotConfig
|
|
3105
|
+
});
|
|
3106
|
+
persistSnapshotRecord({
|
|
3107
|
+
record,
|
|
3108
|
+
saveAs: snapshotConfig.saveAs,
|
|
3109
|
+
saveTo: snapshotConfig.saveTo,
|
|
3110
|
+
snapshots: args.snapshots,
|
|
3111
|
+
resolveArtifactPath: args.resolveArtifactPath
|
|
3112
|
+
});
|
|
3113
|
+
return record;
|
|
3114
|
+
},
|
|
3115
|
+
getSnapshot: (from) => selectSnapshot(args.last, args.snapshots, from),
|
|
3116
|
+
writeArtifactText: (path, text) => {
|
|
3117
|
+
const resolved = args.resolveArtifactPath(path);
|
|
3118
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
3119
|
+
writeFileSync(resolved, text, "utf8");
|
|
3120
|
+
},
|
|
3121
|
+
assertGoldenText: (path, text) => {
|
|
3122
|
+
assertGoldenText(args.resolveGoldenPath(path), text, args.updateGoldens);
|
|
3123
|
+
}
|
|
3124
|
+
}, args.step) ?? args.last;
|
|
2367
3125
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
3126
|
+
//#endregion
|
|
3127
|
+
//#region src/script/input_step_runner_helpers.ts
|
|
3128
|
+
function runMouseStep(session, step) {
|
|
3129
|
+
const modifiers = step.shift || step.alt || step.ctrl ? {
|
|
3130
|
+
shift: step.shift,
|
|
3131
|
+
alt: step.alt,
|
|
3132
|
+
ctrl: step.ctrl
|
|
3133
|
+
} : void 0;
|
|
3134
|
+
session.sendMouse({
|
|
3135
|
+
action: step.action,
|
|
3136
|
+
x: step.x,
|
|
3137
|
+
y: step.y,
|
|
3138
|
+
button: step.button,
|
|
3139
|
+
modifiers
|
|
3140
|
+
});
|
|
2374
3141
|
}
|
|
2375
3142
|
//#endregion
|
|
2376
|
-
//#region src/script/
|
|
2377
|
-
|
|
2378
|
-
regex
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
3143
|
+
//#region src/script/wait_step_runner_helpers.ts
|
|
3144
|
+
async function waitForTextStep(session, step, stepIndex) {
|
|
3145
|
+
const regex = step.regex ? new RegExp(step.regex) : void 0;
|
|
3146
|
+
if (!(await session.waitForText({
|
|
3147
|
+
scope: step.scope,
|
|
3148
|
+
text: step.text,
|
|
3149
|
+
regex,
|
|
3150
|
+
timeoutMs: step.timeoutMs ?? 1e4,
|
|
3151
|
+
intervalMs: step.intervalMs ?? 100
|
|
3152
|
+
})).found) throw new Error(`step ${stepIndex + 1} waitForText not found: ${step.text ?? step.regex ?? ""}`);
|
|
3153
|
+
}
|
|
3154
|
+
async function waitForStableScreenStep(session, step, stepIndex) {
|
|
3155
|
+
if (!(await session.waitForStableScreen({
|
|
3156
|
+
timeoutMs: step.timeoutMs ?? 1e4,
|
|
3157
|
+
quietMs: step.quietMs ?? 400,
|
|
3158
|
+
intervalMs: step.intervalMs ?? 80
|
|
3159
|
+
})).stable) throw new Error(`step ${stepIndex + 1} waitForStableScreen timed out`);
|
|
3160
|
+
}
|
|
3161
|
+
async function waitForExitStep(session, step) {
|
|
3162
|
+
const startedAt = Date.now();
|
|
3163
|
+
const timeoutMs = step.timeoutMs ?? 1e4;
|
|
3164
|
+
const intervalMs = step.intervalMs ?? 50;
|
|
3165
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
3166
|
+
if (session.isClosed()) break;
|
|
3167
|
+
await sleep(intervalMs);
|
|
3168
|
+
}
|
|
3169
|
+
const reason = session.getCloseReason();
|
|
3170
|
+
if (!reason) throw new Error("waitForExit timed out");
|
|
3171
|
+
if (reason.type !== "process_exit") throw new Error("waitForExit: session was closed by user");
|
|
3172
|
+
if (step.exitCode !== void 0 && reason.exitCode !== step.exitCode) throw new Error(`waitForExit: exitCode mismatch (got ${reason.exitCode})`);
|
|
3173
|
+
if (step.signal !== void 0 && reason.signal !== step.signal) throw new Error(`waitForExit: signal mismatch (got ${String(reason.signal ?? "")})`);
|
|
3174
|
+
}
|
|
3175
|
+
//#endregion
|
|
3176
|
+
//#region src/script/step_runner.ts
|
|
3177
|
+
async function runStep(args) {
|
|
3178
|
+
const { step } = args;
|
|
3179
|
+
try {
|
|
3180
|
+
if (step.type === "sendText") {
|
|
3181
|
+
args.session.sendText(step.text, { enter: step.enter });
|
|
3182
|
+
return args.last;
|
|
3183
|
+
}
|
|
3184
|
+
if (step.type === "pressKey") {
|
|
3185
|
+
args.session.pressKey(step.key);
|
|
3186
|
+
return args.last;
|
|
3187
|
+
}
|
|
3188
|
+
if (step.type === "sendMouse") {
|
|
3189
|
+
runMouseStep(args.session, step);
|
|
3190
|
+
return args.last;
|
|
3191
|
+
}
|
|
3192
|
+
if (step.type === "resize") {
|
|
3193
|
+
args.session.resize(step.cols, step.rows);
|
|
3194
|
+
return args.last;
|
|
3195
|
+
}
|
|
3196
|
+
if (step.type === "mark") {
|
|
3197
|
+
args.session.mark(step.label);
|
|
3198
|
+
return args.last;
|
|
3199
|
+
}
|
|
3200
|
+
if (step.type === "sleep") {
|
|
3201
|
+
await sleep(step.ms);
|
|
3202
|
+
return args.last;
|
|
3203
|
+
}
|
|
3204
|
+
if (step.type === "waitForText") {
|
|
3205
|
+
await waitForTextStep(args.session, step, args.stepIndex);
|
|
3206
|
+
return args.last;
|
|
3207
|
+
}
|
|
3208
|
+
if (step.type === "waitForStableScreen") {
|
|
3209
|
+
await waitForStableScreenStep(args.session, step, args.stepIndex);
|
|
3210
|
+
return args.last;
|
|
3211
|
+
}
|
|
3212
|
+
if (step.type === "waitForExit") {
|
|
3213
|
+
await waitForExitStep(args.session, step);
|
|
3214
|
+
return args.last;
|
|
3215
|
+
}
|
|
3216
|
+
if (step.type === "expectMeta") {
|
|
3217
|
+
await expectSessionMeta(args.session, step);
|
|
3218
|
+
return args.last;
|
|
3219
|
+
}
|
|
3220
|
+
if (step.type === "snapshot") {
|
|
3221
|
+
const record = await snapshotStep(args.session, step);
|
|
3222
|
+
persistSnapshotRecord({
|
|
3223
|
+
record,
|
|
3224
|
+
saveAs: step.saveAs,
|
|
3225
|
+
saveTo: step.saveTo,
|
|
3226
|
+
snapshots: args.snapshots,
|
|
3227
|
+
resolveArtifactPath: args.resolveArtifactPath
|
|
3228
|
+
});
|
|
3229
|
+
return record;
|
|
3230
|
+
}
|
|
3231
|
+
if (step.type === "expect") {
|
|
3232
|
+
assertRecordMatches(selectSnapshot(args.last, args.snapshots, step.from), step, args.stepIndex);
|
|
3233
|
+
return args.last;
|
|
3234
|
+
}
|
|
3235
|
+
if (step.type === "assert") {
|
|
3236
|
+
await runAssertStep(args.session, step, args.stepIndex);
|
|
3237
|
+
return args.last;
|
|
3238
|
+
}
|
|
3239
|
+
if (step.type === "assertSemantic") return args.last;
|
|
3240
|
+
if (step.type === "expectGolden") {
|
|
3241
|
+
const record = selectSnapshot(args.last, args.snapshots, step.from);
|
|
3242
|
+
assertGoldenText(args.resolveGoldenPath(step.path), `${record.text}\n`, args.updateGoldens);
|
|
3243
|
+
return args.last;
|
|
3244
|
+
}
|
|
3245
|
+
if (step.type === "custom") return await runCustomStep({
|
|
3246
|
+
step,
|
|
3247
|
+
stepIndex: args.stepIndex,
|
|
3248
|
+
session: args.session,
|
|
3249
|
+
snapshots: args.snapshots,
|
|
3250
|
+
last: args.last,
|
|
3251
|
+
resolveGoldenPath: args.resolveGoldenPath,
|
|
3252
|
+
resolveArtifactPath: args.resolveArtifactPath,
|
|
3253
|
+
updateGoldens: args.updateGoldens,
|
|
3254
|
+
stepHandlers: args.stepHandlers,
|
|
3255
|
+
artifactsDir: args.artifactsDir
|
|
2414
3256
|
});
|
|
2415
|
-
|
|
3257
|
+
throw new Error(`unknown type: ${step.type}`);
|
|
3258
|
+
} catch (error) {
|
|
3259
|
+
throw annotateStepError(error, args.stepIndex, step);
|
|
2416
3260
|
}
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
reportScope: z.enum(["visible", "buffer"]).optional(),
|
|
2428
|
-
reportMaxFrames: z.number().int().positive().optional()
|
|
2429
|
-
});
|
|
2430
|
-
const sendTextStepSchema = z.object({
|
|
2431
|
-
type: z.literal("sendText"),
|
|
2432
|
-
text: z.string(),
|
|
2433
|
-
enter: z.boolean().optional()
|
|
2434
|
-
});
|
|
2435
|
-
const pressKeyStepSchema = z.object({
|
|
2436
|
-
type: z.literal("pressKey"),
|
|
2437
|
-
key: z.string().min(1)
|
|
2438
|
-
});
|
|
2439
|
-
const sendMouseStepSchema = z.object({
|
|
2440
|
-
type: z.literal("sendMouse"),
|
|
2441
|
-
action: z.enum([
|
|
2442
|
-
"down",
|
|
2443
|
-
"up",
|
|
2444
|
-
"move",
|
|
2445
|
-
"click",
|
|
2446
|
-
"scroll_up",
|
|
2447
|
-
"scroll_down"
|
|
2448
|
-
]),
|
|
2449
|
-
x: z.number().int(),
|
|
2450
|
-
y: z.number().int(),
|
|
2451
|
-
button: z.enum([
|
|
2452
|
-
"left",
|
|
2453
|
-
"middle",
|
|
2454
|
-
"right"
|
|
2455
|
-
]).optional(),
|
|
2456
|
-
shift: z.boolean().optional(),
|
|
2457
|
-
alt: z.boolean().optional(),
|
|
2458
|
-
ctrl: z.boolean().optional()
|
|
2459
|
-
});
|
|
2460
|
-
const resizeStepSchema = z.object({
|
|
2461
|
-
type: z.literal("resize"),
|
|
2462
|
-
cols: z.number().int().positive(),
|
|
2463
|
-
rows: z.number().int().positive()
|
|
2464
|
-
});
|
|
2465
|
-
const markStepSchema = z.object({
|
|
2466
|
-
type: z.literal("mark"),
|
|
2467
|
-
label: z.string().optional()
|
|
2468
|
-
});
|
|
2469
|
-
const sleepStepSchema = z.object({
|
|
2470
|
-
type: z.literal("sleep"),
|
|
2471
|
-
ms: z.number().int().nonnegative()
|
|
2472
|
-
});
|
|
2473
|
-
const waitForTextStepSchema = z.object({
|
|
2474
|
-
type: z.literal("waitForText"),
|
|
2475
|
-
scope: z.enum(["visible", "buffer"]).optional(),
|
|
2476
|
-
text: z.string().optional(),
|
|
2477
|
-
regex: z.string().optional(),
|
|
2478
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
2479
|
-
intervalMs: z.number().int().positive().optional()
|
|
2480
|
-
}).superRefine((value, ctx) => {
|
|
2481
|
-
if (!value.text && !value.regex) ctx.addIssue({
|
|
2482
|
-
code: z.ZodIssueCode.custom,
|
|
2483
|
-
message: "waitForText requires text or regex"
|
|
2484
|
-
});
|
|
2485
|
-
});
|
|
2486
|
-
const waitForStableScreenStepSchema = z.object({
|
|
2487
|
-
type: z.literal("waitForStableScreen"),
|
|
2488
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
2489
|
-
quietMs: z.number().int().positive().optional(),
|
|
2490
|
-
intervalMs: z.number().int().positive().optional()
|
|
2491
|
-
});
|
|
2492
|
-
const waitForExitStepSchema = z.object({
|
|
2493
|
-
type: z.literal("waitForExit"),
|
|
2494
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
2495
|
-
intervalMs: z.number().int().positive().optional(),
|
|
2496
|
-
exitCode: z.number().int().optional(),
|
|
2497
|
-
signal: z.union([z.number().int(), z.string()]).optional()
|
|
2498
|
-
});
|
|
2499
|
-
const expectMetaStepSchema = z.object({
|
|
2500
|
-
type: z.literal("expectMeta"),
|
|
2501
|
-
bufferType: z.enum(["normal", "alternate"]).optional(),
|
|
2502
|
-
cols: z.number().int().positive().optional(),
|
|
2503
|
-
rows: z.number().int().positive().optional(),
|
|
2504
|
-
cursor: z.object({
|
|
2505
|
-
x: z.number().int().positive(),
|
|
2506
|
-
y: z.number().int().positive()
|
|
2507
|
-
}).optional()
|
|
2508
|
-
}).superRefine((value, ctx) => {
|
|
2509
|
-
if (value.bufferType === void 0 && value.cols === void 0 && value.rows === void 0 && value.cursor === void 0) ctx.addIssue({
|
|
2510
|
-
code: z.ZodIssueCode.custom,
|
|
2511
|
-
message: "expectMeta requires at least one assertion (bufferType/cols/rows/cursor)"
|
|
2512
|
-
});
|
|
2513
|
-
});
|
|
2514
|
-
const snapshotStepSchema = z.object({
|
|
2515
|
-
type: z.literal("snapshot"),
|
|
2516
|
-
kind: z.enum([
|
|
2517
|
-
"text",
|
|
2518
|
-
"view",
|
|
2519
|
-
"ansi",
|
|
2520
|
-
"view_ansi",
|
|
2521
|
-
"grid"
|
|
2522
|
-
]),
|
|
2523
|
-
scope: z.enum(["visible", "buffer"]).optional(),
|
|
2524
|
-
trimRight: z.boolean().optional(),
|
|
2525
|
-
trimBottom: z.boolean().optional(),
|
|
2526
|
-
maxLines: z.number().int().positive().optional(),
|
|
2527
|
-
tailLines: z.number().int().positive().optional(),
|
|
2528
|
-
lineNumbers: z.boolean().optional(),
|
|
2529
|
-
includeStyles: z.boolean().optional(),
|
|
2530
|
-
mask: z.array(textMaskRuleSchema).optional(),
|
|
2531
|
-
saveAs: z.string().optional(),
|
|
2532
|
-
saveTo: z.string().optional()
|
|
2533
|
-
}).superRefine((value, ctx) => {
|
|
2534
|
-
if (value.maxLines !== void 0 && value.tailLines !== void 0) ctx.addIssue({
|
|
2535
|
-
code: z.ZodIssueCode.custom,
|
|
2536
|
-
message: "snapshot: maxLines and tailLines are mutually exclusive"
|
|
2537
|
-
});
|
|
2538
|
-
});
|
|
2539
|
-
const expectStepSchema = z.object({
|
|
2540
|
-
type: z.literal("expect"),
|
|
2541
|
-
from: z.string().optional(),
|
|
2542
|
-
equals: z.string().optional(),
|
|
2543
|
-
contains: z.array(z.string()).optional(),
|
|
2544
|
-
notContains: z.array(z.string()).optional(),
|
|
2545
|
-
regex: z.string().optional()
|
|
2546
|
-
}).superRefine((value, ctx) => {
|
|
2547
|
-
if (value.equals === void 0 && !value.contains?.length && !value.notContains?.length && !value.regex) ctx.addIssue({
|
|
2548
|
-
code: z.ZodIssueCode.custom,
|
|
2549
|
-
message: "expect requires at least one matcher (equals/contains/notContains/regex)"
|
|
2550
|
-
});
|
|
2551
|
-
});
|
|
2552
|
-
const expectGoldenStepSchema = z.object({
|
|
2553
|
-
type: z.literal("expectGolden"),
|
|
2554
|
-
from: z.string().optional(),
|
|
2555
|
-
path: z.string().min(1)
|
|
2556
|
-
});
|
|
2557
|
-
const customStepSchema = z.object({
|
|
2558
|
-
type: z.literal("custom"),
|
|
2559
|
-
name: z.string().min(1),
|
|
2560
|
-
payload: z.unknown().optional()
|
|
2561
|
-
});
|
|
2562
|
-
const assertStepSchema = z.object({
|
|
2563
|
-
type: z.literal("assert"),
|
|
2564
|
-
scope: z.enum(["visible", "buffer"]).optional(),
|
|
2565
|
-
text: z.string().optional(),
|
|
2566
|
-
regex: z.string().optional(),
|
|
2567
|
-
description: z.string().optional()
|
|
2568
|
-
}).superRefine((value, ctx) => {
|
|
2569
|
-
if (!value.text && !value.regex) ctx.addIssue({
|
|
2570
|
-
code: z.ZodIssueCode.custom,
|
|
2571
|
-
message: "assert requires text or regex"
|
|
2572
|
-
});
|
|
2573
|
-
});
|
|
2574
|
-
const assertSemanticStepSchema = z.object({
|
|
2575
|
-
type: z.literal("assertSemantic"),
|
|
2576
|
-
prompt: z.string().min(1),
|
|
2577
|
-
description: z.string().optional()
|
|
2578
|
-
});
|
|
2579
|
-
const scriptStepSchema = z.union([
|
|
2580
|
-
sendTextStepSchema,
|
|
2581
|
-
pressKeyStepSchema,
|
|
2582
|
-
sendMouseStepSchema,
|
|
2583
|
-
resizeStepSchema,
|
|
2584
|
-
markStepSchema,
|
|
2585
|
-
sleepStepSchema,
|
|
2586
|
-
waitForTextStepSchema,
|
|
2587
|
-
waitForStableScreenStepSchema,
|
|
2588
|
-
waitForExitStepSchema,
|
|
2589
|
-
expectMetaStepSchema,
|
|
2590
|
-
snapshotStepSchema,
|
|
2591
|
-
expectStepSchema,
|
|
2592
|
-
expectGoldenStepSchema,
|
|
2593
|
-
customStepSchema,
|
|
2594
|
-
assertStepSchema,
|
|
2595
|
-
assertSemanticStepSchema
|
|
2596
|
-
]);
|
|
2597
|
-
const scriptSchema = z.object({
|
|
2598
|
-
name: z.string().optional(),
|
|
2599
|
-
artifactsDir: z.string().optional(),
|
|
2600
|
-
launch: launchConfigSchema,
|
|
2601
|
-
trace: scriptTraceSchema.optional(),
|
|
2602
|
-
steps: z.array(scriptStepSchema).min(1)
|
|
2603
|
-
});
|
|
3261
|
+
}
|
|
3262
|
+
function annotateStepError(error, stepIndex, step) {
|
|
3263
|
+
const label = step.type === "custom" ? `custom(${step.name})` : step.type;
|
|
3264
|
+
const prefix = `step ${stepIndex + 1} ${label}`;
|
|
3265
|
+
if (error instanceof Error) {
|
|
3266
|
+
if (!error.message.startsWith("step ")) error.message = `${prefix}: ${error.message}`;
|
|
3267
|
+
return error;
|
|
3268
|
+
}
|
|
3269
|
+
return /* @__PURE__ */ new Error(`${prefix}: ${String(error)}`);
|
|
3270
|
+
}
|
|
2604
3271
|
//#endregion
|
|
2605
3272
|
//#region src/script/runner.ts
|
|
2606
3273
|
async function runScriptFile(scriptPath, options) {
|
|
2607
|
-
|
|
2608
|
-
const parsedJson = JSON.parse(raw);
|
|
2609
|
-
const baseName = basename(scriptPath, extname(scriptPath));
|
|
2610
|
-
return runScript(parsedJson && typeof parsedJson === "object" && !Array.isArray(parsedJson) && !("name" in parsedJson) ? {
|
|
2611
|
-
...parsedJson,
|
|
2612
|
-
name: baseName
|
|
2613
|
-
} : parsedJson, options);
|
|
3274
|
+
return runScript(await loadJsonScriptFileWithDefaultName(scriptPath), options);
|
|
2614
3275
|
}
|
|
2615
3276
|
async function runScript(script, options) {
|
|
2616
3277
|
const parsed = scriptSchema.parse(script);
|
|
2617
3278
|
const scriptName = parsed.name ?? "script";
|
|
2618
3279
|
const artifactsDir = resolveArtifactsDir(parsed, scriptName, options?.artifactsDir);
|
|
2619
3280
|
mkdirSync(artifactsDir, { recursive: true });
|
|
2620
|
-
const
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
let sessions = null;
|
|
2624
|
-
let session;
|
|
2625
|
-
if (backend === "pty") {
|
|
2626
|
-
sessions = new SessionManager({ snapshotRingSize: 50 });
|
|
2627
|
-
if (!launch.command) throw new Error("launch.command is required when backend=pty");
|
|
2628
|
-
session = sessions.launchSession({
|
|
2629
|
-
command: launch.command,
|
|
2630
|
-
args: launch.args ?? [],
|
|
2631
|
-
cwd,
|
|
2632
|
-
env: launch.env,
|
|
2633
|
-
cols: launch.cols,
|
|
2634
|
-
rows: launch.rows,
|
|
2635
|
-
name: launch.name
|
|
2636
|
-
});
|
|
2637
|
-
} else session = await createFrameSessionFromLaunch({
|
|
2638
|
-
launch,
|
|
2639
|
-
cwd,
|
|
2640
|
-
title: scriptName
|
|
3281
|
+
const { session, closeSession } = await launchScriptSession({
|
|
3282
|
+
launch: parsed.launch,
|
|
3283
|
+
scriptName
|
|
2641
3284
|
});
|
|
2642
|
-
const closeSession = () => {
|
|
2643
|
-
if (sessions) {
|
|
2644
|
-
sessions.closeAll();
|
|
2645
|
-
return;
|
|
2646
|
-
}
|
|
2647
|
-
session.close();
|
|
2648
|
-
};
|
|
2649
3285
|
const snapshots = /* @__PURE__ */ new Map();
|
|
2650
3286
|
let last = null;
|
|
2651
3287
|
let currentStepIndex = -1;
|
|
2652
3288
|
let currentStep = null;
|
|
2653
|
-
const
|
|
2654
|
-
const
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
const reportPath = resolveArtifactPath(trace.reportPath ?? `${scriptName}.report.html`);
|
|
3289
|
+
const { resolveArtifactPath, resolveGoldenPath } = createScriptPathResolvers(artifactsDir);
|
|
3290
|
+
const traceArtifacts = resolveScriptTraceArtifacts({
|
|
3291
|
+
script: parsed,
|
|
3292
|
+
scriptName,
|
|
3293
|
+
resolveArtifactPath
|
|
3294
|
+
});
|
|
2660
3295
|
const stepHandlers = options?.steps;
|
|
2661
3296
|
const executionSteps = [];
|
|
2662
3297
|
try {
|
|
@@ -2679,30 +3314,9 @@ async function runScript(script, options) {
|
|
|
2679
3314
|
stepHandlers,
|
|
2680
3315
|
artifactsDir
|
|
2681
3316
|
});
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
scope: "visible",
|
|
2686
|
-
trimRight: true,
|
|
2687
|
-
trimBottom: true,
|
|
2688
|
-
captureFrame: true
|
|
2689
|
-
});
|
|
2690
|
-
const lines = captured.text.split("\n");
|
|
2691
|
-
const view = formatSnapshotView({
|
|
2692
|
-
sessionId: session.id,
|
|
2693
|
-
scope: "visible",
|
|
2694
|
-
hash: captured.hash,
|
|
2695
|
-
lines,
|
|
2696
|
-
meta: session.getMeta(),
|
|
2697
|
-
lineNumbers: true
|
|
2698
|
-
});
|
|
2699
|
-
after = {
|
|
2700
|
-
kind: "view",
|
|
2701
|
-
hash: captured.hash,
|
|
2702
|
-
text: view
|
|
2703
|
-
};
|
|
2704
|
-
last = after;
|
|
2705
|
-
} catch {}
|
|
3317
|
+
const stepProducedNewSnapshot = last !== before;
|
|
3318
|
+
const after = stepProducedNewSnapshot ? last : await snapshotAfterStep(session);
|
|
3319
|
+
if (!stepProducedNewSnapshot && after) last = after;
|
|
2706
3320
|
executionSteps.push({
|
|
2707
3321
|
index: stepIndex,
|
|
2708
3322
|
step,
|
|
@@ -2727,12 +3341,12 @@ async function runScript(script, options) {
|
|
|
2727
3341
|
await writeTraceArtifacts({
|
|
2728
3342
|
session,
|
|
2729
3343
|
artifactsDir,
|
|
2730
|
-
saveCast,
|
|
2731
|
-
castPath,
|
|
2732
|
-
saveReport,
|
|
2733
|
-
reportPath,
|
|
2734
|
-
reportScope:
|
|
2735
|
-
reportMaxFrames:
|
|
3344
|
+
saveCast: traceArtifacts.saveCast,
|
|
3345
|
+
castPath: traceArtifacts.castPath,
|
|
3346
|
+
saveReport: traceArtifacts.saveReport,
|
|
3347
|
+
reportPath: traceArtifacts.reportPath,
|
|
3348
|
+
reportScope: traceArtifacts.reportScope,
|
|
3349
|
+
reportMaxFrames: traceArtifacts.reportMaxFrames,
|
|
2736
3350
|
scriptName,
|
|
2737
3351
|
result: { ok: true },
|
|
2738
3352
|
executionSteps
|
|
@@ -2771,12 +3385,12 @@ async function runScript(script, options) {
|
|
|
2771
3385
|
await writeTraceArtifacts({
|
|
2772
3386
|
session,
|
|
2773
3387
|
artifactsDir,
|
|
2774
|
-
saveCast,
|
|
2775
|
-
castPath,
|
|
2776
|
-
saveReport,
|
|
2777
|
-
reportPath,
|
|
2778
|
-
reportScope:
|
|
2779
|
-
reportMaxFrames:
|
|
3388
|
+
saveCast: traceArtifacts.saveCast,
|
|
3389
|
+
castPath: traceArtifacts.castPath,
|
|
3390
|
+
saveReport: traceArtifacts.saveReport,
|
|
3391
|
+
reportPath: traceArtifacts.reportPath,
|
|
3392
|
+
reportScope: traceArtifacts.reportScope,
|
|
3393
|
+
reportMaxFrames: traceArtifacts.reportMaxFrames,
|
|
2780
3394
|
scriptName,
|
|
2781
3395
|
result: {
|
|
2782
3396
|
ok: false,
|
|
@@ -2794,464 +3408,5 @@ async function runScript(script, options) {
|
|
|
2794
3408
|
throw error;
|
|
2795
3409
|
}
|
|
2796
3410
|
}
|
|
2797
|
-
function resolveArtifactsDir(script, scriptName, override) {
|
|
2798
|
-
if (override?.trim()) return resolve(override.trim());
|
|
2799
|
-
if (script.artifactsDir?.trim()) return resolve(script.artifactsDir.trim());
|
|
2800
|
-
return resolve(".tmp", "runs", scriptName);
|
|
2801
|
-
}
|
|
2802
|
-
async function runStep(args) {
|
|
2803
|
-
const { step } = args;
|
|
2804
|
-
try {
|
|
2805
|
-
if (step.type === "sendText") {
|
|
2806
|
-
args.session.sendText(step.text, { enter: step.enter });
|
|
2807
|
-
return args.last;
|
|
2808
|
-
}
|
|
2809
|
-
if (step.type === "pressKey") {
|
|
2810
|
-
args.session.pressKey(step.key);
|
|
2811
|
-
return args.last;
|
|
2812
|
-
}
|
|
2813
|
-
if (step.type === "sendMouse") {
|
|
2814
|
-
const modifiers = step.shift || step.alt || step.ctrl ? {
|
|
2815
|
-
shift: step.shift,
|
|
2816
|
-
alt: step.alt,
|
|
2817
|
-
ctrl: step.ctrl
|
|
2818
|
-
} : void 0;
|
|
2819
|
-
args.session.sendMouse({
|
|
2820
|
-
action: step.action,
|
|
2821
|
-
x: step.x,
|
|
2822
|
-
y: step.y,
|
|
2823
|
-
button: step.button,
|
|
2824
|
-
modifiers
|
|
2825
|
-
});
|
|
2826
|
-
return args.last;
|
|
2827
|
-
}
|
|
2828
|
-
if (step.type === "resize") {
|
|
2829
|
-
args.session.resize(step.cols, step.rows);
|
|
2830
|
-
return args.last;
|
|
2831
|
-
}
|
|
2832
|
-
if (step.type === "mark") {
|
|
2833
|
-
args.session.mark(step.label);
|
|
2834
|
-
return args.last;
|
|
2835
|
-
}
|
|
2836
|
-
if (step.type === "sleep") {
|
|
2837
|
-
await sleep(step.ms);
|
|
2838
|
-
return args.last;
|
|
2839
|
-
}
|
|
2840
|
-
if (step.type === "waitForText") {
|
|
2841
|
-
const regex = step.regex ? new RegExp(step.regex) : void 0;
|
|
2842
|
-
if (!(await args.session.waitForText({
|
|
2843
|
-
scope: step.scope,
|
|
2844
|
-
text: step.text,
|
|
2845
|
-
regex,
|
|
2846
|
-
timeoutMs: step.timeoutMs ?? 1e4,
|
|
2847
|
-
intervalMs: step.intervalMs ?? 100
|
|
2848
|
-
})).found) throw new Error(`step ${args.stepIndex + 1} waitForText not found: ${step.text ?? step.regex ?? ""}`);
|
|
2849
|
-
return args.last;
|
|
2850
|
-
}
|
|
2851
|
-
if (step.type === "waitForStableScreen") {
|
|
2852
|
-
if (!(await args.session.waitForStableScreen({
|
|
2853
|
-
timeoutMs: step.timeoutMs ?? 1e4,
|
|
2854
|
-
quietMs: step.quietMs ?? 400,
|
|
2855
|
-
intervalMs: step.intervalMs ?? 80
|
|
2856
|
-
})).stable) throw new Error(`step ${args.stepIndex + 1} waitForStableScreen timed out`);
|
|
2857
|
-
return args.last;
|
|
2858
|
-
}
|
|
2859
|
-
if (step.type === "waitForExit") {
|
|
2860
|
-
const startedAt = Date.now();
|
|
2861
|
-
const timeoutMs = step.timeoutMs ?? 1e4;
|
|
2862
|
-
const intervalMs = step.intervalMs ?? 50;
|
|
2863
|
-
while (Date.now() - startedAt <= timeoutMs) {
|
|
2864
|
-
if (args.session.isClosed()) break;
|
|
2865
|
-
await sleep(intervalMs);
|
|
2866
|
-
}
|
|
2867
|
-
const reason = args.session.getCloseReason();
|
|
2868
|
-
if (!reason) throw new Error("waitForExit timed out");
|
|
2869
|
-
if (reason.type !== "process_exit") throw new Error("waitForExit: session was closed by user");
|
|
2870
|
-
if (step.exitCode !== void 0 && reason.exitCode !== step.exitCode) throw new Error(`waitForExit: exitCode mismatch (got ${reason.exitCode})`);
|
|
2871
|
-
if (step.signal !== void 0 && reason.signal !== step.signal) throw new Error(`waitForExit: signal mismatch (got ${String(reason.signal ?? "")})`);
|
|
2872
|
-
return args.last;
|
|
2873
|
-
}
|
|
2874
|
-
if (step.type === "expectMeta") {
|
|
2875
|
-
await args.session.flush();
|
|
2876
|
-
const meta = args.session.getMeta();
|
|
2877
|
-
if (step.bufferType !== void 0 && meta.bufferType !== step.bufferType) throw new Error(`expectMeta.bufferType mismatch (got ${meta.bufferType})`);
|
|
2878
|
-
if (step.cols !== void 0 && meta.cols !== step.cols) throw new Error(`expectMeta.cols mismatch (got ${meta.cols})`);
|
|
2879
|
-
if (step.rows !== void 0 && meta.rows !== step.rows) throw new Error(`expectMeta.rows mismatch (got ${meta.rows})`);
|
|
2880
|
-
if (step.cursor) {
|
|
2881
|
-
const cursorViewportRow = meta.baseY + meta.cursorY - meta.viewportY;
|
|
2882
|
-
const actual = {
|
|
2883
|
-
x: meta.cursorX + 1,
|
|
2884
|
-
y: cursorViewportRow + 1
|
|
2885
|
-
};
|
|
2886
|
-
if (actual.x !== step.cursor.x || actual.y !== step.cursor.y) throw new Error(`expectMeta.cursor mismatch (got ${actual.x},${actual.y})`);
|
|
2887
|
-
}
|
|
2888
|
-
return args.last;
|
|
2889
|
-
}
|
|
2890
|
-
if (step.type === "snapshot") {
|
|
2891
|
-
const record = await snapshotStep(args.session, step);
|
|
2892
|
-
persistSnapshotRecord({
|
|
2893
|
-
record,
|
|
2894
|
-
saveAs: step.saveAs,
|
|
2895
|
-
saveTo: step.saveTo,
|
|
2896
|
-
snapshots: args.snapshots,
|
|
2897
|
-
resolveArtifactPath: args.resolveArtifactPath
|
|
2898
|
-
});
|
|
2899
|
-
return record;
|
|
2900
|
-
}
|
|
2901
|
-
if (step.type === "expect") {
|
|
2902
|
-
assertRecordMatches(selectSnapshot(args.last, args.snapshots, step.from), step, args.stepIndex);
|
|
2903
|
-
return args.last;
|
|
2904
|
-
}
|
|
2905
|
-
if (step.type === "assert") {
|
|
2906
|
-
const regex = step.regex ? new RegExp(step.regex) : void 0;
|
|
2907
|
-
if (!(await args.session.waitForText({
|
|
2908
|
-
scope: step.scope,
|
|
2909
|
-
text: step.text,
|
|
2910
|
-
regex,
|
|
2911
|
-
timeoutMs: 0,
|
|
2912
|
-
intervalMs: 0
|
|
2913
|
-
})).found) throw new Error(`step ${args.stepIndex + 1} assert failed: ${step.description || step.text || step.regex || "pattern mismatch"}`);
|
|
2914
|
-
return args.last;
|
|
2915
|
-
}
|
|
2916
|
-
if (step.type === "assertSemantic") return args.last;
|
|
2917
|
-
if (step.type === "expectGolden") {
|
|
2918
|
-
const record = selectSnapshot(args.last, args.snapshots, step.from);
|
|
2919
|
-
assertGoldenText(args.resolveGoldenPath(step.path), `${record.text}\n`, args.updateGoldens);
|
|
2920
|
-
return args.last;
|
|
2921
|
-
}
|
|
2922
|
-
if (step.type === "custom") {
|
|
2923
|
-
const handler = args.stepHandlers?.[step.name];
|
|
2924
|
-
if (!handler) throw new Error(`custom handler not found: ${step.name}`);
|
|
2925
|
-
const result = await handler({
|
|
2926
|
-
session: args.session,
|
|
2927
|
-
stepIndex: args.stepIndex,
|
|
2928
|
-
last: args.last,
|
|
2929
|
-
snapshots: args.snapshots,
|
|
2930
|
-
artifactsDir: args.artifactsDir,
|
|
2931
|
-
resolveArtifactPath: args.resolveArtifactPath,
|
|
2932
|
-
resolveGoldenPath: args.resolveGoldenPath,
|
|
2933
|
-
updateGoldens: args.updateGoldens,
|
|
2934
|
-
captureSnapshot: async (snapshotConfig) => {
|
|
2935
|
-
const record = await snapshotStep(args.session, {
|
|
2936
|
-
type: "snapshot",
|
|
2937
|
-
...snapshotConfig
|
|
2938
|
-
});
|
|
2939
|
-
persistSnapshotRecord({
|
|
2940
|
-
record,
|
|
2941
|
-
saveAs: snapshotConfig.saveAs,
|
|
2942
|
-
saveTo: snapshotConfig.saveTo,
|
|
2943
|
-
snapshots: args.snapshots,
|
|
2944
|
-
resolveArtifactPath: args.resolveArtifactPath
|
|
2945
|
-
});
|
|
2946
|
-
return record;
|
|
2947
|
-
},
|
|
2948
|
-
getSnapshot: (from) => selectSnapshot(args.last, args.snapshots, from),
|
|
2949
|
-
writeArtifactText: (path, text) => {
|
|
2950
|
-
const resolved = args.resolveArtifactPath(path);
|
|
2951
|
-
mkdirSync(dirname(resolved), { recursive: true });
|
|
2952
|
-
writeFileSync(resolved, text, "utf8");
|
|
2953
|
-
},
|
|
2954
|
-
assertGoldenText: (path, text) => {
|
|
2955
|
-
assertGoldenText(args.resolveGoldenPath(path), text, args.updateGoldens);
|
|
2956
|
-
}
|
|
2957
|
-
}, step);
|
|
2958
|
-
if (!result) return args.last;
|
|
2959
|
-
return result;
|
|
2960
|
-
}
|
|
2961
|
-
throw new Error(`unknown type: ${step.type}`);
|
|
2962
|
-
} catch (error) {
|
|
2963
|
-
throw annotateStepError(error, args.stepIndex, step);
|
|
2964
|
-
}
|
|
2965
|
-
}
|
|
2966
|
-
function persistSnapshotRecord(args) {
|
|
2967
|
-
const saveAs = args.saveAs?.trim();
|
|
2968
|
-
if (saveAs) args.snapshots.set(saveAs, args.record);
|
|
2969
|
-
const saveTo = args.saveTo?.trim();
|
|
2970
|
-
if (!saveTo) return;
|
|
2971
|
-
const path = args.resolveArtifactPath(saveTo);
|
|
2972
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
2973
|
-
writeFileSync(path, `${args.record.text}\n`, "utf8");
|
|
2974
|
-
}
|
|
2975
|
-
function annotateStepError(error, stepIndex, step) {
|
|
2976
|
-
const label = step.type === "custom" ? `custom(${step.name})` : step.type;
|
|
2977
|
-
const prefix = `step ${stepIndex + 1} ${label}`;
|
|
2978
|
-
if (error instanceof Error) {
|
|
2979
|
-
if (!error.message.startsWith("step ")) error.message = `${prefix}: ${error.message}`;
|
|
2980
|
-
return error;
|
|
2981
|
-
}
|
|
2982
|
-
return /* @__PURE__ */ new Error(`${prefix}: ${String(error)}`);
|
|
2983
|
-
}
|
|
2984
|
-
function selectSnapshot(last, snapshots, from) {
|
|
2985
|
-
const key = from?.trim() ? from.trim() : "last";
|
|
2986
|
-
if (key === "last") {
|
|
2987
|
-
if (!last) throw new Error("expect: no previous snapshot (from=last)");
|
|
2988
|
-
return last;
|
|
2989
|
-
}
|
|
2990
|
-
const found = snapshots.get(key);
|
|
2991
|
-
if (!found) throw new Error(`expect: unknown snapshot reference: ${key}`);
|
|
2992
|
-
return found;
|
|
2993
|
-
}
|
|
2994
|
-
function assertRecordMatches(record, step, stepIndex) {
|
|
2995
|
-
if (step.equals !== void 0 && record.text !== step.equals) throw new Error(`step ${stepIndex + 1} expect.equals failed`);
|
|
2996
|
-
if (step.contains && step.contains.length > 0) {
|
|
2997
|
-
for (const item of step.contains) if (!record.text.includes(item)) throw new Error(`step ${stepIndex + 1} expect.contains failed: ${JSON.stringify(item)}`);
|
|
2998
|
-
}
|
|
2999
|
-
if (step.notContains && step.notContains.length > 0) {
|
|
3000
|
-
for (const item of step.notContains) if (record.text.includes(item)) throw new Error(`step ${stepIndex + 1} expect.notContains failed: ${JSON.stringify(item)}`);
|
|
3001
|
-
}
|
|
3002
|
-
if (step.regex) {
|
|
3003
|
-
if (!new RegExp(step.regex).test(record.text)) throw new Error(`step ${stepIndex + 1} expect.regex failed: ${JSON.stringify(step.regex)}`);
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
|
-
function assertGoldenText(path, text, update) {
|
|
3007
|
-
if (update) {
|
|
3008
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
3009
|
-
writeFileSync(path, text, "utf8");
|
|
3010
|
-
return;
|
|
3011
|
-
}
|
|
3012
|
-
if (text !== readFileSync(path, "utf8")) throw new Error(`golden mismatch: ${path}`);
|
|
3013
|
-
}
|
|
3014
|
-
async function writeFailureArtifacts(args) {
|
|
3015
|
-
const { session, artifactsDir, scriptName, stepIndex, step, last, error } = args;
|
|
3016
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
3017
|
-
const errorText = err.stack ?? err.message;
|
|
3018
|
-
writeFileSync(join(artifactsDir, "failure.error.txt"), `${errorText}\n`, "utf8");
|
|
3019
|
-
const stepPayload = {
|
|
3020
|
-
script: scriptName,
|
|
3021
|
-
stepIndex: stepIndex >= 0 ? stepIndex + 1 : null,
|
|
3022
|
-
step: step ?? null,
|
|
3023
|
-
last: last ? {
|
|
3024
|
-
kind: last.kind,
|
|
3025
|
-
hash: last.hash
|
|
3026
|
-
} : null
|
|
3027
|
-
};
|
|
3028
|
-
writeFileSync(join(artifactsDir, "failure.step.json"), `${JSON.stringify(stepPayload, null, 2)}\n`, "utf8");
|
|
3029
|
-
let capturedText = void 0;
|
|
3030
|
-
let capturedHash = void 0;
|
|
3031
|
-
try {
|
|
3032
|
-
const captured = await session.snapshotText({
|
|
3033
|
-
scope: "visible",
|
|
3034
|
-
trimRight: true,
|
|
3035
|
-
trimBottom: true,
|
|
3036
|
-
captureFrame: true
|
|
3037
|
-
});
|
|
3038
|
-
capturedText = captured.text;
|
|
3039
|
-
capturedHash = captured.hash;
|
|
3040
|
-
} catch {}
|
|
3041
|
-
const text = capturedText ?? last?.text;
|
|
3042
|
-
const hash = capturedHash ?? last?.hash ?? "unknown";
|
|
3043
|
-
if (text !== void 0) {
|
|
3044
|
-
writeFileSync(join(artifactsDir, "failure.last.txt"), `${text}\n`, "utf8");
|
|
3045
|
-
const view = formatSnapshotView({
|
|
3046
|
-
sessionId: session.id,
|
|
3047
|
-
scope: "visible",
|
|
3048
|
-
hash,
|
|
3049
|
-
lines: text.split("\n"),
|
|
3050
|
-
meta: session.getMeta(),
|
|
3051
|
-
lineNumbers: true
|
|
3052
|
-
});
|
|
3053
|
-
writeFileSync(join(artifactsDir, "failure.last.view.txt"), `${view}\n`, "utf8");
|
|
3054
|
-
}
|
|
3055
|
-
}
|
|
3056
|
-
async function snapshotStep(session, step) {
|
|
3057
|
-
if (step.kind === "grid") {
|
|
3058
|
-
if (step.mask && step.mask.length > 0) throw new Error("snapshot.kind=grid does not support mask (use text/view instead)");
|
|
3059
|
-
const { grid, hash } = await session.snapshotGrid({
|
|
3060
|
-
trimRight: step.trimRight,
|
|
3061
|
-
includeStyles: step.includeStyles,
|
|
3062
|
-
captureFrame: true
|
|
3063
|
-
});
|
|
3064
|
-
return {
|
|
3065
|
-
kind: step.kind,
|
|
3066
|
-
hash,
|
|
3067
|
-
text: JSON.stringify(grid, null, 2)
|
|
3068
|
-
};
|
|
3069
|
-
}
|
|
3070
|
-
if (step.kind === "ansi" || step.kind === "view_ansi") {
|
|
3071
|
-
const { ansi, hash } = await session.snapshotAnsi({
|
|
3072
|
-
scope: step.scope,
|
|
3073
|
-
trimRight: step.trimRight,
|
|
3074
|
-
trimBottom: step.trimBottom ?? true,
|
|
3075
|
-
maxLines: step.maxLines,
|
|
3076
|
-
tailLines: step.tailLines,
|
|
3077
|
-
mask: step.mask
|
|
3078
|
-
});
|
|
3079
|
-
if (step.kind === "ansi") return {
|
|
3080
|
-
kind: step.kind,
|
|
3081
|
-
hash,
|
|
3082
|
-
text: ansi
|
|
3083
|
-
};
|
|
3084
|
-
const lines = ansi.split("\n");
|
|
3085
|
-
const view = formatSnapshotView({
|
|
3086
|
-
sessionId: session.id,
|
|
3087
|
-
scope: step.scope ?? "visible",
|
|
3088
|
-
hash,
|
|
3089
|
-
lines,
|
|
3090
|
-
meta: session.getMeta(),
|
|
3091
|
-
lineNumbers: step.lineNumbers
|
|
3092
|
-
});
|
|
3093
|
-
return {
|
|
3094
|
-
kind: step.kind,
|
|
3095
|
-
hash,
|
|
3096
|
-
text: view
|
|
3097
|
-
};
|
|
3098
|
-
}
|
|
3099
|
-
const { text, hash } = await session.snapshotText({
|
|
3100
|
-
scope: step.scope,
|
|
3101
|
-
trimRight: step.trimRight,
|
|
3102
|
-
trimBottom: step.trimBottom ?? true,
|
|
3103
|
-
maxLines: step.maxLines,
|
|
3104
|
-
tailLines: step.tailLines,
|
|
3105
|
-
captureFrame: true,
|
|
3106
|
-
mask: step.mask
|
|
3107
|
-
});
|
|
3108
|
-
if (step.kind === "text") return {
|
|
3109
|
-
kind: step.kind,
|
|
3110
|
-
hash,
|
|
3111
|
-
text
|
|
3112
|
-
};
|
|
3113
|
-
const lines = text.split("\n");
|
|
3114
|
-
const view = formatSnapshotView({
|
|
3115
|
-
sessionId: session.id,
|
|
3116
|
-
scope: step.scope ?? "visible",
|
|
3117
|
-
hash,
|
|
3118
|
-
lines,
|
|
3119
|
-
meta: session.getMeta(),
|
|
3120
|
-
lineNumbers: step.lineNumbers
|
|
3121
|
-
});
|
|
3122
|
-
return {
|
|
3123
|
-
kind: step.kind,
|
|
3124
|
-
hash,
|
|
3125
|
-
text: view
|
|
3126
|
-
};
|
|
3127
|
-
}
|
|
3128
|
-
async function writeTraceArtifacts(args) {
|
|
3129
|
-
if (!args.saveCast && !args.saveReport) return;
|
|
3130
|
-
const snapshot = await args.session.snapshotCast();
|
|
3131
|
-
if (args.saveCast) {
|
|
3132
|
-
mkdirSync(dirname(args.castPath), { recursive: true });
|
|
3133
|
-
writeFileSync(args.castPath, snapshot.cast, "utf8");
|
|
3134
|
-
}
|
|
3135
|
-
if (args.saveReport) {
|
|
3136
|
-
const artifactHrefs = buildReportArtifactHrefs({
|
|
3137
|
-
reportPath: args.reportPath,
|
|
3138
|
-
castPath: args.saveCast ? args.castPath : null,
|
|
3139
|
-
artifactsDir: args.artifactsDir,
|
|
3140
|
-
includeFailures: args.result?.ok === false
|
|
3141
|
-
});
|
|
3142
|
-
const html = await generateTraceReportHtml(snapshot.cast, {
|
|
3143
|
-
scope: args.reportScope,
|
|
3144
|
-
maxFrames: args.reportMaxFrames,
|
|
3145
|
-
scriptName: args.scriptName,
|
|
3146
|
-
result: args.result,
|
|
3147
|
-
artifacts: artifactHrefs,
|
|
3148
|
-
steps: args.executionSteps
|
|
3149
|
-
});
|
|
3150
|
-
mkdirSync(dirname(args.reportPath), { recursive: true });
|
|
3151
|
-
writeFileSync(args.reportPath, html, "utf8");
|
|
3152
|
-
ensureAsciinemaPlayerAssets(args.reportPath);
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
function buildReportArtifactHrefs(args) {
|
|
3156
|
-
const items = {};
|
|
3157
|
-
if (args.castPath) items.castHref = relativeHref(args.reportPath, args.castPath);
|
|
3158
|
-
if (args.includeFailures) {
|
|
3159
|
-
items.failureErrorHref = relativeHref(args.reportPath, join(args.artifactsDir, "failure.error.txt"));
|
|
3160
|
-
items.failureStepHref = relativeHref(args.reportPath, join(args.artifactsDir, "failure.step.json"));
|
|
3161
|
-
items.failureLastTextHref = relativeHref(args.reportPath, join(args.artifactsDir, "failure.last.txt"));
|
|
3162
|
-
items.failureLastViewHref = relativeHref(args.reportPath, join(args.artifactsDir, "failure.last.view.txt"));
|
|
3163
|
-
}
|
|
3164
|
-
return Object.keys(items).length ? items : void 0;
|
|
3165
|
-
}
|
|
3166
|
-
function relativeHref(fromFile, toFile) {
|
|
3167
|
-
const normalized = relative(dirname(fromFile), toFile).replace(/\\/g, "/");
|
|
3168
|
-
return normalized.startsWith(".") ? normalized : `./${normalized}`;
|
|
3169
|
-
}
|
|
3170
|
-
function formatStepLabel(step) {
|
|
3171
|
-
return step.type === "custom" ? `custom(${step.name})` : step.type;
|
|
3172
|
-
}
|
|
3173
|
-
function formatPublicStepLabel(step) {
|
|
3174
|
-
const showText = envTruthy(process.env.PTYWRIGHT_REPORT_SHOW_STEP_TEXT);
|
|
3175
|
-
if (step.type === "custom") return `custom(${step.name})`;
|
|
3176
|
-
if (step.type === "sendText") {
|
|
3177
|
-
const enter = step.enter !== void 0 ? ` enter=${String(step.enter)}` : "";
|
|
3178
|
-
if (!showText) return `sendText <redacted> (len=${step.text.length}${enter})`;
|
|
3179
|
-
return `sendText "${truncateInline(step.text)}"${enter ? ` (${enter.trim()})` : ""}`;
|
|
3180
|
-
}
|
|
3181
|
-
if (step.type === "pressKey") return `pressKey ${step.key}`;
|
|
3182
|
-
if (step.type === "sendMouse") return `sendMouse ${step.action} (${step.x},${step.y})`;
|
|
3183
|
-
if (step.type === "resize") return `resize ${step.cols}x${step.rows}`;
|
|
3184
|
-
if (step.type === "mark") return step.label ? `mark ${step.label}` : "mark";
|
|
3185
|
-
if (step.type === "sleep") return `sleep ${step.ms}ms`;
|
|
3186
|
-
if (step.type === "waitForText") {
|
|
3187
|
-
if (!showText) return step.text ? "waitForText (text)" : step.regex ? "waitForText (regex)" : "waitForText";
|
|
3188
|
-
if (step.text) return `waitFor "${truncateInline(step.text)}"`;
|
|
3189
|
-
if (step.regex) return `waitFor /${truncateInline(step.regex)}/`;
|
|
3190
|
-
return "waitForText";
|
|
3191
|
-
}
|
|
3192
|
-
if (step.type === "waitForStableScreen") return "waitForStableScreen";
|
|
3193
|
-
if (step.type === "waitForExit") return "waitForExit";
|
|
3194
|
-
if (step.type === "expectMeta") return "expectMeta";
|
|
3195
|
-
if (step.type === "snapshot") return `snapshot ${step.kind}${step.saveAs ? ` as ${step.saveAs}` : ""}`;
|
|
3196
|
-
if (step.type === "expect") {
|
|
3197
|
-
const parts = [];
|
|
3198
|
-
if (step.equals !== void 0) parts.push("equals");
|
|
3199
|
-
if (step.contains?.length) parts.push(`contains(${step.contains.length})`);
|
|
3200
|
-
if (step.notContains?.length) parts.push(`notContains(${step.notContains.length})`);
|
|
3201
|
-
if (step.regex) parts.push("regex");
|
|
3202
|
-
return parts.length ? `expect ${parts.join(",")}` : "expect";
|
|
3203
|
-
}
|
|
3204
|
-
if (step.type === "expectGolden") return `expectGolden ${step.path}`;
|
|
3205
|
-
if (step.type === "assert") {
|
|
3206
|
-
if (!showText) return step.text ? "assert (text)" : step.regex ? "assert (regex)" : "assert";
|
|
3207
|
-
if (step.text) return `assert "${truncateInline(step.text)}"`;
|
|
3208
|
-
if (step.regex) return `assert /${truncateInline(step.regex)}/`;
|
|
3209
|
-
if (step.description) return `assert "${truncateInline(step.description)}"`;
|
|
3210
|
-
return "assert";
|
|
3211
|
-
}
|
|
3212
|
-
if (step.type === "assertSemantic") return "assertSemantic";
|
|
3213
|
-
return assertUnreachableStep(step);
|
|
3214
|
-
}
|
|
3215
|
-
function truncateInline(text, maxChars = 60) {
|
|
3216
|
-
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\\n");
|
|
3217
|
-
if (normalized.length <= maxChars) return normalized;
|
|
3218
|
-
return `${normalized.slice(0, maxChars)}…(+${normalized.length - maxChars})`;
|
|
3219
|
-
}
|
|
3220
|
-
function assertUnreachableStep(_step) {
|
|
3221
|
-
return "unknown";
|
|
3222
|
-
}
|
|
3223
|
-
function writeTestDataArtifact(args) {
|
|
3224
|
-
try {
|
|
3225
|
-
const testId = basename(args.artifactsDir);
|
|
3226
|
-
const outPath = args.resolveArtifactPath("test.data.js");
|
|
3227
|
-
mkdirSync(dirname(outPath), { recursive: true });
|
|
3228
|
-
const steps = args.executionSteps.map((s) => ({
|
|
3229
|
-
index: s.index + 1,
|
|
3230
|
-
type: s.step.type,
|
|
3231
|
-
label: formatPublicStepLabel(s.step),
|
|
3232
|
-
ok: s.ok,
|
|
3233
|
-
durationMs: s.durationMs,
|
|
3234
|
-
error: s.ok ? null : s.error ?? null
|
|
3235
|
-
}));
|
|
3236
|
-
const data = {
|
|
3237
|
-
version: 1,
|
|
3238
|
-
testId,
|
|
3239
|
-
scriptName: args.scriptName,
|
|
3240
|
-
ok: args.ok,
|
|
3241
|
-
error: args.ok ? null : args.error ?? null,
|
|
3242
|
-
stepCount: steps.length,
|
|
3243
|
-
steps,
|
|
3244
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3245
|
-
};
|
|
3246
|
-
const json = JSON.stringify(data).replaceAll("<", "\\u003c");
|
|
3247
|
-
writeFileSync(outPath, `globalThis.__ptywright = globalThis.__ptywright || {};
|
|
3248
|
-
globalThis.__ptywright.tests = globalThis.__ptywright.tests || {};
|
|
3249
|
-
globalThis.__ptywright.tests[${JSON.stringify(testId)}] = ${json};\n`, "utf8");
|
|
3250
|
-
} catch {}
|
|
3251
|
-
}
|
|
3252
|
-
function envTruthy(value) {
|
|
3253
|
-
const v = value?.trim().toLowerCase();
|
|
3254
|
-
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
3255
|
-
}
|
|
3256
3411
|
//#endregion
|
|
3257
|
-
export {
|
|
3412
|
+
export { jsonForHtml as a, scriptSchema as c, resolvePtyBackend as d, escapeHtml as i, SessionManager as l, runScriptFile as n, ensureAsciinemaPlayerAssets as o, generateTraceReportHtml as r, formatSnapshotView as s, runScript as t, createDefaultPtyAdapter as u };
|