executable-stories-formatters 0.15.1 → 0.17.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/cli.js +669 -301
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +511 -157
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -6
- package/dist/index.d.ts +111 -6
- package/dist/index.js +504 -156
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import "fs";
|
|
3
|
-
import * as
|
|
3
|
+
import * as path12 from "path";
|
|
4
4
|
import * as fsPromises from "fs/promises";
|
|
5
5
|
|
|
6
6
|
// src/converters/acl/status.ts
|
|
@@ -42,6 +42,41 @@ function generateFeatureId(uri) {
|
|
|
42
42
|
function generateScenarioId(featureId, scenarioName) {
|
|
43
43
|
return `${featureId};${slugify(scenarioName)}`;
|
|
44
44
|
}
|
|
45
|
+
function normalizeText(text2) {
|
|
46
|
+
return text2.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
|
|
47
|
+
}
|
|
48
|
+
function tokenize(text2) {
|
|
49
|
+
const normalized = normalizeText(text2);
|
|
50
|
+
return normalized.length === 0 ? [] : normalized.split(" ");
|
|
51
|
+
}
|
|
52
|
+
function behaviourFingerprint(input) {
|
|
53
|
+
const steps = input.steps.map((step) => `${step.keyword.toLowerCase()}:${normalizeText(step.text)}`).join("\n");
|
|
54
|
+
const covers = [...input.covers ?? []].map((path13) => path13.trim()).filter(Boolean).sort().join(",");
|
|
55
|
+
if (steps.length === 0 && covers.length === 0) return "";
|
|
56
|
+
return createHash("sha1").update(`${steps}\0${covers}`).digest("hex").slice(0, 16);
|
|
57
|
+
}
|
|
58
|
+
function jaccard(a, b) {
|
|
59
|
+
if (a.size === 0 && b.size === 0) return 1;
|
|
60
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
61
|
+
let intersection = 0;
|
|
62
|
+
for (const value of a) if (b.has(value)) intersection += 1;
|
|
63
|
+
return intersection / (a.size + b.size - intersection);
|
|
64
|
+
}
|
|
65
|
+
function stepTokens(steps) {
|
|
66
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
67
|
+
for (const step of steps) {
|
|
68
|
+
for (const token of tokenize(step.text)) tokens.add(token);
|
|
69
|
+
}
|
|
70
|
+
return tokens;
|
|
71
|
+
}
|
|
72
|
+
function behaviourSimilarity(a, b) {
|
|
73
|
+
const aSteps = stepTokens(a.steps);
|
|
74
|
+
const bSteps = stepTokens(b.steps);
|
|
75
|
+
if (aSteps.size === 0 || bSteps.size === 0) return 0;
|
|
76
|
+
const stepScore = jaccard(aSteps, bSteps);
|
|
77
|
+
const titleScore = jaccard(new Set(tokenize(a.scenario)), new Set(tokenize(b.scenario)));
|
|
78
|
+
return stepScore * 0.7 + titleScore * 0.3;
|
|
79
|
+
}
|
|
45
80
|
|
|
46
81
|
// src/converters/acl/steps.ts
|
|
47
82
|
function deriveStepResults(steps, scenarioStatus, error) {
|
|
@@ -14239,12 +14274,12 @@ function hasSufficientHistory(entries, min) {
|
|
|
14239
14274
|
}
|
|
14240
14275
|
|
|
14241
14276
|
// src/formatters/html/renderers/scenario.ts
|
|
14242
|
-
function renderTicket(ticket, template,
|
|
14277
|
+
function renderTicket(ticket, template, escapeHtml5) {
|
|
14243
14278
|
const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
|
|
14244
14279
|
if (url) {
|
|
14245
|
-
return `<a class="tag ticket-tag" href="${
|
|
14280
|
+
return `<a class="tag ticket-tag" href="${escapeHtml5(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml5(ticket.id)}</a>`;
|
|
14246
14281
|
}
|
|
14247
|
-
return `<span class="tag ticket-tag">${
|
|
14282
|
+
return `<span class="tag ticket-tag">${escapeHtml5(ticket.id)}</span>`;
|
|
14248
14283
|
}
|
|
14249
14284
|
function renderScenario(args, deps) {
|
|
14250
14285
|
const { tc } = args;
|
|
@@ -14444,7 +14479,7 @@ function flattenTree(roots) {
|
|
|
14444
14479
|
}
|
|
14445
14480
|
return result;
|
|
14446
14481
|
}
|
|
14447
|
-
function buildTooltip(span,
|
|
14482
|
+
function buildTooltip(span, escapeHtml5) {
|
|
14448
14483
|
const parts = [];
|
|
14449
14484
|
parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
|
|
14450
14485
|
if (span.statusMessage) {
|
|
@@ -14462,7 +14497,7 @@ function buildTooltip(span, escapeHtml4) {
|
|
|
14462
14497
|
if (text2.length > TOOLTIP_MAX_LENGTH) {
|
|
14463
14498
|
text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
|
|
14464
14499
|
}
|
|
14465
|
-
return
|
|
14500
|
+
return escapeHtml5(text2);
|
|
14466
14501
|
}
|
|
14467
14502
|
function renderTraceView(args, deps) {
|
|
14468
14503
|
if (!args.spans || args.spans.length === 0) return "";
|
|
@@ -15757,14 +15792,14 @@ var TraceabilityMatrixFormatter = class {
|
|
|
15757
15792
|
lines.push("");
|
|
15758
15793
|
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
15759
15794
|
if (req.covers.length > 0) {
|
|
15760
|
-
lines.push(`Covers: ${req.covers.map((
|
|
15795
|
+
lines.push(`Covers: ${req.covers.map((path13) => `\`${path13}\``).join(", ")}`);
|
|
15761
15796
|
}
|
|
15762
15797
|
lines.push("");
|
|
15763
15798
|
lines.push("| Status | Scenario | Source | Covers |");
|
|
15764
15799
|
lines.push("| --- | --- | --- | --- |");
|
|
15765
15800
|
for (const scenario of req.scenarios) {
|
|
15766
15801
|
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
15767
|
-
const covers = scenario.covers.length > 0 ? scenario.covers.map((
|
|
15802
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path13) => `\`${path13}\``).join(", ") : "";
|
|
15768
15803
|
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
15769
15804
|
}
|
|
15770
15805
|
lines.push("");
|
|
@@ -15866,8 +15901,8 @@ function extractFeatureName(testCases, uri) {
|
|
|
15866
15901
|
return tc.titlePath[0];
|
|
15867
15902
|
}
|
|
15868
15903
|
}
|
|
15869
|
-
const
|
|
15870
|
-
return
|
|
15904
|
+
const basename4 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
|
|
15905
|
+
return basename4.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
15871
15906
|
}
|
|
15872
15907
|
function synthesizeFeature(uri, testCases) {
|
|
15873
15908
|
const featureName = extractFeatureName(testCases, uri);
|
|
@@ -16479,8 +16514,8 @@ function extractDocAttachments(step) {
|
|
|
16479
16514
|
}
|
|
16480
16515
|
return attachments;
|
|
16481
16516
|
}
|
|
16482
|
-
function guessMediaType(
|
|
16483
|
-
const lower =
|
|
16517
|
+
function guessMediaType(path13) {
|
|
16518
|
+
const lower = path13.toLowerCase();
|
|
16484
16519
|
if (lower.endsWith(".png")) return "image/png";
|
|
16485
16520
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
16486
16521
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -16621,11 +16656,11 @@ var CucumberHtmlFormatter = class {
|
|
|
16621
16656
|
for (const envelope of envelopes) {
|
|
16622
16657
|
const accepted = htmlStream.write(envelope);
|
|
16623
16658
|
if (!accepted) {
|
|
16624
|
-
await new Promise((
|
|
16659
|
+
await new Promise((resolve10) => htmlStream.once("drain", resolve10));
|
|
16625
16660
|
}
|
|
16626
16661
|
}
|
|
16627
|
-
await new Promise((
|
|
16628
|
-
collector.on("finish",
|
|
16662
|
+
await new Promise((resolve10, reject) => {
|
|
16663
|
+
collector.on("finish", resolve10);
|
|
16629
16664
|
collector.on("error", reject);
|
|
16630
16665
|
htmlStream.end();
|
|
16631
16666
|
});
|
|
@@ -16663,6 +16698,10 @@ function titleFor(kind) {
|
|
|
16663
16698
|
return "Added";
|
|
16664
16699
|
case "removed":
|
|
16665
16700
|
return "Removed";
|
|
16701
|
+
case "renamed":
|
|
16702
|
+
return "Renamed";
|
|
16703
|
+
case "moved":
|
|
16704
|
+
return "Moved";
|
|
16666
16705
|
case "changed":
|
|
16667
16706
|
return "Changed";
|
|
16668
16707
|
default:
|
|
@@ -16711,6 +16750,8 @@ function createPrCommentSummary(diff, maxScenarios = 10) {
|
|
|
16711
16750
|
addSection(lines, diff, "fixed", maxScenarios);
|
|
16712
16751
|
addSection(lines, diff, "added", maxScenarios);
|
|
16713
16752
|
addSection(lines, diff, "removed", maxScenarios);
|
|
16753
|
+
addSection(lines, diff, "renamed", maxScenarios);
|
|
16754
|
+
addSection(lines, diff, "moved", maxScenarios);
|
|
16714
16755
|
addSection(lines, diff, "changed", maxScenarios);
|
|
16715
16756
|
return lines.join("\n").trimEnd();
|
|
16716
16757
|
}
|
|
@@ -16757,8 +16798,10 @@ function sortDiffs(scenarios) {
|
|
|
16757
16798
|
fixed: 1,
|
|
16758
16799
|
added: 2,
|
|
16759
16800
|
removed: 3,
|
|
16760
|
-
|
|
16761
|
-
|
|
16801
|
+
renamed: 4,
|
|
16802
|
+
moved: 5,
|
|
16803
|
+
changed: 6,
|
|
16804
|
+
unchanged: 7
|
|
16762
16805
|
};
|
|
16763
16806
|
return [...scenarios].sort((a, b) => {
|
|
16764
16807
|
if (rank[a.kind] !== rank[b.kind]) {
|
|
@@ -16773,69 +16816,108 @@ function sortDiffs(scenarios) {
|
|
|
16773
16816
|
return a.scenario.localeCompare(b.scenario);
|
|
16774
16817
|
});
|
|
16775
16818
|
}
|
|
16819
|
+
var SIMILARITY_THRESHOLD = 0.75;
|
|
16820
|
+
function identityInput(tc) {
|
|
16821
|
+
return {
|
|
16822
|
+
scenario: tc.story.scenario,
|
|
16823
|
+
sourceFile: tc.sourceFile,
|
|
16824
|
+
steps: tc.story.steps,
|
|
16825
|
+
covers: tc.story.covers
|
|
16826
|
+
};
|
|
16827
|
+
}
|
|
16828
|
+
function changedFieldsOf(flags) {
|
|
16829
|
+
return Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
|
|
16830
|
+
}
|
|
16831
|
+
function allChangedFlags(errorMessage) {
|
|
16832
|
+
return {
|
|
16833
|
+
status: true,
|
|
16834
|
+
steps: true,
|
|
16835
|
+
docs: true,
|
|
16836
|
+
tags: true,
|
|
16837
|
+
tickets: true,
|
|
16838
|
+
source: true,
|
|
16839
|
+
duration: true,
|
|
16840
|
+
attachments: true,
|
|
16841
|
+
error: Boolean(errorMessage),
|
|
16842
|
+
titlePath: true
|
|
16843
|
+
};
|
|
16844
|
+
}
|
|
16845
|
+
function matchIdentities(removed, added) {
|
|
16846
|
+
const pairs = [];
|
|
16847
|
+
const remainingRemoved = new Set(removed);
|
|
16848
|
+
const remainingAdded = new Set(added);
|
|
16849
|
+
const removedByFp = /* @__PURE__ */ new Map();
|
|
16850
|
+
const addedByFp = /* @__PURE__ */ new Map();
|
|
16851
|
+
for (const tc of remainingRemoved) {
|
|
16852
|
+
const fp = behaviourFingerprint(identityInput(tc));
|
|
16853
|
+
if (fp === "") continue;
|
|
16854
|
+
(removedByFp.get(fp) ?? removedByFp.set(fp, []).get(fp)).push(tc);
|
|
16855
|
+
}
|
|
16856
|
+
for (const tc of remainingAdded) {
|
|
16857
|
+
const fp = behaviourFingerprint(identityInput(tc));
|
|
16858
|
+
if (fp === "") continue;
|
|
16859
|
+
(addedByFp.get(fp) ?? addedByFp.set(fp, []).get(fp)).push(tc);
|
|
16860
|
+
}
|
|
16861
|
+
for (const [fp, removedGroup] of removedByFp) {
|
|
16862
|
+
const addedGroup = addedByFp.get(fp);
|
|
16863
|
+
if (removedGroup.length === 1 && addedGroup && addedGroup.length === 1) {
|
|
16864
|
+
pairs.push({ before: removedGroup[0], after: addedGroup[0], confidence: 1, matchedBy: "fingerprint" });
|
|
16865
|
+
remainingRemoved.delete(removedGroup[0]);
|
|
16866
|
+
remainingAdded.delete(addedGroup[0]);
|
|
16867
|
+
}
|
|
16868
|
+
}
|
|
16869
|
+
for (const before of [...remainingRemoved]) {
|
|
16870
|
+
let best;
|
|
16871
|
+
let bestScore = 0;
|
|
16872
|
+
let tied = false;
|
|
16873
|
+
for (const after of remainingAdded) {
|
|
16874
|
+
const score = behaviourSimilarity(identityInput(before), identityInput(after));
|
|
16875
|
+
if (score > bestScore) {
|
|
16876
|
+
bestScore = score;
|
|
16877
|
+
best = after;
|
|
16878
|
+
tied = false;
|
|
16879
|
+
} else if (score === bestScore) {
|
|
16880
|
+
tied = true;
|
|
16881
|
+
}
|
|
16882
|
+
}
|
|
16883
|
+
if (best && !tied && bestScore >= SIMILARITY_THRESHOLD) {
|
|
16884
|
+
pairs.push({ before, after: best, confidence: Math.round(bestScore * 100) / 100, matchedBy: "similarity" });
|
|
16885
|
+
remainingRemoved.delete(before);
|
|
16886
|
+
remainingAdded.delete(best);
|
|
16887
|
+
}
|
|
16888
|
+
}
|
|
16889
|
+
return {
|
|
16890
|
+
pairs,
|
|
16891
|
+
unmatchedRemoved: [...remainingRemoved],
|
|
16892
|
+
unmatchedAdded: [...remainingAdded]
|
|
16893
|
+
};
|
|
16894
|
+
}
|
|
16895
|
+
function identityKind(before, after) {
|
|
16896
|
+
const titleChanged = before.story.scenario !== after.story.scenario;
|
|
16897
|
+
const fileChanged = before.sourceFile !== after.sourceFile;
|
|
16898
|
+
return fileChanged && !titleChanged ? "moved" : "renamed";
|
|
16899
|
+
}
|
|
16776
16900
|
function diffRuns(baseline, current) {
|
|
16777
16901
|
const baselineById = new Map(baseline.testCases.map((tc) => [tc.id, tc]));
|
|
16778
16902
|
const currentById = new Map(current.testCases.map((tc) => [tc.id, tc]));
|
|
16779
16903
|
const ids = /* @__PURE__ */ new Set([...baselineById.keys(), ...currentById.keys()]);
|
|
16780
16904
|
const scenarios = [];
|
|
16905
|
+
const removedCases = [];
|
|
16906
|
+
const addedCases = [];
|
|
16781
16907
|
for (const id of ids) {
|
|
16782
16908
|
const before = baselineById.get(id);
|
|
16783
16909
|
const after = currentById.get(id);
|
|
16784
16910
|
if (!before && after) {
|
|
16785
|
-
|
|
16786
|
-
status: true,
|
|
16787
|
-
steps: true,
|
|
16788
|
-
docs: true,
|
|
16789
|
-
tags: true,
|
|
16790
|
-
tickets: true,
|
|
16791
|
-
source: true,
|
|
16792
|
-
duration: true,
|
|
16793
|
-
attachments: true,
|
|
16794
|
-
error: Boolean(after.errorMessage),
|
|
16795
|
-
titlePath: true
|
|
16796
|
-
};
|
|
16797
|
-
scenarios.push({
|
|
16798
|
-
kind: "added",
|
|
16799
|
-
id,
|
|
16800
|
-
scenario: after.story.scenario,
|
|
16801
|
-
sourceFile: after.sourceFile,
|
|
16802
|
-
sourceLine: after.sourceLine,
|
|
16803
|
-
current: toScenarioSnapshot(after),
|
|
16804
|
-
flags: flags2,
|
|
16805
|
-
changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
|
|
16806
|
-
});
|
|
16911
|
+
addedCases.push(after);
|
|
16807
16912
|
continue;
|
|
16808
16913
|
}
|
|
16809
16914
|
if (before && !after) {
|
|
16810
|
-
|
|
16811
|
-
status: true,
|
|
16812
|
-
steps: true,
|
|
16813
|
-
docs: true,
|
|
16814
|
-
tags: true,
|
|
16815
|
-
tickets: true,
|
|
16816
|
-
source: true,
|
|
16817
|
-
duration: true,
|
|
16818
|
-
attachments: true,
|
|
16819
|
-
error: Boolean(before.errorMessage),
|
|
16820
|
-
titlePath: true
|
|
16821
|
-
};
|
|
16822
|
-
scenarios.push({
|
|
16823
|
-
kind: "removed",
|
|
16824
|
-
id,
|
|
16825
|
-
scenario: before.story.scenario,
|
|
16826
|
-
sourceFile: before.sourceFile,
|
|
16827
|
-
sourceLine: before.sourceLine,
|
|
16828
|
-
baseline: toScenarioSnapshot(before),
|
|
16829
|
-
flags: flags2,
|
|
16830
|
-
changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
|
|
16831
|
-
});
|
|
16832
|
-
continue;
|
|
16833
|
-
}
|
|
16834
|
-
if (!before || !after) {
|
|
16915
|
+
removedCases.push(before);
|
|
16835
16916
|
continue;
|
|
16836
16917
|
}
|
|
16918
|
+
if (!before || !after) continue;
|
|
16837
16919
|
const flags = buildFlags(before, after);
|
|
16838
|
-
const changedFields =
|
|
16920
|
+
const changedFields = changedFieldsOf(flags);
|
|
16839
16921
|
const kind = getPrimaryKind(before, after, changedFields.length > 0);
|
|
16840
16922
|
scenarios.push({
|
|
16841
16923
|
kind,
|
|
@@ -16850,12 +16932,59 @@ function diffRuns(baseline, current) {
|
|
|
16850
16932
|
durationDeltaMs: after.durationMs - before.durationMs
|
|
16851
16933
|
});
|
|
16852
16934
|
}
|
|
16935
|
+
const { pairs, unmatchedRemoved, unmatchedAdded } = matchIdentities(removedCases, addedCases);
|
|
16936
|
+
for (const { before, after, confidence, matchedBy } of pairs) {
|
|
16937
|
+
const flags = buildFlags(before, after);
|
|
16938
|
+
scenarios.push({
|
|
16939
|
+
kind: identityKind(before, after),
|
|
16940
|
+
id: after.id,
|
|
16941
|
+
previousId: before.id,
|
|
16942
|
+
scenario: after.story.scenario,
|
|
16943
|
+
sourceFile: after.sourceFile,
|
|
16944
|
+
sourceLine: after.sourceLine,
|
|
16945
|
+
baseline: toScenarioSnapshot(before),
|
|
16946
|
+
current: toScenarioSnapshot(after),
|
|
16947
|
+
flags,
|
|
16948
|
+
changedFields: changedFieldsOf(flags),
|
|
16949
|
+
durationDeltaMs: after.durationMs - before.durationMs,
|
|
16950
|
+
matchConfidence: confidence,
|
|
16951
|
+
matchedBy
|
|
16952
|
+
});
|
|
16953
|
+
}
|
|
16954
|
+
for (const after of unmatchedAdded) {
|
|
16955
|
+
const flags = allChangedFlags(after.errorMessage);
|
|
16956
|
+
scenarios.push({
|
|
16957
|
+
kind: "added",
|
|
16958
|
+
id: after.id,
|
|
16959
|
+
scenario: after.story.scenario,
|
|
16960
|
+
sourceFile: after.sourceFile,
|
|
16961
|
+
sourceLine: after.sourceLine,
|
|
16962
|
+
current: toScenarioSnapshot(after),
|
|
16963
|
+
flags,
|
|
16964
|
+
changedFields: changedFieldsOf(flags)
|
|
16965
|
+
});
|
|
16966
|
+
}
|
|
16967
|
+
for (const before of unmatchedRemoved) {
|
|
16968
|
+
const flags = allChangedFlags(before.errorMessage);
|
|
16969
|
+
scenarios.push({
|
|
16970
|
+
kind: "removed",
|
|
16971
|
+
id: before.id,
|
|
16972
|
+
scenario: before.story.scenario,
|
|
16973
|
+
sourceFile: before.sourceFile,
|
|
16974
|
+
sourceLine: before.sourceLine,
|
|
16975
|
+
baseline: toScenarioSnapshot(before),
|
|
16976
|
+
flags,
|
|
16977
|
+
changedFields: changedFieldsOf(flags)
|
|
16978
|
+
});
|
|
16979
|
+
}
|
|
16853
16980
|
const sorted = sortDiffs(scenarios);
|
|
16854
16981
|
const summary = {
|
|
16855
16982
|
totalBaseline: baseline.testCases.length,
|
|
16856
16983
|
totalCurrent: current.testCases.length,
|
|
16857
16984
|
added: sorted.filter((s) => s.kind === "added").length,
|
|
16858
16985
|
removed: sorted.filter((s) => s.kind === "removed").length,
|
|
16986
|
+
renamed: sorted.filter((s) => s.kind === "renamed").length,
|
|
16987
|
+
moved: sorted.filter((s) => s.kind === "moved").length,
|
|
16859
16988
|
changed: sorted.filter((s) => s.kind === "changed").length,
|
|
16860
16989
|
regressed: sorted.filter((s) => s.kind === "regressed").length,
|
|
16861
16990
|
fixed: sorted.filter((s) => s.kind === "fixed").length,
|
|
@@ -16883,6 +17012,10 @@ function statusLabel(kind) {
|
|
|
16883
17012
|
return "Added";
|
|
16884
17013
|
case "removed":
|
|
16885
17014
|
return "Removed";
|
|
17015
|
+
case "renamed":
|
|
17016
|
+
return "Renamed";
|
|
17017
|
+
case "moved":
|
|
17018
|
+
return "Moved";
|
|
16886
17019
|
case "changed":
|
|
16887
17020
|
return "Changed";
|
|
16888
17021
|
default:
|
|
@@ -17208,6 +17341,8 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', fun
|
|
|
17208
17341
|
<div class="summary-card"><strong>${diff.summary.fixed}</strong><span>Fixed</span></div>
|
|
17209
17342
|
<div class="summary-card"><strong>${diff.summary.added}</strong><span>Added</span></div>
|
|
17210
17343
|
<div class="summary-card"><strong>${diff.summary.removed}</strong><span>Removed</span></div>
|
|
17344
|
+
<div class="summary-card"><strong>${diff.summary.renamed}</strong><span>Renamed</span></div>
|
|
17345
|
+
<div class="summary-card"><strong>${diff.summary.moved}</strong><span>Moved</span></div>
|
|
17211
17346
|
<div class="summary-card"><strong>${diff.summary.changed}</strong><span>Changed</span></div>
|
|
17212
17347
|
<div class="summary-card"><strong>${diff.summary.unchanged}</strong><span>Unchanged</span></div>
|
|
17213
17348
|
</section>
|
|
@@ -17268,6 +17403,10 @@ function formatStatus(kind) {
|
|
|
17268
17403
|
return "Added";
|
|
17269
17404
|
case "removed":
|
|
17270
17405
|
return "Removed";
|
|
17406
|
+
case "renamed":
|
|
17407
|
+
return "Renamed";
|
|
17408
|
+
case "moved":
|
|
17409
|
+
return "Moved";
|
|
17271
17410
|
case "changed":
|
|
17272
17411
|
return "Changed";
|
|
17273
17412
|
default:
|
|
@@ -17420,13 +17559,13 @@ var RunDiffMarkdownFormatter = class {
|
|
|
17420
17559
|
lines.push("No regressions or fixes detected. Remaining changes are neutral.");
|
|
17421
17560
|
}
|
|
17422
17561
|
lines.push("");
|
|
17423
|
-
lines.push("| Added | Removed | Regressed | Fixed | Changed | Unchanged |");
|
|
17424
|
-
lines.push("| ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
17562
|
+
lines.push("| Added | Removed | Renamed | Moved | Regressed | Fixed | Changed | Unchanged |");
|
|
17563
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
17425
17564
|
lines.push(
|
|
17426
|
-
`| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
|
|
17565
|
+
`| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.renamed} | ${diff.summary.moved} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
|
|
17427
17566
|
);
|
|
17428
17567
|
lines.push("");
|
|
17429
|
-
for (const kind of ["regressed", "fixed", "added", "removed", "changed"]) {
|
|
17568
|
+
for (const kind of ["regressed", "fixed", "added", "removed", "renamed", "moved", "changed"]) {
|
|
17430
17569
|
const scenarios = diff.scenarios.filter((scenario) => scenario.kind === kind);
|
|
17431
17570
|
if (scenarios.length === 0) continue;
|
|
17432
17571
|
lines.push(`## ${formatStatus(kind)} (${scenarios.length})`);
|
|
@@ -18685,14 +18824,14 @@ ${result.errors.join("\n")}`);
|
|
|
18685
18824
|
}
|
|
18686
18825
|
|
|
18687
18826
|
// src/coverage-index.ts
|
|
18688
|
-
function normalizePath(
|
|
18689
|
-
return
|
|
18827
|
+
function normalizePath(path13) {
|
|
18828
|
+
return path13.replace(/^\.\//, "");
|
|
18690
18829
|
}
|
|
18691
18830
|
function scenariosCoveringPaths(index, paths) {
|
|
18692
18831
|
const queries = paths.map(normalizePath);
|
|
18693
18832
|
return index.scenarios.filter(
|
|
18694
18833
|
(scenario) => scenario.covers.some(
|
|
18695
|
-
(glob) => queries.some((
|
|
18834
|
+
(glob) => queries.some((path13) => matchesPattern(normalizePath(glob), path13))
|
|
18696
18835
|
)
|
|
18697
18836
|
);
|
|
18698
18837
|
}
|
|
@@ -18768,7 +18907,7 @@ function toRun(data, inputType, synthesize) {
|
|
|
18768
18907
|
if (synthesize) raw = synthesizeStories(raw);
|
|
18769
18908
|
return canonicalizeRun(raw);
|
|
18770
18909
|
}
|
|
18771
|
-
async function
|
|
18910
|
+
async function regenerateRun(options, deps = {}) {
|
|
18772
18911
|
const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
|
|
18773
18912
|
const data = JSON.parse(read(path7.resolve(options.input)));
|
|
18774
18913
|
const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
|
|
@@ -18778,7 +18917,10 @@ async function regenerateArtifacts(options, deps = {}) {
|
|
|
18778
18917
|
outputName: options.outputName
|
|
18779
18918
|
});
|
|
18780
18919
|
const result = await generator.generate(run);
|
|
18781
|
-
return [...result.values()].flat();
|
|
18920
|
+
return { files: [...result.values()].flat(), run };
|
|
18921
|
+
}
|
|
18922
|
+
async function regenerateArtifacts(options, deps = {}) {
|
|
18923
|
+
return (await regenerateRun(options, deps)).files;
|
|
18782
18924
|
}
|
|
18783
18925
|
function startWatch(options, deps = {}) {
|
|
18784
18926
|
const log = deps.log ?? ((message) => console.log(message));
|
|
@@ -18821,6 +18963,203 @@ function startWatch(options, deps = {}) {
|
|
|
18821
18963
|
};
|
|
18822
18964
|
}
|
|
18823
18965
|
|
|
18966
|
+
// src/serve.ts
|
|
18967
|
+
import * as fs7 from "fs";
|
|
18968
|
+
import * as http from "http";
|
|
18969
|
+
import * as path8 from "path";
|
|
18970
|
+
function advanceState(prev, run) {
|
|
18971
|
+
if (prev.sessionBaseline === null) {
|
|
18972
|
+
return { sessionBaseline: run, previous: null, current: run, runCount: 1 };
|
|
18973
|
+
}
|
|
18974
|
+
return {
|
|
18975
|
+
sessionBaseline: prev.sessionBaseline,
|
|
18976
|
+
previous: prev.current,
|
|
18977
|
+
current: run,
|
|
18978
|
+
runCount: prev.runCount + 1
|
|
18979
|
+
};
|
|
18980
|
+
}
|
|
18981
|
+
function computeDeltas(state) {
|
|
18982
|
+
if (state.current === null || state.sessionBaseline === null || state.runCount <= 1) {
|
|
18983
|
+
return { session: null, iteration: null };
|
|
18984
|
+
}
|
|
18985
|
+
return {
|
|
18986
|
+
session: diffRuns(state.sessionBaseline, state.current),
|
|
18987
|
+
iteration: state.previous ? diffRuns(state.previous, state.current) : null
|
|
18988
|
+
};
|
|
18989
|
+
}
|
|
18990
|
+
function pluralize(n, word) {
|
|
18991
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
18992
|
+
}
|
|
18993
|
+
function summarizeDiff(diff) {
|
|
18994
|
+
const s = diff.summary;
|
|
18995
|
+
const parts = [];
|
|
18996
|
+
if (s.fixed > 0) parts.push(`+${pluralize(s.fixed, "passing")}`);
|
|
18997
|
+
if (s.regressed > 0) parts.push(`${pluralize(s.regressed, "regressed")}`);
|
|
18998
|
+
if (s.added > 0) parts.push(`${pluralize(s.added, "new behaviour")}`);
|
|
18999
|
+
if (s.removed > 0) parts.push(`${pluralize(s.removed, "removed")}`);
|
|
19000
|
+
const moved = s.renamed + s.moved;
|
|
19001
|
+
if (moved > 0) parts.push(`${pluralize(moved, "renamed")}`);
|
|
19002
|
+
if (s.changed > 0) parts.push(`${pluralize(s.changed, "changed")}`);
|
|
19003
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
19004
|
+
}
|
|
19005
|
+
function escapeHtml3(text2) {
|
|
19006
|
+
return text2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
19007
|
+
}
|
|
19008
|
+
function renderDeltaStrip(state) {
|
|
19009
|
+
if (state.current === null) return "";
|
|
19010
|
+
const { session, iteration } = computeDeltas(state);
|
|
19011
|
+
if (session === null) {
|
|
19012
|
+
const label = `Run #${state.runCount} captured \u2014 baseline pinned. Watching for changes\u2026`;
|
|
19013
|
+
return `<div data-es-live="strip"><strong>Live</strong> \xB7 ${escapeHtml3(label)}</div>`;
|
|
19014
|
+
}
|
|
19015
|
+
const sessionLine = summarizeDiff(session) ?? "no change yet";
|
|
19016
|
+
let detail = "";
|
|
19017
|
+
if (iteration) {
|
|
19018
|
+
const iterationLine = summarizeDiff(iteration);
|
|
19019
|
+
if (iterationLine) detail = ` \xB7 <span data-es-live="iteration">this iteration: ${escapeHtml3(iterationLine)}</span>`;
|
|
19020
|
+
}
|
|
19021
|
+
return [
|
|
19022
|
+
`<div data-es-live="strip">`,
|
|
19023
|
+
`<strong>Live</strong> \xB7 run #${state.runCount} \xB7 `,
|
|
19024
|
+
`<span data-es-live="session">since you started: ${escapeHtml3(sessionLine)}</span>`,
|
|
19025
|
+
detail,
|
|
19026
|
+
`</div>`
|
|
19027
|
+
].join("");
|
|
19028
|
+
}
|
|
19029
|
+
var RELOAD_CLIENT = `<script data-es-live="client">
|
|
19030
|
+
(function () {
|
|
19031
|
+
try {
|
|
19032
|
+
var es = new EventSource("/__es_reload");
|
|
19033
|
+
es.onmessage = function (e) { if (e.data === "reload") location.reload(); };
|
|
19034
|
+
} catch (err) { /* SSE unavailable: stay static */ }
|
|
19035
|
+
})();
|
|
19036
|
+
</script>`;
|
|
19037
|
+
var STRIP_STYLE = `<style data-es-live="style">
|
|
19038
|
+
[data-es-live="strip"]{position:sticky;top:0;z-index:9999;font:14px/1.5 system-ui,sans-serif;
|
|
19039
|
+
padding:8px 16px;background:#0b1021;color:#e6e9f5;border-bottom:1px solid #2a3052}
|
|
19040
|
+
[data-es-live="strip"] strong{color:#7dd3fc}
|
|
19041
|
+
</style>`;
|
|
19042
|
+
function injectLiveBits(html, stripHtml) {
|
|
19043
|
+
let out = html;
|
|
19044
|
+
const bodyOpen = out.match(/<body[^>]*>/i);
|
|
19045
|
+
if (bodyOpen) {
|
|
19046
|
+
const at = bodyOpen.index + bodyOpen[0].length;
|
|
19047
|
+
out = out.slice(0, at) + stripHtml + out.slice(at);
|
|
19048
|
+
} else {
|
|
19049
|
+
out = stripHtml + out;
|
|
19050
|
+
}
|
|
19051
|
+
const tail = STRIP_STYLE + RELOAD_CLIENT;
|
|
19052
|
+
if (/<\/body>/i.test(out)) {
|
|
19053
|
+
out = out.replace(/<\/body>/i, tail + "</body>");
|
|
19054
|
+
} else {
|
|
19055
|
+
out += tail;
|
|
19056
|
+
}
|
|
19057
|
+
return out;
|
|
19058
|
+
}
|
|
19059
|
+
var CONTENT_TYPES = {
|
|
19060
|
+
".html": "text/html; charset=utf-8",
|
|
19061
|
+
".css": "text/css; charset=utf-8",
|
|
19062
|
+
".js": "text/javascript; charset=utf-8",
|
|
19063
|
+
".json": "application/json; charset=utf-8",
|
|
19064
|
+
".svg": "image/svg+xml",
|
|
19065
|
+
".png": "image/png"
|
|
19066
|
+
};
|
|
19067
|
+
function watchInputDir(filePath, listener) {
|
|
19068
|
+
const dir = path8.dirname(filePath);
|
|
19069
|
+
const base = path8.basename(filePath);
|
|
19070
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
19071
|
+
const watcher = fs7.watch(dir, (_event, changed) => {
|
|
19072
|
+
if (!changed || changed === base) listener();
|
|
19073
|
+
});
|
|
19074
|
+
return { close: () => watcher.close() };
|
|
19075
|
+
}
|
|
19076
|
+
function plainText(html) {
|
|
19077
|
+
return html.replace(/<[^>]+>/g, "").trim();
|
|
19078
|
+
}
|
|
19079
|
+
function startServe(options, deps = {}) {
|
|
19080
|
+
const log = deps.log ?? ((message) => console.log(message));
|
|
19081
|
+
const read = deps.readFile ?? ((filePath) => fs7.readFileSync(filePath, "utf8"));
|
|
19082
|
+
const port = options.port ?? 4321;
|
|
19083
|
+
const host = options.host ?? "127.0.0.1";
|
|
19084
|
+
let state = { sessionBaseline: null, previous: null, current: null, runCount: 0 };
|
|
19085
|
+
let htmlPath = null;
|
|
19086
|
+
let stripHtml = renderDeltaStrip(state);
|
|
19087
|
+
const clients = /* @__PURE__ */ new Set();
|
|
19088
|
+
const pushReload = () => {
|
|
19089
|
+
for (const res of clients) res.write("data: reload\n\n");
|
|
19090
|
+
};
|
|
19091
|
+
const handler = (req, res) => {
|
|
19092
|
+
const url = (req.url ?? "/").split("?")[0];
|
|
19093
|
+
if (url === "/__es_reload") {
|
|
19094
|
+
res.writeHead(200, {
|
|
19095
|
+
"Content-Type": "text/event-stream",
|
|
19096
|
+
"Cache-Control": "no-cache",
|
|
19097
|
+
Connection: "keep-alive"
|
|
19098
|
+
});
|
|
19099
|
+
res.write("retry: 1000\n\n");
|
|
19100
|
+
clients.add(res);
|
|
19101
|
+
req.on("close", () => clients.delete(res));
|
|
19102
|
+
return;
|
|
19103
|
+
}
|
|
19104
|
+
if (url === "/" || url === "/index.html") {
|
|
19105
|
+
const html = htmlPath ? read(htmlPath) : "<!doctype html><html><body><h1>executable-stories</h1></body></html>";
|
|
19106
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
19107
|
+
res.end(injectLiveBits(html, stripHtml));
|
|
19108
|
+
return;
|
|
19109
|
+
}
|
|
19110
|
+
const safe = path8.normalize(url).replace(/^(\.\.[/\\])+/, "");
|
|
19111
|
+
const filePath = path8.join(path8.resolve(options.outputDir), safe);
|
|
19112
|
+
if (filePath.startsWith(path8.resolve(options.outputDir)) && fs7.existsSync(filePath)) {
|
|
19113
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
19114
|
+
res.writeHead(200, { "Content-Type": CONTENT_TYPES[ext] ?? "application/octet-stream" });
|
|
19115
|
+
res.end(read(filePath));
|
|
19116
|
+
return;
|
|
19117
|
+
}
|
|
19118
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
19119
|
+
res.end("Not found");
|
|
19120
|
+
};
|
|
19121
|
+
const server = deps.createServer ? deps.createServer(handler) : http.createServer(handler);
|
|
19122
|
+
const watchOptions = {
|
|
19123
|
+
input: options.input,
|
|
19124
|
+
outputDir: options.outputDir,
|
|
19125
|
+
outputName: options.outputName,
|
|
19126
|
+
formats: options.formats,
|
|
19127
|
+
inputType: options.inputType ?? "raw",
|
|
19128
|
+
synthesize: options.synthesize !== false,
|
|
19129
|
+
debounceMs: options.debounceMs
|
|
19130
|
+
};
|
|
19131
|
+
const watchHandle = startWatch(watchOptions, {
|
|
19132
|
+
readFile: read,
|
|
19133
|
+
watch: deps.watch ?? watchInputDir,
|
|
19134
|
+
log: () => {
|
|
19135
|
+
},
|
|
19136
|
+
// serve emits its own per-run line below
|
|
19137
|
+
regenerate: async (input) => {
|
|
19138
|
+
if (!fs7.existsSync(path8.resolve(input))) return [];
|
|
19139
|
+
const { files, run } = await regenerateRun({ ...watchOptions, input }, { readFile: read });
|
|
19140
|
+
htmlPath = files.find((f) => f.endsWith(".html")) ?? htmlPath;
|
|
19141
|
+
state = advanceState(state, run);
|
|
19142
|
+
stripHtml = renderDeltaStrip(state);
|
|
19143
|
+
log(`Run #${state.runCount}: ${plainText(stripHtml)}`);
|
|
19144
|
+
pushReload();
|
|
19145
|
+
return files;
|
|
19146
|
+
}
|
|
19147
|
+
});
|
|
19148
|
+
server.listen(port, host);
|
|
19149
|
+
const address = server.address();
|
|
19150
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
19151
|
+
log(`Live docs: http://${host}:${boundPort} (Ctrl+C to stop)`);
|
|
19152
|
+
return {
|
|
19153
|
+
port: boundPort,
|
|
19154
|
+
close: () => {
|
|
19155
|
+
watchHandle.close();
|
|
19156
|
+
for (const res of clients) res.end();
|
|
19157
|
+
clients.clear();
|
|
19158
|
+
server.close();
|
|
19159
|
+
}
|
|
19160
|
+
};
|
|
19161
|
+
}
|
|
19162
|
+
|
|
18824
19163
|
// src/behavior-diff.ts
|
|
18825
19164
|
function classifyStatusChange(baseline, current) {
|
|
18826
19165
|
if (baseline === void 0) return "added";
|
|
@@ -18858,7 +19197,10 @@ function diffStoryReports(baseline, current) {
|
|
|
18858
19197
|
};
|
|
18859
19198
|
});
|
|
18860
19199
|
const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
|
|
18861
|
-
for (const s of scenarios)
|
|
19200
|
+
for (const s of scenarios) {
|
|
19201
|
+
if (s.kind === "renamed" || s.kind === "moved") continue;
|
|
19202
|
+
summary[s.kind] += 1;
|
|
19203
|
+
}
|
|
18862
19204
|
return { schemaVersion: "1.0", summary, scenarios };
|
|
18863
19205
|
}
|
|
18864
19206
|
|
|
@@ -19457,27 +19799,27 @@ function pickleStepArgumentToDocs(ps) {
|
|
|
19457
19799
|
}
|
|
19458
19800
|
|
|
19459
19801
|
// src/utils/git-info.ts
|
|
19460
|
-
import * as
|
|
19461
|
-
import * as
|
|
19802
|
+
import * as fs8 from "fs";
|
|
19803
|
+
import * as path9 from "path";
|
|
19462
19804
|
function readGitSha(cwd = process.cwd()) {
|
|
19463
19805
|
const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
|
|
19464
19806
|
if (envSha) return envSha;
|
|
19465
19807
|
const gitDir = findGitDir(cwd);
|
|
19466
19808
|
if (!gitDir) return void 0;
|
|
19467
19809
|
try {
|
|
19468
|
-
const headPath =
|
|
19469
|
-
const head =
|
|
19810
|
+
const headPath = path9.join(gitDir, "HEAD");
|
|
19811
|
+
const head = fs8.readFileSync(headPath, "utf8").trim();
|
|
19470
19812
|
if (!head.startsWith("ref:")) {
|
|
19471
19813
|
return head;
|
|
19472
19814
|
}
|
|
19473
19815
|
const refPath = head.replace("ref:", "").trim();
|
|
19474
|
-
const refFile =
|
|
19475
|
-
if (
|
|
19476
|
-
return
|
|
19816
|
+
const refFile = path9.join(gitDir, refPath);
|
|
19817
|
+
if (fs8.existsSync(refFile)) {
|
|
19818
|
+
return fs8.readFileSync(refFile, "utf8").trim();
|
|
19477
19819
|
}
|
|
19478
|
-
const packedRefs =
|
|
19479
|
-
if (
|
|
19480
|
-
const content =
|
|
19820
|
+
const packedRefs = path9.join(gitDir, "packed-refs");
|
|
19821
|
+
if (fs8.existsSync(packedRefs)) {
|
|
19822
|
+
const content = fs8.readFileSync(packedRefs, "utf8");
|
|
19481
19823
|
for (const line of content.split("\n")) {
|
|
19482
19824
|
if (!line || line.startsWith("#") || line.startsWith("^")) continue;
|
|
19483
19825
|
const [sha, ref] = line.split(" ");
|
|
@@ -19492,19 +19834,19 @@ function readGitSha(cwd = process.cwd()) {
|
|
|
19492
19834
|
function findGitDir(start) {
|
|
19493
19835
|
let current = start;
|
|
19494
19836
|
while (true) {
|
|
19495
|
-
const candidate =
|
|
19496
|
-
if (
|
|
19497
|
-
const stat =
|
|
19837
|
+
const candidate = path9.join(current, ".git");
|
|
19838
|
+
if (fs8.existsSync(candidate)) {
|
|
19839
|
+
const stat = fs8.statSync(candidate);
|
|
19498
19840
|
if (stat.isFile()) {
|
|
19499
|
-
const content =
|
|
19841
|
+
const content = fs8.readFileSync(candidate, "utf8").trim();
|
|
19500
19842
|
const match = content.match(/^gitdir: (.+)$/);
|
|
19501
19843
|
if (match) {
|
|
19502
|
-
return
|
|
19844
|
+
return path9.resolve(current, match[1]);
|
|
19503
19845
|
}
|
|
19504
19846
|
}
|
|
19505
19847
|
return candidate;
|
|
19506
19848
|
}
|
|
19507
|
-
const parent =
|
|
19849
|
+
const parent = path9.dirname(current);
|
|
19508
19850
|
if (parent === current) return void 0;
|
|
19509
19851
|
current = parent;
|
|
19510
19852
|
}
|
|
@@ -19515,8 +19857,8 @@ function readBranchName(cwd = process.cwd()) {
|
|
|
19515
19857
|
const gitDir = findGitDir(cwd);
|
|
19516
19858
|
if (!gitDir) return void 0;
|
|
19517
19859
|
try {
|
|
19518
|
-
const headPath =
|
|
19519
|
-
const head =
|
|
19860
|
+
const headPath = path9.join(gitDir, "HEAD");
|
|
19861
|
+
const head = fs8.readFileSync(headPath, "utf8").trim();
|
|
19520
19862
|
if (head.startsWith("ref:")) {
|
|
19521
19863
|
const refPath = head.replace("ref:", "").trim();
|
|
19522
19864
|
const match = refPath.match(/^refs\/heads\/(.+)$/);
|
|
@@ -19553,8 +19895,8 @@ function nanosecondsToMs(ns) {
|
|
|
19553
19895
|
}
|
|
19554
19896
|
|
|
19555
19897
|
// src/utils/metadata.ts
|
|
19556
|
-
import * as
|
|
19557
|
-
import * as
|
|
19898
|
+
import * as fs9 from "fs";
|
|
19899
|
+
import * as path10 from "path";
|
|
19558
19900
|
var versionCache = /* @__PURE__ */ new Map();
|
|
19559
19901
|
function readPackageVersion(root) {
|
|
19560
19902
|
if (versionCache.has(root)) {
|
|
@@ -19565,18 +19907,18 @@ function readPackageVersion(root) {
|
|
|
19565
19907
|
return version;
|
|
19566
19908
|
}
|
|
19567
19909
|
function findPackageVersion(startDir) {
|
|
19568
|
-
let current =
|
|
19910
|
+
let current = path10.resolve(startDir);
|
|
19569
19911
|
while (true) {
|
|
19570
|
-
const pkgPath =
|
|
19912
|
+
const pkgPath = path10.join(current, "package.json");
|
|
19571
19913
|
try {
|
|
19572
|
-
if (
|
|
19573
|
-
const raw =
|
|
19914
|
+
if (fs9.existsSync(pkgPath)) {
|
|
19915
|
+
const raw = fs9.readFileSync(pkgPath, "utf8");
|
|
19574
19916
|
const parsed = JSON.parse(raw);
|
|
19575
19917
|
return parsed.version;
|
|
19576
19918
|
}
|
|
19577
19919
|
} catch {
|
|
19578
19920
|
}
|
|
19579
|
-
const parent =
|
|
19921
|
+
const parent = path10.dirname(current);
|
|
19580
19922
|
if (parent === current) {
|
|
19581
19923
|
return void 0;
|
|
19582
19924
|
}
|
|
@@ -20904,18 +21246,18 @@ function deriveChangeType(tags) {
|
|
|
20904
21246
|
}
|
|
20905
21247
|
return "unknown";
|
|
20906
21248
|
}
|
|
20907
|
-
function extensionOf(
|
|
20908
|
-
const base =
|
|
21249
|
+
function extensionOf(path13) {
|
|
21250
|
+
const base = path13.split("/").pop() ?? path13;
|
|
20909
21251
|
const dot = base.lastIndexOf(".");
|
|
20910
21252
|
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
20911
21253
|
}
|
|
20912
|
-
function isTestFile(
|
|
20913
|
-
return TEST_INFIX.test(
|
|
21254
|
+
function isTestFile(path13) {
|
|
21255
|
+
return TEST_INFIX.test(path13);
|
|
20914
21256
|
}
|
|
20915
|
-
function isReviewableSource(
|
|
20916
|
-
if (isTestFile(
|
|
20917
|
-
if (
|
|
20918
|
-
return CODE_EXTENSIONS.has(extensionOf(
|
|
21257
|
+
function isReviewableSource(path13) {
|
|
21258
|
+
if (isTestFile(path13)) return false;
|
|
21259
|
+
if (path13.endsWith(".d.ts")) return false;
|
|
21260
|
+
return CODE_EXTENSIONS.has(extensionOf(path13));
|
|
20919
21261
|
}
|
|
20920
21262
|
function testBaseKey(testFile) {
|
|
20921
21263
|
return testFile.replace(TEST_INFIX, "");
|
|
@@ -21019,7 +21361,7 @@ function toClaim(testCase, changedSourcePaths) {
|
|
|
21019
21361
|
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
21020
21362
|
const key = testBaseKey(testCase.sourceFile);
|
|
21021
21363
|
const coversFiles = changedSourcePaths.filter(
|
|
21022
|
-
(
|
|
21364
|
+
(path13) => sourceBaseKey(path13) === key
|
|
21023
21365
|
);
|
|
21024
21366
|
return {
|
|
21025
21367
|
id: testCase.id,
|
|
@@ -21268,7 +21610,7 @@ var ReviewMarkdownFormatter = class {
|
|
|
21268
21610
|
};
|
|
21269
21611
|
|
|
21270
21612
|
// src/formatters/review-html.ts
|
|
21271
|
-
function
|
|
21613
|
+
function escapeHtml4(value) {
|
|
21272
21614
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
21273
21615
|
}
|
|
21274
21616
|
var STRENGTH_LABEL = {
|
|
@@ -21290,22 +21632,22 @@ function statusIcon3(status) {
|
|
|
21290
21632
|
}
|
|
21291
21633
|
}
|
|
21292
21634
|
function formatStep3(step) {
|
|
21293
|
-
return `<li><strong>${
|
|
21635
|
+
return `<li><strong>${escapeHtml4(step.keyword)}</strong> ${escapeHtml4(step.text)}</li>`;
|
|
21294
21636
|
}
|
|
21295
21637
|
function inlineDoc(doc) {
|
|
21296
21638
|
switch (doc.kind) {
|
|
21297
21639
|
case "note":
|
|
21298
|
-
return
|
|
21640
|
+
return escapeHtml4(doc.text);
|
|
21299
21641
|
case "section":
|
|
21300
|
-
return `<strong>${
|
|
21642
|
+
return `<strong>${escapeHtml4(doc.title)}</strong>: ${escapeHtml4(doc.markdown)}`;
|
|
21301
21643
|
case "kv":
|
|
21302
|
-
return `${
|
|
21644
|
+
return `${escapeHtml4(doc.label)}: ${escapeHtml4(String(doc.value))}`;
|
|
21303
21645
|
case "code":
|
|
21304
|
-
return `${
|
|
21646
|
+
return `${escapeHtml4(doc.label)}: <code>${escapeHtml4(doc.content)}</code>`;
|
|
21305
21647
|
case "link":
|
|
21306
|
-
return `${
|
|
21648
|
+
return `${escapeHtml4(doc.label)}: ${escapeHtml4(doc.url)}`;
|
|
21307
21649
|
default:
|
|
21308
|
-
return
|
|
21650
|
+
return escapeHtml4(doc.kind);
|
|
21309
21651
|
}
|
|
21310
21652
|
}
|
|
21311
21653
|
function renderEvidenceArtifacts(testCase) {
|
|
@@ -21313,7 +21655,7 @@ function renderEvidenceArtifacts(testCase) {
|
|
|
21313
21655
|
for (const att of testCase.attachments) {
|
|
21314
21656
|
if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
|
|
21315
21657
|
parts.push(
|
|
21316
|
-
`<img class="shot" alt="${
|
|
21658
|
+
`<img class="shot" alt="${escapeHtml4(att.name)}" src="data:${escapeHtml4(att.mediaType)};base64,${att.body}" />`
|
|
21317
21659
|
);
|
|
21318
21660
|
}
|
|
21319
21661
|
}
|
|
@@ -21328,22 +21670,22 @@ function renderTicketPills(claim) {
|
|
|
21328
21670
|
const tickets = claim.testCase.story.tickets ?? [];
|
|
21329
21671
|
if (tickets.length === 0) return "";
|
|
21330
21672
|
return `<div class="ticket-row">${tickets.map((ticket) => {
|
|
21331
|
-
const label =
|
|
21673
|
+
const label = escapeHtml4(ticket.id);
|
|
21332
21674
|
if (ticket.url) {
|
|
21333
|
-
return `<a class="ticket-pill" href="${
|
|
21675
|
+
return `<a class="ticket-pill" href="${escapeHtml4(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
|
|
21334
21676
|
}
|
|
21335
21677
|
return `<span class="ticket-pill">${label}</span>`;
|
|
21336
21678
|
}).join("")}</div>`;
|
|
21337
21679
|
}
|
|
21338
21680
|
function renderClaimCard(claim) {
|
|
21339
21681
|
const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
|
|
21340
|
-
const search =
|
|
21682
|
+
const search = escapeHtml4(
|
|
21341
21683
|
`${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
|
|
21342
21684
|
).toLowerCase();
|
|
21343
21685
|
const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
|
|
21344
|
-
const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${
|
|
21345
|
-
const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${
|
|
21346
|
-
const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${
|
|
21686
|
+
const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml4(r)}</li>`).join("")}</ul>`;
|
|
21687
|
+
const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml4(claim.intent)}</div>` : "";
|
|
21688
|
+
const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml4(f)}</code>`).join(", ")}</p>` : "";
|
|
21347
21689
|
const docs = (claim.testCase.story.docs ?? []).filter(
|
|
21348
21690
|
(d) => d.kind === "section" || d.kind === "note"
|
|
21349
21691
|
);
|
|
@@ -21353,9 +21695,9 @@ function renderClaimCard(claim) {
|
|
|
21353
21695
|
<header class="claim-header">
|
|
21354
21696
|
<div>
|
|
21355
21697
|
<span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
|
|
21356
|
-
${claim.changeType !== "unknown" ? `<span class="change-pill">${
|
|
21357
|
-
<h3>${statusIcon3(claim.status)} ${
|
|
21358
|
-
<p class="source">${
|
|
21698
|
+
${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml4(claim.changeType)}</span>` : ""}
|
|
21699
|
+
<h3>${statusIcon3(claim.status)} ${escapeHtml4(claim.scenario)}</h3>
|
|
21700
|
+
<p class="source">${escapeHtml4(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
|
|
21359
21701
|
${renderTicketPills(claim)}
|
|
21360
21702
|
</div>
|
|
21361
21703
|
</header>
|
|
@@ -21370,18 +21712,18 @@ function renderClaimCard(claim) {
|
|
|
21370
21712
|
</article>`;
|
|
21371
21713
|
}
|
|
21372
21714
|
function renderChangedFileRow(file) {
|
|
21373
|
-
const claims = file.claims.length > 0 ? file.claims.map((c) => `${
|
|
21715
|
+
const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml4(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
|
|
21374
21716
|
return `<tr data-band="${file.band}">
|
|
21375
21717
|
<td><span class="band-dot band-${file.band}"></span></td>
|
|
21376
|
-
<td><code>${
|
|
21377
|
-
<td>${
|
|
21718
|
+
<td><code>${escapeHtml4(file.path)}</code></td>
|
|
21719
|
+
<td>${escapeHtml4(file.changeKind)}</td>
|
|
21378
21720
|
<td>${claims}</td>
|
|
21379
21721
|
</tr>`;
|
|
21380
21722
|
}
|
|
21381
21723
|
function renderAudienceSection2(title, claims) {
|
|
21382
21724
|
if (claims.length === 0) return "";
|
|
21383
21725
|
return `<section class="audience-section">
|
|
21384
|
-
<h2>${
|
|
21726
|
+
<h2>${escapeHtml4(title)} <span class="count">${claims.length}</span></h2>
|
|
21385
21727
|
<div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
|
|
21386
21728
|
</section>`;
|
|
21387
21729
|
}
|
|
@@ -21471,13 +21813,13 @@ var ReviewHtmlFormatter = class {
|
|
|
21471
21813
|
const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
|
|
21472
21814
|
applyTheme(getEffectiveTheme());` : "";
|
|
21473
21815
|
const themeAttr = this.darkMode ? ' data-theme="light"' : "";
|
|
21474
|
-
const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${
|
|
21816
|
+
const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml4(context.baseRef ?? "base")} \u2192 ${escapeHtml4(context.headRef ?? "head")}</p>` : "";
|
|
21475
21817
|
return `<!doctype html>
|
|
21476
21818
|
<html lang="en"${themeAttr}>
|
|
21477
21819
|
<head>
|
|
21478
21820
|
<meta charset="utf-8" />
|
|
21479
21821
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
21480
|
-
<title>${
|
|
21822
|
+
<title>${escapeHtml4(this.title)}</title>
|
|
21481
21823
|
<style>
|
|
21482
21824
|
${this.theme.css}
|
|
21483
21825
|
${REVIEW_CSS}
|
|
@@ -21487,7 +21829,7 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
21487
21829
|
<main>
|
|
21488
21830
|
<div class="hero-card card">
|
|
21489
21831
|
<div class="review-header">
|
|
21490
|
-
<h1>${
|
|
21832
|
+
<h1>${escapeHtml4(this.title)}</h1>
|
|
21491
21833
|
${themeToggleHtml}
|
|
21492
21834
|
</div>
|
|
21493
21835
|
${refsLine}
|
|
@@ -21502,7 +21844,7 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
21502
21844
|
</section>
|
|
21503
21845
|
<section class="card priority-banner">
|
|
21504
21846
|
<h2>Review priority</h2>
|
|
21505
|
-
<p class="subtle">${
|
|
21847
|
+
<p class="subtle">${escapeHtml4(priority)}</p>
|
|
21506
21848
|
</section>
|
|
21507
21849
|
${changedFilesPanel}
|
|
21508
21850
|
<section class="toolbar">
|
|
@@ -21550,8 +21892,8 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
21550
21892
|
};
|
|
21551
21893
|
|
|
21552
21894
|
// src/deploy/ledger.ts
|
|
21553
|
-
import * as
|
|
21554
|
-
import * as
|
|
21895
|
+
import * as fs10 from "fs";
|
|
21896
|
+
import * as path11 from "path";
|
|
21555
21897
|
function createEmptyLedger() {
|
|
21556
21898
|
return {
|
|
21557
21899
|
deployments: [],
|
|
@@ -21559,12 +21901,12 @@ function createEmptyLedger() {
|
|
|
21559
21901
|
};
|
|
21560
21902
|
}
|
|
21561
21903
|
function loadLedger(ledgerPath) {
|
|
21562
|
-
const resolved =
|
|
21563
|
-
if (!
|
|
21904
|
+
const resolved = path11.resolve(ledgerPath);
|
|
21905
|
+
if (!fs10.existsSync(resolved)) {
|
|
21564
21906
|
return createEmptyLedger();
|
|
21565
21907
|
}
|
|
21566
21908
|
try {
|
|
21567
|
-
const raw = JSON.parse(
|
|
21909
|
+
const raw = JSON.parse(fs10.readFileSync(resolved, "utf8"));
|
|
21568
21910
|
if (raw.schemaVersion !== 1) {
|
|
21569
21911
|
throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
|
|
21570
21912
|
}
|
|
@@ -21575,10 +21917,10 @@ function loadLedger(ledgerPath) {
|
|
|
21575
21917
|
}
|
|
21576
21918
|
}
|
|
21577
21919
|
function saveLedger(ledger, ledgerPath) {
|
|
21578
|
-
const resolved =
|
|
21579
|
-
const dir =
|
|
21580
|
-
|
|
21581
|
-
|
|
21920
|
+
const resolved = path11.resolve(ledgerPath);
|
|
21921
|
+
const dir = path11.dirname(resolved);
|
|
21922
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
21923
|
+
fs10.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
|
|
21582
21924
|
}
|
|
21583
21925
|
function getLatestDeployment(ledger, environment) {
|
|
21584
21926
|
return [...ledger.deployments].reverse().find((d) => d.environment === environment);
|
|
@@ -21698,11 +22040,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
21698
22040
|
const ext = FORMAT_EXTENSIONS[format];
|
|
21699
22041
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
21700
22042
|
if (mode === "aggregated") {
|
|
21701
|
-
return toPosix(
|
|
22043
|
+
return toPosix(path12.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
|
|
21702
22044
|
}
|
|
21703
22045
|
const normalizedSource = toPosix(sourceFile);
|
|
21704
|
-
const dirOfSource =
|
|
21705
|
-
let baseName =
|
|
22046
|
+
const dirOfSource = path12.posix.dirname(normalizedSource);
|
|
22047
|
+
let baseName = path12.posix.basename(normalizedSource);
|
|
21706
22048
|
for (const testExt of TEST_EXTENSIONS) {
|
|
21707
22049
|
if (baseName.endsWith(testExt)) {
|
|
21708
22050
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -21711,12 +22053,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
21711
22053
|
}
|
|
21712
22054
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
21713
22055
|
if (colocatedStyle === "adjacent") {
|
|
21714
|
-
return toPosix(
|
|
22056
|
+
return toPosix(path12.posix.join(dirOfSource, fileName));
|
|
21715
22057
|
}
|
|
21716
22058
|
if (colocatedStyle === "flat") {
|
|
21717
|
-
return toPosix(
|
|
22059
|
+
return toPosix(path12.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
|
|
21718
22060
|
}
|
|
21719
|
-
return toPosix(
|
|
22061
|
+
return toPosix(path12.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
21720
22062
|
}
|
|
21721
22063
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
21722
22064
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -21929,8 +22271,8 @@ var ReportGenerator = class {
|
|
|
21929
22271
|
if (astroPaths) {
|
|
21930
22272
|
for (const mdPath of astroPaths) {
|
|
21931
22273
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
21932
|
-
const mdDir =
|
|
21933
|
-
const assetsDir =
|
|
22274
|
+
const mdDir = path12.dirname(mdPath);
|
|
22275
|
+
const assetsDir = path12.resolve(this.options.astro.assetsDir);
|
|
21934
22276
|
const result = copyMarkdownAssets({
|
|
21935
22277
|
markdown: content,
|
|
21936
22278
|
markdownDir: mdDir,
|
|
@@ -21961,9 +22303,9 @@ var ReportGenerator = class {
|
|
|
21961
22303
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
21962
22304
|
const ext = FORMAT_EXTENSIONS[format];
|
|
21963
22305
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
21964
|
-
const outputPath = toPosix(
|
|
22306
|
+
const outputPath = toPosix(path12.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
|
|
21965
22307
|
const content = await this.formatContent(run, format);
|
|
21966
|
-
const dir =
|
|
22308
|
+
const dir = path12.dirname(outputPath);
|
|
21967
22309
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
21968
22310
|
await this.deps.writeFile(outputPath, content);
|
|
21969
22311
|
return [outputPath];
|
|
@@ -21975,7 +22317,7 @@ var ReportGenerator = class {
|
|
|
21975
22317
|
testCases
|
|
21976
22318
|
};
|
|
21977
22319
|
const content = await this.formatContent(groupRun, format);
|
|
21978
|
-
const dir =
|
|
22320
|
+
const dir = path12.dirname(outputPath);
|
|
21979
22321
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
21980
22322
|
await this.deps.writeFile(outputPath, content);
|
|
21981
22323
|
writtenPaths.push(outputPath);
|
|
@@ -22127,7 +22469,7 @@ async function generateRunComparison(args) {
|
|
|
22127
22469
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
22128
22470
|
for (const format of args.formats) {
|
|
22129
22471
|
const ext = format === "html" ? ".html" : ".md";
|
|
22130
|
-
const outputPath = toPosix(
|
|
22472
|
+
const outputPath = toPosix(path12.join(outputDir, `${outputName}${ext}`));
|
|
22131
22473
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
22132
22474
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
22133
22475
|
files.push(outputPath);
|
|
@@ -22176,6 +22518,7 @@ export {
|
|
|
22176
22518
|
adaptJestRun,
|
|
22177
22519
|
adaptPlaywrightRun,
|
|
22178
22520
|
adaptVitestRun,
|
|
22521
|
+
advanceState,
|
|
22179
22522
|
assertValidRun,
|
|
22180
22523
|
buildCheck,
|
|
22181
22524
|
buildGoal,
|
|
@@ -22188,6 +22531,7 @@ export {
|
|
|
22188
22531
|
canonicalizeRun,
|
|
22189
22532
|
classifyStatusChange,
|
|
22190
22533
|
clearVersionCache,
|
|
22534
|
+
computeDeltas,
|
|
22191
22535
|
computeTestMetrics,
|
|
22192
22536
|
copyMarkdownAssets,
|
|
22193
22537
|
createPrCommentSummary,
|
|
@@ -22210,6 +22554,7 @@ export {
|
|
|
22210
22554
|
getEnvironmentDrift,
|
|
22211
22555
|
gradeEvidence,
|
|
22212
22556
|
hasSufficientHistory,
|
|
22557
|
+
injectLiveBits,
|
|
22213
22558
|
isReviewableSource,
|
|
22214
22559
|
isTestFile,
|
|
22215
22560
|
joinNameAndExt,
|
|
@@ -22231,7 +22576,9 @@ export {
|
|
|
22231
22576
|
readPackageVersion,
|
|
22232
22577
|
recordDeployment,
|
|
22233
22578
|
regenerateArtifacts,
|
|
22579
|
+
regenerateRun,
|
|
22234
22580
|
renderCheck,
|
|
22581
|
+
renderDeltaStrip,
|
|
22235
22582
|
renderGoal,
|
|
22236
22583
|
renderTriage,
|
|
22237
22584
|
resolveAttachment,
|
|
@@ -22247,6 +22594,7 @@ export {
|
|
|
22247
22594
|
sendWebhookNotification,
|
|
22248
22595
|
signBody,
|
|
22249
22596
|
slugify,
|
|
22597
|
+
startServe,
|
|
22250
22598
|
startWatch,
|
|
22251
22599
|
stripAnsi,
|
|
22252
22600
|
toBehaviorManifest,
|