ptywright 0.1.1 → 0.2.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 +287 -1
- package/dist/agent.mjs +2 -0
- package/dist/bin/ptywright.mjs +6 -0
- package/dist/cli-DIUx2w6X.mjs +3587 -0
- package/dist/cli.mjs +2 -0
- package/{src/index.ts → dist/index.mjs} +7 -9
- package/dist/mcp.mjs +2 -0
- package/dist/pty-cassette.mjs +24 -0
- package/dist/pty_like-Cpkh_O9B.mjs +404 -0
- package/dist/runner-DzZlFrt1.mjs +1897 -0
- package/dist/runner-zApMYWZx.mjs +3257 -0
- package/dist/script.mjs +2 -0
- package/dist/server-VHuEWWj_.mjs +3068 -0
- package/dist/session.mjs +2 -0
- package/dist/terminal_session-DopC7Xg6.mjs +893 -0
- package/package.json +28 -21
- package/schemas/ptywright-agent-cassette.schema.json +57 -0
- package/schemas/ptywright-agent-check.schema.json +122 -0
- package/schemas/ptywright-agent-manifest.schema.json +107 -0
- package/schemas/ptywright-agent-promote.schema.json +146 -0
- package/schemas/ptywright-agent-replay-summary.schema.json +140 -0
- package/schemas/ptywright-agent-run.schema.json +126 -0
- package/schemas/ptywright-agent.schema.json +182 -0
- package/schemas/ptywright-pty-cassette.schema.json +86 -0
- package/schemas/ptywright-script-manifest.schema.json +75 -0
- package/schemas/ptywright-script-run-summary.schema.json +114 -0
- package/schemas/ptywright-script.schema.json +55 -3
- package/bin/ptywright +0 -4
- package/src/cli.ts +0 -414
- package/src/generator/doc_parser.ts +0 -341
- package/src/generator/generate.ts +0 -161
- package/src/generator/index.ts +0 -10
- package/src/generator/script_generator.ts +0 -209
- package/src/generator/step_extractor.ts +0 -397
- package/src/mcp/http_server.ts +0 -174
- package/src/mcp/script_recording.ts +0 -238
- package/src/mcp/server.ts +0 -1348
- package/src/pty/bun_pty_adapter.ts +0 -34
- package/src/pty/bun_terminal_adapter.ts +0 -149
- package/src/pty/pty_adapter.ts +0 -31
- package/src/script/dsl.ts +0 -188
- package/src/script/module.ts +0 -43
- package/src/script/path.ts +0 -151
- package/src/script/run.ts +0 -108
- package/src/script/run_all.ts +0 -229
- package/src/script/runner.ts +0 -983
- package/src/script/schema.ts +0 -237
- package/src/script/steps/assert_snapshot_equals.ts +0 -21
- package/src/script/steps/index.ts +0 -2
- package/src/script/suite_report.ts +0 -626
- package/src/session/session_manager.ts +0 -145
- package/src/session/terminal_session.ts +0 -473
- package/src/terminal/ansi.ts +0 -142
- package/src/terminal/keys.ts +0 -180
- package/src/terminal/mask.ts +0 -70
- package/src/terminal/mouse.ts +0 -75
- package/src/terminal/snapshot.ts +0 -196
- package/src/terminal/style.ts +0 -121
- package/src/terminal/view.ts +0 -49
- package/src/trace/asciicast.ts +0 -20
- package/src/trace/asciinema_player_assets.ts +0 -44
- package/src/trace/cast_to_txt.ts +0 -116
- package/src/trace/recorder.ts +0 -110
- package/src/trace/report.ts +0 -2092
- package/src/types.ts +0 -86
- package/src/util/hash.ts +0 -8
- 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('"', """) + '">' + label + "</a>";
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function escapeText(s) {
|
|
452
|
-
return (s || "")
|
|
453
|
-
.replaceAll("&", "&")
|
|
454
|
-
.replaceAll("<", "<")
|
|
455
|
-
.replaceAll(">", ">");
|
|
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("&", "&")
|
|
622
|
-
.replaceAll("<", "<")
|
|
623
|
-
.replaceAll(">", ">")
|
|
624
|
-
.replaceAll('"', """)
|
|
625
|
-
.replaceAll("'", "'");
|
|
626
|
-
}
|