executable-stories-formatters 0.7.14 → 0.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +489 -60
- package/dist/cli.js.map +1 -1
- package/dist/{index-fqrm5-Xr.d.cts → index-BiAYcEiz.d.cts} +1 -1
- package/dist/{index-fqrm5-Xr.d.ts → index-BiAYcEiz.d.ts} +1 -1
- package/dist/index.cjs +460 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +274 -13
- package/dist/index.d.ts +274 -13
- package/dist/index.js +454 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/story-report-v1.json +410 -0
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
5
|
import * as fs8 from "fs";
|
|
6
|
-
import * as
|
|
6
|
+
import * as path9 from "path";
|
|
7
7
|
|
|
8
8
|
// src/validation/schema-validator.ts
|
|
9
9
|
import Ajv from "ajv/dist/2020.js";
|
|
@@ -492,17 +492,17 @@ function validateRawRun(data) {
|
|
|
492
492
|
return { valid: true, errors: [] };
|
|
493
493
|
}
|
|
494
494
|
const errors = (validate.errors ?? []).map((err) => {
|
|
495
|
-
const
|
|
495
|
+
const path10 = err.instancePath || "/";
|
|
496
496
|
const message = err.message ?? "unknown error";
|
|
497
497
|
if (err.keyword === "additionalProperties") {
|
|
498
498
|
const extra = err.params.additionalProperty;
|
|
499
|
-
return `${
|
|
499
|
+
return `${path10}: ${message} \u2014 '${extra}'`;
|
|
500
500
|
}
|
|
501
501
|
if (err.keyword === "enum") {
|
|
502
502
|
const allowed = err.params.allowedValues;
|
|
503
|
-
return `${
|
|
503
|
+
return `${path10}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
|
|
504
504
|
}
|
|
505
|
-
return `${
|
|
505
|
+
return `${path10}: ${message}`;
|
|
506
506
|
});
|
|
507
507
|
return { valid: false, errors };
|
|
508
508
|
}
|
|
@@ -976,7 +976,7 @@ ${result.errors.join("\n")}`);
|
|
|
976
976
|
|
|
977
977
|
// src/index.ts
|
|
978
978
|
import "fs";
|
|
979
|
-
import * as
|
|
979
|
+
import * as path7 from "path";
|
|
980
980
|
import * as fsPromises from "fs/promises";
|
|
981
981
|
|
|
982
982
|
// src/converters/acl/lines.ts
|
|
@@ -1355,9 +1355,298 @@ ${doc.markdown}`,
|
|
|
1355
1355
|
}
|
|
1356
1356
|
};
|
|
1357
1357
|
|
|
1358
|
+
// src/converters/story-report.ts
|
|
1359
|
+
import { posix as path2 } from "path";
|
|
1360
|
+
|
|
1361
|
+
// src/types/story-report.ts
|
|
1362
|
+
var STORY_REPORT_SCHEMA_VERSION = "1.0";
|
|
1363
|
+
|
|
1364
|
+
// src/converters/story-report.ts
|
|
1365
|
+
function reportSlug(text2) {
|
|
1366
|
+
return text2.toLowerCase().replace(/[/\\.]+/g, "-").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
1367
|
+
}
|
|
1368
|
+
function toRelativeSourceFile(sourceFile, projectRoot) {
|
|
1369
|
+
if (!sourceFile) return sourceFile;
|
|
1370
|
+
const normalized = sourceFile.split(path2.sep).join("/");
|
|
1371
|
+
const root = projectRoot.split(path2.sep).join("/").replace(/\/$/, "");
|
|
1372
|
+
if (root && normalized.startsWith(root + "/")) return normalized.slice(root.length + 1);
|
|
1373
|
+
return normalized;
|
|
1374
|
+
}
|
|
1375
|
+
function fileBasenameTitle(sourceFile) {
|
|
1376
|
+
const base = sourceFile.split("/").pop() ?? sourceFile;
|
|
1377
|
+
return base.replace(/\.(story\.)?(test|spec)\.[tj]sx?$/, "").replace(/\.[tj]sx?$/, "");
|
|
1378
|
+
}
|
|
1379
|
+
function emptySummary() {
|
|
1380
|
+
return { total: 0, passed: 0, failed: 0, skipped: 0, pending: 0, durationMs: 0 };
|
|
1381
|
+
}
|
|
1382
|
+
function addToSummary(summary, status, durationMs) {
|
|
1383
|
+
summary.total += 1;
|
|
1384
|
+
summary[status] += 1;
|
|
1385
|
+
summary.durationMs += durationMs;
|
|
1386
|
+
}
|
|
1387
|
+
function isKeyword(value) {
|
|
1388
|
+
return value === "Given" || value === "When" || value === "Then" || value === "And" || value === "But";
|
|
1389
|
+
}
|
|
1390
|
+
function copyDocEntries(entries) {
|
|
1391
|
+
if (!entries || entries.length === 0) return [];
|
|
1392
|
+
return entries.map(copyDocEntry);
|
|
1393
|
+
}
|
|
1394
|
+
function copyDocEntry(entry) {
|
|
1395
|
+
const children = entry.children ? { children: copyDocEntries(entry.children) } : {};
|
|
1396
|
+
switch (entry.kind) {
|
|
1397
|
+
case "note":
|
|
1398
|
+
return { kind: "note", text: entry.text, phase: entry.phase, ...children };
|
|
1399
|
+
case "tag":
|
|
1400
|
+
return { kind: "tag", names: [...entry.names], phase: entry.phase, ...children };
|
|
1401
|
+
case "kv":
|
|
1402
|
+
return { kind: "kv", label: entry.label, value: entry.value, phase: entry.phase, ...children };
|
|
1403
|
+
case "code":
|
|
1404
|
+
return {
|
|
1405
|
+
kind: "code",
|
|
1406
|
+
label: entry.label,
|
|
1407
|
+
content: entry.content,
|
|
1408
|
+
...entry.lang ? { lang: entry.lang } : {},
|
|
1409
|
+
phase: entry.phase,
|
|
1410
|
+
...children
|
|
1411
|
+
};
|
|
1412
|
+
case "table":
|
|
1413
|
+
return {
|
|
1414
|
+
kind: "table",
|
|
1415
|
+
label: entry.label,
|
|
1416
|
+
columns: [...entry.columns],
|
|
1417
|
+
rows: entry.rows.map((r) => [...r]),
|
|
1418
|
+
phase: entry.phase,
|
|
1419
|
+
...children
|
|
1420
|
+
};
|
|
1421
|
+
case "link":
|
|
1422
|
+
return { kind: "link", label: entry.label, url: entry.url, phase: entry.phase, ...children };
|
|
1423
|
+
case "section":
|
|
1424
|
+
return { kind: "section", title: entry.title, markdown: entry.markdown, phase: entry.phase, ...children };
|
|
1425
|
+
case "mermaid":
|
|
1426
|
+
return {
|
|
1427
|
+
kind: "mermaid",
|
|
1428
|
+
code: entry.code,
|
|
1429
|
+
...entry.title ? { title: entry.title } : {},
|
|
1430
|
+
phase: entry.phase,
|
|
1431
|
+
...children
|
|
1432
|
+
};
|
|
1433
|
+
case "screenshot":
|
|
1434
|
+
return {
|
|
1435
|
+
kind: "screenshot",
|
|
1436
|
+
path: entry.path,
|
|
1437
|
+
...entry.alt ? { alt: entry.alt } : {},
|
|
1438
|
+
phase: entry.phase,
|
|
1439
|
+
...children
|
|
1440
|
+
};
|
|
1441
|
+
case "custom":
|
|
1442
|
+
return {
|
|
1443
|
+
kind: "custom",
|
|
1444
|
+
type: entry.type,
|
|
1445
|
+
data: entry.data,
|
|
1446
|
+
phase: entry.phase,
|
|
1447
|
+
...children
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
function buildStep(args) {
|
|
1452
|
+
const step = {
|
|
1453
|
+
id: `${args.scenarioId}--step-${args.index}`,
|
|
1454
|
+
index: args.index,
|
|
1455
|
+
keyword: args.keyword,
|
|
1456
|
+
text: args.text,
|
|
1457
|
+
status: args.status,
|
|
1458
|
+
durationMs: args.durationMs,
|
|
1459
|
+
docEntries: args.docEntries
|
|
1460
|
+
};
|
|
1461
|
+
if (args.errorMessage !== void 0) step.errorMessage = args.errorMessage;
|
|
1462
|
+
if (args.mode !== void 0) step.mode = args.mode;
|
|
1463
|
+
return step;
|
|
1464
|
+
}
|
|
1465
|
+
function buildSteps(scenarioId, tc) {
|
|
1466
|
+
const declared = tc.story.steps ?? [];
|
|
1467
|
+
const results = tc.stepResults;
|
|
1468
|
+
const max = Math.max(declared.length, results.length);
|
|
1469
|
+
const steps = [];
|
|
1470
|
+
for (let i = 0; i < max; i++) {
|
|
1471
|
+
const decl = declared[i];
|
|
1472
|
+
const res = results[i];
|
|
1473
|
+
const keywordSource = decl?.keyword;
|
|
1474
|
+
const keyword = keywordSource && isKeyword(keywordSource) ? keywordSource : "Given";
|
|
1475
|
+
const text2 = decl?.text ?? "";
|
|
1476
|
+
const status = res?.status ?? "pending";
|
|
1477
|
+
const durationMs = res?.durationMs ?? decl?.durationMs ?? 0;
|
|
1478
|
+
const docEntries = copyDocEntries(decl?.docs);
|
|
1479
|
+
steps.push(buildStep({
|
|
1480
|
+
scenarioId,
|
|
1481
|
+
index: i,
|
|
1482
|
+
keyword,
|
|
1483
|
+
text: text2,
|
|
1484
|
+
status,
|
|
1485
|
+
durationMs,
|
|
1486
|
+
...res?.errorMessage !== void 0 ? { errorMessage: res.errorMessage } : {},
|
|
1487
|
+
...decl?.mode !== void 0 ? { mode: decl.mode } : {},
|
|
1488
|
+
docEntries
|
|
1489
|
+
}));
|
|
1490
|
+
}
|
|
1491
|
+
return steps;
|
|
1492
|
+
}
|
|
1493
|
+
function buildAttachments(tc) {
|
|
1494
|
+
return tc.attachments.map((a) => ({
|
|
1495
|
+
name: a.name,
|
|
1496
|
+
mediaType: a.mediaType,
|
|
1497
|
+
body: a.body,
|
|
1498
|
+
contentEncoding: a.contentEncoding
|
|
1499
|
+
}));
|
|
1500
|
+
}
|
|
1501
|
+
function buildScenario(tc, featureId) {
|
|
1502
|
+
const titleRaw = tc.story.scenario?.trim() || "(untitled scenario)";
|
|
1503
|
+
const id = `${featureId}--${reportSlug(titleRaw) || `case-${tc.id}`}`;
|
|
1504
|
+
const steps = buildSteps(id, tc);
|
|
1505
|
+
const scenario = {
|
|
1506
|
+
id,
|
|
1507
|
+
title: titleRaw,
|
|
1508
|
+
status: tc.status,
|
|
1509
|
+
durationMs: tc.durationMs,
|
|
1510
|
+
tags: [...tc.tags],
|
|
1511
|
+
retry: tc.retry,
|
|
1512
|
+
retries: tc.retries,
|
|
1513
|
+
docEntries: copyDocEntries(tc.story.docs),
|
|
1514
|
+
steps,
|
|
1515
|
+
attachments: buildAttachments(tc)
|
|
1516
|
+
};
|
|
1517
|
+
if (tc.sourceLine && tc.sourceLine > 0) scenario.sourceLine = tc.sourceLine;
|
|
1518
|
+
if (tc.errorMessage !== void 0) scenario.errorMessage = tc.errorMessage;
|
|
1519
|
+
if (tc.errorStack !== void 0) scenario.errorStack = tc.errorStack;
|
|
1520
|
+
const tickets = tc.story.tickets;
|
|
1521
|
+
if (tickets && tickets.length > 0) {
|
|
1522
|
+
scenario.tickets = tickets.map((t) => t.url ? { id: t.id, url: t.url } : { id: t.id });
|
|
1523
|
+
}
|
|
1524
|
+
return scenario;
|
|
1525
|
+
}
|
|
1526
|
+
function deriveFeatureTitle(group, relSourceFile) {
|
|
1527
|
+
for (const tc of group) {
|
|
1528
|
+
const head = tc.titlePath?.[0];
|
|
1529
|
+
if (head && head.trim()) return head.trim();
|
|
1530
|
+
}
|
|
1531
|
+
return fileBasenameTitle(relSourceFile);
|
|
1532
|
+
}
|
|
1533
|
+
function compareScenarios(a, b) {
|
|
1534
|
+
const aLine = a.sourceLine ?? Number.POSITIVE_INFINITY;
|
|
1535
|
+
const bLine = b.sourceLine ?? Number.POSITIVE_INFINITY;
|
|
1536
|
+
if (aLine !== bLine) return aLine - bLine;
|
|
1537
|
+
return a.title.localeCompare(b.title);
|
|
1538
|
+
}
|
|
1539
|
+
function buildFeature(relSourceFile, group) {
|
|
1540
|
+
const id = `feature-${reportSlug(relSourceFile.replace(/\.[^.]+$/, "")) || "untitled"}`;
|
|
1541
|
+
const title = deriveFeatureTitle(group, relSourceFile);
|
|
1542
|
+
const summary = emptySummary();
|
|
1543
|
+
const scenarios = [];
|
|
1544
|
+
for (const tc of group) {
|
|
1545
|
+
const scenario = buildScenario(tc, id);
|
|
1546
|
+
scenarios.push(scenario);
|
|
1547
|
+
addToSummary(summary, scenario.status, scenario.durationMs);
|
|
1548
|
+
}
|
|
1549
|
+
scenarios.sort(compareScenarios);
|
|
1550
|
+
return { id, title, sourceFile: relSourceFile, summary, scenarios };
|
|
1551
|
+
}
|
|
1552
|
+
function ensureUniqueFeatureIds(features) {
|
|
1553
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1554
|
+
for (const f of features) {
|
|
1555
|
+
const count = seen.get(f.id) ?? 0;
|
|
1556
|
+
if (count > 0) f.id = `${f.id}-${count + 1}`;
|
|
1557
|
+
seen.set(f.id, count + 1);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
function ensureUniqueScenarioIds(feature) {
|
|
1561
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1562
|
+
for (const s of feature.scenarios) {
|
|
1563
|
+
const count = seen.get(s.id) ?? 0;
|
|
1564
|
+
if (count > 0) {
|
|
1565
|
+
const newId = `${s.id}-${count + 1}`;
|
|
1566
|
+
for (const step of s.steps) {
|
|
1567
|
+
step.id = step.id.replace(s.id, newId);
|
|
1568
|
+
}
|
|
1569
|
+
s.id = newId;
|
|
1570
|
+
}
|
|
1571
|
+
seen.set(s.id, count + 1);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
function toStoryReport(run) {
|
|
1575
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1576
|
+
for (const tc of run.testCases) {
|
|
1577
|
+
const rel = toRelativeSourceFile(tc.sourceFile, run.projectRoot);
|
|
1578
|
+
const existing = groups.get(rel);
|
|
1579
|
+
if (existing) existing.push(tc);
|
|
1580
|
+
else groups.set(rel, [tc]);
|
|
1581
|
+
}
|
|
1582
|
+
const features = [];
|
|
1583
|
+
for (const [rel, group] of groups) {
|
|
1584
|
+
features.push(buildFeature(rel, group));
|
|
1585
|
+
}
|
|
1586
|
+
features.sort((a, b) => a.title.localeCompare(b.title));
|
|
1587
|
+
ensureUniqueFeatureIds(features);
|
|
1588
|
+
for (const f of features) ensureUniqueScenarioIds(f);
|
|
1589
|
+
const summary = emptySummary();
|
|
1590
|
+
for (const f of features) {
|
|
1591
|
+
summary.total += f.summary.total;
|
|
1592
|
+
summary.passed += f.summary.passed;
|
|
1593
|
+
summary.failed += f.summary.failed;
|
|
1594
|
+
summary.skipped += f.summary.skipped;
|
|
1595
|
+
summary.pending += f.summary.pending;
|
|
1596
|
+
summary.durationMs += f.summary.durationMs;
|
|
1597
|
+
}
|
|
1598
|
+
const report = {
|
|
1599
|
+
schemaVersion: STORY_REPORT_SCHEMA_VERSION,
|
|
1600
|
+
runId: run.runId,
|
|
1601
|
+
startedAtMs: run.startedAtMs,
|
|
1602
|
+
finishedAtMs: run.finishedAtMs,
|
|
1603
|
+
durationMs: run.durationMs,
|
|
1604
|
+
projectRoot: run.projectRoot,
|
|
1605
|
+
summary,
|
|
1606
|
+
features
|
|
1607
|
+
};
|
|
1608
|
+
if (run.packageVersion) report.packageVersion = run.packageVersion;
|
|
1609
|
+
if (run.gitSha) report.gitSha = run.gitSha;
|
|
1610
|
+
if (run.ci) {
|
|
1611
|
+
const ci = { name: run.ci.name };
|
|
1612
|
+
if (run.ci.url) ci.url = run.ci.url;
|
|
1613
|
+
if (run.ci.buildNumber) ci.buildNumber = run.ci.buildNumber;
|
|
1614
|
+
if (run.ci.branch) ci.branch = run.ci.branch;
|
|
1615
|
+
if (run.ci.commitSha) ci.commitSha = run.ci.commitSha;
|
|
1616
|
+
if (run.ci.prNumber) ci.prNumber = run.ci.prNumber;
|
|
1617
|
+
report.ci = ci;
|
|
1618
|
+
}
|
|
1619
|
+
if (run.coverage) {
|
|
1620
|
+
const cov = {};
|
|
1621
|
+
if (run.coverage.linesPct !== void 0) cov.linesPct = run.coverage.linesPct;
|
|
1622
|
+
if (run.coverage.branchesPct !== void 0) cov.branchesPct = run.coverage.branchesPct;
|
|
1623
|
+
if (run.coverage.functionsPct !== void 0) cov.functionsPct = run.coverage.functionsPct;
|
|
1624
|
+
if (run.coverage.statementsPct !== void 0) cov.statementsPct = run.coverage.statementsPct;
|
|
1625
|
+
report.coverage = cov;
|
|
1626
|
+
}
|
|
1627
|
+
return report;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/formatters/story-report-json.ts
|
|
1631
|
+
var StoryReportJsonFormatter = class {
|
|
1632
|
+
options;
|
|
1633
|
+
constructor(options = {}) {
|
|
1634
|
+
this.options = {
|
|
1635
|
+
pretty: options.pretty ?? true
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
toReport(run) {
|
|
1639
|
+
return toStoryReport(run);
|
|
1640
|
+
}
|
|
1641
|
+
format(run) {
|
|
1642
|
+
const report = toStoryReport(run);
|
|
1643
|
+
return this.options.pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
|
|
1358
1647
|
// src/formatters/html/renderers/index.ts
|
|
1359
1648
|
import * as fs2 from "fs";
|
|
1360
|
-
import * as
|
|
1649
|
+
import * as path3 from "path";
|
|
1361
1650
|
|
|
1362
1651
|
// src/formatters/html/template.ts
|
|
1363
1652
|
var JS_THEME = `
|
|
@@ -2166,6 +2455,80 @@ function escapeHtml(str) {
|
|
|
2166
2455
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2167
2456
|
}
|
|
2168
2457
|
|
|
2458
|
+
// src/theme/tokens.ts
|
|
2459
|
+
var ES_THEME_TOKENS_CSS = `
|
|
2460
|
+
:root,
|
|
2461
|
+
[data-theme="light"] {
|
|
2462
|
+
--es-color-bg: #ffffff;
|
|
2463
|
+
--es-color-fg: #111827;
|
|
2464
|
+
--es-color-muted: #6b7280;
|
|
2465
|
+
--es-color-border: #e5e7eb;
|
|
2466
|
+
--es-color-surface: #f9fafb;
|
|
2467
|
+
--es-color-link: #2563eb;
|
|
2468
|
+
--es-color-passed: #16a34a;
|
|
2469
|
+
--es-color-failed: #dc2626;
|
|
2470
|
+
--es-color-skipped: #9ca3af;
|
|
2471
|
+
--es-color-pending: #d97706;
|
|
2472
|
+
--es-color-passed-bg: #f0fdf4;
|
|
2473
|
+
--es-color-failed-bg: #fef2f2;
|
|
2474
|
+
--es-color-skipped-bg: #f3f4f6;
|
|
2475
|
+
--es-color-pending-bg: #fffbeb;
|
|
2476
|
+
--es-font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
2477
|
+
--es-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
2478
|
+
--es-size-base: 1rem;
|
|
2479
|
+
--es-size-sm: 0.875rem;
|
|
2480
|
+
--es-size-xs: 0.75rem;
|
|
2481
|
+
--es-size-h1: 1.875rem;
|
|
2482
|
+
--es-size-h2: 1.5rem;
|
|
2483
|
+
--es-size-h3: 1.25rem;
|
|
2484
|
+
--es-space-1: 0.25rem;
|
|
2485
|
+
--es-space-2: 0.5rem;
|
|
2486
|
+
--es-space-3: 0.75rem;
|
|
2487
|
+
--es-space-4: 1rem;
|
|
2488
|
+
--es-space-6: 1.5rem;
|
|
2489
|
+
--es-space-8: 2rem;
|
|
2490
|
+
--es-radius: 0.5rem;
|
|
2491
|
+
--es-line: 1.6;
|
|
2492
|
+
--es-measure: 72ch;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
@media (prefers-color-scheme: dark) {
|
|
2496
|
+
:root {
|
|
2497
|
+
--es-color-bg: #0b0f17;
|
|
2498
|
+
--es-color-fg: #e5e7eb;
|
|
2499
|
+
--es-color-muted: #9ca3af;
|
|
2500
|
+
--es-color-border: #1f2937;
|
|
2501
|
+
--es-color-surface: #111827;
|
|
2502
|
+
--es-color-link: #60a5fa;
|
|
2503
|
+
--es-color-passed: #4ade80;
|
|
2504
|
+
--es-color-failed: #f87171;
|
|
2505
|
+
--es-color-skipped: #6b7280;
|
|
2506
|
+
--es-color-pending: #fbbf24;
|
|
2507
|
+
--es-color-passed-bg: rgba(74, 222, 128, 0.08);
|
|
2508
|
+
--es-color-failed-bg: rgba(248, 113, 113, 0.08);
|
|
2509
|
+
--es-color-skipped-bg: rgba(107, 114, 128, 0.08);
|
|
2510
|
+
--es-color-pending-bg: rgba(251, 191, 36, 0.08);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
[data-theme="dark"] {
|
|
2515
|
+
--es-color-bg: #0b0f17;
|
|
2516
|
+
--es-color-fg: #e5e7eb;
|
|
2517
|
+
--es-color-muted: #9ca3af;
|
|
2518
|
+
--es-color-border: #1f2937;
|
|
2519
|
+
--es-color-surface: #111827;
|
|
2520
|
+
--es-color-link: #60a5fa;
|
|
2521
|
+
--es-color-passed: #4ade80;
|
|
2522
|
+
--es-color-failed: #f87171;
|
|
2523
|
+
--es-color-skipped: #6b7280;
|
|
2524
|
+
--es-color-pending: #fbbf24;
|
|
2525
|
+
--es-color-passed-bg: rgba(74, 222, 128, 0.08);
|
|
2526
|
+
--es-color-failed-bg: rgba(248, 113, 113, 0.08);
|
|
2527
|
+
--es-color-skipped-bg: rgba(107, 114, 128, 0.08);
|
|
2528
|
+
--es-color-pending-bg: rgba(251, 191, 36, 0.08);
|
|
2529
|
+
}
|
|
2530
|
+
`.trim();
|
|
2531
|
+
|
|
2169
2532
|
// src/formatters/html/styles.ts
|
|
2170
2533
|
var CSS_STYLES = `
|
|
2171
2534
|
/* ============================================================================
|
|
@@ -2173,6 +2536,14 @@ var CSS_STYLES = `
|
|
|
2173
2536
|
============================================================================ */
|
|
2174
2537
|
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap');
|
|
2175
2538
|
|
|
2539
|
+
/* ============================================================================
|
|
2540
|
+
executable-stories canonical tokens (--es-*).
|
|
2541
|
+
Shared with executable-stories-react. Override on :root or any ancestor of
|
|
2542
|
+
the report to re-color both the standalone HTML and the React component.
|
|
2543
|
+
============================================================================ */
|
|
2544
|
+
${ES_THEME_TOKENS_CSS}
|
|
2545
|
+
|
|
2546
|
+
|
|
2176
2547
|
/* ============================================================================
|
|
2177
2548
|
CSS Custom Properties - Light Mode (Default)
|
|
2178
2549
|
Cucumber-branded shadcn/ui base theme
|
|
@@ -14415,7 +14786,7 @@ var SCREENSHOT_MIME_BY_EXT = {
|
|
|
14415
14786
|
};
|
|
14416
14787
|
function readScreenshotAsDataUri(filePath) {
|
|
14417
14788
|
try {
|
|
14418
|
-
const ext =
|
|
14789
|
+
const ext = path3.extname(filePath).slice(1).toLowerCase();
|
|
14419
14790
|
const mime = SCREENSHOT_MIME_BY_EXT[ext];
|
|
14420
14791
|
if (!mime) return void 0;
|
|
14421
14792
|
if (!fs2.existsSync(filePath)) return void 0;
|
|
@@ -15892,8 +16263,8 @@ function extractDocAttachments(step) {
|
|
|
15892
16263
|
}
|
|
15893
16264
|
return attachments;
|
|
15894
16265
|
}
|
|
15895
|
-
function guessMediaType(
|
|
15896
|
-
const lower =
|
|
16266
|
+
function guessMediaType(path10) {
|
|
16267
|
+
const lower = path10.toLowerCase();
|
|
15897
16268
|
if (lower.endsWith(".png")) return "image/png";
|
|
15898
16269
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
15899
16270
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -16968,7 +17339,7 @@ function selectTestCases(args, deps) {
|
|
|
16968
17339
|
|
|
16969
17340
|
// src/bundler/bundle-assets.ts
|
|
16970
17341
|
import * as fs4 from "fs";
|
|
16971
|
-
import * as
|
|
17342
|
+
import * as path5 from "path";
|
|
16972
17343
|
|
|
16973
17344
|
// src/bundler/scan-html-assets.ts
|
|
16974
17345
|
function scanHtmlAssets(html) {
|
|
@@ -16999,7 +17370,7 @@ function isLocalAssetRef(ref) {
|
|
|
16999
17370
|
|
|
17000
17371
|
// src/bundler/copy-asset.ts
|
|
17001
17372
|
import * as fs3 from "fs";
|
|
17002
|
-
import * as
|
|
17373
|
+
import * as path4 from "path";
|
|
17003
17374
|
import * as crypto from "crypto";
|
|
17004
17375
|
function copyAsset(sourcePath, assetsDir) {
|
|
17005
17376
|
if (!fs3.existsSync(assetsDir)) {
|
|
@@ -17007,10 +17378,10 @@ function copyAsset(sourcePath, assetsDir) {
|
|
|
17007
17378
|
}
|
|
17008
17379
|
const content = fs3.readFileSync(sourcePath);
|
|
17009
17380
|
const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 8);
|
|
17010
|
-
const ext =
|
|
17011
|
-
const baseName = sanitize(
|
|
17381
|
+
const ext = path4.extname(sourcePath);
|
|
17382
|
+
const baseName = sanitize(path4.basename(sourcePath, ext));
|
|
17012
17383
|
const destName = `${baseName}-${hash}${ext}`;
|
|
17013
|
-
const destPath =
|
|
17384
|
+
const destPath = path4.join(assetsDir, destName);
|
|
17014
17385
|
if (!fs3.existsSync(destPath)) {
|
|
17015
17386
|
fs3.copyFileSync(sourcePath, destPath);
|
|
17016
17387
|
}
|
|
@@ -17022,14 +17393,14 @@ function sanitize(name) {
|
|
|
17022
17393
|
|
|
17023
17394
|
// src/bundler/bundle-assets.ts
|
|
17024
17395
|
function bundleAssets(htmlPath, options = {}) {
|
|
17025
|
-
const htmlDir =
|
|
17026
|
-
const assetsDir =
|
|
17396
|
+
const htmlDir = path5.dirname(htmlPath);
|
|
17397
|
+
const assetsDir = path5.join(htmlDir, "assets");
|
|
17027
17398
|
let html = fs4.readFileSync(htmlPath, "utf8");
|
|
17028
17399
|
const refs = scanHtmlAssets(html);
|
|
17029
17400
|
let copiedCount = 0;
|
|
17030
17401
|
const missing = [];
|
|
17031
17402
|
for (const ref of refs) {
|
|
17032
|
-
const absolutePath =
|
|
17403
|
+
const absolutePath = path5.resolve(htmlDir, ref);
|
|
17033
17404
|
if (!fs4.existsSync(absolutePath)) {
|
|
17034
17405
|
missing.push(ref);
|
|
17035
17406
|
continue;
|
|
@@ -17527,14 +17898,14 @@ function groupBy7(items, keyFn) {
|
|
|
17527
17898
|
|
|
17528
17899
|
// src/formatters/astro-assets.ts
|
|
17529
17900
|
import * as fs5 from "fs";
|
|
17530
|
-
import * as
|
|
17901
|
+
import * as path6 from "path";
|
|
17531
17902
|
var SKIP_PREFIXES = ["http://", "https://", "data:", "#"];
|
|
17532
17903
|
function isLocalPath(src) {
|
|
17533
17904
|
const trimmed = src.trim();
|
|
17534
17905
|
if (SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
|
|
17535
17906
|
return false;
|
|
17536
17907
|
}
|
|
17537
|
-
return !
|
|
17908
|
+
return !path6.posix.isAbsolute(trimmed) && !path6.win32.isAbsolute(trimmed);
|
|
17538
17909
|
}
|
|
17539
17910
|
function stripCodeContent(markdown) {
|
|
17540
17911
|
let result = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
|
|
@@ -17628,7 +17999,7 @@ function copyMarkdownAssets(options) {
|
|
|
17628
17999
|
const pathMap = /* @__PURE__ */ new Map();
|
|
17629
18000
|
const missing = [];
|
|
17630
18001
|
for (const ref of refs) {
|
|
17631
|
-
const absPath =
|
|
18002
|
+
const absPath = path6.resolve(markdownDir, ref);
|
|
17632
18003
|
if (!fs5.existsSync(absPath)) {
|
|
17633
18004
|
if (!allowMissing) {
|
|
17634
18005
|
throw new Error(`Asset not found: ${absPath}`);
|
|
@@ -18905,7 +19276,8 @@ var FORMAT_EXTENSIONS = {
|
|
|
18905
19276
|
junit: ".junit.xml",
|
|
18906
19277
|
"cucumber-json": ".cucumber.json",
|
|
18907
19278
|
"cucumber-messages": ".ndjson",
|
|
18908
|
-
confluence: ".adf.json"
|
|
19279
|
+
confluence: ".adf.json",
|
|
19280
|
+
"story-report-json": ".story-report.json"
|
|
18909
19281
|
};
|
|
18910
19282
|
var TEST_EXTENSIONS = [
|
|
18911
19283
|
".test.ts",
|
|
@@ -18932,11 +19304,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
18932
19304
|
const ext = FORMAT_EXTENSIONS[format];
|
|
18933
19305
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
18934
19306
|
if (mode === "aggregated") {
|
|
18935
|
-
return toPosix(
|
|
19307
|
+
return toPosix(path7.join(baseOutputDir, `${effectiveName}${ext}`));
|
|
18936
19308
|
}
|
|
18937
19309
|
const normalizedSource = toPosix(sourceFile);
|
|
18938
|
-
const dirOfSource =
|
|
18939
|
-
let baseName =
|
|
19310
|
+
const dirOfSource = path7.posix.dirname(normalizedSource);
|
|
19311
|
+
let baseName = path7.posix.basename(normalizedSource);
|
|
18940
19312
|
for (const testExt of TEST_EXTENSIONS) {
|
|
18941
19313
|
if (baseName.endsWith(testExt)) {
|
|
18942
19314
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -18945,9 +19317,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
18945
19317
|
}
|
|
18946
19318
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
18947
19319
|
if (colocatedStyle === "adjacent") {
|
|
18948
|
-
return toPosix(
|
|
19320
|
+
return toPosix(path7.posix.join(dirOfSource, fileName));
|
|
18949
19321
|
}
|
|
18950
|
-
return toPosix(
|
|
19322
|
+
return toPosix(path7.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
18951
19323
|
}
|
|
18952
19324
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
18953
19325
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -19030,6 +19402,9 @@ var ReportGenerator = class {
|
|
|
19030
19402
|
cucumberJson: {
|
|
19031
19403
|
pretty: options.cucumberJson?.pretty ?? false
|
|
19032
19404
|
},
|
|
19405
|
+
storyReportJson: {
|
|
19406
|
+
pretty: options.storyReportJson?.pretty ?? true
|
|
19407
|
+
},
|
|
19033
19408
|
cucumberMessages: {
|
|
19034
19409
|
uriStrategy: options.cucumberMessages?.uriStrategy ?? "sourceFile",
|
|
19035
19410
|
includeSynthetics: options.cucumberMessages?.includeSynthetics ?? true,
|
|
@@ -19145,8 +19520,8 @@ var ReportGenerator = class {
|
|
|
19145
19520
|
if (astroPaths) {
|
|
19146
19521
|
for (const mdPath of astroPaths) {
|
|
19147
19522
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
19148
|
-
const mdDir =
|
|
19149
|
-
const assetsDir =
|
|
19523
|
+
const mdDir = path7.dirname(mdPath);
|
|
19524
|
+
const assetsDir = path7.resolve(this.options.astro.assetsDir);
|
|
19150
19525
|
const result = copyMarkdownAssets({
|
|
19151
19526
|
markdown: content,
|
|
19152
19527
|
markdownDir: mdDir,
|
|
@@ -19177,9 +19552,9 @@ var ReportGenerator = class {
|
|
|
19177
19552
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
19178
19553
|
const ext = FORMAT_EXTENSIONS[format];
|
|
19179
19554
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
19180
|
-
const outputPath = toPosix(
|
|
19555
|
+
const outputPath = toPosix(path7.join(this.options.outputDir, `${effectiveName}${ext}`));
|
|
19181
19556
|
const content = await this.formatContent(run, format);
|
|
19182
|
-
const dir =
|
|
19557
|
+
const dir = path7.dirname(outputPath);
|
|
19183
19558
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
19184
19559
|
await this.deps.writeFile(outputPath, content);
|
|
19185
19560
|
return [outputPath];
|
|
@@ -19191,7 +19566,7 @@ var ReportGenerator = class {
|
|
|
19191
19566
|
testCases
|
|
19192
19567
|
};
|
|
19193
19568
|
const content = await this.formatContent(groupRun, format);
|
|
19194
|
-
const dir =
|
|
19569
|
+
const dir = path7.dirname(outputPath);
|
|
19195
19570
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
19196
19571
|
await this.deps.writeFile(outputPath, content);
|
|
19197
19572
|
writtenPaths.push(outputPath);
|
|
@@ -19298,6 +19673,12 @@ var ReportGenerator = class {
|
|
|
19298
19673
|
});
|
|
19299
19674
|
return formatter.format(run);
|
|
19300
19675
|
}
|
|
19676
|
+
case "story-report-json": {
|
|
19677
|
+
const formatter = new StoryReportJsonFormatter({
|
|
19678
|
+
pretty: this.options.storyReportJson.pretty
|
|
19679
|
+
});
|
|
19680
|
+
return formatter.format(run);
|
|
19681
|
+
}
|
|
19301
19682
|
default:
|
|
19302
19683
|
throw new Error(`Unknown format: ${format}`);
|
|
19303
19684
|
}
|
|
@@ -19311,7 +19692,7 @@ async function generateRunComparison(args) {
|
|
|
19311
19692
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
19312
19693
|
for (const format of args.formats) {
|
|
19313
19694
|
const ext = format === "html" ? ".html" : ".md";
|
|
19314
|
-
const outputPath = toPosix(
|
|
19695
|
+
const outputPath = toPosix(path7.join(outputDir, `${outputName}${ext}`));
|
|
19315
19696
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
19316
19697
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
19317
19698
|
files.push(outputPath);
|
|
@@ -19321,9 +19702,9 @@ async function generateRunComparison(args) {
|
|
|
19321
19702
|
|
|
19322
19703
|
// src/init-astro.ts
|
|
19323
19704
|
import * as fs7 from "fs";
|
|
19324
|
-
import * as
|
|
19705
|
+
import * as path8 from "path";
|
|
19325
19706
|
import { fileURLToPath } from "url";
|
|
19326
|
-
var __dirname =
|
|
19707
|
+
var __dirname = path8.dirname(fileURLToPath(import.meta.url));
|
|
19327
19708
|
function initAstro(options = {}) {
|
|
19328
19709
|
const targetDir = options.targetDir ?? "./story-docs";
|
|
19329
19710
|
const force = options.force ?? false;
|
|
@@ -19335,7 +19716,7 @@ function initAstro(options = {}) {
|
|
|
19335
19716
|
);
|
|
19336
19717
|
}
|
|
19337
19718
|
}
|
|
19338
|
-
const templateDir =
|
|
19719
|
+
const templateDir = path8.resolve(__dirname, "..", "templates", "astro-starlight");
|
|
19339
19720
|
if (!fs7.existsSync(templateDir)) {
|
|
19340
19721
|
throw new Error(
|
|
19341
19722
|
`Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
|
|
@@ -19348,8 +19729,8 @@ function copyDirRecursive(src, dest) {
|
|
|
19348
19729
|
fs7.mkdirSync(dest, { recursive: true });
|
|
19349
19730
|
const entries = fs7.readdirSync(src, { withFileTypes: true });
|
|
19350
19731
|
for (const entry of entries) {
|
|
19351
|
-
const srcPath =
|
|
19352
|
-
const destPath =
|
|
19732
|
+
const srcPath = path8.join(src, entry.name);
|
|
19733
|
+
const destPath = path8.join(dest, entry.name);
|
|
19353
19734
|
if (entry.isDirectory()) {
|
|
19354
19735
|
copyDirRecursive(srcPath, destPath);
|
|
19355
19736
|
} else {
|
|
@@ -19380,6 +19761,7 @@ var EXIT_SCHEMA_VALIDATION = 1;
|
|
|
19380
19761
|
var EXIT_CANONICAL_VALIDATION = 2;
|
|
19381
19762
|
var EXIT_GENERATION = 3;
|
|
19382
19763
|
var EXIT_USAGE = 4;
|
|
19764
|
+
var EXIT_COMPARE_GATE = 5;
|
|
19383
19765
|
var HELP_TEXT = `
|
|
19384
19766
|
executable-stories \u2014 Generate reports from test results JSON.
|
|
19385
19767
|
|
|
@@ -19404,7 +19786,7 @@ SUBCOMMANDS
|
|
|
19404
19786
|
publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
|
|
19405
19787
|
|
|
19406
19788
|
OPTIONS
|
|
19407
|
-
--format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, or custom names from config (default: html)
|
|
19789
|
+
--format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, or custom names from config (default: html)
|
|
19408
19790
|
astro Themed Markdown (for Astro docs sites with matching CSS)
|
|
19409
19791
|
confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
|
|
19410
19792
|
html Custom HTML report (accessible, dark mode, mermaid)
|
|
@@ -19413,6 +19795,7 @@ OPTIONS
|
|
|
19413
19795
|
junit JUnit XML
|
|
19414
19796
|
cucumber-json Cucumber JSON
|
|
19415
19797
|
cucumber-messages Raw NDJSON (Cucumber Messages)
|
|
19798
|
+
story-report-json StoryReport v1 JSON (consumed by executable-stories-react and other UI renderers)
|
|
19416
19799
|
--config <path> Path to executable-stories.config.js (default: ./executable-stories.config.js)
|
|
19417
19800
|
--input-type <type> Input type: raw, canonical, or ndjson (default: raw)
|
|
19418
19801
|
--output-dir <dir> Output directory (default: reports)
|
|
@@ -19443,6 +19826,9 @@ OPTIONS
|
|
|
19443
19826
|
--baseline-dir <dir> Directory to scan when --baseline auto is used
|
|
19444
19827
|
--pr-summary Print a PR-friendly markdown summary after compare
|
|
19445
19828
|
--pr-summary-file <path> Write the PR-friendly markdown summary to a file
|
|
19829
|
+
--fail-on-regression Exit non-zero when any regression is detected in compare
|
|
19830
|
+
--fail-on-added-failures Exit non-zero when newly added scenarios are failing
|
|
19831
|
+
--max-regressions <n> Exit non-zero when regressions exceed threshold
|
|
19446
19832
|
--emit-canonical <path> Write canonical JSON to given path
|
|
19447
19833
|
--help Show this help message
|
|
19448
19834
|
|
|
@@ -19506,6 +19892,7 @@ EXIT CODES
|
|
|
19506
19892
|
2 Canonical validation failure
|
|
19507
19893
|
3 Formatter/generation failure
|
|
19508
19894
|
4 Bad arguments / usage error
|
|
19895
|
+
5 Compare gate failed
|
|
19509
19896
|
`.trim();
|
|
19510
19897
|
async function parseCliArgs(argv) {
|
|
19511
19898
|
const args = argv.slice(2);
|
|
@@ -19607,6 +19994,9 @@ async function parseCliArgs(argv) {
|
|
|
19607
19994
|
"allow-missing-assets": { type: "boolean", default: false },
|
|
19608
19995
|
"pr-summary": { type: "boolean", default: false },
|
|
19609
19996
|
"pr-summary-file": { type: "string" },
|
|
19997
|
+
"fail-on-regression": { type: "boolean", default: false },
|
|
19998
|
+
"fail-on-added-failures": { type: "boolean", default: false },
|
|
19999
|
+
"max-regressions": { type: "string" },
|
|
19610
20000
|
"config": { type: "string" },
|
|
19611
20001
|
help: { type: "boolean", default: false }
|
|
19612
20002
|
},
|
|
@@ -19647,7 +20037,7 @@ async function parseCliArgs(argv) {
|
|
|
19647
20037
|
}
|
|
19648
20038
|
const pluginConfig = await loadConfig(values["config"]);
|
|
19649
20039
|
const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
|
|
19650
|
-
const builtInFormats = /* @__PURE__ */ new Set(["astro", "confluence", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
|
|
20040
|
+
const builtInFormats = /* @__PURE__ */ new Set(["astro", "confluence", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "story-report-json"]);
|
|
19651
20041
|
const formatStr = values.format;
|
|
19652
20042
|
const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
|
|
19653
20043
|
const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
|
|
@@ -19655,7 +20045,7 @@ async function parseCliArgs(argv) {
|
|
|
19655
20045
|
const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
|
|
19656
20046
|
if (unknownFormats.length > 0) {
|
|
19657
20047
|
const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
|
|
19658
|
-
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, confluence, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html${knownCustom}.`);
|
|
20048
|
+
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, confluence, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, story-report-json${knownCustom}.`);
|
|
19659
20049
|
process.exit(EXIT_USAGE);
|
|
19660
20050
|
}
|
|
19661
20051
|
const formats = builtInRequested;
|
|
@@ -19714,6 +20104,12 @@ async function parseCliArgs(argv) {
|
|
|
19714
20104
|
console.error(`Error: --max-history-runs must be a positive integer, got "${maxHistoryRunsStr}".`);
|
|
19715
20105
|
process.exit(EXIT_USAGE);
|
|
19716
20106
|
}
|
|
20107
|
+
const maxRegressionsStr = values["max-regressions"];
|
|
20108
|
+
const maxRegressions = maxRegressionsStr !== void 0 ? parseInt(maxRegressionsStr, 10) : void 0;
|
|
20109
|
+
if (maxRegressionsStr !== void 0 && (isNaN(maxRegressions) || maxRegressions < 0)) {
|
|
20110
|
+
console.error(`Error: --max-regressions must be a non-negative integer, got "${maxRegressionsStr}".`);
|
|
20111
|
+
process.exit(EXIT_USAGE);
|
|
20112
|
+
}
|
|
19717
20113
|
const sortTestCasesRaw = values["sort-test-cases"];
|
|
19718
20114
|
const validSortModes = /* @__PURE__ */ new Set(["id", "source", "none"]);
|
|
19719
20115
|
if (!validSortModes.has(sortTestCasesRaw)) {
|
|
@@ -19774,6 +20170,9 @@ async function parseCliArgs(argv) {
|
|
|
19774
20170
|
allowMissingAssets: values["allow-missing-assets"],
|
|
19775
20171
|
prSummary: values["pr-summary"],
|
|
19776
20172
|
prSummaryFile: values["pr-summary-file"],
|
|
20173
|
+
failOnRegression: values["fail-on-regression"],
|
|
20174
|
+
failOnAddedFailures: values["fail-on-added-failures"],
|
|
20175
|
+
maxRegressions,
|
|
19777
20176
|
config: values["config"]
|
|
19778
20177
|
};
|
|
19779
20178
|
return { args: cliArgs, pluginConfig, customRequested };
|
|
@@ -19782,7 +20181,7 @@ async function readInput(args) {
|
|
|
19782
20181
|
if (args.stdin) {
|
|
19783
20182
|
return readStdin();
|
|
19784
20183
|
}
|
|
19785
|
-
const filePath =
|
|
20184
|
+
const filePath = path9.resolve(args.inputFile);
|
|
19786
20185
|
if (!fs8.existsSync(filePath)) {
|
|
19787
20186
|
console.error(`Error: File not found: ${filePath}`);
|
|
19788
20187
|
process.exit(EXIT_USAGE);
|
|
@@ -19790,7 +20189,7 @@ async function readInput(args) {
|
|
|
19790
20189
|
return fs8.readFileSync(filePath, "utf8");
|
|
19791
20190
|
}
|
|
19792
20191
|
function readFileInput(filePath) {
|
|
19793
|
-
const resolved =
|
|
20192
|
+
const resolved = path9.resolve(filePath);
|
|
19794
20193
|
if (!fs8.existsSync(resolved)) {
|
|
19795
20194
|
console.error(`Error: File not found: ${resolved}`);
|
|
19796
20195
|
process.exit(EXIT_USAGE);
|
|
@@ -19928,14 +20327,14 @@ function tryNormalizeRunFromText(text2, args) {
|
|
|
19928
20327
|
}
|
|
19929
20328
|
}
|
|
19930
20329
|
function listBaselineCandidates(currentFile, args) {
|
|
19931
|
-
const baselineDir =
|
|
19932
|
-
const currentResolved =
|
|
20330
|
+
const baselineDir = path9.resolve(args.baselineDir ?? path9.dirname(currentFile));
|
|
20331
|
+
const currentResolved = path9.resolve(currentFile);
|
|
19933
20332
|
if (!fs8.existsSync(baselineDir)) {
|
|
19934
20333
|
console.error(`Error: baseline directory not found: ${baselineDir}`);
|
|
19935
20334
|
process.exit(EXIT_USAGE);
|
|
19936
20335
|
}
|
|
19937
20336
|
const entries = fs8.readdirSync(baselineDir, { withFileTypes: true });
|
|
19938
|
-
return entries.filter((entry) => entry.isFile()).map((entry) =>
|
|
20337
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path9.join(baselineDir, entry.name)).filter((candidate) => path9.resolve(candidate) !== currentResolved).filter(
|
|
19939
20338
|
(candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
|
|
19940
20339
|
);
|
|
19941
20340
|
}
|
|
@@ -19950,7 +20349,7 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
19950
20349
|
}
|
|
19951
20350
|
if (comparable.length === 0) {
|
|
19952
20351
|
console.error(
|
|
19953
|
-
`Error: no compatible baseline files found in ${
|
|
20352
|
+
`Error: no compatible baseline files found in ${path9.resolve(args.baselineDir ?? path9.dirname(currentFile))}.`
|
|
19954
20353
|
);
|
|
19955
20354
|
process.exit(EXIT_USAGE);
|
|
19956
20355
|
}
|
|
@@ -19973,6 +20372,13 @@ async function main() {
|
|
|
19973
20372
|
try {
|
|
19974
20373
|
const result = await generateCompareReports(baseline, current, baselineFile, args);
|
|
19975
20374
|
printCompareResult(result, args, startMs);
|
|
20375
|
+
const gateFailures = evaluateCompareGate(result, args);
|
|
20376
|
+
if (gateFailures.length > 0) {
|
|
20377
|
+
for (const failure of gateFailures) {
|
|
20378
|
+
console.error(`Compare gate failed: ${failure}`);
|
|
20379
|
+
}
|
|
20380
|
+
process.exit(EXIT_COMPARE_GATE);
|
|
20381
|
+
}
|
|
19976
20382
|
process.exit(EXIT_SUCCESS);
|
|
19977
20383
|
} catch (err) {
|
|
19978
20384
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -20039,8 +20445,8 @@ async function main() {
|
|
|
20039
20445
|
process.exit(EXIT_SCHEMA_VALIDATION);
|
|
20040
20446
|
}
|
|
20041
20447
|
if (args.emitCanonical) {
|
|
20042
|
-
const outPath =
|
|
20043
|
-
fs8.mkdirSync(
|
|
20448
|
+
const outPath = path9.resolve(args.emitCanonical);
|
|
20449
|
+
fs8.mkdirSync(path9.dirname(outPath), { recursive: true });
|
|
20044
20450
|
fs8.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
20045
20451
|
}
|
|
20046
20452
|
try {
|
|
@@ -20098,8 +20504,8 @@ ${msg}`);
|
|
|
20098
20504
|
}
|
|
20099
20505
|
const run = data;
|
|
20100
20506
|
if (args.emitCanonical) {
|
|
20101
|
-
const outPath =
|
|
20102
|
-
fs8.mkdirSync(
|
|
20507
|
+
const outPath = path9.resolve(args.emitCanonical);
|
|
20508
|
+
fs8.mkdirSync(path9.dirname(outPath), { recursive: true });
|
|
20103
20509
|
fs8.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
20104
20510
|
}
|
|
20105
20511
|
try {
|
|
@@ -20156,8 +20562,8 @@ ${msg}`);
|
|
|
20156
20562
|
process.exit(EXIT_CANONICAL_VALIDATION);
|
|
20157
20563
|
}
|
|
20158
20564
|
if (args.emitCanonical) {
|
|
20159
|
-
const outPath =
|
|
20160
|
-
fs8.mkdirSync(
|
|
20565
|
+
const outPath = path9.resolve(args.emitCanonical);
|
|
20566
|
+
fs8.mkdirSync(path9.dirname(outPath), { recursive: true });
|
|
20161
20567
|
fs8.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
|
|
20162
20568
|
}
|
|
20163
20569
|
try {
|
|
@@ -20183,7 +20589,7 @@ function runCustomFormatters(run, customRequested, formatters, args) {
|
|
|
20183
20589
|
const ext = formatter.fileExtension ?? formatName;
|
|
20184
20590
|
const baseName = args.outputName ?? "report";
|
|
20185
20591
|
const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
|
|
20186
|
-
const filepath =
|
|
20592
|
+
const filepath = path9.join(outputDir, filename);
|
|
20187
20593
|
fs8.mkdirSync(outputDir, { recursive: true });
|
|
20188
20594
|
fs8.writeFileSync(filepath, content, "utf8");
|
|
20189
20595
|
console.log(`Generated: ${filepath}`);
|
|
@@ -20235,7 +20641,7 @@ async function dispatchNotifications(run, args) {
|
|
|
20235
20641
|
}
|
|
20236
20642
|
function runHistoryPipeline(run, args) {
|
|
20237
20643
|
if (!args.historyFile) return;
|
|
20238
|
-
const historyPath =
|
|
20644
|
+
const historyPath = path9.resolve(args.historyFile);
|
|
20239
20645
|
const store = loadHistory(
|
|
20240
20646
|
{ filePath: historyPath },
|
|
20241
20647
|
{
|
|
@@ -20254,7 +20660,7 @@ function runHistoryPipeline(run, args) {
|
|
|
20254
20660
|
run,
|
|
20255
20661
|
maxRuns: args.maxHistoryRuns
|
|
20256
20662
|
});
|
|
20257
|
-
const dir =
|
|
20663
|
+
const dir = path9.dirname(historyPath);
|
|
20258
20664
|
fs8.mkdirSync(dir, { recursive: true });
|
|
20259
20665
|
saveHistory(
|
|
20260
20666
|
{ filePath: historyPath, store: updated },
|
|
@@ -20331,6 +20737,9 @@ async function generateCompareReports(baseline, current, baselineFile, args) {
|
|
|
20331
20737
|
return {
|
|
20332
20738
|
files: result.files,
|
|
20333
20739
|
baselineFile,
|
|
20740
|
+
addedFailures: result.diff.scenarios.filter(
|
|
20741
|
+
(scenario) => scenario.kind === "added" && scenario.current?.status === "failed"
|
|
20742
|
+
).length,
|
|
20334
20743
|
summary: result.diff.summary,
|
|
20335
20744
|
prSummary: args.prSummary || args.prSummaryFile ? createPrCommentSummary(result.diff) : void 0
|
|
20336
20745
|
};
|
|
@@ -20356,8 +20765,8 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
|
20356
20765
|
function printCompareResult(result, args, startMs) {
|
|
20357
20766
|
const durationMs = Date.now() - startMs;
|
|
20358
20767
|
if (result.prSummary && args.prSummaryFile) {
|
|
20359
|
-
const outputPath =
|
|
20360
|
-
fs8.mkdirSync(
|
|
20768
|
+
const outputPath = path9.resolve(args.prSummaryFile);
|
|
20769
|
+
fs8.mkdirSync(path9.dirname(outputPath), { recursive: true });
|
|
20361
20770
|
fs8.writeFileSync(outputPath, result.prSummary, "utf8");
|
|
20362
20771
|
}
|
|
20363
20772
|
if (args.jsonSummary) {
|
|
@@ -20366,6 +20775,7 @@ function printCompareResult(result, args, startMs) {
|
|
|
20366
20775
|
{
|
|
20367
20776
|
files: result.files,
|
|
20368
20777
|
baselineFile: result.baselineFile,
|
|
20778
|
+
addedFailures: result.addedFailures,
|
|
20369
20779
|
diff: result.summary,
|
|
20370
20780
|
prSummary: result.prSummary,
|
|
20371
20781
|
durationMs
|
|
@@ -20385,6 +20795,25 @@ function printCompareResult(result, args, startMs) {
|
|
|
20385
20795
|
console.log(result.prSummary);
|
|
20386
20796
|
}
|
|
20387
20797
|
}
|
|
20798
|
+
function evaluateCompareGate(result, args) {
|
|
20799
|
+
const failures = [];
|
|
20800
|
+
if (args.failOnRegression && result.summary.regressed > 0) {
|
|
20801
|
+
failures.push(
|
|
20802
|
+
`regressions detected (${result.summary.regressed}) with --fail-on-regression`
|
|
20803
|
+
);
|
|
20804
|
+
}
|
|
20805
|
+
if (args.failOnAddedFailures && result.addedFailures > 0) {
|
|
20806
|
+
failures.push(
|
|
20807
|
+
`new failing scenarios detected (${result.addedFailures}) with --fail-on-added-failures`
|
|
20808
|
+
);
|
|
20809
|
+
}
|
|
20810
|
+
if (args.maxRegressions !== void 0 && result.summary.regressed > args.maxRegressions) {
|
|
20811
|
+
failures.push(
|
|
20812
|
+
`regressions ${result.summary.regressed} exceed --max-regressions ${args.maxRegressions}`
|
|
20813
|
+
);
|
|
20814
|
+
}
|
|
20815
|
+
return failures;
|
|
20816
|
+
}
|
|
20388
20817
|
async function runPublishConfluence(rawArgs) {
|
|
20389
20818
|
const { values, positionals } = parseArgs({
|
|
20390
20819
|
args: rawArgs,
|
|
@@ -20457,7 +20886,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
20457
20886
|
console.error("Error: --title is required when creating a new page");
|
|
20458
20887
|
process.exit(EXIT_USAGE);
|
|
20459
20888
|
}
|
|
20460
|
-
const adf = fs8.readFileSync(
|
|
20889
|
+
const adf = fs8.readFileSync(path9.resolve(inputFile), "utf8");
|
|
20461
20890
|
if (dryRun) {
|
|
20462
20891
|
console.log(
|
|
20463
20892
|
JSON.stringify(
|
|
@@ -20563,7 +20992,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
20563
20992
|
process.exit(EXIT_USAGE);
|
|
20564
20993
|
}
|
|
20565
20994
|
const mode = modeRaw;
|
|
20566
|
-
const adf = fs8.readFileSync(
|
|
20995
|
+
const adf = fs8.readFileSync(path9.resolve(inputFile), "utf8");
|
|
20567
20996
|
if (dryRun) {
|
|
20568
20997
|
console.log(
|
|
20569
20998
|
JSON.stringify(
|