ptywright 0.1.1 → 0.3.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.
Files changed (67) hide show
  1. package/README.md +318 -1
  2. package/dist/agent.mjs +2 -0
  3. package/dist/bin/ptywright.mjs +6 -0
  4. package/dist/cli-CfvlbRoZ.mjs +3585 -0
  5. package/dist/cli.mjs +2 -0
  6. package/{src/index.ts → dist/index.mjs} +7 -9
  7. package/dist/mcp.mjs +2 -0
  8. package/dist/pty-cassette.mjs +24 -0
  9. package/dist/pty_like-Cpkh_O9B.mjs +404 -0
  10. package/dist/runner-zApMYWZx.mjs +3257 -0
  11. package/dist/runner-zi0nItvB.mjs +1874 -0
  12. package/dist/script.mjs +2 -0
  13. package/dist/server-BC3yo-dq.mjs +3068 -0
  14. package/dist/session.mjs +2 -0
  15. package/dist/terminal_session-DopC7Xg6.mjs +893 -0
  16. package/package.json +28 -21
  17. package/schemas/ptywright-agent-cassette.schema.json +57 -0
  18. package/schemas/ptywright-agent-check.schema.json +122 -0
  19. package/schemas/ptywright-agent-manifest.schema.json +107 -0
  20. package/schemas/ptywright-agent-promote.schema.json +146 -0
  21. package/schemas/ptywright-agent-replay-summary.schema.json +140 -0
  22. package/schemas/ptywright-agent-run.schema.json +126 -0
  23. package/schemas/ptywright-agent.schema.json +166 -0
  24. package/schemas/ptywright-pty-cassette.schema.json +86 -0
  25. package/schemas/ptywright-script-manifest.schema.json +75 -0
  26. package/schemas/ptywright-script-run-summary.schema.json +114 -0
  27. package/schemas/ptywright-script.schema.json +55 -3
  28. package/bin/ptywright +0 -4
  29. package/src/cli.ts +0 -414
  30. package/src/generator/doc_parser.ts +0 -341
  31. package/src/generator/generate.ts +0 -161
  32. package/src/generator/index.ts +0 -10
  33. package/src/generator/script_generator.ts +0 -209
  34. package/src/generator/step_extractor.ts +0 -397
  35. package/src/mcp/http_server.ts +0 -174
  36. package/src/mcp/script_recording.ts +0 -238
  37. package/src/mcp/server.ts +0 -1348
  38. package/src/pty/bun_pty_adapter.ts +0 -34
  39. package/src/pty/bun_terminal_adapter.ts +0 -149
  40. package/src/pty/pty_adapter.ts +0 -31
  41. package/src/script/dsl.ts +0 -188
  42. package/src/script/module.ts +0 -43
  43. package/src/script/path.ts +0 -151
  44. package/src/script/run.ts +0 -108
  45. package/src/script/run_all.ts +0 -229
  46. package/src/script/runner.ts +0 -983
  47. package/src/script/schema.ts +0 -237
  48. package/src/script/steps/assert_snapshot_equals.ts +0 -21
  49. package/src/script/steps/index.ts +0 -2
  50. package/src/script/suite_report.ts +0 -626
  51. package/src/session/session_manager.ts +0 -145
  52. package/src/session/terminal_session.ts +0 -473
  53. package/src/terminal/ansi.ts +0 -142
  54. package/src/terminal/keys.ts +0 -180
  55. package/src/terminal/mask.ts +0 -70
  56. package/src/terminal/mouse.ts +0 -75
  57. package/src/terminal/snapshot.ts +0 -196
  58. package/src/terminal/style.ts +0 -121
  59. package/src/terminal/view.ts +0 -49
  60. package/src/trace/asciicast.ts +0 -20
  61. package/src/trace/asciinema_player_assets.ts +0 -44
  62. package/src/trace/cast_to_txt.ts +0 -116
  63. package/src/trace/recorder.ts +0 -110
  64. package/src/trace/report.ts +0 -2092
  65. package/src/types.ts +0 -86
  66. package/src/util/hash.ts +0 -8
  67. package/src/util/sleep.ts +0 -5
