@schwabyio/ata 0.0.0-stage → 0.20.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/dist/ata.js ADDED
@@ -0,0 +1,1938 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __ataCreateRequire } from 'node:module';
3
+ const require = __ataCreateRequire(import.meta.url);
4
+ import {
5
+ DEFAULT_SETTINGS,
6
+ DEFAULT_SOURCE,
7
+ ProjectError,
8
+ SETTING_KEYS,
9
+ SettingsError,
10
+ UsageError,
11
+ colorWanted,
12
+ envNameOf,
13
+ failureDetails,
14
+ field,
15
+ idOf,
16
+ isDirectory,
17
+ loadProject,
18
+ nameCell,
19
+ nameWidthFor,
20
+ paint,
21
+ parseArgs,
22
+ rule,
23
+ selectCollections,
24
+ stepCountOf,
25
+ summary,
26
+ tableHeader,
27
+ tableRow,
28
+ tableWidth,
29
+ tally,
30
+ totalsOf
31
+ } from "./chunks/chunk-I3XKICH2.js";
32
+ import {
33
+ CALLOUTS,
34
+ COLLECTIONS_DIR,
35
+ MARK_GLYPH,
36
+ PROJECT_FILE,
37
+ RULES_FILE,
38
+ RulesError,
39
+ TEARDOWN_AFTER_STOP_MS,
40
+ bodyAsObject,
41
+ buildChecks,
42
+ canonical,
43
+ checkedBody,
44
+ checkedDiffersFromRaw,
45
+ codeTokens,
46
+ formatPath,
47
+ isInside,
48
+ iterationName,
49
+ lintProject,
50
+ loadRules,
51
+ markCode,
52
+ markHeaders,
53
+ markInlineCode,
54
+ markLines,
55
+ markStatus,
56
+ markText,
57
+ parseMarkdown,
58
+ parsePath,
59
+ readProject,
60
+ referenceProblems,
61
+ resolvePath,
62
+ resultName,
63
+ ruleDoc,
64
+ ruleValueText,
65
+ samePath,
66
+ sortArraysBy,
67
+ toJsonLines,
68
+ unattempted,
69
+ waitText,
70
+ wireNote
71
+ } from "./chunks/chunk-IY7FUPUW.js";
72
+
73
+ // src/main.ts
74
+ import fs from "node:fs/promises";
75
+ import path2 from "node:path";
76
+
77
+ // src/pool.ts
78
+ import { Worker } from "node:worker_threads";
79
+ var REPORT_MS = 5e3;
80
+ function workerRunner(workerUrl2, timeoutMs) {
81
+ return (job) => new Promise((resolve) => {
82
+ const worker = new Worker(workerUrl2, { workerData: job });
83
+ const timedOut = `Timed out after ${timeoutMs} ms (timeoutCollection)`;
84
+ let late = false;
85
+ let settled = false;
86
+ let gone;
87
+ const settle = (outcome) => {
88
+ if (settled) return;
89
+ settled = true;
90
+ clearTimeout(timer);
91
+ clearTimeout(gone);
92
+ resolve(outcome);
93
+ void worker.terminate();
94
+ };
95
+ const timer = setTimeout(() => {
96
+ late = true;
97
+ worker.postMessage({ type: "timeout" });
98
+ gone = setTimeout(
99
+ () => settle({ ok: false, message: timedOut }),
100
+ TEARDOWN_AFTER_STOP_MS + REPORT_MS
101
+ );
102
+ }, timeoutMs);
103
+ worker.once(
104
+ "message",
105
+ (outcome) => settle(late ? timedOutAfter(timedOut, outcome) : outcome)
106
+ );
107
+ worker.once("error", (error) => settle({ ok: false, message: error.message }));
108
+ worker.once(
109
+ "exit",
110
+ (code) => settle({ ok: false, message: `The run stopped before it finished (exit code ${code})` })
111
+ );
112
+ });
113
+ }
114
+ function timedOutAfter(message, outcome) {
115
+ const teardown = outcome.ok ? outcome.summary.results.filter((r) => r.stage === "teardown") : [];
116
+ if (teardown.length === 0) return { ok: false, message };
117
+ const passed = teardown.filter((result) => result.status === "pass").length;
118
+ return {
119
+ ok: false,
120
+ message: `${message}. Teardown ran after it: ${passed} of ${teardown.length} passed.`
121
+ };
122
+ }
123
+ async function runPool(items, limit, run2) {
124
+ let next = 0;
125
+ const lane = async () => {
126
+ while (next < items.length) await run2(items[next++]);
127
+ };
128
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, lane));
129
+ }
130
+
131
+ // src/rules.ts
132
+ import path from "node:path";
133
+
134
+ // src/json.ts
135
+ var JSON_FORMAT_VERSION = 1;
136
+ var BODY_LIMIT = 256 * 1024;
137
+ function jsonReport(run2, reports) {
138
+ const totals = totalsOf(run2.collections.map((c) => c.tally));
139
+ const failed = totals.collections.failed;
140
+ return {
141
+ formatVersion: JSON_FORMAT_VERSION,
142
+ tool: { name: "ata", version: run2.version },
143
+ project: { name: run2.project, root: run2.root },
144
+ run: {
145
+ result: failed === 0 ? "passed" : "failed",
146
+ startedAt: new Date(run2.startedAt).toISOString(),
147
+ durationMs: Math.round(run2.durationMs),
148
+ environment: run2.environment,
149
+ /** The feature flag values the run used, where each came from, and the command run (SPEC.md §2.9). */
150
+ flags: {
151
+ values: run2.flags ?? {},
152
+ sources: run2.flagSources ?? {},
153
+ command: run2.flagCommand ?? null
154
+ },
155
+ settings: {
156
+ limitConcurrency: run2.concurrency,
157
+ timeoutCollection: run2.timeoutCollection,
158
+ bail: run2.bail,
159
+ tags: run2.tags,
160
+ notTags: run2.notTags ?? []
161
+ }
162
+ },
163
+ totals,
164
+ leftOut: {
165
+ excluded: run2.excluded,
166
+ excludedSteps: run2.excludedSteps ?? [],
167
+ untagged: run2.untagged
168
+ },
169
+ reports,
170
+ collections: run2.collections.map(collectionOf)
171
+ };
172
+ }
173
+ function collectionOf(c) {
174
+ const { outcome, tally: tally2 } = c;
175
+ return {
176
+ id: c.id,
177
+ file: c.file,
178
+ status: tally2.skipped ? "skipped" : tally2.passed ? "passed" : "failed",
179
+ startedAt: new Date(c.startedAt).toISOString(),
180
+ durationMs: Math.round(c.durationMs),
181
+ counts: { steps: tally2.steps, assertions: tally2.assertions },
182
+ /** Why it did not run at all, or null when it ran. */
183
+ error: outcome.ok ? null : outcome.message,
184
+ /** The data file that ran it once per row (SPEC.md §2.8), or null. */
185
+ dataFile: outcome.ok ? outcome.data : null,
186
+ /** Steps a bail left unrun. */
187
+ notRun: outcome.ok ? unattempted(outcome.summary) : tally2.steps.total,
188
+ steps: outcome.ok ? outcome.summary.results.map(
189
+ (result, i) => stepOf(result, outcome.steps[i] ?? { index: i, line: null })
190
+ ) : []
191
+ };
192
+ }
193
+ function stepOf(result, ref) {
194
+ const truncated = {};
195
+ const clip2 = (field2, text) => {
196
+ if (text === null || text.length <= BODY_LIMIT) return text;
197
+ truncated[field2] = text.length;
198
+ return text.slice(0, BODY_LIMIT);
199
+ };
200
+ const request = { ...result.request, body: clip2("request.body", result.request.body) };
201
+ const response = result.response ? { ...result.response, body: clip2("response.body", result.response.body) ?? "" } : null;
202
+ return {
203
+ /** The step's place in the file's `steps`, from 0, and its line, from 1. */
204
+ step: { index: ref.index, line: ref.line },
205
+ /** The data file row it ran with, from 1, or null without a data file. */
206
+ iteration: ref.iteration ?? null,
207
+ ...result,
208
+ request,
209
+ response,
210
+ /** Fields cut short, with their full length in characters. */
211
+ ...Object.keys(truncated).length > 0 ? { truncated } : {}
212
+ };
213
+ }
214
+ function jsonListing(options) {
215
+ return {
216
+ formatVersion: JSON_FORMAT_VERSION,
217
+ project: options.project,
218
+ environments: options.environments,
219
+ environmentType: options.environmentType,
220
+ settings: { values: options.settings.values, sources: options.settings.sources },
221
+ flags: { values: options.flags.values, sources: options.flags.sources },
222
+ tags: options.tags,
223
+ notTags: options.notTags ?? [],
224
+ collections: options.targets.map((target) => ({
225
+ id: target.id,
226
+ file: fileOf(target.collection),
227
+ directory: target.collection.directory,
228
+ steps: target.steps ?? target.collection.doc.steps.map((_, i) => i),
229
+ stepCount: target.collection.doc.steps.length,
230
+ dataFile: target.collection.dataFile ? {
231
+ file: `collections/${target.collection.dataFile.relativePath}`,
232
+ rows: target.collection.dataFile.rows
233
+ } : null,
234
+ tags: target.collection.doc.tags ?? [],
235
+ broken: target.broken
236
+ })),
237
+ leftOut: {
238
+ excluded: options.excluded,
239
+ excludedSteps: options.excludedSteps,
240
+ untagged: options.untagged
241
+ },
242
+ problems: options.problems
243
+ };
244
+ }
245
+ var fileOf = (collection) => `collections/${collection.relativePath}`;
246
+ var jsonError = (message, exitCode) => ({
247
+ formatVersion: JSON_FORMAT_VERSION,
248
+ error: { message, exitCode }
249
+ });
250
+
251
+ // src/rules.ts
252
+ async function openRules(root) {
253
+ const info = await readProject(root);
254
+ if (info.source === null && !await isDirectory(path.join(root, COLLECTIONS_DIR))) {
255
+ throw new ProjectError(
256
+ `There is no ${COLLECTIONS_DIR}/ or ${PROJECT_FILE} in ${root}.
257
+ ata lint and ata rules run from a project folder, or a global project's.`
258
+ );
259
+ }
260
+ if (info.problems.length > 0) {
261
+ throw new ProjectError(
262
+ `${PROJECT_FILE} must be fixed first, to know which rules apply:
263
+ ` + info.problems.map((problem) => ` ${problem.message}`).join("\n")
264
+ );
265
+ }
266
+ return {
267
+ root,
268
+ name: info.doc?.name ?? path.basename(root),
269
+ uses: info.global?.uses ?? null,
270
+ global: info.global ? { root: info.global.root } : null,
271
+ loaded: await loadRules(root, info.global)
272
+ };
273
+ }
274
+ function rulesLine(project, p) {
275
+ if (project.loaded.files.length > 0) return field("Rules from", project.loaded.files.join(", "));
276
+ const where = project.uses ? `no ${RULES_FILE} here or in ${project.uses}` : `no ${RULES_FILE} here`;
277
+ return field("Rules", p.dim(`none: ${where}`));
278
+ }
279
+ var PADDED_VALUE = 32;
280
+ async function rulesCommand(root, { out, emit, p }) {
281
+ const project = await openRules(root);
282
+ const { settings } = project.loaded;
283
+ if (emit) {
284
+ emit({
285
+ formatVersion: JSON_FORMAT_VERSION,
286
+ project: { name: project.name, root: project.root },
287
+ files: project.loaded.files,
288
+ rules: settings.map((setting) => ({ ...setting, doc: ruleDoc(setting.rule) })),
289
+ guides: project.loaded.guides
290
+ });
291
+ return;
292
+ }
293
+ out(field("Project", `${project.name} ${p.dim(project.root)}`));
294
+ out(rulesLine(project, p));
295
+ if (settings.length > 0) {
296
+ out("");
297
+ const ruleWidth = Math.max(...settings.map((s) => s.rule.length));
298
+ const widths = settings.map((s) => ruleValueText(s.value).length);
299
+ const valueWidth = Math.max(0, ...widths.filter((width) => width <= PADDED_VALUE));
300
+ for (const setting of settings) {
301
+ const value = ruleValueText(setting.value).padEnd(valueWidth);
302
+ out(
303
+ `${p.bold(setting.rule.padEnd(ruleWidth))} ${setting.on ? value : p.dim(value)} ${p.dim(setting.source)}`
304
+ );
305
+ out(` ${ruleDoc(setting.rule)}`);
306
+ }
307
+ }
308
+ for (const guide of project.loaded.guides) {
309
+ out("");
310
+ out(`${p.bold("Guide")} ${p.dim(`(${guide.source})`)}`);
311
+ for (const line of guide.text.trimEnd().split("\n")) out(line === "" ? "" : ` ${line}`);
312
+ }
313
+ }
314
+ var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
315
+ function checkedLine(checked) {
316
+ return [
317
+ plural(checked.collections, "collection", "collections"),
318
+ plural(checked.requests, "reusable requests file", "reusable requests files"),
319
+ plural(checked.bases, "base collection", "base collections"),
320
+ plural(checked.endpoints, "endpoints file", "endpoints files")
321
+ ].join(", ");
322
+ }
323
+ function findingLine(finding, p) {
324
+ const place = finding.line === null ? finding.file : `${finding.file}:${finding.line}`;
325
+ if (finding.rule === null) return `${place} ${p.red(finding.message)}`;
326
+ return `${place} ${finding.message} ${p.dim(`[${finding.rule} from ${finding.source}]`)}`;
327
+ }
328
+ async function lintCommand(root, { out, emit, p }) {
329
+ const project = await openRules(root);
330
+ const { loaded } = project;
331
+ const result = loaded.settings.some((setting) => setting.on) ? await lintProject(root, loaded, project.global) : { findings: [], checked: { collections: 0, requests: 0, bases: 0, endpoints: 0 } };
332
+ const found = result.findings.length > 0;
333
+ if (emit) {
334
+ emit({
335
+ formatVersion: JSON_FORMAT_VERSION,
336
+ project: { name: project.name, root: project.root },
337
+ files: loaded.files,
338
+ rules: loaded.rules,
339
+ checked: result.checked,
340
+ findings: result.findings
341
+ });
342
+ return found;
343
+ }
344
+ out(field("Project", `${project.name} ${p.dim(project.root)}`));
345
+ out(rulesLine(project, p));
346
+ const inEffect = loaded.settings.filter((setting) => setting.on).length;
347
+ if (inEffect === 0) {
348
+ out(p.dim("Nothing to check."));
349
+ return false;
350
+ }
351
+ out(field("In effect", `${plural(inEffect, "rule", "rules")} ${p.dim("(ata rules lists them)")}`));
352
+ out(field("Checked", checkedLine(result.checked)));
353
+ out("");
354
+ for (const finding of result.findings) out(findingLine(finding, p));
355
+ if (found) {
356
+ const files = new Set(result.findings.map((finding) => finding.file)).size;
357
+ out("");
358
+ out(
359
+ p.red(
360
+ `${plural(result.findings.length, "finding", "findings")} in ${plural(files, "file", "files")}`
361
+ )
362
+ );
363
+ } else {
364
+ out(p.green("No findings: every file follows the rules."));
365
+ }
366
+ return found;
367
+ }
368
+
369
+ // src/html.ts
370
+ var BODY_LIMIT2 = 256 * 1024;
371
+ var BINARY_LIMIT = 1024 * 1024;
372
+ var REPO_URL = "https://github.com/schwabyio/accelerator";
373
+ var SUMMARY_PAGE = "summary.html";
374
+ function htmlReport(run2) {
375
+ return [
376
+ { path: SUMMARY_PAGE, html: summaryPage(run2) },
377
+ ...run2.collections.map((c) => ({ path: pageOf(c.id), html: collectionPage(run2, c) }))
378
+ ];
379
+ }
380
+ var pageOf = (id) => `${id}.html`;
381
+ var flagList = (flags) => {
382
+ const entries = Object.entries(flags);
383
+ return entries.length > 0 ? entries.map(([name, value]) => `${name}=${value}`).join(", ") : "none";
384
+ };
385
+ var collectionStatus = (t) => t.skipped ? "skipped" : t.passed ? "passed" : "failed";
386
+ function summaryPage(run2) {
387
+ const tallies = run2.collections.map((c) => c.tally);
388
+ const sum = (pick) => tallies.reduce((n, t) => n + pick(t), 0);
389
+ const statuses = tallies.map(collectionStatus);
390
+ const count = (status) => statuses.filter((s) => s === status).length;
391
+ const passed = count("failed") === 0;
392
+ const none = '<span class="dim">none</span>';
393
+ const leftOut = (n, word = "collection") => `<a href="#not-run">${plural2(n, word)} \u2193</a>`;
394
+ const excludedSteps = run2.excludedSteps ?? [];
395
+ const settings = [
396
+ ["Environment", run2.environment ? escape(run2.environment) : none],
397
+ ["Concurrency Limit", String(run2.concurrency)],
398
+ ["Collection Timeout", timeoutText(run2.timeoutCollection)],
399
+ ["Tags", run2.tags.length > 0 ? escape(run2.tags.join(", ")) : none],
400
+ ...run2.notTags?.length ? [["Not Tags", escape(run2.notTags.join(", "))]] : [],
401
+ ["Feature Flags", flagChips(run2.flags ?? {}) || none],
402
+ ...run2.flagCommand ? [["Flag Command", `<code>${escape(run2.flagCommand)}</code>`]] : [],
403
+ ...run2.bail ? [["Bail", "on"]] : [],
404
+ ["Excluded", run2.excluded.length > 0 ? leftOut(run2.excluded.length) : none],
405
+ ...excludedSteps.length > 0 ? [["Excluded Steps", leftOut(excludedSteps.length, "step")]] : [],
406
+ ...run2.untagged > 0 ? [["Left Out By Tags", leftOut(run2.untagged)]] : []
407
+ ];
408
+ const rows = run2.collections.map((c, i) => {
409
+ const t = c.tally;
410
+ const status = statuses[i];
411
+ return `<tr class="s-${status}" data-status="${status}" data-search="${escape(c.id.toLowerCase())}"><td class="r n">${i + 1}</td><td class="name"><a href="${escape(encodeURI(pageOf(c.id)))}" target="_blank" rel="noopener">${breakable(c.id)}</a></td><td class="r">${duration(c.durationMs)}</td><td class="r gs">${t.steps.total}</td><td class="r">${num(t.steps.passed)}</td><td class="r">${num(t.steps.failed, true)}</td><td class="r">${num(t.steps.errored, true)}</td><td class="r">${num(t.steps.skipped)}</td><td class="r gs">${t.assertions.total}</td><td class="r">${num(t.assertions.passed)}</td><td class="r">${num(t.assertions.failed, true)}</td><td class="gs">${pill(status)}</td></tr>`;
412
+ }).join("\n");
413
+ const notRun = run2.excluded.length > 0 || excludedSteps.length > 0 || run2.untagged > 0 ? card(
414
+ "Not Run",
415
+ [
416
+ run2.excluded.length > 0 ? `<h3 class="group-h">Excluded <span class="dim">\xB7 ${run2.excluded.length} \xB7 <code>exclude: true</code> in the collection file</span></h3>
417
+ <ul class="excluded">${run2.excluded.map((id) => `<li>${escape(id)}</li>`).join("")}</ul>` : "",
418
+ excludedSteps.length > 0 ? `<h3 class="group-h">Excluded steps <span class="dim">\xB7 ${excludedSteps.length} \xB7 <code>exclude: true</code> on the step</span></h3>
419
+ <ul class="excluded">${excludedSteps.map((step) => `<li>${escape(step.id)} \u203A ${escape(step.name)}</li>`).join("")}</ul>` : "",
420
+ run2.untagged > 0 ? `<h3 class="group-h">Left out by tags <span class="dim">\xB7 ${plural2(run2.untagged, "collection")}</span></h3>` : ""
421
+ ].join("\n"),
422
+ { id: "not-run" }
423
+ ) : "";
424
+ return page({
425
+ title: `${passed ? "PASSED" : "FAILED"} \xB7 ${run2.project} \xB7 ata Summary Results`,
426
+ run: run2,
427
+ body: `${pageHead(passed ? "passed" : "failed", `${escape(run2.project)} <span class="kind">\xB7 Summary Results</span>`, run2.startedAt)}
428
+
429
+ ${card("Settings", `<dl class="pairs">
430
+ ${settings.map(([label, value]) => `<div><dt>${label}</dt><dd>${value}</dd></div>`).join("\n")}
431
+ </dl>`)}
432
+
433
+ ${card(
434
+ "Summary Stats",
435
+ `<div class="stats">
436
+ ${ringStat("Collections", [
437
+ ["passed", count("passed")],
438
+ ["failed", count("failed")],
439
+ ["skipped", count("skipped")]
440
+ ])}
441
+ ${ringStat("Tests", [
442
+ ["passed", sum((t) => t.steps.passed)],
443
+ ["failed", sum((t) => t.steps.failed)],
444
+ ["errored", sum((t) => t.steps.errored)],
445
+ ["skipped", sum((t) => t.steps.skipped)]
446
+ ])}
447
+ ${ringStat("Assertions", [
448
+ ["passed", sum((t) => t.assertions.passed)],
449
+ ["failed", sum((t) => t.assertions.failed)]
450
+ ])}
451
+ ${timeStat("Total Run Time", run2.durationMs)}
452
+ </div>`
453
+ )}
454
+
455
+ ${card(
456
+ "Results Overview",
457
+ `<div class="scroll-x"><table class="overview">
458
+ <thead><tr class="groups"><th colspan="3"></th><th colspan="5" class="gs">Tests</th><th colspan="3" class="gs">Assertions</th><th class="gs"></th></tr>
459
+ <tr><th class="r">#</th><th>Collection ID</th><th class="r">Run Time</th><th class="r gs">Total</th><th class="r">Passed</th><th class="r">Failed</th><th class="r">Errored</th><th class="r">Skipped</th><th class="r gs">Total</th><th class="r">Passed</th><th class="r">Failed</th><th class="gs">Result</th></tr></thead>
460
+ <tbody>
461
+ ${rows}
462
+ </tbody></table></div>`,
463
+ {
464
+ flush: true,
465
+ // As a collection page's Tests: the search by the title, what to show at the far end.
466
+ tools: `${searchBox("Search collections")}<div class="tools-end">${chips({ passed: count("passed"), failed: count("failed"), skipped: count("skipped") })}</div>`
467
+ }
468
+ )}
469
+ ${notRun}`
470
+ });
471
+ }
472
+ function collectionPage(run2, c) {
473
+ const t = c.tally;
474
+ const status = collectionStatus(t);
475
+ const results = c.outcome.ok ? c.outcome.summary.results : [];
476
+ const tests = !c.outcome.ok ? "" : [
477
+ ...results.map((r, i) => testSection(r, i, c.outcome.ok ? c.outcome.steps[i] : void 0)),
478
+ unattempted(c.outcome.summary) > 0 ? notRunSection(unattempted(c.outcome.summary)) : ""
479
+ ].join("\n");
480
+ const groups = results.map((r) => group(statusOf(r)));
481
+ const data = c.outcome.ok ? c.outcome.data : null;
482
+ return page({
483
+ title: `${status.toUpperCase()} \xB7 ${c.id} \xB7 ata Collection Results`,
484
+ run: run2,
485
+ body: `<nav class="crumbs"><a href="${SUMMARY_PAGE}">Summary</a><span aria-hidden="true">/</span><span>${escape(c.id)}</span></nav>
486
+ ${pageHead(status, escape(c.id), c.startedAt)}
487
+
488
+ ${card(
489
+ "Results Overview",
490
+ [
491
+ c.docs?.trim() ? markdown(c.docs) : "",
492
+ data ? `<dl class="pairs"><div><dt>Data File</dt><dd><code>${escape(data.file)}</code></dd></div><div><dt>Iterations</dt><dd>${thousands(data.rows)}</dd></div></dl>` : "",
493
+ !c.outcome.ok ? `<section class="sub problem"><h3>Error</h3><pre>${escape(c.outcome.message)}</pre></section>` : "",
494
+ `<div class="stats">
495
+ ${ringStat("Tests", [
496
+ ["passed", t.steps.passed],
497
+ ["failed", t.steps.failed],
498
+ ["errored", t.steps.errored],
499
+ ["skipped", t.steps.skipped]
500
+ ])}
501
+ ${ringStat("Assertions", [
502
+ ["passed", t.assertions.passed],
503
+ ["failed", t.assertions.failed]
504
+ ])}
505
+ ${timeStat("Total Run Time", c.durationMs)}
506
+ </div>`
507
+ ].filter(Boolean).join("\n"),
508
+ { tools: `<code class="file">${escape(c.file)}</code>` }
509
+ )}
510
+
511
+ <div class="toolbar"><h2>Tests</h2>${groups.length > 0 ? searchBox("Search tests") : ""}<div class="tools-end">${chips(
512
+ {
513
+ passed: groups.filter((g) => g === "passed").length,
514
+ failed: groups.filter((g) => g === "failed").length,
515
+ skipped: groups.filter((g) => g === "skipped").length
516
+ }
517
+ )}${groups.length === 0 ? "" : (
518
+ // Says what a click does: expand while any test is closed, as passed and skipped ones start.
519
+ `<button type="button" class="btn" id="expand-all">${groups.some((g) => g !== "failed") ? "Expand All" : "Collapse All"}</button>`
520
+ )}</div></div>
521
+ <div class="tests">
522
+ ${tests || '<p class="dim empty">No tests were run.</p>'}
523
+ </div>`
524
+ });
525
+ }
526
+ var statusOf = (r) => r.status === "pass" ? "passed" : r.status === "fail" ? "failed" : r.status === "error" ? "errored" : "skipped";
527
+ var group = (status) => status === "errored" ? "failed" : status;
528
+ function testSection(r, index, ref) {
529
+ const status = statusOf(r);
530
+ const named = iterationName(resultName(r), ref);
531
+ const name = r.use ? `${escape(named)} <span class="dim">(${escape(r.use.set)} ${r.use.child + 1}/${r.use.of})</span>` : escape(named);
532
+ const method = r.request.method.toUpperCase();
533
+ const res = r.response;
534
+ const waits = method === "WAIT";
535
+ const outcome = r.skipped ? '<span class="dim">skipped</span>' : waits ? !r.error ? `<span class="dim">waited ${duration(r.durationMs)}</span>` : r.error.code === "ABORTED" ? `<span class="c-failed">stopped after ${duration(r.durationMs)}</span>` : '<span class="c-failed">did not wait</span>' : res ? `${statusCode(res)}<span class="dim">${duration(res.timings.totalMs)}</span>` : r.error?.elapsedMs !== void 0 ? `<span class="c-failed">no response after ${duration(r.error.elapsedMs)}</span>` : '<span class="c-failed">no response</span>';
536
+ const id = `t${index + 1}`;
537
+ const body = r.skipped ? `<p class="skip-reason">${escape(r.skipped.reason)} \u2014 this test was not run</p>` : waits ? r.error ? errorSection(r) : `<p class="wait-note">Waited ${escape(waitedFor(r.request.url))} before the next step. Nothing was sent.</p>` : [
538
+ r.error ? errorSection(r) : "",
539
+ requestSection(r.request),
540
+ responseSection(r, id),
541
+ assertionsSection(r.assertions),
542
+ r.logs?.length ? consoleSection(r) : ""
543
+ ].filter(Boolean).join("\n");
544
+ return `<details class="test s-${status}" id="${id}" data-status="${group(status)}" data-search="${escape(named.toLowerCase())}"${status === "passed" || status === "skipped" ? "" : " open"}>
545
+ <summary><span class="n">${index + 1}</span><span class="method ${methodClass(method)}">${escape(method)}</span><span class="name">${name}</span><span class="res">${outcome}${mark(status)}<span class="chev" aria-hidden="true">\u25B6</span></span></summary>
546
+ <div class="test-body">
547
+ ${body}
548
+ </div>
549
+ </details>`;
550
+ }
551
+ function notRunSection(count) {
552
+ return `<details class="test s-skipped" data-status="skipped" data-search="">
553
+ <summary><span class="n"></span><span class="name">${plural2(count, "test")} not run</span><span class="res">${mark("skipped")}<span class="chev" aria-hidden="true">\u25B6</span></span></summary>
554
+ <div class="test-body"><p class="skip-reason">The collection stopped at its first failure (bail), so ${count === 1 ? "this test was" : "these tests were"} not run.</p></div>
555
+ </details>`;
556
+ }
557
+ function errorSection(r) {
558
+ const e = r.error;
559
+ const where = [e.script && `${e.script} script`, e.line && `line ${e.line}`].filter(Boolean).join(", ");
560
+ const detail = e.stack ? `
561
+
562
+ ${userFrames(e.stack)}` : "";
563
+ return `<section class="sub problem"><h3>${escape(capitalise(e.phase))} Error</h3><pre>${escape(`${where ? `(${where}) ` : ""}${e.message}${detail}`)}</pre></section>`;
564
+ }
565
+ function rawRequest(request) {
566
+ const head = [`${request.method} ${request.url}`, ...request.headers.map(headerLine)].join("\n");
567
+ return request.body ? `${head}
568
+
569
+ ${request.body}` : head;
570
+ }
571
+ function rawResponse(response) {
572
+ const head = [
573
+ [response.httpVersion, response.status, response.statusText].filter(Boolean).join(" "),
574
+ ...response.headers.map(headerLine)
575
+ ].join("\n");
576
+ const body = response.bodyEncoding === "base64" ? `[binary body, ${bytes(response.sizeBytes)}]` : response.body;
577
+ return body ? `${head}
578
+
579
+ ${body}` : head;
580
+ }
581
+ var headerLine = (header) => `${header.name}: ${header.value}`;
582
+ var exchange = (title, formatted, raw, rawNote, rawId = "") => `<section class="sub" data-switch data-exchange>
583
+ <div class="sub-h"><h3>${title}</h3><span class="seg" role="group" aria-label="${title} view"><button type="button" data-show="formatted" aria-pressed="true">Formatted</button><button type="button" data-show="raw" aria-pressed="false">Raw</button></span></div>
584
+ <div data-part="formatted">
585
+ ${formatted}
586
+ </div>
587
+ <div data-part="raw" hidden>${part(title === "Request" ? "As sent" : "As received", rawNote, copyButton("raw"), `<pre class="raw" data-raw${rawId ? ` id="${rawId}"` : ""}>${escape(clip(raw))}</pre>`)}</div>
588
+ </section>`;
589
+ function requestSection(request) {
590
+ const { headers, body } = request;
591
+ const missing = [headers.length === 0 && "no headers", !body && "no body"].filter(Boolean);
592
+ const formatted = [
593
+ `<div class="line"><span class="method ${methodClass(request.method)}">${escape(request.method.toUpperCase())}</span><code class="url">${escape(request.url || "\u2014")}</code>${request.url ? copyButton("url", "Copy URL") : ""}</div>`,
594
+ headers.length > 0 ? part("Headers", String(headers.length), copyButton("headers"), headerTable(headers)) : "",
595
+ body ? part(
596
+ "Body",
597
+ [headerValue(headers, "content-type"), bytes(Buffer.byteLength(body))].filter(Boolean).map((s) => escape(s)).join(" \xB7 "),
598
+ copyButton("body"),
599
+ codeView(requestBodyLines(body), false)
600
+ ) : "",
601
+ missing.length > 0 ? `<p class="dim small none">${capitalise(missing.join(" \xB7 "))}</p>` : ""
602
+ ].filter(Boolean).join("\n");
603
+ return exchange(
604
+ "Request",
605
+ formatted,
606
+ rawRequest(request),
607
+ "method and URL, headers, a blank line, then the body"
608
+ );
609
+ }
610
+ function requestBodyLines(body) {
611
+ if (/^\s*[{[]/.test(body)) {
612
+ try {
613
+ return toJsonLines(JSON.parse(body)).map((line) => ({
614
+ text: line.text,
615
+ html: highlightJson(line.text)
616
+ }));
617
+ } catch {
618
+ }
619
+ }
620
+ return body.split("\n").map((text) => ({ text, html: escape(text) }));
621
+ }
622
+ var ENDED_BY = {
623
+ close: "closed by the server",
624
+ maxEvents: "stopped at maxEvents",
625
+ streamTimeout: "stopped at streamTimeout",
626
+ untilEvent: "stopped at untilEvent",
627
+ testsPassed: "stopped once the tests passed",
628
+ limit: "stopped at the safety limit",
629
+ stopped: "stopped by hand",
630
+ held: "took the events held"
631
+ };
632
+ var connectionNote = (stream) => stream.connection ? `, connection ${stream.connection.name} ${stream.connection.open ? "open" : "closed"}` : "";
633
+ function noResponse(r) {
634
+ const e = r.error;
635
+ if (e?.elapsedMs === void 0) return "No response was received.";
636
+ const wire = wireNote(e);
637
+ return `No response was received: the request failed after ${duration(e.elapsedMs)}${wire ? `, sent over ${wire}` : ""}.`;
638
+ }
639
+ function responseSection(r, id) {
640
+ const res = r.response;
641
+ if (!res) {
642
+ return `<section class="sub"><h3>Response</h3><p class="c-failed none">${escape(noResponse(r))}</p></section>`;
643
+ }
644
+ const checks = buildChecks(r.assertions);
645
+ const status = markStatus(checks).mark;
646
+ const extra = [
647
+ `first byte ${duration(res.timings.ttfbMs)}`,
648
+ bytes(res.sizeBytes),
649
+ res.redirectCount > 0 ? plural2(res.redirectCount, "redirect") : "",
650
+ res.stream ? `${plural2(res.stream.at.length, "event")}, ${ENDED_BY[res.stream.endedBy]}${connectionNote(res.stream)}` : "",
651
+ wireNote(res) ?? ""
652
+ ].filter(Boolean);
653
+ const formatted = [
654
+ `<div class="line">${statusCode(res)}${status ? `<span class="mk-inline mk-${status}" title="${status === "fail" ? "A status check failed" : "Status checked"}">${MARK_GLYPH[status]}</span>` : ""}<span class="dim small">${duration(res.timings.totalMs)} \xB7 ${escape(extra.join(" \xB7 "))}</span></div>`,
655
+ res.headers.length > 0 ? part(
656
+ "Headers",
657
+ String(res.headers.length),
658
+ copyButton("headers"),
659
+ headerTable(res.headers, markHeaders(res.headers, checks))
660
+ ) : "",
661
+ responseBody(r, res, checks, id)
662
+ ].filter(Boolean).join("\n");
663
+ return exchange(
664
+ "Response",
665
+ formatted,
666
+ rawResponse(res),
667
+ "status, headers, a blank line, then the body",
668
+ `${id}-raw`
669
+ );
670
+ }
671
+ function responseBody(r, res, checks, id) {
672
+ if (res.bodyKind === "empty") return '<p class="dim small none">Empty body</p>';
673
+ const rawId = `${id}-raw`;
674
+ const type = headerValue(res.headers, "content-type") ?? res.bodyKind;
675
+ const how = checkedDiffersFromRaw(res, r.sortedBy) ? [
676
+ res.bodyKind === "xml" ? "converted from XML" : "",
677
+ res.bodyKind === "events" ? "read as events" : "",
678
+ r.sortedBy?.length ? `sorted by ${r.sortedBy.join(", ")}` : ""
679
+ ].filter(Boolean) : [];
680
+ const meta = escape([type, bytes(res.sizeBytes), ...how].join(" \xB7 "));
681
+ if (res.bodyEncoding === "base64") return binaryBody(res, meta, id);
682
+ const copy = copyButton("body");
683
+ const checked = checkedBody(res, r.sortedBy);
684
+ if (!checked.ok) {
685
+ return part(
686
+ "Body",
687
+ meta,
688
+ copy,
689
+ `<p class="${res.bodyKind === "binary" ? "dim" : "c-failed"} small pad">${escape(checked.message)}</p><pre class="raw">${escape(clip(res.body))}</pre>`
690
+ );
691
+ }
692
+ const ignored = (r.ignored ?? []).map((entry) => parsePath(entry.path));
693
+ const marks = markLines(checked.lines, checks, ignored);
694
+ const structured = res.bodyKind !== "text" && res.bodyKind !== "html";
695
+ const value = structured ? checkedValue(res, r.sortedBy) : void 0;
696
+ const noted = /* @__PURE__ */ new Set();
697
+ const lines = checked.lines.map((line, i) => {
698
+ const marked = marks[i];
699
+ const failing = marked.mark === "fail" ? marked.about.filter((k) => checks[k].assertion.status === "fail" && !noted.has(k)) : [];
700
+ for (const k of failing) noted.add(k);
701
+ const note = failing.length ? `<span class="note">${MARK_GLYPH.fail} ${escape(failing.map((k) => checks[k].assertion.message ?? checks[k].assertion.name).join("; "))}</span>` : "";
702
+ const viewer = value === void 0 ? null : stringViewer(value, line, `${rawId}-v${i}`);
703
+ return {
704
+ text: line.text,
705
+ html: viewer?.html ?? (structured ? highlightJson(line.text) : escape(line.text)),
706
+ mark: marked.mark,
707
+ note: `${viewer?.button ?? ""}${note}`,
708
+ after: viewer?.panel
709
+ };
710
+ });
711
+ const view = codeView(lines, true);
712
+ if (res.bodyKind === "html") {
713
+ return part(
714
+ "Body",
715
+ meta,
716
+ `${renderSwitch()}${copy}`,
717
+ `<div data-part="rendered">${htmlFrame(rawId, true, "The response body, rendered")}</div>
718
+ <div data-part="source" hidden>${view}</div>`,
719
+ true
720
+ );
721
+ }
722
+ return part("Body", meta, copy, view);
723
+ }
724
+ function binaryBody(res, meta, id) {
725
+ const type = (headerValue(res.headers, "content-type") ?? "").split(";")[0].trim().toLowerCase();
726
+ if (res.sizeBytes > BINARY_LIMIT) {
727
+ return part(
728
+ "Body",
729
+ meta,
730
+ "",
731
+ `<p class="dim small pad">Over ${bytes(BINARY_LIMIT)}, too big to put in the report.</p>`
732
+ );
733
+ }
734
+ const uri = escape(`data:${type || "application/octet-stream"};base64,${res.body}`);
735
+ if (type.startsWith("image/")) {
736
+ return part(
737
+ "Body",
738
+ meta,
739
+ copyButton("image", "Copy data URI"),
740
+ `<div class="img-view"><span class="checker"><img loading="lazy" src="${uri}" alt="The response body"></span><span class="dim small"><span data-dims></span>${escape(type)}</span></div>`
741
+ );
742
+ }
743
+ const subtype = type.split("/")[1] ?? "";
744
+ const extension = /^[a-z0-9]{1,8}$/.test(subtype) ? subtype : "bin";
745
+ return part(
746
+ "Body",
747
+ meta,
748
+ `<a class="btn sm" href="${uri}" download="${id}-body.${extension}">Download</a>`,
749
+ '<p class="dim small pad">Binary, so not shown as text. Download saves it as it came.</p>'
750
+ );
751
+ }
752
+ function checkedValue(res, sortedBy) {
753
+ const parsed = bodyAsObject(res);
754
+ if (!parsed.ok) return void 0;
755
+ return sortedBy ? sortArraysBy(parsed.body, sortedBy) : parsed.body;
756
+ }
757
+ function stringViewer(value, line, id) {
758
+ if (line.path.length === 0 || !/"[,]?$/.test(line.text)) return null;
759
+ const found = resolvePath(value, line.path)[0]?.value;
760
+ if (typeof found !== "string") return null;
761
+ const path3 = formatPath(line.path);
762
+ const close = `<button type="button" class="btn sm" data-close="${id}">Close</button>`;
763
+ if (DATA_IMAGE.test(found)) {
764
+ const literal = JSON.stringify(found);
765
+ const at = line.text.lastIndexOf(literal);
766
+ const shown = `${line.text.slice(0, at)}${JSON.stringify(`${found.slice(0, 40)}\u2026`)}${line.text.slice(at + literal.length)}`;
767
+ const comma = found.indexOf(",");
768
+ const size = Math.floor((found.length - comma - 1) * 3 / 4) - (found.match(/=+$/)?.[0].length ?? 0);
769
+ return {
770
+ html: `${highlightJson(shown)}<span class="dim"> (${bytes(found.length)})</span>`,
771
+ button: `<button type="button" class="peek" data-peek="${id}" aria-expanded="false">View image</button>`,
772
+ panel: `<div class="peek-row" id="${id}" hidden><div class="peek-panel">
773
+ <div class="peek-h"><code>${escape(path3)}</code><span class="dim">${escape(found.slice(5, found.indexOf(";")))} \xB7 <span data-dims></span>${bytes(size)}</span><span class="end"><button type="button" class="btn sm" data-copy="image">Copy data URI</button>${close}</span></div>
774
+ <div class="img-view"><span class="checker"><img loading="lazy" src="${escape(found)}" alt="The image at ${escape(path3)}"></span></div>
775
+ </div></div>`
776
+ };
777
+ }
778
+ if (looksLikeHtml(found)) {
779
+ return {
780
+ button: `<button type="button" class="peek" data-peek="${id}" aria-expanded="false">View HTML</button>`,
781
+ panel: `<div class="peek-row" id="${id}" hidden><div class="peek-panel" data-switch>
782
+ <div class="peek-h"><code>${escape(path3)}</code><span class="dim">HTML \xB7 ${bytes(Buffer.byteLength(found))}</span><span class="end">${renderSwitch()}<button type="button" class="btn sm" data-copy="source">Copy</button>${close}</span></div>
783
+ <div data-part="rendered">${htmlFrame(`${id}-source`, false, `The HTML at ${path3}, rendered`)}</div>
784
+ <pre class="raw" data-part="source" id="${id}-source" hidden>${escape(found)}</pre>
785
+ </div></div>`
786
+ };
787
+ }
788
+ return null;
789
+ }
790
+ var DATA_IMAGE = /^data:image\/[\w.+-]+;base64,[A-Za-z0-9+/]+=*$/;
791
+ var looksLikeHtml = (text) => /^\s*<(!doctype\s+html|[a-z][a-z0-9-]*)[\s>/]/i.test(text) && /<\/[a-z][a-z0-9-]*\s*>/i.test(text);
792
+ var htmlFrame = (from, body, title) => `<div class="html-frame"><iframe sandbox="" title="${escape(title)}" data-from="${from}"${body ? " data-body" : ""}></iframe></div><p class="frame-note">Sandboxed: scripts don't run, and nothing loads from the network.</p>`;
793
+ var renderSwitch = () => '<span class="seg" role="group" aria-label="HTML view"><button type="button" data-show="rendered" aria-pressed="true">Rendered</button><button type="button" data-show="source" aria-pressed="false">Source</button></span>';
794
+ function headerTable(headers, marks) {
795
+ const anyMarked = marks?.some((m) => m.mark !== null) ?? false;
796
+ return `<table class="kv">${headers.map((h, i) => {
797
+ const mark2 = marks?.[i]?.mark ?? null;
798
+ return `<tr${mark2 ? ` class="mk-${mark2}"` : ""}>${anyMarked ? `<td class="mk">${mark2 ? MARK_GLYPH[mark2] : ""}</td>` : ""}<td class="k">${escape(h.name)}</td><td>${escape(h.value)}</td></tr>`;
799
+ }).join("")}</table>`;
800
+ }
801
+ function assertionsSection(assertions) {
802
+ const failed = assertions.filter((a) => a.status === "fail").length;
803
+ const head = `<h3>Assertions <span class="dim">${assertions.length} \xB7 ${assertions.length - failed} passed \xB7 ${failed} failed</span></h3>`;
804
+ if (assertions.length === 0)
805
+ return `<section class="sub">${head}<p class="dim small none">No assertions.</p></section>`;
806
+ const items = assertions.map((a) => {
807
+ const failing = a.status === "fail";
808
+ const detail = [
809
+ a.message ? `<div class="check-msg">${escape(a.message)}</div>` : "",
810
+ failing && (a.expected !== void 0 || a.actual !== void 0) ? `<dl class="ea">${a.expected !== void 0 ? `<dt>expected</dt><dd><code>${escape(a.expected)}</code></dd>` : ""}${a.actual !== void 0 ? `<dt>actual</dt><dd><code>${escape(a.actual)}</code></dd>` : ""}</dl>` : "",
811
+ a.unasserted?.length ? `<dl class="ea"><dt>not asserted</dt><dd><code>${escape(a.unasserted.join(", "))}</code></dd></dl>` : ""
812
+ ].join("");
813
+ const status = failing ? "failed" : "passed";
814
+ return `<li class="s-${status}">${mark(status)}<div class="check"><div class="check-name">${escape(a.name)}</div>${detail}</div>${a.source ? `<span class="src">${escape(sourceText(a.source))}</span>` : ""}</li>`;
815
+ });
816
+ return `<section class="sub">${head}<ul class="checks">
817
+ ${items.join("\n")}
818
+ </ul></section>`;
819
+ }
820
+ var sourceText = (source) => `${source.script === "step" ? "tests" : `${source.script} tests`}:${source.line}${source.check ? ` \xB7 ${source.check.file}:${source.check.line}` : ""}`;
821
+ function consoleSection(r) {
822
+ const lines = r.logs.map(
823
+ (l) => `<span class="log-${l.level}">[${escape(l.phase)} ${l.level}] ${escape(l.message)}</span>`
824
+ );
825
+ return `<section class="sub"><h3>Console</h3><pre class="console">${lines.join("\n")}</pre></section>`;
826
+ }
827
+ function codeView(all, marked) {
828
+ const { lines, more } = cutLines(all);
829
+ const rows = lines.map(
830
+ (line, i) => `<div class="ln${line.mark ? ` mk-${line.mark}` : ""}"><span class="no">${i + 1}</span>${marked ? `<span class="mk" aria-hidden="true">${line.mark ? MARK_GLYPH[line.mark] : ""}</span>` : ""}<span class="tx">${line.html}${line.note ?? ""}</span></div>${line.after ?? ""}`
831
+ );
832
+ if (more > 0) {
833
+ rows.push(
834
+ `<div class="ln cut"><span class="no"></span><span class="tx">\u2026 ${bytes(more)} more not shown</span></div>`
835
+ );
836
+ }
837
+ return `<div class="codeview">${rows.join("\n")}</div>`;
838
+ }
839
+ function cutLines(lines) {
840
+ let left = BODY_LIMIT2;
841
+ for (let i = 0; i < lines.length; i++) {
842
+ const length = lines[i].text.length + 1;
843
+ if (length > left) {
844
+ const total = lines.reduce((n, line) => n + line.text.length + 1, 0) - 1;
845
+ const kept = lines.slice(0, i);
846
+ const cut = lines[i].text.slice(0, left);
847
+ if (cut) kept.push({ text: cut, html: escape(cut) });
848
+ return { lines: kept, more: total - BODY_LIMIT2 };
849
+ }
850
+ left -= length;
851
+ }
852
+ return { lines, more: 0 };
853
+ }
854
+ function highlightJson(text) {
855
+ const match = /^(\s*)("(?:[^"\\]|\\.)*": )?(.*?)(,?)$/.exec(text);
856
+ if (!match) return escape(text);
857
+ const [, indent = "", key, value = "", comma = ""] = match;
858
+ const kind = value.startsWith('"') ? "j-s" : /^-?\d/.test(value) ? "j-n" : /^(true|false|null)$/.test(value) ? "j-l" : "";
859
+ return `${indent}${key ? `<span class="j-k">${escape(key.slice(0, -2))}</span>: ` : ""}${kind ? `<span class="${kind}">${escape(value)}</span>` : escape(value)}${comma}`;
860
+ }
861
+ function page(options) {
862
+ return `<!doctype html>
863
+ <html lang="en">
864
+ <head>
865
+ <meta charset="utf-8">
866
+ <meta name="viewport" content="width=device-width, initial-scale=1">
867
+ <title>${escape(options.title)}</title>
868
+ <style>${STYLE}</style>
869
+ <script>${SCRIPT}</script>
870
+ </head>
871
+ <body>
872
+ <main class="page">
873
+ ${options.body}
874
+ <footer class="foot">Generated by <a href="${REPO_URL}" target="_blank" rel="noopener noreferrer" title="Accelerator Test Automation on GitHub">ata ${escape(options.run.version)} <span aria-hidden="true">\u2197</span></a></footer>
875
+ </main>
876
+ </body>
877
+ </html>
878
+ `;
879
+ }
880
+ var pageHead = (status, title, startedAt) => `<header class="head"><div class="title">${pill(status, true)}<h1>${title}</h1></div><div class="meta"><span>${escape(timeOfRun(startedAt))}</span></div></header>`;
881
+ var card = (title, body, options = {}) => `<section class="card"${options.id ? ` id="${options.id}"` : ""}><div class="card-h"><h2>${title}</h2>${options.tools ?? ""}</div><div class="card-body${options.flush ? " flush" : ""}">
882
+ ${body}
883
+ </div></section>`;
884
+ var part = (title, meta, tools, body, switches = false) => `<div class="part"${switches ? " data-switch" : ""}><div class="part-h"><span>${title}</span><span class="dim">${meta}</span><span class="end">${tools}</span></div>${body}</div>`;
885
+ var copyButton = (what, label = "Copy") => `<button type="button" class="btn sm" data-copy="${what}">${label}</button>`;
886
+ var pill = (status, solid = false) => `<span class="pill${solid ? " solid" : ""} s-${status}">${status}</span>`;
887
+ var MARKS = { passed: "\u2713", failed: "\u2715", errored: "!", skipped: "\u2298" };
888
+ var mark = (status) => `<span class="mark s-${status}" role="img" aria-label="${status}">${MARKS[status]}</span>`;
889
+ var num = (n, bad = false) => n === 0 ? '<span class="zero">0</span>' : bad ? `<span class="hot">${n}</span>` : String(n);
890
+ function statusCode(res) {
891
+ const kind = res.status >= 500 ? "server" : res.status >= 400 ? "client" : res.status >= 300 ? "redirect" : "ok";
892
+ return `<span class="status-code k-${kind}">${escape(`${res.status} ${res.statusText}`.trim())}</span>`;
893
+ }
894
+ var methodClass = (method) => `m-${method.toLowerCase().replace(/[^a-z]/g, "")}`;
895
+ var waitedFor = (url) => {
896
+ const ms = /^(\d+) ms$/.exec(url)?.[1];
897
+ return ms === void 0 ? url : waitText(Number(ms));
898
+ };
899
+ function ringStat(label, parts) {
900
+ const total = parts.reduce((n, [, v]) => n + v, 0);
901
+ const passed = parts.find(([s]) => s === "passed")?.[1] ?? 0;
902
+ const ran = total - (parts.find(([s]) => s === "skipped")?.[1] ?? 0);
903
+ const rate = ran === 0 ? "\u2014" : `${Math.floor(passed / ran * 100)}%`;
904
+ return `<div class="stat"><div class="ring" title="${thousands(passed)} of ${thousands(ran)} that ran passed">${ring(parts)}<b>${rate}</b></div><div><div class="stat-label">${label}</div><div class="stat-total">${thousands(total)}</div><div class="legend">${parts.map(([s, v]) => `<span class="${v ? `s-${s}` : "zero"}"><i></i>${thousands(v)} ${s}</span>`).join("")}</div></div></div>`;
905
+ }
906
+ var timeStat = (label, ms) => `<div class="stat"><div class="ring clock">${clock}</div><div><div class="stat-label">${label}</div><div class="stat-time" title="${thousands(Math.round(ms))} ms">${runTime(ms).replace(/(\d) /g, "$1&nbsp;")}</div></div></div>`;
907
+ var clock = `<svg viewBox="0 0 64 64" width="64" height="64" aria-hidden="true"><circle cx="32" cy="32" r="25" fill="none" stroke-width="7" style="stroke:var(--surface-alt)"/><path d="M32 19v13l9 6" fill="none" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="stroke:var(--dim)"/></svg>`;
908
+ function ring(parts) {
909
+ const R = 25;
910
+ const C = 2 * Math.PI * R;
911
+ const live = parts.filter(([, v]) => v > 0);
912
+ const total = live.reduce((n, [, v]) => n + v, 0);
913
+ const track = `<circle cx="32" cy="32" r="${R}" fill="none" stroke-width="7" style="stroke:var(--surface-alt)"/>`;
914
+ if (total === 0)
915
+ return `<svg viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">${track}</svg>`;
916
+ const gap = live.length > 1 ? 2 : 0;
917
+ const lengths = live.map(([, v]) => Math.max(v / total * C, 5));
918
+ const biggest = lengths.indexOf(Math.max(...lengths));
919
+ lengths[biggest] -= lengths.reduce((a, b) => a + b, 0) - C;
920
+ let offset = 0;
921
+ const arcs = live.map(([s], i) => {
922
+ const length = lengths[i];
923
+ const arc = `<circle class="s-${s}" cx="32" cy="32" r="${R}" fill="none" stroke-width="7" style="stroke:var(--s)" stroke-dasharray="${(length - gap).toFixed(2)} ${(C - length + gap).toFixed(2)}" stroke-dashoffset="${(-offset).toFixed(2)}" transform="rotate(-90 32 32)"/>`;
924
+ offset += length;
925
+ return arc;
926
+ });
927
+ return `<svg viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">${track}${arcs.join("")}</svg>`;
928
+ }
929
+ var flagChips = (flags) => Object.entries(flags).map(([name, value]) => `<code class="flag">${escape(`${name}=${value}`)}</code>`).join("");
930
+ var chips = (counts) => ["passed", "failed", "skipped"].filter((s) => counts[s] > 0).map(
931
+ (s) => `<button type="button" class="chip s-${s}" data-hide="${s}" aria-pressed="true" title="Show or hide ${s}">${mark(s)}${capitalise(s)} <b>${counts[s]}</b></button>`
932
+ ).join("");
933
+ var searchBox = (label) => `<input type="search" class="search" placeholder="${label}" aria-label="${label}">`;
934
+ var headerValue = (headers, name) => headers.find((h) => h.name.toLowerCase() === name)?.value;
935
+ function clip(text) {
936
+ return text.length > BODY_LIMIT2 ? `${text.slice(0, BODY_LIMIT2)}
937
+ \u2026 ${bytes(text.length - BODY_LIMIT2)} more not shown` : text;
938
+ }
939
+ var userFrames = (stack) => stack.split("\n").filter((line) => !/^\s+at .*(\bnode:|file:\/\/)/.test(line)).join("\n");
940
+ var capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
941
+ var plural2 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
942
+ var thousands = (n) => n.toLocaleString("en-US");
943
+ var duration = (ms) => ms < 1e3 ? `${Math.round(ms)} ms` : `${(ms / 1e3).toFixed(2)} s`;
944
+ function runTime(ms) {
945
+ const hundredths = Math.round(ms / 10);
946
+ const hours = Math.floor(hundredths / 36e4);
947
+ const minutes = Math.floor(hundredths % 36e4 / 6e3);
948
+ const seconds2 = hundredths % 6e3;
949
+ return [
950
+ hours > 0 ? plural2(hours, "hour") : "",
951
+ minutes > 0 ? plural2(minutes, "minute") : "",
952
+ seconds2 > 0 || hundredths < 6e3 ? `${(seconds2 / 100).toFixed(2)} seconds` : ""
953
+ ].filter(Boolean).join(" ");
954
+ }
955
+ function timeoutText(ms) {
956
+ const round = ms >= 36e5 && ms % 36e5 === 0 ? `${ms / 36e5} h` : ms >= 6e4 && ms % 6e4 === 0 ? `${ms / 6e4} min` : ms >= 1e3 && ms % 1e3 === 0 ? `${ms / 1e3} s` : "";
957
+ return `${ms.toLocaleString("en-US")} ms${round ? ` <span class="dim">\xB7 ${round}</span>` : ""}`;
958
+ }
959
+ var timeOfRun = (epochMs) => {
960
+ const at = new Date(epochMs);
961
+ return `${at.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", year: "numeric" })} \xB7 ${at.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit", timeZoneName: "short" })}`;
962
+ };
963
+ function bytes(n) {
964
+ if (n < 1024) return `${n} B`;
965
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
966
+ return `${(n / 1024 / 1024).toFixed(1)} MB`;
967
+ }
968
+ var breakable = (value) => escape(value).replace(/([_/.-])/g, "$1<wbr>");
969
+ function escape(value) {
970
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
971
+ }
972
+ function markdown(source) {
973
+ return `<div class="md">${parseMarkdown(source).map(mdBlock).join("")}</div>`;
974
+ }
975
+ function mdBlock(block) {
976
+ switch (block.type) {
977
+ case "code":
978
+ return `<pre class="md-code">${markCode(
979
+ codeTokens(block.text, block.language) ?? [{ text: block.text, kind: null }]
980
+ ).map((piece) => {
981
+ const classes = [piece.kind && `tok-${piece.kind}`, piece.variable && "md-var"].filter(Boolean).join(" ");
982
+ return classes ? `<span class="${classes}">${escape(piece.text)}</span>` : escape(piece.text);
983
+ }).join("")}</pre>`;
984
+ case "heading": {
985
+ const level = Math.min(block.level + 3, 6);
986
+ return `<h${level} class="md-h">${mdInline(block.content)}</h${level}>`;
987
+ }
988
+ case "rule":
989
+ return '<hr class="md-hr">';
990
+ case "table":
991
+ return `<table class="md-table"><thead><tr>${block.header.map((cell) => `<th>${mdInline(cell)}</th>`).join("")}</tr></thead><tbody>${block.rows.map((row) => `<tr>${row.map((cell) => `<td>${mdInline(cell)}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
992
+ case "quote":
993
+ return `<blockquote class="md-quote">${block.blocks.map(mdBlock).join("")}</blockquote>`;
994
+ case "callout": {
995
+ const { label, icon } = CALLOUTS[block.kind];
996
+ return `<div class="md-callout md-callout-${block.kind}" role="note"><p class="md-callout-title"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${icon}"/></svg>${label}</p>${block.blocks.map(mdBlock).join("")}</div>`;
997
+ }
998
+ case "list": {
999
+ const tag = block.ordered ? "ol" : "ul";
1000
+ return `<${tag} class="md-list">${block.items.map(
1001
+ (item) => `<li>${mdInline(item.content)}${item.children ? mdBlock(item.children) : ""}</li>`
1002
+ ).join("")}</${tag}>`;
1003
+ }
1004
+ case "paragraph":
1005
+ return `<p class="md-p">${mdInline(block.content)}</p>`;
1006
+ }
1007
+ }
1008
+ function mdInline(nodes) {
1009
+ return nodes.map((node) => {
1010
+ switch (node.type) {
1011
+ case "text":
1012
+ return markText(node.text).map(mdMark).join("");
1013
+ case "code":
1014
+ return `<code class="md-inline-code">${markInlineCode(node.text).map(mdMark).join("")}</code>`;
1015
+ case "strong":
1016
+ return `<strong>${mdInline(node.children)}</strong>`;
1017
+ case "em":
1018
+ return `<em>${mdInline(node.children)}</em>`;
1019
+ case "link":
1020
+ return `<a href="${escape(node.href)}" target="_blank" rel="noreferrer noopener">${escape(node.label)}</a>`;
1021
+ }
1022
+ }).join("");
1023
+ }
1024
+ function mdMark(piece) {
1025
+ const text = escape(piece.text);
1026
+ switch (piece.mark) {
1027
+ case null:
1028
+ return text;
1029
+ case "variable":
1030
+ return `<span class="md-var">${text}</span>`;
1031
+ case "method":
1032
+ return `<span class="md-method m-${piece.method.toLowerCase()}">${text}</span>`;
1033
+ case "status":
1034
+ return `<span class="md-status md-status-${piece.status}">${text}</span>`;
1035
+ }
1036
+ }
1037
+ var STYLE = `
1038
+ :root{color-scheme:light dark;--bg:#f6f7f9;--surface:#fff;--surface-alt:#eef0f4;--border:#d6dae2;--border-soft:#e6e9ee;--text:#16181d;--dim:#6a7280;--faint:#9aa1ad;--accent:#2f6df6;--link:#1f5fd6;--passed:#107a48;--failed:#cc1f43;--errored:#a65f00;--skipped:#0b7fb8;--on-status:#fff;--ok:#107a48;--redirect:#8a6100;--client:#b23c17;--server:#a01a2b;--m-get:#1f5fd6;--m-post:#0b7a43;--m-put:#946200;--m-patch:#0b7f84;--m-delete:#c42b2b;--m-head:#7c45d0;--m-options:#3f5f80;--j-key:#7c45d0;--j-str:#8a5a2b;--j-num:#0b7f84;--j-lit:#946200;--canvas:#fff;--check-a:#fff;--check-b:#eceef2;--sans:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace}
1039
+ @media (prefers-color-scheme:dark){:root{--bg:#11131a;--surface:#171a22;--surface-alt:#1e222c;--border:#2b303c;--border-soft:#232733;--text:#e6e8ee;--dim:#949bab;--faint:#666e7e;--accent:#5b8cff;--link:#6b9bff;--passed:#46c98a;--failed:#ff5b78;--errored:#f2a93b;--skipped:#56c2ee;--on-status:#11131a;--ok:#46c98a;--redirect:#e0b24d;--client:#f08a5d;--server:#f0607a;--m-get:#6b9bff;--m-post:#4fd08f;--m-put:#eab54e;--m-patch:#4fd1d1;--m-delete:#ff6f6f;--m-head:#b991ff;--m-options:#8fb4dc;--j-key:#b991ff;--j-str:#d9a066;--j-num:#4fd1d1;--j-lit:#eab54e;--check-a:#262b36;--check-b:#1d212a}}
1040
+ *{box-sizing:border-box}
1041
+ [hidden]{display:none!important}
1042
+ body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 var(--sans);-webkit-font-smoothing:antialiased}
1043
+ .page{max-width:1320px;margin:0 auto;padding:28px clamp(16px,3vw,32px) 24px}
1044
+ a{color:var(--link);text-decoration:none}
1045
+ a:hover{text-decoration:underline}
1046
+ :focus-visible{outline:2px solid var(--accent);outline-offset:2px}
1047
+ code,pre{font-family:var(--mono);font-size:12.5px}
1048
+ pre{margin:0;white-space:pre-wrap;word-break:break-word}
1049
+ .dim{color:var(--dim)}
1050
+ .small{font-size:12.5px}
1051
+ .hidden,.unmatched{display:none!important}
1052
+ .s-passed{--s:var(--passed)}.s-failed{--s:var(--failed)}.s-errored{--s:var(--errored)}.s-skipped{--s:var(--skipped)}
1053
+ .c-failed{color:var(--failed)}
1054
+ .crumbs{display:flex;flex-wrap:wrap;gap:4px 8px;font-size:13px;color:var(--dim);margin-bottom:10px;overflow-wrap:anywhere}
1055
+ .head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px 24px;margin-bottom:18px}
1056
+ .title{display:flex;align-items:center;gap:12px;min-width:0}
1057
+ .title h1{margin:0;font-size:20px;font-weight:650;letter-spacing:-.01em;overflow-wrap:anywhere}
1058
+ .kind{color:var(--dim);font-weight:400}
1059
+ .meta{display:flex;flex-wrap:wrap;gap:4px 14px;color:var(--dim);font-size:13px;font-variant-numeric:tabular-nums}
1060
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:8px;margin-bottom:16px;min-width:0}
1061
+ .card-h{display:flex;flex-wrap:wrap;align-items:center;gap:8px 10px;padding:10px 16px;border-bottom:1px solid var(--border-soft)}
1062
+ .card-h h2{margin:0 6px 0 0;font-size:13.5px;font-weight:650}
1063
+ .card-h .file{margin-left:auto}
1064
+ .card-h .file{color:var(--dim);overflow-wrap:anywhere}
1065
+ .card-body{padding:16px}
1066
+ .card-body>*+*{margin-top:16px}
1067
+ .card-body.flush{padding:0}
1068
+ .pairs{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:14px 28px;margin:0}
1069
+ .pairs dt{font-size:12px;color:var(--dim);margin-bottom:1px}
1070
+ .pairs dd{margin:0;overflow-wrap:anywhere}
1071
+ .flag{font-size:12px;padding:1px 6px;border-radius:4px;background:var(--surface-alt);display:inline-block;margin:0 4px 2px 0}
1072
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr));gap:20px 36px}
1073
+ .stat{display:flex;gap:16px;align-items:center;min-width:0}
1074
+ .ring{position:relative;width:64px;height:64px;flex:none}
1075
+ .ring svg{display:block}
1076
+ .ring b{position:absolute;inset:0;display:grid;place-items:center;font-size:13.5px;font-variant-numeric:tabular-nums}
1077
+ .stat-label{font-size:12px;color:var(--dim)}
1078
+ .stat-total{font-size:24px;font-weight:650;font-variant-numeric:tabular-nums;line-height:1;margin-top:3px}
1079
+ .stat-time{font-size:17px;font-weight:650;font-variant-numeric:tabular-nums;line-height:1.25;margin-top:3px}
1080
+ .legend{display:flex;flex-wrap:wrap;gap:4px 12px;margin-top:6px;font-size:12.5px;font-variant-numeric:tabular-nums}
1081
+ .legend span{display:inline-flex;align-items:center;gap:5px}
1082
+ .legend i{width:8px;height:8px;border-radius:2px;background:var(--s)}
1083
+ .legend .zero i{background:var(--border)}
1084
+ .zero{color:var(--faint)}
1085
+ .hot{color:var(--failed);font-weight:700}
1086
+ .pill{display:inline-flex;align-items:center;padding:3px 10px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--s);background:color-mix(in srgb,var(--s) 14%,transparent);white-space:nowrap}
1087
+ .pill.solid{background:var(--s);color:var(--on-status)}
1088
+ .mark{flex:none;display:inline-grid;place-items:center;width:18px;height:18px;border-radius:50%;color:var(--s);background:color-mix(in srgb,var(--s) 16%,transparent);font-size:10.5px;font-weight:800;line-height:1}
1089
+ .status-code{font:600 12.5px var(--mono);white-space:nowrap}
1090
+ .k-ok{color:var(--ok)}.k-redirect{color:var(--redirect)}.k-client{color:var(--client)}.k-server{color:var(--server)}
1091
+ .method{font:700 11.5px var(--mono);letter-spacing:.02em}
1092
+ .m-get{color:var(--m-get)}.m-post{color:var(--m-post)}.m-put{color:var(--m-put)}.m-patch{color:var(--m-patch)}.m-delete{color:var(--m-delete)}.m-head{color:var(--m-head)}.m-options{color:var(--m-options)}
1093
+ .search{font:inherit;font-size:13px;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);min-width:0;flex:1 1 180px;max-width:300px}
1094
+ .chip{font:inherit;font-size:12.5px;display:inline-flex;align-items:center;gap:6px;padding:3px 10px 3px 4px;border-radius:999px;border:1px solid var(--border);background:var(--surface);color:var(--text);cursor:pointer;white-space:nowrap}
1095
+ .chip b{font-variant-numeric:tabular-nums}
1096
+ .chip[aria-pressed="false"]{opacity:.45}
1097
+ .btn{font:inherit;font-size:12.5px;padding:4px 12px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);cursor:pointer;white-space:nowrap}
1098
+ .btn.sm{font-size:11.5px;padding:2px 9px;border-radius:5px}
1099
+ .btn:hover,.chip:hover{border-color:var(--faint)}
1100
+ .seg{display:inline-flex;border:1px solid var(--border);border-radius:7px;padding:1px;background:var(--surface-alt)}
1101
+ .seg button{font:inherit;font-size:11.5px;border:none;background:none;color:var(--dim);padding:2px 9px;border-radius:5px;cursor:pointer}
1102
+ .seg button[aria-pressed="true"]{background:var(--surface);color:var(--text);box-shadow:0 1px 2px rgba(0,0,0,.1)}
1103
+ .scroll-x{overflow-x:auto}
1104
+ .overview{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums;font-size:13px}
1105
+ .overview th{font-size:11.5px;font-weight:600;color:var(--dim);text-align:left;padding:6px 10px;white-space:nowrap}
1106
+ .overview thead tr:last-child th{border-bottom:1px solid var(--border)}
1107
+ .overview .groups th{padding:10px 10px 0;text-transform:uppercase;letter-spacing:.08em;font-size:10.5px;color:var(--faint)}
1108
+ .overview td{padding:7px 10px;border-bottom:1px solid var(--border-soft);vertical-align:top}
1109
+ .overview tbody tr:last-child td{border-bottom:none}
1110
+ .overview .r{text-align:right}
1111
+ .overview .gs{border-left:1px solid var(--border-soft)}
1112
+ .overview td.name{min-width:10rem;overflow-wrap:anywhere}
1113
+ @media (max-width:960px){.overview th,.overview td{padding-left:6px;padding-right:6px}.overview .groups th{padding-left:6px;padding-right:6px}}
1114
+ .overview td.n{color:var(--faint)}
1115
+ .overview tbody tr:hover td{background:var(--surface-alt)}
1116
+ .overview tr.s-failed td:first-child{box-shadow:inset 3px 0 0 var(--failed)}
1117
+ .group-h{margin:0 0 8px;font-size:13px;font-weight:600}
1118
+ .group-h .dim{font-weight:400}
1119
+ .excluded{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:6px 24px}
1120
+ .excluded li{font:12.5px var(--mono);display:flex;gap:8px;align-items:center;overflow-wrap:anywhere}
1121
+ .excluded li::before{content:"\u2298";color:var(--faint);font-family:var(--sans)}
1122
+ .toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:10px 12px;margin:28px 0 10px}
1123
+ .toolbar h2{margin:0 8px 0 0;font-size:17px;font-weight:650}
1124
+ .tools-end{margin-left:auto;display:flex;flex-wrap:wrap;gap:6px;align-items:center}
1125
+ .tests{border:1px solid var(--border);border-radius:8px;background:var(--surface);overflow:hidden}
1126
+ .test+.test{border-top:1px solid var(--border)}
1127
+ .test>summary{display:flex;align-items:center;gap:10px;padding:11px 14px;cursor:pointer;list-style:none}
1128
+ .test>summary::-webkit-details-marker{display:none}
1129
+ .test>summary .n{width:1.6em;color:var(--faint);font-variant-numeric:tabular-nums;text-align:right;flex:none}
1130
+ .test>summary .method{width:52px;flex:none}
1131
+ .test>summary .name{flex:1;min-width:0;font-weight:500;overflow-wrap:anywhere}
1132
+ .test>summary .res{display:flex;align-items:center;gap:10px;font-size:12.5px;flex:none;font-variant-numeric:tabular-nums}
1133
+ .chev{color:var(--faint);font-size:12px;transition:transform .15s}
1134
+ .test[open]>summary .chev{transform:rotate(90deg)}
1135
+ .test[open]>summary{background:var(--surface-alt)}
1136
+ .test.s-failed>summary,.test.s-errored>summary{box-shadow:inset 3px 0 0 var(--s)}
1137
+ .test-body{padding:16px;display:grid;gap:20px;border-top:1px solid var(--border-soft);min-width:0}
1138
+ .empty{margin:0;padding:16px}
1139
+ .skip-reason{margin:0;color:var(--skipped)}
1140
+ .wait-note{margin:0;color:var(--dim)}
1141
+ .sub{min-width:0}
1142
+ .sub h3{margin:0 0 8px;font-size:11px;font-weight:650;text-transform:uppercase;letter-spacing:.08em;color:var(--dim)}
1143
+ .sub h3 .dim{text-transform:none;letter-spacing:0;font-weight:400;margin-left:6px}
1144
+ .sub-h{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;margin-bottom:8px}
1145
+ .sub-h h3{margin:0}
1146
+ .line{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 10px;min-width:0}
1147
+ .line .url{overflow-wrap:anywhere;min-width:0}
1148
+ .line .btn{margin-left:auto;align-self:center}
1149
+ .mk-inline{font-weight:700}
1150
+ .none{margin:8px 0 0}
1151
+ .part{margin-top:10px;border:1px solid var(--border-soft);border-radius:6px;overflow:hidden;background:var(--surface);min-width:0}
1152
+ .part-h{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;padding:5px 6px 5px 10px;background:var(--surface-alt);font-size:12px;font-weight:600;border-bottom:1px solid var(--border-soft)}
1153
+ .part-h .dim{font-weight:400;overflow-wrap:anywhere}
1154
+ .end{margin-left:auto;display:flex;flex-wrap:wrap;gap:6px;align-items:center}
1155
+ .pad{margin:0;padding:8px 10px}
1156
+ .kv{width:100%;border-collapse:collapse;font:12px/1.45 var(--mono)}
1157
+ .kv td{padding:3px 10px;border-top:1px solid var(--border-soft);vertical-align:top;overflow-wrap:anywhere}
1158
+ .kv tr:first-child td{border-top:none}
1159
+ .kv .k{color:var(--dim);white-space:nowrap;width:1%}
1160
+ .kv .mk{width:1%;padding-right:0;text-align:center}
1161
+ .raw{max-height:440px;overflow:auto;padding:8px 10px;font:12px/1.55 var(--mono);white-space:pre-wrap;word-break:break-all}
1162
+ .codeview{font:12px/1.6 var(--mono);max-height:min(70vh,640px);overflow:auto;padding:6px 0}
1163
+ .codeview:has(.peek-row:not([hidden])){max-height:none}
1164
+ .ln{display:flex;padding-right:12px}
1165
+ .ln .no{flex:none;width:3.2em;text-align:right;padding-right:10px;color:var(--faint);user-select:none}
1166
+ .ln .mk{flex:none;width:1.4em;text-align:center;user-select:none}
1167
+ .ln .tx{flex:1;min-width:0;white-space:pre-wrap;word-break:break-all}
1168
+ .ln.cut .tx{color:var(--dim);font-style:italic}
1169
+ .ln.mk-fail,.kv .mk-fail{background:color-mix(in srgb,var(--failed) 12%,transparent)}
1170
+ .mk-pass .mk,.mk-inline.mk-pass{color:var(--passed)}
1171
+ .mk-fail .mk,.mk-inline.mk-fail{color:var(--failed);font-weight:700}
1172
+ .mk-unasserted .mk,.mk-inline.mk-unasserted{color:var(--errored);font-weight:700}
1173
+ .mk-ignored .mk{color:var(--faint)}
1174
+ .ln .note{color:var(--failed);font:600 11.5px var(--sans);margin-left:12px;white-space:normal}
1175
+ .j-k{color:var(--j-key)}.j-s{color:var(--j-str)}.j-n{color:var(--j-num)}.j-l{color:var(--j-lit)}
1176
+ .peek{font:600 11px var(--sans);margin-left:10px;padding:1px 9px;border-radius:999px;border:1px solid var(--border);background:var(--surface);color:var(--link);cursor:pointer;white-space:nowrap}
1177
+ .peek[aria-expanded="true"]{background:var(--surface-alt);border-color:var(--faint)}
1178
+ .peek-row{padding:4px 12px 10px 4.6em}
1179
+ .peek-panel{border:1px solid var(--border);border-radius:8px;background:var(--surface);overflow:hidden;font:13px/1.5 var(--sans)}
1180
+ .peek-h{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;padding:5px 6px 5px 10px;background:var(--surface-alt);border-bottom:1px solid var(--border-soft);font-size:12px}
1181
+ .peek-h code{font-weight:600;overflow-wrap:anywhere}
1182
+ .html-frame{height:300px;min-height:120px;resize:vertical;overflow:hidden;background:var(--canvas)}
1183
+ .html-frame iframe{display:block;width:100%;height:100%;border:0}
1184
+ .frame-note{margin:0;padding:5px 10px;font-size:11.5px;color:var(--dim);border-top:1px solid var(--border-soft)}
1185
+ .img-view{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px 12px;padding:12px}
1186
+ .checker{display:inline-block;max-width:100%;padding:10px;border-radius:6px;background-color:var(--check-a);background-image:conic-gradient(var(--check-b) 25%,transparent 0 50%,var(--check-b) 0 75%,transparent 0);background-size:16px 16px}
1187
+ .checker img{display:block;max-width:100%;height:auto}
1188
+ .checks{list-style:none;margin:0;padding:0;border:1px solid var(--border-soft);border-radius:6px;background:var(--surface)}
1189
+ .checks li{display:flex;gap:10px;padding:8px 10px;align-items:flex-start}
1190
+ .checks li+li{border-top:1px solid var(--border-soft)}
1191
+ .check{flex:1;min-width:0}
1192
+ .check-name{font:12.5px var(--mono);overflow-wrap:anywhere}
1193
+ .check-msg{font-size:13px;margin-top:2px;color:var(--dim)}
1194
+ .s-failed .check-msg{color:var(--failed)}
1195
+ .ea{display:grid;grid-template-columns:auto 1fr;gap:2px 10px;margin:6px 0 0;font-size:12.5px}
1196
+ .ea dt{color:var(--dim)}
1197
+ .ea dd{margin:0;overflow-wrap:anywhere}
1198
+ .src{font:11.5px var(--mono);color:var(--faint);flex:none;white-space:nowrap}
1199
+ .problem pre{color:var(--failed);background:color-mix(in srgb,var(--failed) 7%,var(--surface));border:1px solid color-mix(in srgb,var(--failed) 30%,var(--border));border-radius:6px;padding:8px 10px}
1200
+ .console{background:var(--surface-alt);border-radius:6px;padding:8px 10px}
1201
+ .console .log-warn{color:var(--errored)}.console .log-error{color:var(--failed)}
1202
+ .md>*:first-child{margin-top:0}.md>*:last-child{margin-bottom:0}
1203
+ .md-p{margin:0 0 8px}
1204
+ .md-h{margin:12px 0 6px;font-size:1rem;font-weight:600}
1205
+ .md-list{margin:0 0 8px;padding-left:24px}
1206
+ .md-list .md-list{margin:2px 0 0}
1207
+ .md-code{margin:0 0 8px;padding:8px 10px;background:var(--surface-alt);border-radius:6px}
1208
+ .md-inline-code{background:var(--surface-alt);padding:1px 4px;border-radius:3px}
1209
+ .md-quote{margin:0 0 8px;padding-left:12px;border-left:3px solid var(--border);color:var(--dim)}
1210
+ .md-hr{border:none;border-top:1px solid var(--border);margin:10px 0}
1211
+ .md-table{border-collapse:collapse;margin:0 0 8px}
1212
+ .md-table th,.md-table td{border:1px solid var(--border);padding:4px 8px;text-align:left}
1213
+ .md-table th{background:var(--surface-alt)}
1214
+ .md-code .tok-name,.md-code .tok-tag,.md-code .tok-function{color:var(--j-key)}.md-code .tok-string{color:var(--j-str)}.md-code .tok-number,.md-code .tok-attribute{color:var(--j-num)}.md-code .tok-literal{color:var(--j-lit)}.md-code .tok-keyword{color:var(--accent)}.md-code .tok-comment{color:var(--dim);font-style:italic}.md-code .tok-punctuation,.md-code .tok-meta{color:var(--dim)}
1215
+ .md .md-var{color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,transparent);border-radius:3px}
1216
+ .md-method{font-weight:700}.md-status{font-weight:600}.md-status-1xx{color:var(--dim)}.md-status-2xx{color:var(--ok)}.md-status-3xx{color:var(--redirect)}.md-status-4xx{color:var(--client)}.md-status-5xx{color:var(--server)}
1217
+ .md-callout{margin:0 0 8px;padding:8px 12px;border-left:3px solid var(--callout);border-radius:0 6px 6px 0;background:color-mix(in srgb,var(--callout) 8%,transparent)}.md-callout>:last-child{margin-bottom:0}
1218
+ .md-callout-title{display:flex;align-items:center;gap:6px;margin:0 0 4px;color:var(--callout);font-weight:600}
1219
+ .md-callout-note{--callout:var(--accent)}.md-callout-tip{--callout:var(--ok)}.md-callout-important{--callout:var(--m-head)}.md-callout-warning{--callout:var(--redirect)}.md-callout-caution{--callout:var(--server)}
1220
+ .foot{margin-top:28px;padding-top:16px;border-top:1px solid var(--border-soft);display:flex;justify-content:center;align-items:center;gap:8px;color:var(--dim);font-size:12.5px}
1221
+ .foot a{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border:1px solid var(--border);border-radius:999px;background:var(--surface);color:var(--text);font-weight:600}
1222
+ .foot a:hover{border-color:var(--faint);text-decoration:none}
1223
+ .foot a span{color:var(--dim);font-weight:400}
1224
+ @media (max-width:560px){.test>summary .res .status-code,.test>summary .method{display:none}}
1225
+ @media (prefers-reduced-motion:reduce){.chev{transition:none}}
1226
+ `;
1227
+ var SCRIPT = String.raw`
1228
+ // What an HTML frame may do beyond showing itself: nothing it could load from.
1229
+ const SANDBOX = '<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; img-src data:; style-src \'unsafe-inline\'; font-src data:; form-action \'none\'">'
1230
+ function filter() {
1231
+ const hidden = new Set([...document.querySelectorAll('[data-hide][aria-pressed="false"]')].map((b) => b.dataset.hide))
1232
+ const input = document.querySelector('.search')
1233
+ const query = input ? input.value.trim().toLowerCase() : ''
1234
+ for (const el of document.querySelectorAll('[data-status][data-search]')) {
1235
+ el.classList.toggle('hidden', hidden.has(el.dataset.status))
1236
+ el.classList.toggle('unmatched', query !== '' && !el.dataset.search.includes(query))
1237
+ }
1238
+ }
1239
+ // The button says what a click will do: Expand All while any test is closed,
1240
+ // Collapse All once every one is open — however they got that way.
1241
+ const tests = () => [...document.querySelectorAll('details.test')]
1242
+ function syncExpandAll() {
1243
+ const button = document.getElementById('expand-all')
1244
+ if (button) button.textContent = tests().some((d) => !d.open) ? 'Expand All' : 'Collapse All'
1245
+ }
1246
+ // A frame is filled when first shown, from the text it names: a response's raw
1247
+ // text, whose body follows the first blank line, or a viewer's source.
1248
+ function loadFrames(scope) {
1249
+ for (const frame of scope.querySelectorAll('iframe[data-from]:not([srcdoc])')) {
1250
+ if (frame.offsetParent === null) continue
1251
+ const text = document.getElementById(frame.dataset.from).textContent
1252
+ const html = frame.hasAttribute('data-body') ? text.slice(text.indexOf('\n\n') + 2) : text
1253
+ frame.srcdoc = SANDBOX + html.replace(/<meta[^>]*http-equiv\s*=\s*["']?refresh[^>]*>/gi, '')
1254
+ }
1255
+ }
1256
+ // Every part of a request or response is read from its raw text, as sent or received.
1257
+ function copied(button) {
1258
+ const what = button.dataset.copy
1259
+ if (what === 'image') return button.closest('.peek-panel, .part').querySelector('img').getAttribute('src')
1260
+ if (what === 'source') return button.closest('.peek-panel').querySelector('[data-part="source"]').textContent
1261
+ const raw = button.closest('[data-exchange]').querySelector('[data-raw]').textContent
1262
+ const blank = raw.indexOf('\n\n')
1263
+ const head = (blank < 0 ? raw : raw.slice(0, blank)).split('\n')
1264
+ if (what === 'raw') return raw
1265
+ if (what === 'body') return blank < 0 ? '' : raw.slice(blank + 2)
1266
+ if (what === 'headers') return head.slice(1).join('\n')
1267
+ return head[0].replace(/^\S+ /, '')
1268
+ }
1269
+ function copy(text, button) {
1270
+ button.dataset.label ??= button.textContent
1271
+ const done = (label) => {
1272
+ button.textContent = label
1273
+ setTimeout(() => { button.textContent = button.dataset.label }, 1500)
1274
+ }
1275
+ const byHand = () => {
1276
+ const area = document.createElement('textarea')
1277
+ area.value = text
1278
+ area.style.position = 'fixed'
1279
+ area.style.opacity = '0'
1280
+ document.body.append(area)
1281
+ area.select()
1282
+ const ok = document.execCommand('copy')
1283
+ area.remove()
1284
+ done(ok ? 'Copied' : 'Copy failed')
1285
+ }
1286
+ if (navigator.clipboard) navigator.clipboard.writeText(text).then(() => done('Copied'), byHand)
1287
+ else byHand()
1288
+ }
1289
+ document.addEventListener('click', (event) => {
1290
+ const button = event.target.closest('button')
1291
+ if (!button) return
1292
+ if (button.dataset.hide) {
1293
+ button.setAttribute('aria-pressed', String(button.getAttribute('aria-pressed') === 'false'))
1294
+ filter()
1295
+ } else if (button.id === 'expand-all') {
1296
+ const open = tests().some((d) => !d.open)
1297
+ for (const d of tests()) d.open = open
1298
+ syncExpandAll()
1299
+ } else if (button.dataset.show) {
1300
+ const box = button.closest('[data-switch]')
1301
+ for (const b of box.querySelectorAll('[data-show]')) {
1302
+ if (b.closest('[data-switch]') === box) b.setAttribute('aria-pressed', String(b === button))
1303
+ }
1304
+ for (const part of box.querySelectorAll(':scope > [data-part]')) part.hidden = part.dataset.part !== button.dataset.show
1305
+ loadFrames(box)
1306
+ } else if (button.dataset.peek) {
1307
+ const open = button.getAttribute('aria-expanded') !== 'true'
1308
+ button.setAttribute('aria-expanded', String(open))
1309
+ const panel = document.getElementById(button.dataset.peek)
1310
+ panel.hidden = !open
1311
+ if (open) loadFrames(panel)
1312
+ } else if (button.dataset.close) {
1313
+ document.getElementById(button.dataset.close).hidden = true
1314
+ document.querySelector('[data-peek="' + button.dataset.close + '"]')?.setAttribute('aria-expanded', 'false')
1315
+ } else if (button.dataset.copy) {
1316
+ copy(copied(button), button)
1317
+ }
1318
+ })
1319
+ document.addEventListener('input', (event) => {
1320
+ if (event.target.matches('.search')) filter()
1321
+ })
1322
+ document.addEventListener('toggle', (event) => {
1323
+ if (!event.target.matches('details.test')) return
1324
+ syncExpandAll()
1325
+ if (event.target.open) loadFrames(event.target)
1326
+ }, true)
1327
+ // An image's size, once it has loaded.
1328
+ document.addEventListener('load', (event) => {
1329
+ const dims = event.target.tagName === 'IMG' && event.target.closest('.peek-panel, .part')?.querySelector('[data-dims]')
1330
+ if (dims) dims.textContent = event.target.naturalWidth + ' × ' + event.target.naturalHeight + ' · '
1331
+ }, true)
1332
+ document.addEventListener('DOMContentLoaded', () => {
1333
+ syncExpandAll()
1334
+ loadFrames(document)
1335
+ })
1336
+ `;
1337
+
1338
+ // src/junit.ts
1339
+ import os from "node:os";
1340
+ function junitReport(run2) {
1341
+ const suites = run2.collections.map(
1342
+ (collection) => suite(collection, run2.environment, run2.flags ?? {})
1343
+ );
1344
+ const total = suites.reduce(
1345
+ (sum, s) => ({
1346
+ tests: sum.tests + s.counts.tests,
1347
+ failures: sum.failures + s.counts.failures,
1348
+ errors: sum.errors + s.counts.errors,
1349
+ skipped: sum.skipped + s.counts.skipped
1350
+ }),
1351
+ { tests: 0, failures: 0, errors: 0, skipped: 0 }
1352
+ );
1353
+ return [
1354
+ '<?xml version="1.0" encoding="UTF-8"?>',
1355
+ `<testsuites${attributes({
1356
+ name: `ata: ${run2.project}`,
1357
+ ...total,
1358
+ time: seconds(run2.durationMs),
1359
+ timestamp: timestamp(run2.startedAt)
1360
+ })}>`,
1361
+ ...suites.flatMap((s) => s.lines),
1362
+ "</testsuites>",
1363
+ ""
1364
+ ].join("\n");
1365
+ }
1366
+ function suite(collection, environment, flags) {
1367
+ const cases = [];
1368
+ const counts = { tests: 0, failures: 0, errors: 0, skipped: 0 };
1369
+ const { outcome, id } = collection;
1370
+ if (!outcome.ok) {
1371
+ counts.tests = 1;
1372
+ counts.errors = 1;
1373
+ cases.push(
1374
+ testcase(id, "(collection)", 0, [
1375
+ `<error${attributes({ message: outcome.message, type: "collection" })}>${escape2(outcome.message)}</error>`
1376
+ ])
1377
+ );
1378
+ } else {
1379
+ for (const [i, result] of outcome.summary.results.entries()) {
1380
+ counts.tests++;
1381
+ const body = [];
1382
+ if (result.status === "error") {
1383
+ counts.errors++;
1384
+ const { phase, message, stack } = result.error ?? {
1385
+ phase: "http",
1386
+ message: "no response",
1387
+ stack: void 0
1388
+ };
1389
+ body.push(
1390
+ `<error${attributes({ message: `${phase} error: ${message}`, type: phase })}>${escape2(userFrames2(stack) ?? message)}</error>`
1391
+ );
1392
+ } else if (result.status === "fail") {
1393
+ counts.failures++;
1394
+ const failed = result.assertions.filter((a) => a.status === "fail");
1395
+ body.push(
1396
+ `<failure${attributes({
1397
+ message: failed.map(summarize).join("; "),
1398
+ type: "assertion"
1399
+ })}>${escape2(failed.map(describe).join("\n\n"))}</failure>`
1400
+ );
1401
+ } else if (result.status === "skipped") {
1402
+ counts.skipped++;
1403
+ body.push(
1404
+ result.skipped ? `<skipped${attributes({ message: result.skipped.reason })}/>` : "<skipped/>"
1405
+ );
1406
+ }
1407
+ if (result.logs?.length) {
1408
+ const logged = result.logs.map((log) => `[${log.phase} ${log.level}] ${log.message}`);
1409
+ body.push(`<system-out>${escape2(logged.join("\n"))}</system-out>`);
1410
+ }
1411
+ cases.push(
1412
+ testcase(id, iterationName(caseName(result), outcome.steps[i]), result.durationMs, body)
1413
+ );
1414
+ }
1415
+ const unrun = unattempted(outcome.summary);
1416
+ if (unrun > 0) {
1417
+ counts.tests++;
1418
+ counts.skipped++;
1419
+ const message = `${unrun} step${unrun === 1 ? "" : "s"} not run: the collection stopped at its first failure (bail)`;
1420
+ cases.push(testcase(id, "(not run)", 0, [`<skipped${attributes({ message })}/>`]));
1421
+ }
1422
+ }
1423
+ return {
1424
+ counts,
1425
+ lines: [
1426
+ ` <testsuite${attributes({
1427
+ name: id,
1428
+ ...counts,
1429
+ time: seconds(collection.durationMs),
1430
+ timestamp: timestamp(collection.startedAt),
1431
+ hostname: os.hostname()
1432
+ })}>`,
1433
+ " <properties>",
1434
+ ` <property${attributes({ name: "environment", value: environment ?? "" })}/>`,
1435
+ ...Object.entries(flags).map(
1436
+ ([name, value]) => ` <property${attributes({ name: `flag.${name}`, value: String(value) })}/>`
1437
+ ),
1438
+ " </properties>",
1439
+ ...cases.flat(),
1440
+ " </testsuite>"
1441
+ ]
1442
+ };
1443
+ }
1444
+ function testcase(classname, name, durationMs, body) {
1445
+ const open = ` <testcase${attributes({ name, classname, time: seconds(durationMs) })}`;
1446
+ if (body.length === 0) return [`${open}/>`];
1447
+ return [`${open}>`, ...body.map((line) => ` ${line}`), " </testcase>"];
1448
+ }
1449
+ var caseName = (result) => result.use ? `${resultName(result)} [${result.use.set} ${result.use.child + 1}/${result.use.of}]` : resultName(result);
1450
+ var summarize = (a) => a.message ? `${a.name}: ${a.message}` : a.name;
1451
+ function describe(a) {
1452
+ const lines = [summarize(a)];
1453
+ if (a.expected !== void 0) lines.push(` expected: ${a.expected}`);
1454
+ if (a.actual !== void 0) lines.push(` actual: ${a.actual}`);
1455
+ if (a.unasserted?.length) lines.push(` not asserted: ${a.unasserted.join(", ")}`);
1456
+ return lines.join("\n");
1457
+ }
1458
+ var userFrames2 = (stack) => stack?.split("\n").filter((line) => !/^\s+at .*(\bnode:|file:\/\/)/.test(line)).join("\n");
1459
+ var seconds = (ms) => (ms / 1e3).toFixed(3);
1460
+ var timestamp = (epochMs) => new Date(epochMs).toISOString().slice(0, 19);
1461
+ function attributes(values) {
1462
+ return Object.entries(values).map(([key, value]) => ` ${key}="${escapeAttribute(String(value))}"`).join("");
1463
+ }
1464
+ var escapeAttribute = (value) => escape2(value).replace(/"/g, "&quot;").replace(/\n/g, "&#10;").replace(/\r/g, "&#13;").replace(/\t/g, "&#9;");
1465
+ function escape2(value) {
1466
+ let kept = "";
1467
+ for (const char of value) if (allowedInXml(char.codePointAt(0))) kept += char;
1468
+ return kept.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1469
+ }
1470
+ var allowedInXml = (code) => code === 9 || code === 10 || code === 13 || code >= 32 && code <= 55295 || code >= 57344 && code <= 65533 || code >= 65536;
1471
+
1472
+ // src/main.ts
1473
+ var VERSION = true ? "0.20.0" : "0.0.0-dev";
1474
+ var JUNIT_DIR = "junit";
1475
+ var JUNIT_FILE = "junit.xml";
1476
+ var HTML_DIR = "html";
1477
+ var JSON_DIR = "json";
1478
+ var JSON_FILE = "results.json";
1479
+ var EXIT = { passed: 0, failed: 1, unusable: 2 };
1480
+ async function main(argv, cli) {
1481
+ let ctx = cli;
1482
+ let p = paint(ctx.color);
1483
+ let emit = null;
1484
+ try {
1485
+ const args = parseArgs(argv);
1486
+ if (args.json && !args.help && !args.version && args.command !== null) {
1487
+ emit = (value) => cli.out(JSON.stringify(value, null, 2));
1488
+ ctx = { ...cli, out: () => {
1489
+ }, color: false };
1490
+ p = paint(false);
1491
+ }
1492
+ if (args.version) {
1493
+ ctx.out(VERSION);
1494
+ return EXIT.passed;
1495
+ }
1496
+ if (args.help || args.command === null || args.command === "help") {
1497
+ ctx.out(usage(p));
1498
+ return EXIT.passed;
1499
+ }
1500
+ const command = args.command;
1501
+ if (command === "lint" || command === "rules") {
1502
+ const given = [
1503
+ ...Object.keys(args.overrides).map((key) => `--${key}`),
1504
+ ...args.flags.map(() => "--flag")
1505
+ ];
1506
+ if (given.length > 0) {
1507
+ throw new UsageError(`ata ${command} takes no settings or flags: ${given.join(", ")}`);
1508
+ }
1509
+ const root = await canonical(ctx.cwd);
1510
+ const output = { out: ctx.out, emit, p };
1511
+ if (command === "rules") {
1512
+ await rulesCommand(root, output);
1513
+ return EXIT.passed;
1514
+ }
1515
+ return await lintCommand(root, output) ? EXIT.failed : EXIT.passed;
1516
+ }
1517
+ const project = await openProject(ctx, args.overrides, args.flags);
1518
+ for (const warning of project.warnings) ctx.err(p.yellow(`warning: ${warning}`));
1519
+ if (command === "g" || command === "get") {
1520
+ const selection = selectFor(project, { kind: "all" });
1521
+ const problems = await listingProblems(project, selection);
1522
+ const code = problems.length > 0 || selection.targets.some((target) => target.broken) ? EXIT.failed : EXIT.passed;
1523
+ if (emit) {
1524
+ emit(
1525
+ jsonListing({
1526
+ project: { name: project.name, root: project.root },
1527
+ environments: project.environments,
1528
+ environmentType: project.settings.environmentType,
1529
+ settings: { values: project.settings, sources: project.settingSources },
1530
+ flags: project.flags,
1531
+ tags: project.settings.tags,
1532
+ notTags: project.settings.notTags,
1533
+ targets: selection.targets,
1534
+ excluded: selection.excluded,
1535
+ excludedSteps: selection.excludedSteps,
1536
+ untagged: selection.untagged,
1537
+ problems
1538
+ })
1539
+ );
1540
+ return code;
1541
+ }
1542
+ list(project, selection, problems, ctx, p);
1543
+ return code;
1544
+ }
1545
+ const request = command === "a" || command === "all" ? { kind: "all" } : { kind: "list", selectors: command.split(",").filter((s) => s.trim() !== "") };
1546
+ return await run(project, selectFor(project, request), ctx, p, emit);
1547
+ } catch (cause) {
1548
+ if (cause instanceof UsageError || cause instanceof SettingsError || cause instanceof ProjectError || cause instanceof RulesError) {
1549
+ ctx.err(p.red(cause.message));
1550
+ const json = emit ?? (argv.includes("--json") ? (v) => cli.out(JSON.stringify(v, null, 2)) : null);
1551
+ json?.(jsonError(cause.message, EXIT.unusable));
1552
+ return EXIT.unusable;
1553
+ }
1554
+ throw cause;
1555
+ }
1556
+ }
1557
+ var openProject = async (ctx, overrides, flagArgs = []) => loadProject({ root: await canonical(ctx.cwd), env: ctx.env, overrides, flagArgs });
1558
+ var selectFor = (project, request) => selectCollections(project.collections, request, {
1559
+ tags: project.settings.tags,
1560
+ notTags: project.settings.notTags
1561
+ });
1562
+ async function listingProblems(project, selection) {
1563
+ const listed = new Set(selection.targets.map((target) => target.id));
1564
+ const stepsExcluded = new Set(selection.excludedSteps.map((step) => step.id));
1565
+ const problems = [];
1566
+ for (const collection of project.collections) {
1567
+ const id = idOf(collection);
1568
+ const leftOut = listed.has(id) ? null : collection.doc.exclude === true || stepsExcluded.has(id) ? "excluded" : "tags";
1569
+ if (collection.problems.length > 0) {
1570
+ const message = collection.problems.map((problem) => problem.message).join("; ");
1571
+ if (leftOut) problems.push({ id, leftOut, step: null, message });
1572
+ continue;
1573
+ }
1574
+ for (const problem of await referenceProblems(collection.doc, collection.path)) {
1575
+ problems.push({ id, leftOut, ...problem });
1576
+ }
1577
+ }
1578
+ return problems;
1579
+ }
1580
+ function list(project, selection, problems, ctx, p) {
1581
+ const { settings } = project;
1582
+ ctx.out(field("Project", `${project.name} ${p.dim(project.root)}`));
1583
+ ctx.out(
1584
+ field(
1585
+ "Environments",
1586
+ project.environments.length > 0 ? project.environments.map((name) => name === settings.environmentType ? p.bold(`${name} \u25C0`) : name).join(", ") : p.dim("none")
1587
+ )
1588
+ );
1589
+ ctx.out(field("Flags", flagLine(project.flags, p)));
1590
+ if (project.caFiles.length > 0) ctx.out(field("CA files", project.caFiles.join(", ")));
1591
+ for (const line of settingLines(project, p)) ctx.out(line);
1592
+ ctx.out("");
1593
+ const width = nameWidthFor(selection.targets.map((t) => t.id));
1594
+ const dirWidth = Math.max(
1595
+ "Folder".length,
1596
+ ...selection.targets.map((t) => (t.collection.directory ?? "").length)
1597
+ );
1598
+ ctx.out(
1599
+ p.bold(
1600
+ `${"#".padStart(4)} ${"Collection".padEnd(width)} ${"Steps".padStart(7)} ${"Rows".padStart(5)} ${"Folder".padEnd(dirWidth)} Tags`
1601
+ )
1602
+ );
1603
+ selection.targets.forEach((target, i) => {
1604
+ const { doc } = target.collection;
1605
+ const count = target.broken ? "broken" : target.steps ? `${target.steps.length}/${doc.steps.length}` : String(doc.steps.length);
1606
+ const steps2 = target.broken ? p.red(count.padStart(7)) : count.padStart(7);
1607
+ const tags = [.../* @__PURE__ */ new Set([...doc.tags ?? [], ...doc.steps.flatMap((s) => s.tags ?? [])])];
1608
+ const rows = target.collection.dataFile ? String(target.collection.dataFile.rows) : "";
1609
+ ctx.out(
1610
+ `${String(i + 1).padStart(4)} ${nameCell(target.id, width)} ${steps2} ${rows.padStart(5)} ${(target.collection.directory ?? "").padEnd(dirWidth)} ${p.dim(tags.join(", "))}`
1611
+ );
1612
+ });
1613
+ ctx.out("");
1614
+ for (const target of selection.targets.filter((t) => t.broken)) {
1615
+ ctx.out(`${p.red("broken")} ${target.id}: ${target.broken}`);
1616
+ }
1617
+ const leftOut = { excluded: " (excluded)", tags: " (left out by tags)" };
1618
+ for (const problem of problems) {
1619
+ const step = problem.step === null ? "" : `, ${problem.stage ? `${problem.stage} ` : ""}step ${problem.step + 1}`;
1620
+ const where = `${problem.id}${problem.leftOut ? leftOut[problem.leftOut] : ""}${step}`;
1621
+ ctx.out(`${p.red("broken")} ${where}: ${problem.message}`);
1622
+ }
1623
+ const excluded = [
1624
+ ...selection.excluded,
1625
+ ...selection.excludedSteps.map((step) => `${step.id} \u203A ${step.name}`)
1626
+ ];
1627
+ if (excluded.length > 0) {
1628
+ ctx.out(field("Excluded", `${excluded.join(", ")} ${p.dim("(exclude: true)")}`));
1629
+ }
1630
+ if (selection.untagged > 0) {
1631
+ ctx.out(field("By tags", `${selection.untagged} collection(s) left out by tags`));
1632
+ }
1633
+ const steps = selection.targets.reduce((n, t) => n + stepCountOf(t), 0);
1634
+ ctx.out(field("Total", `${selection.targets.length} collections, ${steps} steps`));
1635
+ }
1636
+ function settingLines(project, p) {
1637
+ const rows = Object.keys(SETTING_KEYS).filter((key) => project.settingSources[key] !== DEFAULT_SOURCE).map((key) => ({
1638
+ cell: `${key}: ${shownSetting(project.settings[key])}`,
1639
+ source: project.settingSources[key]
1640
+ }));
1641
+ if (rows.length === 0) return [field("Settings", p.dim("all defaults"))];
1642
+ const width = Math.max(...rows.map((row) => row.cell.length));
1643
+ const indent = " ".repeat(field("Settings", "").length);
1644
+ return rows.map(
1645
+ (row, i) => `${i === 0 ? field("Settings", "") : indent}${row.cell.padEnd(width)} ${p.dim(row.source)}`
1646
+ );
1647
+ }
1648
+ var shownSetting = (value) => Array.isArray(value) ? value.length > 0 ? value.join(", ") : "[]" : String(value ?? "none");
1649
+ async function run(project, selection, ctx, p, emit = null) {
1650
+ const { settings } = project;
1651
+ const width = nameWidthFor(selection.targets.map((t) => t.id));
1652
+ const line = rule(p, tableWidth(width));
1653
+ ctx.out(line);
1654
+ ctx.out(p.bold(`ata ${VERSION}`));
1655
+ ctx.out(line);
1656
+ ctx.out(field("Project", `${project.name} ${p.dim(project.root)}`));
1657
+ ctx.out(field("Environment", settings.environmentType ?? p.dim("none")));
1658
+ ctx.out(field("Concurrency", String(settings.limitConcurrency)));
1659
+ ctx.out(field("Flags", flagLine(project.flags, p)));
1660
+ ctx.out(field("Timeout", `${settings.timeoutCollection} ms per collection`));
1661
+ if (project.caFiles.length > 0) ctx.out(field("CA files", project.caFiles.join(", ")));
1662
+ if (settings.tags.length > 0) ctx.out(field("Tags", settings.tags.join(", ")));
1663
+ if (settings.notTags.length > 0) ctx.out(field("Not tags", settings.notTags.join(", ")));
1664
+ if (settings.bail) ctx.out(field("Bail", "on: a collection stops at its first failing step"));
1665
+ const reportNames = [
1666
+ settings.generateJUnitResults ? `${JUNIT_DIR}/${JUNIT_FILE}` : "",
1667
+ settings.generateJsonResults ? `${JSON_DIR}/${JSON_FILE}` : "",
1668
+ wantsHtml(settings) ? `${HTML_DIR}/${SUMMARY_PAGE}` : ""
1669
+ ].filter(Boolean);
1670
+ if (reportNames.length > 0) {
1671
+ ctx.out(
1672
+ field("Reports", reportNames.map((n) => `${settings.testResultsBasePath}/${n}`).join(", "))
1673
+ );
1674
+ }
1675
+ const excludedSteps = selection.excludedSteps.length;
1676
+ const leftOut = [
1677
+ selection.excluded.length > 0 ? `${selection.excluded.length} excluded` : "",
1678
+ excludedSteps > 0 ? `${excludedSteps} ${excludedSteps === 1 ? "step" : "steps"} excluded` : "",
1679
+ selection.untagged > 0 ? `${selection.untagged} left out by tags` : ""
1680
+ ].filter(Boolean);
1681
+ ctx.out(
1682
+ field(
1683
+ "Collections",
1684
+ `${selection.targets.length} to run${leftOut.length > 0 ? p.dim(` (${leftOut.join(", ")})`) : ""}`
1685
+ )
1686
+ );
1687
+ ctx.out(line);
1688
+ if (selection.targets.length === 0) {
1689
+ ctx.err(p.red("Nothing to run: no collection matched."));
1690
+ emit?.(jsonError("Nothing to run: no collection matched.", EXIT.failed));
1691
+ return EXIT.failed;
1692
+ }
1693
+ ctx.out(tableHeader(width, p));
1694
+ const base = settings.testResultsBasePath;
1695
+ const reportsDir = path2.isAbsolute(base) ? path2.resolve(base) : path2.resolve(project.root, ...base.split(/[\\/]+/));
1696
+ const htmlDir = path2.join(reportsDir, HTML_DIR);
1697
+ const problem = await clearResults(reportsDir, project.root);
1698
+ if (problem) {
1699
+ ctx.err(p.red(problem));
1700
+ emit?.(jsonError(problem, EXIT.unusable));
1701
+ return EXIT.unusable;
1702
+ }
1703
+ const runner = ctx.runner(settings);
1704
+ const tallies = [];
1705
+ const finished = /* @__PURE__ */ new Map();
1706
+ const startedAt = Date.now();
1707
+ const started = performance.now();
1708
+ await runPool(selection.targets, settings.limitConcurrency, async (target) => {
1709
+ const collectionStartedAt = Date.now();
1710
+ const collectionStarted = performance.now();
1711
+ const outcome = target.broken ? { ok: false, message: target.broken } : await runner({
1712
+ file: target.collection.path,
1713
+ projectRoot: project.root,
1714
+ environment: settings.environmentType,
1715
+ steps: target.steps,
1716
+ bail: settings.bail,
1717
+ flags: project.flags.values
1718
+ });
1719
+ const collectionMs = performance.now() - collectionStarted;
1720
+ const result = tally(target, outcome, collectionMs);
1721
+ finished.set(target, {
1722
+ id: target.id,
1723
+ file: `${COLLECTIONS_DIR}/${target.collection.relativePath}`,
1724
+ outcome,
1725
+ startedAt: collectionStartedAt,
1726
+ durationMs: collectionMs,
1727
+ tally: result,
1728
+ docs: target.collection.doc.docs
1729
+ });
1730
+ tallies.push(result);
1731
+ ctx.out(tableRow(tallies.length, result, width, p));
1732
+ });
1733
+ const durationMs = performance.now() - started;
1734
+ ctx.out(line);
1735
+ const details = failureDetails(tallies, p);
1736
+ if (details.length > 0) {
1737
+ ctx.out(p.bold("Failures"));
1738
+ ctx.out("");
1739
+ for (const detail of details) ctx.out(detail);
1740
+ ctx.out(line);
1741
+ }
1742
+ const totals = totalsOf(tallies);
1743
+ for (const total of summary(totals, durationMs, p)) ctx.out(total);
1744
+ const run2 = {
1745
+ flags: project.flags.values,
1746
+ flagSources: project.flags.sources,
1747
+ flagCommand: project.flags.command?.command ?? null,
1748
+ project: project.name,
1749
+ root: project.root,
1750
+ version: VERSION,
1751
+ environment: settings.environmentType,
1752
+ concurrency: settings.limitConcurrency,
1753
+ timeoutCollection: settings.timeoutCollection,
1754
+ tags: settings.tags,
1755
+ notTags: settings.notTags,
1756
+ bail: settings.bail,
1757
+ excluded: selection.excluded,
1758
+ excludedSteps: selection.excludedSteps,
1759
+ untagged: selection.untagged,
1760
+ startedAt,
1761
+ durationMs,
1762
+ // In the order selected, whichever finished first, so two reports compare.
1763
+ collections: selection.targets.flatMap((target) => finished.get(target) ?? [])
1764
+ };
1765
+ const reports = [];
1766
+ if (settings.generateJUnitResults) {
1767
+ reports.push({
1768
+ label: "JUnit",
1769
+ dir: path2.join(reportsDir, JUNIT_DIR),
1770
+ shown: JUNIT_FILE,
1771
+ files: () => [{ path: JUNIT_FILE, text: junitReport(run2) }]
1772
+ });
1773
+ }
1774
+ const paths = {
1775
+ ...settings.generateJUnitResults ? { junit: path2.join(reportsDir, JUNIT_DIR, JUNIT_FILE) } : {},
1776
+ ...wantsHtml(settings) ? { html: path2.join(htmlDir, SUMMARY_PAGE) } : {},
1777
+ ...settings.generateJsonResults ? { json: path2.join(reportsDir, JSON_DIR, JSON_FILE) } : {}
1778
+ };
1779
+ if (settings.generateJsonResults) {
1780
+ reports.push({
1781
+ label: "JSON",
1782
+ dir: path2.join(reportsDir, JSON_DIR),
1783
+ shown: JSON_FILE,
1784
+ files: () => [
1785
+ { path: JSON_FILE, text: `${JSON.stringify(jsonReport(run2, paths), null, 2)}
1786
+ ` }
1787
+ ]
1788
+ });
1789
+ }
1790
+ if (wantsHtml(settings)) {
1791
+ reports.push({
1792
+ label: "HTML",
1793
+ dir: htmlDir,
1794
+ shown: SUMMARY_PAGE,
1795
+ files: () => htmlReport(run2).map((page2) => ({ path: page2.path, text: page2.html }))
1796
+ });
1797
+ }
1798
+ let reportFailed = false;
1799
+ for (const report of reports) {
1800
+ const shown = path2.join(report.dir, report.shown);
1801
+ try {
1802
+ for (const file of report.files()) {
1803
+ const target = path2.join(report.dir, ...file.path.split("/"));
1804
+ await fs.mkdir(path2.dirname(target), { recursive: true });
1805
+ await fs.writeFile(target, file.text);
1806
+ }
1807
+ ctx.out(field(report.label, shown));
1808
+ if (report.label === "HTML" && settings.autoOpenTestResultHtml) ctx.open(shown);
1809
+ } catch (cause) {
1810
+ ctx.err(
1811
+ p.red(`Could not write the ${report.label} report to ${shown}: ${cause.message}`)
1812
+ );
1813
+ reportFailed = true;
1814
+ }
1815
+ }
1816
+ ctx.out(line);
1817
+ emit?.(jsonReport(run2, paths));
1818
+ if (reportFailed) return EXIT.unusable;
1819
+ return totals.collections.failed === 0 ? EXIT.passed : EXIT.failed;
1820
+ }
1821
+ function usage(p) {
1822
+ const settings = Object.keys(SETTING_KEYS).flatMap((key) => {
1823
+ const fallback = DEFAULT_SETTINGS[key];
1824
+ const shown = Array.isArray(fallback) ? fallback.length > 0 ? fallback.join(",") : "none" : String(fallback ?? "none");
1825
+ return [
1826
+ ` ${p.bold(`--${key}`)} ${p.dim(`(default ${shown}; ${envNameOf(key)})`)}`,
1827
+ ` ${SETTING_KEYS[key].doc}`
1828
+ ];
1829
+ });
1830
+ return [
1831
+ `${p.bold("ata")} ${VERSION} \u2014 Accelerator Test Automation`,
1832
+ "",
1833
+ ` ${p.bold("Usage:")} ata <command> [--settingsKey value]...`,
1834
+ "",
1835
+ " Run from a project folder: the one holding collections/ and settings.yml.",
1836
+ "",
1837
+ ` ${p.bold("Commands:")}`,
1838
+ " g, get List the collections `ata all` would run, without running them.",
1839
+ " Exits 1 on a broken collection, or a use:, extends: or body",
1840
+ " file that would stop a run, in any collection.",
1841
+ " a, all Run every collection, less those and the steps with",
1842
+ " exclude: true.",
1843
+ " <list> Run the collections and folders named, comma separated,",
1844
+ " in that order: smoke,checkout/sessions,payments",
1845
+ " A folder leaves out its excluded collections and steps;",
1846
+ " a collection named on its own runs every step.",
1847
+ " A collection called all, get, lint or rules: give it as all.yml.",
1848
+ " lint Check the project\u2019s files against its rules: its rules.yml,",
1849
+ " over its global project\u2019s. Exits 1 on a finding.",
1850
+ " rules List the rules in effect, the file each came from, what each",
1851
+ " means, and the project\u2019s guide. Neither needs settings.yml.",
1852
+ "",
1853
+ ` ${p.bold("Options:")}`,
1854
+ " --json Print the results as JSON on stdout instead of the table; with",
1855
+ " get, the collections it would run; with lint and rules, the",
1856
+ " findings and the rules. Errors are JSON too.",
1857
+ " --flag name=value",
1858
+ " Override a feature flag for this run; any number of them.",
1859
+ " ATA_FLAG_<name>=value does the same from the environment.",
1860
+ "",
1861
+ ` ${p.bold("Settings")} (the global project's settings.yml, then this project's, then`,
1862
+ " ATA_* environment variables, then --flags; ata get shows where each came from):",
1863
+ ...settings,
1864
+ "",
1865
+ ` ${p.bold("Exit code:")} 0 passed, 1 failed, 2 could not run (or write a report).`
1866
+ ].join("\n");
1867
+ }
1868
+ var wantsHtml = (settings) => settings.generateHtmlResults || settings.autoOpenTestResultHtml;
1869
+ var RESULT_DIRS = {
1870
+ [JUNIT_DIR]: (name) => name === JUNIT_FILE,
1871
+ [HTML_DIR]: (name) => name.endsWith(".html"),
1872
+ [JSON_DIR]: (name) => name === JSON_FILE
1873
+ };
1874
+ var SYSTEM_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db"]);
1875
+ async function clearResults(dir, projectRoot) {
1876
+ if (samePath(dir, projectRoot) || isInside(dir, projectRoot)) {
1877
+ return `testResultsBasePath is ${dir}, which holds the project: ata deletes its test results folder before each run, so point it at a folder of its own.`;
1878
+ }
1879
+ let entries;
1880
+ try {
1881
+ entries = await fs.readdir(dir);
1882
+ } catch {
1883
+ return null;
1884
+ }
1885
+ const others = entries.filter((entry) => !(entry in RESULT_DIRS) && !SYSTEM_FILES.has(entry));
1886
+ for (const [folder, ours] of Object.entries(RESULT_DIRS)) {
1887
+ if (!entries.includes(folder)) continue;
1888
+ const inside = await fs.readdir(path2.join(dir, folder)).catch(() => []);
1889
+ others.push(
1890
+ ...inside.filter((name) => !ours(name) && !SYSTEM_FILES.has(name)).map((name) => `${folder}/${name}`)
1891
+ );
1892
+ }
1893
+ if (others.length > 0) {
1894
+ return `${dir} holds files ata did not write (${others.slice(0, 5).join(", ")}${others.length > 5 ? ", \u2026" : ""}), so it will not delete it before the run. Point testResultsBasePath somewhere else, or remove them.`;
1895
+ }
1896
+ try {
1897
+ await fs.rm(dir, { recursive: true, force: true });
1898
+ return null;
1899
+ } catch (cause) {
1900
+ return `Could not delete ${dir} before the run: ${cause.message}`;
1901
+ }
1902
+ }
1903
+ function flagLine(flags, p) {
1904
+ const list2 = flagList(flags.values);
1905
+ const overridden = Object.values(flags.sources).filter((s) => s === "override").length;
1906
+ const notes = [
1907
+ flags.command ? `from ${flags.command.command}` : "",
1908
+ overridden > 0 ? `${overridden} overridden` : ""
1909
+ ].filter(Boolean);
1910
+ return notes.length > 0 ? `${list2} ${p.dim(`(${notes.join("; ")})`)}` : list2;
1911
+ }
1912
+
1913
+ // src/open.ts
1914
+ import { spawn } from "node:child_process";
1915
+ function openFile(file) {
1916
+ const [command, args] = process.platform === "darwin" ? ["open", [file]] : process.platform === "win32" ? ["explorer.exe", [file]] : ["xdg-open", [file]];
1917
+ try {
1918
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
1919
+ child.on("error", () => {
1920
+ });
1921
+ child.unref();
1922
+ } catch {
1923
+ }
1924
+ }
1925
+
1926
+ // src/ata.ts
1927
+ var workerUrl = new URL("./worker.js", import.meta.url);
1928
+ process.exitCode = await main(process.argv.slice(2), {
1929
+ cwd: process.cwd(),
1930
+ env: process.env,
1931
+ out: (line) => process.stdout.write(`${line}
1932
+ `),
1933
+ err: (line) => process.stderr.write(`${line}
1934
+ `),
1935
+ color: colorWanted(process.stdout, process.env),
1936
+ runner: (settings) => workerRunner(workerUrl, settings.timeoutCollection),
1937
+ open: openFile
1938
+ });