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