@@ -1,626 +0,0 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { basename, dirname, join, relative } from "node:path";
3
-
4
- import type { RunScriptPathResult } from "./path";
5
-
6
- export type SuiteReportEntry = {
7
- filePath: string;
8
- durationMs: number;
9
- result: RunScriptPathResult;
10
- };
11
-
12
- export type SuiteRunFailureArtifacts = {
13
- lastTextPath: string;
14
- lastViewPath: string;
15
- stepPath: string;
16
- errorPath: string;
17
- };
18
-
19
- export type SuiteRunSummary = {
20
- version: 1;
21
- ok: boolean;
22
- dir: string;
23
- suiteDir: string;
24
- totalCount: number;
25
- failureCount: number;
26
- durationMs: number;
27
- reportPath: string;
28
- summaryPath: string;
29
- entries: Array<{
30
- filePath: string;
31
- filePathRel: string;
32
- scriptName: string;
33
- ok: boolean;
34
- durationMs: number;
35
- artifactsDir?: string;
36
- reportPath?: string;
37
- castPath?: string;
38
- error?: string;
39
- failureArtifacts?: SuiteRunFailureArtifacts;
40
- }>;
41
- };
42
-
43
- export function writeSuiteReportArtifacts(args: {
44
- dir: string;
45
- suiteDir: string;
46
- durationMs: number;
47
- entries: SuiteReportEntry[];
48
- }): { reportPath: string; summaryPath: string } {
49
- mkdirSync(args.suiteDir, { recursive: true });
50
-
51
- const reportPath = join(args.suiteDir, "index.html");
52
- const summaryPath = join(args.suiteDir, "run.summary.json");
53
-
54
- const failures = args.entries.filter((e) => !e.result.ok);
55
-
56
- const summary: SuiteRunSummary = {
57
- version: 1,
58
- ok: failures.length === 0,
59
- dir: args.dir,
60
- suiteDir: args.suiteDir,
61
- totalCount: args.entries.length,
62
- failureCount: failures.length,
63
- durationMs: args.durationMs,
64
- reportPath,
65
- summaryPath,
66
- entries: args.entries.map((entry) => {
67
- const filePathRel = normalizePath(relative(process.cwd(), entry.filePath));
68
- const scriptName =
69
- entry.result.scriptName ?? basename(entry.filePath).replace(/\.(json|ts)$/i, "");
70
-
71
- const common = {
72
- filePath: entry.filePath,
73
- filePathRel,
74
- scriptName,
75
- ok: entry.result.ok,
76
- durationMs: entry.durationMs,
77
- artifactsDir: entry.result.artifactsDir,
78
- reportPath: entry.result.reportPath,
79
- castPath: entry.result.castPath,
80
- };
81
-
82
- if (entry.result.ok) return common;
83
-
84
- return {
85
- ...common,
86
- ok: false,
87
- error: entry.result.error,
88
- failureArtifacts: entry.result.failureArtifacts as SuiteRunFailureArtifacts | undefined,
89
- };
90
- }),
91
- };
92
-
93
- writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
94
-
95
- const html = renderSuiteReportHtml({ reportPath, summaryPath, summary });
96
- writeFileSync(reportPath, html, "utf8");
97
-
98
- return { reportPath, summaryPath };
99
- }
100
-
101
- function renderSuiteReportHtml(args: {
102
- reportPath: string;
103
- summaryPath: string;
104
- summary: SuiteRunSummary;
105
- }): string {
106
- const title = "ptywright script report";
107
-
108
- const resultLabel = args.summary.ok ? "PASS" : "FAIL";
109
- const resultClass = args.summary.ok ? "pass" : "fail";
110
-
111
- const summaryHref = relativeHref(args.reportPath, args.summaryPath);
112
-
113
- const uiData = {
114
- version: 1,
115
- summary: {
116
- ok: args.summary.ok,
117
- dir: args.summary.dir,
118
- suiteDir: args.summary.suiteDir,
119
- totalCount: args.summary.totalCount,
120
- failureCount: args.summary.failureCount,
121
- durationMs: args.summary.durationMs,
122
- summaryHref,
123
- },
124
- entries: args.summary.entries.map((entry) => {
125
- const reportHref = entry.reportPath ? relativeHref(args.reportPath, entry.reportPath) : null;
126
- const castHref = entry.castPath ? relativeHref(args.reportPath, entry.castPath) : null;
127
- const playHref = reportHref ? `${reportHref}#cast-playback` : null;
128
- const lastHref =
129
- !entry.ok && entry.failureArtifacts?.lastViewPath
130
- ? relativeHref(args.reportPath, entry.failureArtifacts.lastViewPath)
131
- : null;
132
- const errorHref =
133
- !entry.ok && entry.failureArtifacts?.errorPath
134
- ? relativeHref(args.reportPath, entry.failureArtifacts.errorPath)
135
- : null;
136
- const dataKey = entry.artifactsDir ? basename(entry.artifactsDir) : null;
137
- const dataHref =
138
- entry.artifactsDir && dataKey
139
- ? relativeHref(args.reportPath, join(entry.artifactsDir, "test.data.js"))
140
- : null;
141
-
142
- return {
143
- id: entry.filePathRel,
144
- ok: entry.ok,
145
- scriptName: entry.scriptName,
146
- filePathRel: entry.filePathRel,
147
- durationMs: entry.durationMs,
148
- error: entry.ok ? null : (entry.error ?? null),
149
- artifactsDir: entry.artifactsDir ?? null,
150
- dataKey,
151
- hrefs: {
152
- report: reportHref,
153
- play: playHref,
154
- cast: castHref,
155
- last: lastHref,
156
- error: errorHref,
157
- data: dataHref,
158
- },
159
- };
160
- }),
161
- };
162
-
163
- return `<!doctype html>
164
- <html lang="en">
165
- <head>
166
- <meta charset="utf-8" />
167
- <meta name="viewport" content="width=device-width, initial-scale=1" />
168
- <title>${escapeHtml(title)}</title>
169
- <style>
170
- :root {
171
- color-scheme: light dark;
172
- }
173
- body {
174
- margin: 0;
175
- font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto,
176
- Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
177
- line-height: 1.4;
178
- }
179
- a {
180
- color: inherit;
181
- }
182
- header {
183
- padding: 16px;
184
- border-bottom: 1px solid color-mix(in oklab, currentColor 20%, transparent);
185
- }
186
- header h1 {
187
- margin: 0 0 8px 0;
188
- font-size: 18px;
189
- }
190
- header .badges {
191
- display: flex;
192
- gap: 8px;
193
- flex-wrap: wrap;
194
- margin: 6px 0 10px 0;
195
- }
196
- .badge {
197
- display: inline-flex;
198
- align-items: center;
199
- border-radius: 999px;
200
- padding: 2px 10px;
201
- font-size: 12px;
202
- border: 1px solid color-mix(in oklab, currentColor 16%, transparent);
203
- background: color-mix(in oklab, currentColor 6%, transparent);
204
- }
205
- .badge.pass {
206
- background: color-mix(in oklab, #16a34a 18%, transparent);
207
- border-color: color-mix(in oklab, #16a34a 45%, transparent);
208
- }
209
- .badge.fail {
210
- background: color-mix(in oklab, #ef4444 18%, transparent);
211
- border-color: color-mix(in oklab, #ef4444 45%, transparent);
212
- }
213
- header .meta {
214
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
215
- "Liberation Mono", "Courier New", monospace;
216
- font-size: 12px;
217
- opacity: 0.8;
218
- white-space: pre-wrap;
219
- }
220
- main {
221
- display: grid;
222
- grid-template-columns: 360px 1fr;
223
- min-height: calc(100vh - 110px);
224
- }
225
- aside {
226
- border-right: 1px solid color-mix(in oklab, currentColor 14%, transparent);
227
- padding: 12px;
228
- }
229
- section {
230
- padding: 16px;
231
- }
232
- .controls {
233
- display: flex;
234
- gap: 8px;
235
- flex-wrap: wrap;
236
- align-items: center;
237
- margin-bottom: 10px;
238
- }
239
- .input {
240
- width: 100%;
241
- box-sizing: border-box;
242
- padding: 8px 10px;
243
- border-radius: 10px;
244
- border: 1px solid color-mix(in oklab, currentColor 16%, transparent);
245
- background: color-mix(in oklab, currentColor 4%, transparent);
246
- color: inherit;
247
- }
248
- .chip {
249
- cursor: pointer;
250
- user-select: none;
251
- }
252
- .chip[aria-pressed="true"] {
253
- background: color-mix(in oklab, #0ea5e9 18%, transparent);
254
- border-color: color-mix(in oklab, #0ea5e9 45%, transparent);
255
- }
256
- .list {
257
- list-style: none;
258
- padding: 0;
259
- margin: 0;
260
- display: flex;
261
- flex-direction: column;
262
- gap: 6px;
263
- }
264
- .item {
265
- border: 1px solid color-mix(in oklab, currentColor 14%, transparent);
266
- border-radius: 12px;
267
- padding: 10px;
268
- cursor: pointer;
269
- background: color-mix(in oklab, currentColor 2%, transparent);
270
- }
271
- .item:hover {
272
- background: color-mix(in oklab, currentColor 6%, transparent);
273
- }
274
- .item[aria-selected="true"] {
275
- border-color: color-mix(in oklab, #0ea5e9 55%, transparent);
276
- background: color-mix(in oklab, #0ea5e9 10%, transparent);
277
- }
278
- .item .top {
279
- display: flex;
280
- gap: 8px;
281
- align-items: center;
282
- }
283
- .item .name {
284
- font-weight: 600;
285
- overflow: hidden;
286
- text-overflow: ellipsis;
287
- white-space: nowrap;
288
- }
289
- .item .sub {
290
- margin-top: 6px;
291
- display: flex;
292
- gap: 8px;
293
- flex-wrap: wrap;
294
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
295
- "Liberation Mono", "Courier New", monospace;
296
- font-size: 12px;
297
- opacity: 0.8;
298
- }
299
- .mono {
300
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
301
- "Liberation Mono", "Courier New", monospace;
302
- font-size: 12px;
303
- }
304
- .error {
305
- color: color-mix(in oklab, #ef4444 70%, currentColor);
306
- }
307
- .kv {
308
- display: grid;
309
- grid-template-columns: 110px 1fr;
310
- gap: 8px 12px;
311
- margin-top: 12px;
312
- }
313
- .kv .k {
314
- opacity: 0.75;
315
- }
316
- .links {
317
- display: flex;
318
- gap: 10px;
319
- flex-wrap: wrap;
320
- margin-top: 10px;
321
- }
322
- .muted {
323
- opacity: 0.75;
324
- }
325
- @media (max-width: 920px) {
326
- main {
327
- grid-template-columns: 1fr;
328
- }
329
- aside {
330
- border-right: none;
331
- border-bottom: 1px solid color-mix(in oklab, currentColor 14%, transparent);
332
- }
333
- }
334
- </style>
335
- </head>
336
- <body>
337
- <header>
338
- <h1>${escapeHtml(title)}</h1>
339
- <div class="badges">
340
- <span class="badge ${resultClass}">result=${escapeHtml(resultLabel)}</span>
341
- <span class="badge">count=${args.summary.totalCount}</span>
342
- <span class="badge">failures=${args.summary.failureCount}</span>
343
- <span class="badge">duration=${escapeHtml(formatDuration(args.summary.durationMs))}</span>
344
- </div>
345
- <div class="meta">dir=${escapeHtml(args.summary.dir)}
346
- summary=<a href="${escapeHtml(summaryHref)}">run.summary.json</a></div>
347
- </header>
348
- <main>
349
- <aside>
350
- <div class="controls">
351
- <input id="search" class="input mono" placeholder="Search…" autocomplete="off" />
352
- <button id="filterAll" class="badge chip" type="button" aria-pressed="true">all</button>
353
- <button id="filterPass" class="badge chip pass" type="button" aria-pressed="false">pass</button>
354
- <button id="filterFail" class="badge chip fail" type="button" aria-pressed="false">fail</button>
355
- <span id="visibleCount" class="badge">visible=0</span>
356
- </div>
357
- <ol id="list" class="list"></ol>
358
- </aside>
359
- <section>
360
- <div id="details">
361
- <div class="muted">Select a test from the left.</div>
362
- </div>
363
- </section>
364
- </main>
365
- <script id="suiteData" type="application/json">${jsonForHtml(uiData)}</script>
366
- <script>
367
- (function () {
368
- const dataEl = document.getElementById("suiteData");
369
- const listEl = document.getElementById("list");
370
- const detailsEl = document.getElementById("details");
371
- const searchEl = document.getElementById("search");
372
- const visibleCountEl = document.getElementById("visibleCount");
373
- const filterAllEl = document.getElementById("filterAll");
374
- const filterPassEl = document.getElementById("filterPass");
375
- const filterFailEl = document.getElementById("filterFail");
376
- if (!dataEl || !listEl || !detailsEl || !searchEl) return;
377
-
378
- /** @type {{entries: any[]}} */
379
- const raw = JSON.parse(dataEl.textContent || "{}");
380
- const entries = Array.isArray(raw.entries) ? raw.entries : [];
381
- let filter = "all";
382
- let selectedId = null;
383
- const dataLoaders = Object.create(null);
384
-
385
- function setPressed(el, on) {
386
- el.setAttribute("aria-pressed", on ? "true" : "false");
387
- }
388
-
389
- function applyFilter() {
390
- const q = (searchEl.value || "").trim().toLowerCase();
391
- const out = [];
392
- for (const e of entries) {
393
- if (!e) continue;
394
- if (filter === "pass" && !e.ok) continue;
395
- if (filter === "fail" && e.ok) continue;
396
- if (q) {
397
- const hay = (e.scriptName + " " + e.filePathRel).toLowerCase();
398
- if (!hay.includes(q)) continue;
399
- }
400
- out.push(e);
401
- }
402
- renderList(out);
403
- if (visibleCountEl) visibleCountEl.textContent = "visible=" + out.length;
404
- }
405
-
406
- function renderList(items) {
407
- listEl.textContent = "";
408
- for (const e of items) {
409
- const li = document.createElement("li");
410
- li.className = "item";
411
- li.setAttribute("role", "option");
412
- li.dataset.id = e.id;
413
- li.setAttribute("aria-selected", e.id === selectedId ? "true" : "false");
414
-
415
- const top = document.createElement("div");
416
- top.className = "top";
417
- const badge = document.createElement("span");
418
- badge.className = "badge " + (e.ok ? "pass" : "fail");
419
- badge.textContent = e.ok ? "PASS" : "FAIL";
420
- const name = document.createElement("div");
421
- name.className = "name";
422
- name.textContent = e.scriptName;
423
- top.appendChild(badge);
424
- top.appendChild(name);
425
-
426
- const sub = document.createElement("div");
427
- sub.className = "sub";
428
- const file = document.createElement("span");
429
- file.textContent = e.filePathRel;
430
- const dur = document.createElement("span");
431
- dur.textContent = "dur=" + (e.durationMs < 1000 ? e.durationMs + "ms" : (e.durationMs / 1000).toFixed(2) + "s");
432
- sub.appendChild(file);
433
- sub.appendChild(dur);
434
-
435
- li.appendChild(top);
436
- li.appendChild(sub);
437
- li.addEventListener("click", function () {
438
- selectedId = e.id;
439
- applyFilter();
440
- renderDetails(e);
441
- });
442
- listEl.appendChild(li);
443
- }
444
- }
445
-
446
- function linkHtml(href, label) {
447
- if (!href) return "";
448
- return '<a class="mono" href="' + href.replaceAll('"', "&quot;") + '">' + label + "</a>";
449
- }
450
-
451
- function escapeText(s) {
452
- return (s || "")
453
- .replaceAll("&", "&amp;")
454
- .replaceAll("<", "&lt;")
455
- .replaceAll(">", "&gt;");
456
- }
457
-
458
- function getTestData(e) {
459
- const store = globalThis.__ptywright && globalThis.__ptywright.tests;
460
- if (!store || !e || !e.dataKey) return null;
461
- return store[e.dataKey] || null;
462
- }
463
-
464
- function ensureTestDataLoaded(e, cb) {
465
- if (!e || !e.hrefs || !e.hrefs.data || !e.dataKey) return cb(null);
466
- const existing = getTestData(e);
467
- if (existing) return cb(existing);
468
-
469
- if (dataLoaders[e.dataKey]) {
470
- dataLoaders[e.dataKey].push(cb);
471
- return;
472
- }
473
- dataLoaders[e.dataKey] = [cb];
474
-
475
- const script = document.createElement("script");
476
- script.src = e.hrefs.data;
477
- script.async = true;
478
- script.onload = function () {
479
- const loaded = getTestData(e);
480
- const cbs = dataLoaders[e.dataKey] || [];
481
- delete dataLoaders[e.dataKey];
482
- for (const fn of cbs) fn(loaded);
483
- };
484
- script.onerror = function () {
485
- const cbs = dataLoaders[e.dataKey] || [];
486
- delete dataLoaders[e.dataKey];
487
- for (const fn of cbs) fn(null);
488
- };
489
- document.head.appendChild(script);
490
- }
491
-
492
- function renderDetails(e) {
493
- const links = [
494
- linkHtml(e.hrefs && e.hrefs.report, "report"),
495
- linkHtml(e.hrefs && e.hrefs.play, "play"),
496
- linkHtml(e.hrefs && e.hrefs.cast, "cast"),
497
- linkHtml(e.hrefs && e.hrefs.last, "last"),
498
- linkHtml(e.hrefs && e.hrefs.error, "error"),
499
- ].filter(Boolean);
500
-
501
- const data = getTestData(e);
502
- const stepsHtml = (() => {
503
- if (data && Array.isArray(data.steps)) {
504
- const rows = data.steps
505
- .map(function (s) {
506
- const badge = '<span class="badge ' + (s.ok ? "pass" : "fail") + '">' + (s.ok ? "PASS" : "FAIL") + "</span>";
507
- const dur = typeof s.durationMs === "number"
508
- ? (s.durationMs < 1000 ? s.durationMs + "ms" : (s.durationMs / 1000).toFixed(2) + "s")
509
- : "";
510
- const err = !s.ok && s.error ? '<div class="mono error" style="margin-top: 4px;">' + escapeText(s.error) + "</div>" : "";
511
- return '<div class="item" style="cursor: default;">' +
512
- '<div class="top">' + badge +
513
- '<div class="name mono" style="font-weight: 600;">' + escapeText(s.label || s.type || "") + "</div>" +
514
- "</div>" +
515
- '<div class="sub"><span>step=' + escapeText(String(s.index)) + "</span><span>dur=" + escapeText(dur) + "</span></div>" +
516
- err +
517
- "</div>";
518
- })
519
- .join("");
520
- return '<h3 style="margin: 16px 0 8px 0;">Steps</h3>' +
521
- '<div class="muted mono">count=' + escapeText(String(data.stepCount || data.steps.length)) + "</div>" +
522
- '<div style="margin-top: 10px; display: flex; flex-direction: column; gap: 8px;">' + rows + "</div>";
523
- }
524
-
525
- if (e.hrefs && e.hrefs.data && e.dataKey) {
526
- return '<h3 style="margin: 16px 0 8px 0;">Steps</h3>' +
527
- '<div class="muted">Step details are available. Click to load.</div>' +
528
- '<div style="margin-top: 10px;">' +
529
- '<button id="loadSteps" class="badge chip" type="button">load steps</button>' +
530
- "</div>";
531
- }
532
-
533
- return "";
534
- })();
535
-
536
- detailsEl.innerHTML =
537
- '<div class="badges">' +
538
- '<span class="badge ' + (e.ok ? "pass" : "fail") + '">status=' + (e.ok ? "PASS" : "FAIL") + "</span>" +
539
- '<span class="badge">duration=' + (e.durationMs < 1000 ? e.durationMs + "ms" : (e.durationMs / 1000).toFixed(2) + "s") + "</span>" +
540
- "</div>" +
541
- '<h2 style="margin: 10px 0 6px 0;">' + escapeText(e.scriptName) + "</h2>" +
542
- '<div class="kv mono">' +
543
- '<div class="k">file</div><div class="v">' + escapeText(e.filePathRel) + "</div>" +
544
- '<div class="k">artifacts</div><div class="v">' +
545
- (e.artifactsDir
546
- ? escapeText(e.artifactsDir)
547
- : '<span class="muted">(none)</span>') +
548
- "</div>" +
549
- "</div>" +
550
- (links.length ? '<div class="links">' + links.join(" ") + "</div>" : "") +
551
- (!e.ok && e.error ? '<pre class="mono error" style="margin-top: 12px; white-space: pre-wrap;">' + escapeText(e.error) + "</pre>" : "") +
552
- stepsHtml;
553
-
554
- const loadBtn = document.getElementById("loadSteps");
555
- if (loadBtn) {
556
- loadBtn.addEventListener("click", function () {
557
- ensureTestDataLoaded(e, function () {
558
- renderDetails(e);
559
- });
560
- });
561
- }
562
- }
563
-
564
- filterAllEl.addEventListener("click", function () {
565
- filter = "all";
566
- setPressed(filterAllEl, true);
567
- setPressed(filterPassEl, false);
568
- setPressed(filterFailEl, false);
569
- applyFilter();
570
- });
571
- filterPassEl.addEventListener("click", function () {
572
- filter = "pass";
573
- setPressed(filterAllEl, false);
574
- setPressed(filterPassEl, true);
575
- setPressed(filterFailEl, false);
576
- applyFilter();
577
- });
578
- filterFailEl.addEventListener("click", function () {
579
- filter = "fail";
580
- setPressed(filterAllEl, false);
581
- setPressed(filterPassEl, false);
582
- setPressed(filterFailEl, true);
583
- applyFilter();
584
- });
585
- searchEl.addEventListener("input", applyFilter);
586
-
587
- applyFilter();
588
- if (entries[0]) {
589
- selectedId = entries[0].id;
590
- renderDetails(entries[0]);
591
- applyFilter();
592
- }
593
- })();
594
- </script>
595
- </body>
596
- </html>`;
597
- }
598
-
599
- function jsonForHtml(data: unknown): string {
600
- return JSON.stringify(data).replaceAll("<", "\\u003c");
601
- }
602
-
603
- function formatDuration(ms: number): string {
604
- const safe = Math.max(0, Math.trunc(ms));
605
- if (safe < 1000) return `${safe}ms`;
606
- return `${(safe / 1000).toFixed(2)}s`;
607
- }
608
-
609
- function relativeHref(fromFile: string, toFile: string): string {
610
- const rel = relative(dirname(fromFile), toFile);
611
- const normalized = normalizePath(rel);
612
- return normalized.startsWith(".") ? normalized : `./${normalized}`;
613
- }
614
-
615
- function normalizePath(path: string): string {
616
- return path.replace(/\\/g, "/");
617
- }
618
-
619
- function escapeHtml(text: string): string {
620
- return text
621
- .replaceAll("&", "&amp;")
622
- .replaceAll("<", "&lt;")
623
- .replaceAll(">", "&gt;")
624
- .replaceAll('"', "&quot;")
625
- .replaceAll("'", "&#39;");
626
- }