executable-stories-formatters 0.8.0 → 0.10.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/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +1306 -96
- package/dist/cli.js.map +1 -1
- package/dist/{index-BiAYcEiz.d.cts → index-CbWFyoTx.d.cts} +161 -4
- package/dist/{index-BiAYcEiz.d.ts → index-CbWFyoTx.d.ts} +161 -4
- package/dist/index.cjs +1196 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +413 -126
- package/dist/index.d.ts +413 -126
- package/dist/index.js +1178 -55
- package/dist/index.js.map +1 -1
- package/package.json +3 -5
- package/schemas/behavior-manifest-v1.json +65 -0
- package/schemas/examples/dotnet.json +84 -20
- package/schemas/examples/go.json +77 -20
- package/schemas/examples/junit5.json +84 -20
- package/schemas/examples/pytest.json +92 -20
- package/schemas/examples/rust.json +84 -20
- package/schemas/raw-run.schema.json +49 -2
- package/schemas/scenario-index-v1.json +88 -0
- package/schemas/story-report-v1.json +5 -0
- package/bin/intent.js +0 -3
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
AstroFormatter: () => AstroFormatter,
|
|
34
|
+
BehaviorManifestJsonFormatter: () => BehaviorManifestJsonFormatter,
|
|
34
35
|
ConfluenceFormatter: () => ConfluenceFormatter,
|
|
35
36
|
CucumberHtmlFormatter: () => CucumberHtmlFormatter,
|
|
36
37
|
CucumberJsonFormatter: () => CucumberJsonFormatter,
|
|
@@ -44,29 +45,37 @@ __export(src_exports, {
|
|
|
44
45
|
MIN_PERF_SAMPLES: () => MIN_PERF_SAMPLES,
|
|
45
46
|
MarkdownFormatter: () => MarkdownFormatter,
|
|
46
47
|
ReportGenerator: () => ReportGenerator,
|
|
48
|
+
ReviewHtmlFormatter: () => ReviewHtmlFormatter,
|
|
49
|
+
ReviewMarkdownFormatter: () => ReviewMarkdownFormatter,
|
|
47
50
|
RunDiffHtmlFormatter: () => RunDiffHtmlFormatter,
|
|
48
51
|
RunDiffMarkdownFormatter: () => RunDiffMarkdownFormatter,
|
|
49
52
|
STORY_META_KEY: () => STORY_META_KEY,
|
|
50
53
|
STORY_REPORT_SCHEMA_MAJOR: () => STORY_REPORT_SCHEMA_MAJOR,
|
|
51
54
|
STORY_REPORT_SCHEMA_VERSION: () => STORY_REPORT_SCHEMA_VERSION,
|
|
55
|
+
ScenarioIndexJsonFormatter: () => ScenarioIndexJsonFormatter,
|
|
52
56
|
StoryReportJsonFormatter: () => StoryReportJsonFormatter,
|
|
53
57
|
adaptJestRun: () => adaptJestRun,
|
|
54
58
|
adaptPlaywrightRun: () => adaptPlaywrightRun,
|
|
55
59
|
adaptVitestRun: () => adaptVitestRun,
|
|
56
60
|
assertValidRun: () => assertValidRun,
|
|
61
|
+
buildReview: () => buildReview,
|
|
57
62
|
bundleAssets: () => bundleAssets,
|
|
58
63
|
calculateFlakiness: () => calculateFlakiness,
|
|
59
64
|
calculateStability: () => calculateStability,
|
|
60
65
|
canonicalizeRun: () => canonicalizeRun,
|
|
66
|
+
classifyStatusChange: () => classifyStatusChange,
|
|
61
67
|
clearVersionCache: () => clearVersionCache,
|
|
62
68
|
computeTestMetrics: () => computeTestMetrics,
|
|
63
69
|
copyMarkdownAssets: () => copyMarkdownAssets,
|
|
64
70
|
createPrCommentSummary: () => createPrCommentSummary,
|
|
65
71
|
createReportGenerator: () => createReportGenerator,
|
|
72
|
+
deriveAudience: () => deriveAudience,
|
|
73
|
+
deriveChangeType: () => deriveChangeType,
|
|
66
74
|
deriveStepResults: () => deriveStepResults,
|
|
67
75
|
detectCI: () => detectCI4,
|
|
68
76
|
detectPerformanceTrend: () => detectPerformanceTrend,
|
|
69
77
|
diffRuns: () => diffRuns,
|
|
78
|
+
diffStoryReports: () => diffStoryReports,
|
|
70
79
|
findGitDir: () => findGitDir,
|
|
71
80
|
formatDuration: () => formatDuration3,
|
|
72
81
|
generateRunComparison: () => generateRunComparison,
|
|
@@ -74,7 +83,10 @@ __export(src_exports, {
|
|
|
74
83
|
generateTestCaseId: () => generateTestCaseId,
|
|
75
84
|
getAvailableThemes: () => getAvailableThemes,
|
|
76
85
|
getCssOnlyThemes: () => getCssOnlyThemes,
|
|
86
|
+
gradeEvidence: () => gradeEvidence,
|
|
77
87
|
hasSufficientHistory: () => hasSufficientHistory,
|
|
88
|
+
isReviewableSource: () => isReviewableSource,
|
|
89
|
+
isTestFile: () => isTestFile,
|
|
78
90
|
listScenarios: () => listScenarios,
|
|
79
91
|
loadHistory: () => loadHistory,
|
|
80
92
|
mergeStepResults: () => mergeStepResults,
|
|
@@ -91,29 +103,34 @@ __export(src_exports, {
|
|
|
91
103
|
readBranchName: () => readBranchName,
|
|
92
104
|
readGitSha: () => readGitSha,
|
|
93
105
|
readPackageVersion: () => readPackageVersion,
|
|
106
|
+
regenerateArtifacts: () => regenerateArtifacts,
|
|
94
107
|
resolveAttachment: () => resolveAttachment,
|
|
95
108
|
resolveAttachments: () => resolveAttachments,
|
|
96
109
|
resolveTheme: () => resolveTheme,
|
|
97
110
|
resolveTraceUrl: () => resolveTraceUrl,
|
|
98
111
|
rewriteAssetPaths: () => rewriteAssetPaths,
|
|
99
112
|
saveHistory: () => saveHistory,
|
|
113
|
+
scenariosCoveringPaths: () => scenariosCoveringPaths,
|
|
100
114
|
sendNotifications: () => sendNotifications,
|
|
101
115
|
sendSlackNotification: () => sendSlackNotification,
|
|
102
116
|
sendTeamsNotification: () => sendTeamsNotification,
|
|
103
117
|
sendWebhookNotification: () => sendWebhookNotification,
|
|
104
118
|
signBody: () => signBody,
|
|
105
119
|
slugify: () => slugify,
|
|
120
|
+
startWatch: () => startWatch,
|
|
106
121
|
stripAnsi: () => stripAnsi,
|
|
122
|
+
toBehaviorManifest: () => toBehaviorManifest,
|
|
107
123
|
toCIInfo: () => toCIInfo,
|
|
108
124
|
toRawCIInfo: () => toRawCIInfo,
|
|
125
|
+
toScenarioIndex: () => toScenarioIndex,
|
|
109
126
|
toStoryReport: () => toStoryReport,
|
|
110
127
|
tryGetActiveOtelContext: () => tryGetActiveOtelContext,
|
|
111
128
|
updateHistory: () => updateHistory,
|
|
112
129
|
validateCanonicalRun: () => validateCanonicalRun
|
|
113
130
|
});
|
|
114
131
|
module.exports = __toCommonJS(src_exports);
|
|
115
|
-
var
|
|
116
|
-
var
|
|
132
|
+
var fs9 = require("fs");
|
|
133
|
+
var path10 = __toESM(require("path"), 1);
|
|
117
134
|
var fsPromises = __toESM(require("fs/promises"), 1);
|
|
118
135
|
|
|
119
136
|
// src/converters/acl/status.ts
|
|
@@ -394,7 +411,8 @@ function canonicalizeTestCase(raw, options, projectRoot) {
|
|
|
394
411
|
projectName: raw.projectName,
|
|
395
412
|
retry: raw.retry ?? 0,
|
|
396
413
|
retries: raw.retries ?? 0,
|
|
397
|
-
tags
|
|
414
|
+
tags,
|
|
415
|
+
...raw.evidence ? { evidence: raw.evidence } : {}
|
|
398
416
|
};
|
|
399
417
|
}
|
|
400
418
|
function normalizeTags(story) {
|
|
@@ -958,6 +976,9 @@ function buildScenario(tc, featureId) {
|
|
|
958
976
|
if (tickets && tickets.length > 0) {
|
|
959
977
|
scenario.tickets = tickets.map((t) => t.url ? { id: t.id, url: t.url } : { id: t.id });
|
|
960
978
|
}
|
|
979
|
+
if (tc.story.covers && tc.story.covers.length > 0) {
|
|
980
|
+
scenario.covers = [...tc.story.covers];
|
|
981
|
+
}
|
|
961
982
|
return scenario;
|
|
962
983
|
}
|
|
963
984
|
function deriveFeatureTitle(group, relSourceFile) {
|
|
@@ -1081,6 +1102,181 @@ var StoryReportJsonFormatter = class {
|
|
|
1081
1102
|
}
|
|
1082
1103
|
};
|
|
1083
1104
|
|
|
1105
|
+
// src/formatters/scenario-index-json.ts
|
|
1106
|
+
var ScenarioIndexJsonFormatter = class {
|
|
1107
|
+
options;
|
|
1108
|
+
constructor(options = {}) {
|
|
1109
|
+
this.options = {
|
|
1110
|
+
pretty: options.pretty ?? true,
|
|
1111
|
+
filters: options.filters
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
toIndex(run) {
|
|
1115
|
+
return toScenarioIndex(toStoryReport(run), this.options.filters);
|
|
1116
|
+
}
|
|
1117
|
+
format(run) {
|
|
1118
|
+
const index = this.toIndex(run);
|
|
1119
|
+
return this.options.pretty ? JSON.stringify(index, null, 2) : JSON.stringify(index);
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
function toScenarioIndex(report, filters = {}) {
|
|
1123
|
+
const scenarios = report.features.flatMap(
|
|
1124
|
+
(feature) => feature.scenarios.map((scenario) => toScenarioIndexItem(feature, scenario))
|
|
1125
|
+
).filter((scenario) => matchesFilters(scenario, filters));
|
|
1126
|
+
return {
|
|
1127
|
+
schemaVersion: "1.0",
|
|
1128
|
+
runId: report.runId,
|
|
1129
|
+
generatedAtMs: report.finishedAtMs,
|
|
1130
|
+
summary: summarize(scenarios),
|
|
1131
|
+
scenarios
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function toScenarioIndexItem(feature, scenario) {
|
|
1135
|
+
return {
|
|
1136
|
+
id: scenario.id,
|
|
1137
|
+
title: scenario.title,
|
|
1138
|
+
status: scenario.status,
|
|
1139
|
+
feature: feature.title,
|
|
1140
|
+
sourceFile: feature.sourceFile,
|
|
1141
|
+
sourceLine: scenario.sourceLine,
|
|
1142
|
+
tags: scenario.tags,
|
|
1143
|
+
tickets: scenario.tickets ?? [],
|
|
1144
|
+
covers: scenario.covers ?? [],
|
|
1145
|
+
durationMs: scenario.durationMs,
|
|
1146
|
+
steps: scenario.steps.map((step) => ({
|
|
1147
|
+
id: step.id,
|
|
1148
|
+
index: step.index,
|
|
1149
|
+
keyword: step.keyword,
|
|
1150
|
+
text: step.text,
|
|
1151
|
+
status: step.status,
|
|
1152
|
+
durationMs: step.durationMs,
|
|
1153
|
+
errorMessage: step.errorMessage,
|
|
1154
|
+
docKinds: step.docEntries.map((entry) => entry.kind)
|
|
1155
|
+
})),
|
|
1156
|
+
docKinds: scenario.docEntries.map((entry) => entry.kind),
|
|
1157
|
+
error: scenario.errorMessage ? { message: scenario.errorMessage, stack: scenario.errorStack } : void 0
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
function matchesFilters(scenario, filters) {
|
|
1161
|
+
if (filters.statuses?.length && !filters.statuses.includes(scenario.status)) {
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
if (filters.tags?.length && !filters.tags.some((tag) => scenario.tags.includes(tag))) {
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
if (filters.sourceFiles?.length && !filters.sourceFiles.some((sourceFile) => scenario.sourceFile.includes(sourceFile))) {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
return true;
|
|
1171
|
+
}
|
|
1172
|
+
function summarize(scenarios) {
|
|
1173
|
+
return {
|
|
1174
|
+
total: scenarios.length,
|
|
1175
|
+
passed: scenarios.filter((scenario) => scenario.status === "passed").length,
|
|
1176
|
+
failed: scenarios.filter((scenario) => scenario.status === "failed").length,
|
|
1177
|
+
skipped: scenarios.filter((scenario) => scenario.status === "skipped").length,
|
|
1178
|
+
pending: scenarios.filter((scenario) => scenario.status === "pending").length,
|
|
1179
|
+
durationMs: scenarios.reduce((total, scenario) => total + scenario.durationMs, 0)
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/formatters/behavior-manifest-json.ts
|
|
1184
|
+
var BehaviorManifestJsonFormatter = class {
|
|
1185
|
+
pretty;
|
|
1186
|
+
constructor(options = {}) {
|
|
1187
|
+
this.pretty = options.pretty ?? true;
|
|
1188
|
+
}
|
|
1189
|
+
toManifest(run) {
|
|
1190
|
+
return toBehaviorManifest(toStoryReport(run));
|
|
1191
|
+
}
|
|
1192
|
+
format(run) {
|
|
1193
|
+
const manifest = this.toManifest(run);
|
|
1194
|
+
return this.pretty ? JSON.stringify(manifest, null, 2) : JSON.stringify(manifest);
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
function toBehaviorManifest(report) {
|
|
1198
|
+
const index = toScenarioIndex(report);
|
|
1199
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1200
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
1201
|
+
const docKinds = /* @__PURE__ */ new Set();
|
|
1202
|
+
const debuggerIssues = [];
|
|
1203
|
+
for (const scenario of index.scenarios) {
|
|
1204
|
+
const source = bySource.get(scenario.sourceFile) ?? {
|
|
1205
|
+
path: scenario.sourceFile,
|
|
1206
|
+
scenarioCount: 0,
|
|
1207
|
+
failed: 0,
|
|
1208
|
+
tags: []
|
|
1209
|
+
};
|
|
1210
|
+
source.scenarioCount += 1;
|
|
1211
|
+
if (scenario.status === "failed") source.failed += 1;
|
|
1212
|
+
source.tags = [.../* @__PURE__ */ new Set([...source.tags, ...scenario.tags])].sort();
|
|
1213
|
+
bySource.set(scenario.sourceFile, source);
|
|
1214
|
+
for (const tag of scenario.tags) {
|
|
1215
|
+
const tagEntry = byTag.get(tag) ?? { name: tag, scenarioCount: 0 };
|
|
1216
|
+
tagEntry.scenarioCount += 1;
|
|
1217
|
+
byTag.set(tag, tagEntry);
|
|
1218
|
+
}
|
|
1219
|
+
for (const kind of scenario.docKinds) docKinds.add(kind);
|
|
1220
|
+
for (const step of scenario.steps) {
|
|
1221
|
+
for (const kind of step.docKinds) docKinds.add(kind);
|
|
1222
|
+
}
|
|
1223
|
+
if (!scenarioHasDocs(scenario)) {
|
|
1224
|
+
debuggerIssues.push({
|
|
1225
|
+
severity: "warning",
|
|
1226
|
+
code: "missing-docs",
|
|
1227
|
+
scenarioId: scenario.id,
|
|
1228
|
+
title: scenario.title,
|
|
1229
|
+
message: "Scenario has no doc entries."
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
if (scenario.tags.length === 0) {
|
|
1233
|
+
debuggerIssues.push({
|
|
1234
|
+
severity: "warning",
|
|
1235
|
+
code: "missing-tags",
|
|
1236
|
+
scenarioId: scenario.id,
|
|
1237
|
+
title: scenario.title,
|
|
1238
|
+
message: "Scenario has no tags."
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
if (scenario.covers.length === 0) {
|
|
1242
|
+
debuggerIssues.push({
|
|
1243
|
+
severity: "warning",
|
|
1244
|
+
code: "missing-covers",
|
|
1245
|
+
scenarioId: scenario.id,
|
|
1246
|
+
title: scenario.title,
|
|
1247
|
+
message: "Scenario declares no covers (product-code paths), so code\u2192scenario lookup cannot find it."
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
if (scenario.sourceLine === void 0) {
|
|
1251
|
+
debuggerIssues.push({
|
|
1252
|
+
severity: "warning",
|
|
1253
|
+
code: "missing-source-line",
|
|
1254
|
+
scenarioId: scenario.id,
|
|
1255
|
+
title: scenario.title,
|
|
1256
|
+
message: "Scenario has no source line."
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
const scenariosWithDocs = index.scenarios.filter(scenarioHasDocs).length;
|
|
1261
|
+
return {
|
|
1262
|
+
schemaVersion: "1.0",
|
|
1263
|
+
runId: report.runId,
|
|
1264
|
+
generatedAtMs: report.finishedAtMs,
|
|
1265
|
+
summary: index.summary,
|
|
1266
|
+
sourceFiles: [...bySource.values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
1267
|
+
tags: [...byTag.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
1268
|
+
docCoverage: {
|
|
1269
|
+
scenariosWithDocs,
|
|
1270
|
+
scenariosWithoutDocs: index.scenarios.length - scenariosWithDocs,
|
|
1271
|
+
docKinds: [...docKinds].sort()
|
|
1272
|
+
},
|
|
1273
|
+
debugger: debuggerIssues
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function scenarioHasDocs(scenario) {
|
|
1277
|
+
return scenario.docKinds.length > 0 || scenario.steps.some((step) => step.docKinds.length > 0);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1084
1280
|
// src/formatters/html/renderers/index.ts
|
|
1085
1281
|
var fs2 = __toESM(require("fs"), 1);
|
|
1086
1282
|
var path3 = __toESM(require("path"), 1);
|
|
@@ -13927,7 +14123,7 @@ function renderDocEntry(entry, deps) {
|
|
|
13927
14123
|
// src/formatters/html/renderers/steps.ts
|
|
13928
14124
|
var CONTINUATION_KEYWORDS = ["And", "But", "*"];
|
|
13929
14125
|
function renderStep(step, stepResult, index, deps) {
|
|
13930
|
-
const
|
|
14126
|
+
const statusIcon4 = stepResult ? deps.getStatusIcon(stepResult.status) : "\u25CB";
|
|
13931
14127
|
const statusClass = stepResult ? `status-${stepResult.status}` : "";
|
|
13932
14128
|
const duration = stepResult && stepResult.durationMs > 0 ? `${stepResult.durationMs}ms` : "";
|
|
13933
14129
|
const keywordTrimmed = step.keyword.trim();
|
|
@@ -13936,7 +14132,7 @@ function renderStep(step, stepResult, index, deps) {
|
|
|
13936
14132
|
const stepDocs = deps.renderDocs(step.docs, "step-docs");
|
|
13937
14133
|
const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
|
|
13938
14134
|
return `<div class="${stepClass}" data-keyword="${deps.escapeHtml(keywordTrimmed)}" data-text="${deps.escapeHtml(step.text)}">
|
|
13939
|
-
<span class="step-status ${statusClass}">${
|
|
14135
|
+
<span class="step-status ${statusClass}">${statusIcon4}</span>
|
|
13940
14136
|
<span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
|
|
13941
14137
|
<span class="step-text">${textHtml}</span>
|
|
13942
14138
|
<span class="step-duration">${duration}</span>
|
|
@@ -13986,16 +14182,16 @@ function hasSufficientHistory(entries, min) {
|
|
|
13986
14182
|
}
|
|
13987
14183
|
|
|
13988
14184
|
// src/formatters/html/renderers/scenario.ts
|
|
13989
|
-
function renderTicket(ticket, template,
|
|
14185
|
+
function renderTicket(ticket, template, escapeHtml4) {
|
|
13990
14186
|
const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
|
|
13991
14187
|
if (url) {
|
|
13992
|
-
return `<a class="tag ticket-tag" href="${
|
|
14188
|
+
return `<a class="tag ticket-tag" href="${escapeHtml4(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml4(ticket.id)}</a>`;
|
|
13993
14189
|
}
|
|
13994
|
-
return `<span class="tag ticket-tag">${
|
|
14190
|
+
return `<span class="tag ticket-tag">${escapeHtml4(ticket.id)}</span>`;
|
|
13995
14191
|
}
|
|
13996
14192
|
function renderScenario(args, deps) {
|
|
13997
14193
|
const { tc } = args;
|
|
13998
|
-
const
|
|
14194
|
+
const statusIcon4 = deps.getStatusIcon(tc.status);
|
|
13999
14195
|
const statusClass = `status-${tc.status}`;
|
|
14000
14196
|
const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
|
|
14001
14197
|
const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
|
|
@@ -14065,7 +14261,7 @@ function renderScenario(args, deps) {
|
|
|
14065
14261
|
<div class="scenario-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
|
|
14066
14262
|
<div class="scenario-info">
|
|
14067
14263
|
<div class="scenario-title">
|
|
14068
|
-
<span class="status-icon ${statusClass}">${
|
|
14264
|
+
<span class="status-icon ${statusClass}">${statusIcon4}</span>
|
|
14069
14265
|
<span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
|
|
14070
14266
|
</div>
|
|
14071
14267
|
<div class="scenario-meta">${tags}${tickets}${outcomeBadge}${sourceLink}${traceBadge}${metricBadges}</div>
|
|
@@ -14191,7 +14387,7 @@ function flattenTree(roots) {
|
|
|
14191
14387
|
}
|
|
14192
14388
|
return result;
|
|
14193
14389
|
}
|
|
14194
|
-
function buildTooltip(span,
|
|
14390
|
+
function buildTooltip(span, escapeHtml4) {
|
|
14195
14391
|
const parts = [];
|
|
14196
14392
|
parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
|
|
14197
14393
|
if (span.statusMessage) {
|
|
@@ -14209,7 +14405,7 @@ function buildTooltip(span, escapeHtml3) {
|
|
|
14209
14405
|
if (text2.length > TOOLTIP_MAX_LENGTH) {
|
|
14210
14406
|
text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
|
|
14211
14407
|
}
|
|
14212
|
-
return
|
|
14408
|
+
return escapeHtml4(text2);
|
|
14213
14409
|
}
|
|
14214
14410
|
function renderTraceView(args, deps) {
|
|
14215
14411
|
if (!args.spans || args.spans.length === 0) return "";
|
|
@@ -14432,11 +14628,11 @@ function renderToc(args, deps) {
|
|
|
14432
14628
|
const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
|
|
14433
14629
|
const featureSlug = `feature-${slugify(file)}`;
|
|
14434
14630
|
const scenarios = testCases.map((tc) => {
|
|
14435
|
-
const
|
|
14631
|
+
const statusIcon4 = deps.getStatusIcon(tc.status);
|
|
14436
14632
|
const statusClass = `status-${tc.status}`;
|
|
14437
14633
|
const failedClass = tc.status === "failed" ? " toc-failed" : "";
|
|
14438
14634
|
return `<a class="toc-scenario${failedClass}" href="#scenario-${tc.id}">
|
|
14439
|
-
<span class="toc-status ${statusClass}">${
|
|
14635
|
+
<span class="toc-status ${statusClass}">${statusIcon4}</span>
|
|
14440
14636
|
${deps.escapeHtml(tc.story.scenario)}
|
|
14441
14637
|
</a>`;
|
|
14442
14638
|
}).join("\n");
|
|
@@ -15949,8 +16145,8 @@ function extractDocAttachments(step) {
|
|
|
15949
16145
|
}
|
|
15950
16146
|
return attachments;
|
|
15951
16147
|
}
|
|
15952
|
-
function guessMediaType(
|
|
15953
|
-
const lower =
|
|
16148
|
+
function guessMediaType(path11) {
|
|
16149
|
+
const lower = path11.toLowerCase();
|
|
15954
16150
|
if (lower.endsWith(".png")) return "image/png";
|
|
15955
16151
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
15956
16152
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -16091,11 +16287,11 @@ var CucumberHtmlFormatter = class {
|
|
|
16091
16287
|
for (const envelope of envelopes) {
|
|
16092
16288
|
const accepted = htmlStream.write(envelope);
|
|
16093
16289
|
if (!accepted) {
|
|
16094
|
-
await new Promise((
|
|
16290
|
+
await new Promise((resolve8) => htmlStream.once("drain", resolve8));
|
|
16095
16291
|
}
|
|
16096
16292
|
}
|
|
16097
|
-
await new Promise((
|
|
16098
|
-
collector.on("finish",
|
|
16293
|
+
await new Promise((resolve8, reject) => {
|
|
16294
|
+
collector.on("finish", resolve8);
|
|
16099
16295
|
collector.on("error", reject);
|
|
16100
16296
|
htmlStream.end();
|
|
16101
16297
|
});
|
|
@@ -18084,6 +18280,184 @@ ${result.errors.join("\n")}`);
|
|
|
18084
18280
|
}
|
|
18085
18281
|
}
|
|
18086
18282
|
|
|
18283
|
+
// src/coverage-index.ts
|
|
18284
|
+
function normalizePath(path11) {
|
|
18285
|
+
return path11.replace(/^\.\//, "");
|
|
18286
|
+
}
|
|
18287
|
+
function scenariosCoveringPaths(index, paths) {
|
|
18288
|
+
const queries = paths.map(normalizePath);
|
|
18289
|
+
return index.scenarios.filter(
|
|
18290
|
+
(scenario) => scenario.covers.some(
|
|
18291
|
+
(glob) => queries.some((path11) => matchesPattern(normalizePath(glob), path11))
|
|
18292
|
+
)
|
|
18293
|
+
);
|
|
18294
|
+
}
|
|
18295
|
+
|
|
18296
|
+
// src/watch.ts
|
|
18297
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
18298
|
+
var path7 = __toESM(require("path"), 1);
|
|
18299
|
+
|
|
18300
|
+
// src/converters/synthesize.ts
|
|
18301
|
+
var KEYWORD_MAP = {
|
|
18302
|
+
given: "Given",
|
|
18303
|
+
when: "When",
|
|
18304
|
+
then: "Then",
|
|
18305
|
+
and: "And",
|
|
18306
|
+
but: "But"
|
|
18307
|
+
};
|
|
18308
|
+
function normalizeKeyword(keyword) {
|
|
18309
|
+
return KEYWORD_MAP[keyword.toLowerCase()] ?? keyword;
|
|
18310
|
+
}
|
|
18311
|
+
function normalizeStepKeywords(steps) {
|
|
18312
|
+
return steps.map((step) => ({
|
|
18313
|
+
...step,
|
|
18314
|
+
keyword: normalizeKeyword(step.keyword)
|
|
18315
|
+
}));
|
|
18316
|
+
}
|
|
18317
|
+
function deriveScenarioName(tc) {
|
|
18318
|
+
if (tc.title) return tc.title;
|
|
18319
|
+
if (tc.titlePath && tc.titlePath.length > 0) {
|
|
18320
|
+
return tc.titlePath[tc.titlePath.length - 1];
|
|
18321
|
+
}
|
|
18322
|
+
return "Untitled";
|
|
18323
|
+
}
|
|
18324
|
+
function synthesizeStories(raw) {
|
|
18325
|
+
return {
|
|
18326
|
+
...raw,
|
|
18327
|
+
testCases: raw.testCases.map(synthesizeTestCase)
|
|
18328
|
+
};
|
|
18329
|
+
}
|
|
18330
|
+
function synthesizeTestCase(tc) {
|
|
18331
|
+
if (tc.story == null) {
|
|
18332
|
+
const scenario = deriveScenarioName(tc);
|
|
18333
|
+
return {
|
|
18334
|
+
...tc,
|
|
18335
|
+
story: {
|
|
18336
|
+
scenario,
|
|
18337
|
+
steps: [{ keyword: "Then", text: scenario }]
|
|
18338
|
+
}
|
|
18339
|
+
};
|
|
18340
|
+
}
|
|
18341
|
+
const steps = tc.story.steps;
|
|
18342
|
+
if (!steps || steps.length === 0) {
|
|
18343
|
+
return {
|
|
18344
|
+
...tc,
|
|
18345
|
+
story: {
|
|
18346
|
+
...tc.story,
|
|
18347
|
+
steps: [{ keyword: "Then", text: tc.story.scenario }]
|
|
18348
|
+
}
|
|
18349
|
+
};
|
|
18350
|
+
}
|
|
18351
|
+
return {
|
|
18352
|
+
...tc,
|
|
18353
|
+
story: {
|
|
18354
|
+
...tc.story,
|
|
18355
|
+
steps: normalizeStepKeywords(steps)
|
|
18356
|
+
}
|
|
18357
|
+
};
|
|
18358
|
+
}
|
|
18359
|
+
|
|
18360
|
+
// src/watch.ts
|
|
18361
|
+
function toRun(data, inputType, synthesize) {
|
|
18362
|
+
if (inputType === "canonical") return data;
|
|
18363
|
+
let raw = data;
|
|
18364
|
+
if (synthesize) raw = synthesizeStories(raw);
|
|
18365
|
+
return canonicalizeRun(raw);
|
|
18366
|
+
}
|
|
18367
|
+
async function regenerateArtifacts(options, deps = {}) {
|
|
18368
|
+
const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
|
|
18369
|
+
const data = JSON.parse(read(path7.resolve(options.input)));
|
|
18370
|
+
const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
|
|
18371
|
+
const generator = new ReportGenerator({
|
|
18372
|
+
formats: options.formats,
|
|
18373
|
+
outputDir: options.outputDir,
|
|
18374
|
+
outputName: options.outputName
|
|
18375
|
+
});
|
|
18376
|
+
const result = await generator.generate(run);
|
|
18377
|
+
return [...result.values()].flat();
|
|
18378
|
+
}
|
|
18379
|
+
function startWatch(options, deps = {}) {
|
|
18380
|
+
const log = deps.log ?? ((message) => console.log(message));
|
|
18381
|
+
const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
|
|
18382
|
+
const watchFn = deps.watch ?? ((filePath, listener) => fs6.watch(filePath, listener));
|
|
18383
|
+
const debounceMs = options.debounceMs ?? 150;
|
|
18384
|
+
let timer;
|
|
18385
|
+
let running = false;
|
|
18386
|
+
let pending = false;
|
|
18387
|
+
const run = async () => {
|
|
18388
|
+
if (running) {
|
|
18389
|
+
pending = true;
|
|
18390
|
+
return;
|
|
18391
|
+
}
|
|
18392
|
+
running = true;
|
|
18393
|
+
try {
|
|
18394
|
+
const files = await regenerate(options.input);
|
|
18395
|
+
log(`Regenerated ${files.length} artifact file(s) from ${options.input}`);
|
|
18396
|
+
} catch (error) {
|
|
18397
|
+
log(`Watch regeneration failed: ${error.message}`);
|
|
18398
|
+
} finally {
|
|
18399
|
+
running = false;
|
|
18400
|
+
if (pending) {
|
|
18401
|
+
pending = false;
|
|
18402
|
+
trigger();
|
|
18403
|
+
}
|
|
18404
|
+
}
|
|
18405
|
+
};
|
|
18406
|
+
const trigger = () => {
|
|
18407
|
+
if (timer) clearTimeout(timer);
|
|
18408
|
+
timer = setTimeout(() => void run(), debounceMs);
|
|
18409
|
+
};
|
|
18410
|
+
trigger();
|
|
18411
|
+
const watcher = watchFn(path7.resolve(options.input), trigger);
|
|
18412
|
+
return {
|
|
18413
|
+
close: () => {
|
|
18414
|
+
if (timer) clearTimeout(timer);
|
|
18415
|
+
watcher.close();
|
|
18416
|
+
}
|
|
18417
|
+
};
|
|
18418
|
+
}
|
|
18419
|
+
|
|
18420
|
+
// src/behavior-diff.ts
|
|
18421
|
+
function classifyStatusChange(baseline, current) {
|
|
18422
|
+
if (baseline === void 0) return "added";
|
|
18423
|
+
if (current === void 0) return "removed";
|
|
18424
|
+
if (baseline === current) return "unchanged";
|
|
18425
|
+
if (baseline === "passed" && current === "failed") return "regressed";
|
|
18426
|
+
if (baseline === "failed" && current === "passed") return "fixed";
|
|
18427
|
+
return "changed";
|
|
18428
|
+
}
|
|
18429
|
+
function scenarioMap(report) {
|
|
18430
|
+
const map = /* @__PURE__ */ new Map();
|
|
18431
|
+
for (const feature of report.features) {
|
|
18432
|
+
for (const scenario of feature.scenarios) {
|
|
18433
|
+
map.set(scenario.id, { scenario, sourceFile: feature.sourceFile });
|
|
18434
|
+
}
|
|
18435
|
+
}
|
|
18436
|
+
return map;
|
|
18437
|
+
}
|
|
18438
|
+
function diffStoryReports(baseline, current) {
|
|
18439
|
+
const base = scenarioMap(baseline);
|
|
18440
|
+
const curr = scenarioMap(current);
|
|
18441
|
+
const ids = [.../* @__PURE__ */ new Set([...base.keys(), ...curr.keys()])];
|
|
18442
|
+
const scenarios = ids.map((id) => {
|
|
18443
|
+
const b = base.get(id);
|
|
18444
|
+
const c = curr.get(id);
|
|
18445
|
+
const kind = classifyStatusChange(b?.scenario.status, c?.scenario.status);
|
|
18446
|
+
const meta = c ?? b;
|
|
18447
|
+
return {
|
|
18448
|
+
id,
|
|
18449
|
+
title: meta.scenario.title,
|
|
18450
|
+
sourceFile: meta.sourceFile,
|
|
18451
|
+
kind,
|
|
18452
|
+
baselineStatus: b?.scenario.status,
|
|
18453
|
+
currentStatus: c?.scenario.status
|
|
18454
|
+
};
|
|
18455
|
+
});
|
|
18456
|
+
const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
|
|
18457
|
+
for (const s of scenarios) summary[s.kind] += 1;
|
|
18458
|
+
return { schemaVersion: "1.0", summary, scenarios };
|
|
18459
|
+
}
|
|
18460
|
+
|
|
18087
18461
|
// src/publishers/confluence.ts
|
|
18088
18462
|
function parseAdf(adf) {
|
|
18089
18463
|
let parsed;
|
|
@@ -18679,27 +19053,27 @@ function pickleStepArgumentToDocs(ps) {
|
|
|
18679
19053
|
}
|
|
18680
19054
|
|
|
18681
19055
|
// src/utils/git-info.ts
|
|
18682
|
-
var
|
|
18683
|
-
var
|
|
19056
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
19057
|
+
var path8 = __toESM(require("path"), 1);
|
|
18684
19058
|
function readGitSha(cwd = process.cwd()) {
|
|
18685
19059
|
const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
|
|
18686
19060
|
if (envSha) return envSha;
|
|
18687
19061
|
const gitDir = findGitDir(cwd);
|
|
18688
19062
|
if (!gitDir) return void 0;
|
|
18689
19063
|
try {
|
|
18690
|
-
const headPath =
|
|
18691
|
-
const head =
|
|
19064
|
+
const headPath = path8.join(gitDir, "HEAD");
|
|
19065
|
+
const head = fs7.readFileSync(headPath, "utf8").trim();
|
|
18692
19066
|
if (!head.startsWith("ref:")) {
|
|
18693
19067
|
return head;
|
|
18694
19068
|
}
|
|
18695
19069
|
const refPath = head.replace("ref:", "").trim();
|
|
18696
|
-
const refFile =
|
|
18697
|
-
if (
|
|
18698
|
-
return
|
|
19070
|
+
const refFile = path8.join(gitDir, refPath);
|
|
19071
|
+
if (fs7.existsSync(refFile)) {
|
|
19072
|
+
return fs7.readFileSync(refFile, "utf8").trim();
|
|
18699
19073
|
}
|
|
18700
|
-
const packedRefs =
|
|
18701
|
-
if (
|
|
18702
|
-
const content =
|
|
19074
|
+
const packedRefs = path8.join(gitDir, "packed-refs");
|
|
19075
|
+
if (fs7.existsSync(packedRefs)) {
|
|
19076
|
+
const content = fs7.readFileSync(packedRefs, "utf8");
|
|
18703
19077
|
for (const line of content.split("\n")) {
|
|
18704
19078
|
if (!line || line.startsWith("#") || line.startsWith("^")) continue;
|
|
18705
19079
|
const [sha, ref] = line.split(" ");
|
|
@@ -18714,19 +19088,19 @@ function readGitSha(cwd = process.cwd()) {
|
|
|
18714
19088
|
function findGitDir(start) {
|
|
18715
19089
|
let current = start;
|
|
18716
19090
|
while (true) {
|
|
18717
|
-
const candidate =
|
|
18718
|
-
if (
|
|
18719
|
-
const stat =
|
|
19091
|
+
const candidate = path8.join(current, ".git");
|
|
19092
|
+
if (fs7.existsSync(candidate)) {
|
|
19093
|
+
const stat = fs7.statSync(candidate);
|
|
18720
19094
|
if (stat.isFile()) {
|
|
18721
|
-
const content =
|
|
19095
|
+
const content = fs7.readFileSync(candidate, "utf8").trim();
|
|
18722
19096
|
const match = content.match(/^gitdir: (.+)$/);
|
|
18723
19097
|
if (match) {
|
|
18724
|
-
return
|
|
19098
|
+
return path8.resolve(current, match[1]);
|
|
18725
19099
|
}
|
|
18726
19100
|
}
|
|
18727
19101
|
return candidate;
|
|
18728
19102
|
}
|
|
18729
|
-
const parent =
|
|
19103
|
+
const parent = path8.dirname(current);
|
|
18730
19104
|
if (parent === current) return void 0;
|
|
18731
19105
|
current = parent;
|
|
18732
19106
|
}
|
|
@@ -18737,8 +19111,8 @@ function readBranchName(cwd = process.cwd()) {
|
|
|
18737
19111
|
const gitDir = findGitDir(cwd);
|
|
18738
19112
|
if (!gitDir) return void 0;
|
|
18739
19113
|
try {
|
|
18740
|
-
const headPath =
|
|
18741
|
-
const head =
|
|
19114
|
+
const headPath = path8.join(gitDir, "HEAD");
|
|
19115
|
+
const head = fs7.readFileSync(headPath, "utf8").trim();
|
|
18742
19116
|
if (head.startsWith("ref:")) {
|
|
18743
19117
|
const refPath = head.replace("ref:", "").trim();
|
|
18744
19118
|
const match = refPath.match(/^refs\/heads\/(.+)$/);
|
|
@@ -18775,8 +19149,8 @@ function nanosecondsToMs(ns) {
|
|
|
18775
19149
|
}
|
|
18776
19150
|
|
|
18777
19151
|
// src/utils/metadata.ts
|
|
18778
|
-
var
|
|
18779
|
-
var
|
|
19152
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
19153
|
+
var path9 = __toESM(require("path"), 1);
|
|
18780
19154
|
var versionCache = /* @__PURE__ */ new Map();
|
|
18781
19155
|
function readPackageVersion(root) {
|
|
18782
19156
|
if (versionCache.has(root)) {
|
|
@@ -18787,18 +19161,18 @@ function readPackageVersion(root) {
|
|
|
18787
19161
|
return version;
|
|
18788
19162
|
}
|
|
18789
19163
|
function findPackageVersion(startDir) {
|
|
18790
|
-
let current =
|
|
19164
|
+
let current = path9.resolve(startDir);
|
|
18791
19165
|
while (true) {
|
|
18792
|
-
const pkgPath =
|
|
19166
|
+
const pkgPath = path9.join(current, "package.json");
|
|
18793
19167
|
try {
|
|
18794
|
-
if (
|
|
18795
|
-
const raw =
|
|
19168
|
+
if (fs8.existsSync(pkgPath)) {
|
|
19169
|
+
const raw = fs8.readFileSync(pkgPath, "utf8");
|
|
18796
19170
|
const parsed = JSON.parse(raw);
|
|
18797
19171
|
return parsed.version;
|
|
18798
19172
|
}
|
|
18799
19173
|
} catch {
|
|
18800
19174
|
}
|
|
18801
|
-
const parent =
|
|
19175
|
+
const parent = path9.dirname(current);
|
|
18802
19176
|
if (parent === current) {
|
|
18803
19177
|
return void 0;
|
|
18804
19178
|
}
|
|
@@ -19669,12 +20043,22 @@ function listScenarios(args, _deps) {
|
|
|
19669
20043
|
const { testCases, format } = args;
|
|
19670
20044
|
if (format === "json") {
|
|
19671
20045
|
const items = testCases.map((tc) => ({
|
|
20046
|
+
id: tc.id,
|
|
19672
20047
|
scenario: tc.story.scenario,
|
|
19673
20048
|
status: tc.status,
|
|
19674
20049
|
sourceFile: tc.sourceFile,
|
|
19675
20050
|
sourceLine: tc.sourceLine,
|
|
20051
|
+
suitePath: tc.story.suitePath ?? tc.titlePath.slice(0, -1),
|
|
19676
20052
|
tags: tc.tags,
|
|
19677
|
-
|
|
20053
|
+
tickets: tc.story.tickets ?? [],
|
|
20054
|
+
covers: tc.story.covers ?? [],
|
|
20055
|
+
durationMs: tc.durationMs,
|
|
20056
|
+
error: tc.errorMessage ? {
|
|
20057
|
+
message: tc.errorMessage,
|
|
20058
|
+
stack: tc.errorStack
|
|
20059
|
+
} : void 0,
|
|
20060
|
+
steps: tc.story.steps.map((step, index) => toScenarioStep(step, index, tc)),
|
|
20061
|
+
docKinds: collectDocKinds(tc)
|
|
19678
20062
|
}));
|
|
19679
20063
|
return JSON.stringify(items, null, 2);
|
|
19680
20064
|
}
|
|
@@ -19747,10 +20131,730 @@ function listScenarios(args, _deps) {
|
|
|
19747
20131
|
];
|
|
19748
20132
|
return lines.join("\n");
|
|
19749
20133
|
}
|
|
20134
|
+
function toScenarioStep(step, index, testCase) {
|
|
20135
|
+
const result = testCase.stepResults.find(
|
|
20136
|
+
(candidate) => candidate.index === index || candidate.stepId === step.id
|
|
20137
|
+
);
|
|
20138
|
+
return {
|
|
20139
|
+
id: step.id,
|
|
20140
|
+
index,
|
|
20141
|
+
keyword: step.keyword,
|
|
20142
|
+
text: step.text,
|
|
20143
|
+
status: result?.status ?? testCase.status,
|
|
20144
|
+
durationMs: result?.durationMs ?? step.durationMs ?? 0,
|
|
20145
|
+
errorMessage: result?.errorMessage,
|
|
20146
|
+
mode: step.mode,
|
|
20147
|
+
docKinds: (step.docs ?? []).map((doc) => doc.kind)
|
|
20148
|
+
};
|
|
20149
|
+
}
|
|
20150
|
+
function collectDocKinds(testCase) {
|
|
20151
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
20152
|
+
for (const doc of testCase.story.docs ?? []) {
|
|
20153
|
+
kinds.add(doc.kind);
|
|
20154
|
+
}
|
|
20155
|
+
for (const step of testCase.story.steps) {
|
|
20156
|
+
for (const doc of step.docs ?? []) {
|
|
20157
|
+
kinds.add(doc.kind);
|
|
20158
|
+
}
|
|
20159
|
+
}
|
|
20160
|
+
return [...kinds].sort();
|
|
20161
|
+
}
|
|
20162
|
+
|
|
20163
|
+
// src/review/conventions.ts
|
|
20164
|
+
var CHANGE_TAG_PREFIX = "change:";
|
|
20165
|
+
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
20166
|
+
var VALID_CHANGE_TYPES = /* @__PURE__ */ new Set([
|
|
20167
|
+
"feature",
|
|
20168
|
+
"bugfix",
|
|
20169
|
+
"refactor",
|
|
20170
|
+
"perf",
|
|
20171
|
+
"deps"
|
|
20172
|
+
]);
|
|
20173
|
+
var STAKEHOLDER_FILE = /(?:\.e2e\.)|(?:^|\/)e2e\/|(?:\.spec\.)/i;
|
|
20174
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
20175
|
+
"ts",
|
|
20176
|
+
"tsx",
|
|
20177
|
+
"js",
|
|
20178
|
+
"jsx",
|
|
20179
|
+
"mjs",
|
|
20180
|
+
"cjs",
|
|
20181
|
+
"py",
|
|
20182
|
+
"go",
|
|
20183
|
+
"rs",
|
|
20184
|
+
"kt",
|
|
20185
|
+
"kts",
|
|
20186
|
+
"java",
|
|
20187
|
+
"cs",
|
|
20188
|
+
"rb"
|
|
20189
|
+
]);
|
|
20190
|
+
var TEST_INFIX = /\.(?:story\.)?(?:int\.|e2e\.|unit\.)?(?:test|spec|cy)\.[a-z]+$/i;
|
|
20191
|
+
function deriveAudience(sourceFile, tags) {
|
|
20192
|
+
const override = tags.map((t) => t.toLowerCase()).find((t) => t.startsWith(AUDIENCE_TAG_PREFIX));
|
|
20193
|
+
if (override) {
|
|
20194
|
+
const value = override.slice(AUDIENCE_TAG_PREFIX.length);
|
|
20195
|
+
if (value === "stakeholder" || value === "engineer") return value;
|
|
20196
|
+
}
|
|
20197
|
+
return STAKEHOLDER_FILE.test(sourceFile) ? "stakeholder" : "engineer";
|
|
20198
|
+
}
|
|
20199
|
+
function deriveChangeType(tags) {
|
|
20200
|
+
for (const tag of tags) {
|
|
20201
|
+
const lower = tag.toLowerCase();
|
|
20202
|
+
if (lower.startsWith(CHANGE_TAG_PREFIX)) {
|
|
20203
|
+
const value = lower.slice(CHANGE_TAG_PREFIX.length);
|
|
20204
|
+
if (VALID_CHANGE_TYPES.has(value)) return value;
|
|
20205
|
+
}
|
|
20206
|
+
}
|
|
20207
|
+
return "unknown";
|
|
20208
|
+
}
|
|
20209
|
+
function extensionOf(path11) {
|
|
20210
|
+
const base = path11.split("/").pop() ?? path11;
|
|
20211
|
+
const dot = base.lastIndexOf(".");
|
|
20212
|
+
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
20213
|
+
}
|
|
20214
|
+
function isTestFile(path11) {
|
|
20215
|
+
return TEST_INFIX.test(path11);
|
|
20216
|
+
}
|
|
20217
|
+
function isReviewableSource(path11) {
|
|
20218
|
+
if (isTestFile(path11)) return false;
|
|
20219
|
+
if (path11.endsWith(".d.ts")) return false;
|
|
20220
|
+
return CODE_EXTENSIONS.has(extensionOf(path11));
|
|
20221
|
+
}
|
|
20222
|
+
function testBaseKey(testFile) {
|
|
20223
|
+
return testFile.replace(TEST_INFIX, "");
|
|
20224
|
+
}
|
|
20225
|
+
function sourceBaseKey(sourceFile) {
|
|
20226
|
+
const dot = sourceFile.lastIndexOf(".");
|
|
20227
|
+
const slash = sourceFile.lastIndexOf("/");
|
|
20228
|
+
return dot > slash ? sourceFile.slice(0, dot) : sourceFile;
|
|
20229
|
+
}
|
|
20230
|
+
|
|
20231
|
+
// src/review/build-review.ts
|
|
20232
|
+
var STRENGTH_RANK = {
|
|
20233
|
+
none: 0,
|
|
20234
|
+
weak: 1,
|
|
20235
|
+
moderate: 2,
|
|
20236
|
+
strong: 3
|
|
20237
|
+
};
|
|
20238
|
+
var INTENT_SECTION_TITLE = /\b(why|intent|approach|rationale|reasoning)\b/i;
|
|
20239
|
+
function findDoc(docs, predicate) {
|
|
20240
|
+
if (!docs) return void 0;
|
|
20241
|
+
for (const doc of docs) {
|
|
20242
|
+
if (predicate(doc)) return doc;
|
|
20243
|
+
const nested = findDoc(doc.children, predicate);
|
|
20244
|
+
if (nested) return nested;
|
|
20245
|
+
}
|
|
20246
|
+
return void 0;
|
|
20247
|
+
}
|
|
20248
|
+
function anyDoc(docs, predicate) {
|
|
20249
|
+
return findDoc(docs, predicate) !== void 0;
|
|
20250
|
+
}
|
|
20251
|
+
function extractIntent(testCase) {
|
|
20252
|
+
const docs = testCase.story.docs;
|
|
20253
|
+
const section = findDoc(
|
|
20254
|
+
docs,
|
|
20255
|
+
(d) => d.kind === "section" && INTENT_SECTION_TITLE.test(d.title)
|
|
20256
|
+
);
|
|
20257
|
+
if (section && section.kind === "section") return section.markdown;
|
|
20258
|
+
const note = findDoc(docs, (d) => d.kind === "note");
|
|
20259
|
+
if (note && note.kind === "note") return note.text;
|
|
20260
|
+
return void 0;
|
|
20261
|
+
}
|
|
20262
|
+
function hasScreenshot(testCase) {
|
|
20263
|
+
if (testCase.attachments.some((a) => a.mediaType.startsWith("image/"))) {
|
|
20264
|
+
return true;
|
|
20265
|
+
}
|
|
20266
|
+
if (anyDoc(testCase.story.docs, (d) => d.kind === "screenshot")) return true;
|
|
20267
|
+
return testCase.story.steps.some(
|
|
20268
|
+
(step) => anyDoc(step.docs, (d) => d.kind === "screenshot")
|
|
20269
|
+
);
|
|
20270
|
+
}
|
|
20271
|
+
function hasOtelTrace(testCase) {
|
|
20272
|
+
return (testCase.story.otelSpans?.length ?? 0) > 0;
|
|
20273
|
+
}
|
|
20274
|
+
function gradeEvidence(testCase, audience) {
|
|
20275
|
+
if (testCase.status !== "passed") {
|
|
20276
|
+
return {
|
|
20277
|
+
strength: "none",
|
|
20278
|
+
reasons: [`test is ${testCase.status} \u2014 the proof does not hold`]
|
|
20279
|
+
};
|
|
20280
|
+
}
|
|
20281
|
+
const ev = testCase.evidence;
|
|
20282
|
+
const screenshot = hasScreenshot(testCase);
|
|
20283
|
+
const otel = hasOtelTrace(testCase);
|
|
20284
|
+
const isIntegration = /\.int\.test\./i.test(testCase.sourceFile);
|
|
20285
|
+
const mutation = ev?.mutationScorePct;
|
|
20286
|
+
const changedCov = ev?.changedLineCoveragePct;
|
|
20287
|
+
const strong2 = [];
|
|
20288
|
+
if (ev?.failingFirstVerified) {
|
|
20289
|
+
strong2.push("failing-first verified (red on base ref, green on head)");
|
|
20290
|
+
}
|
|
20291
|
+
if (typeof mutation === "number" && mutation >= 80) {
|
|
20292
|
+
strong2.push(`mutation score ${mutation}% (\u226580%)`);
|
|
20293
|
+
}
|
|
20294
|
+
if (screenshot && otel) {
|
|
20295
|
+
strong2.push("backed by screenshot + OTEL trace");
|
|
20296
|
+
} else if (audience === "stakeholder" && (screenshot || otel)) {
|
|
20297
|
+
strong2.push(`stakeholder proof: ${screenshot ? "screenshot" : "OTEL trace"}`);
|
|
20298
|
+
}
|
|
20299
|
+
if (strong2.length > 0) return { strength: "strong", reasons: strong2 };
|
|
20300
|
+
const moderate = [];
|
|
20301
|
+
if (screenshot) moderate.push("screenshot attached");
|
|
20302
|
+
if (otel) moderate.push("OTEL trace attached");
|
|
20303
|
+
if (typeof mutation === "number" && mutation >= 50) {
|
|
20304
|
+
moderate.push(`mutation score ${mutation}%`);
|
|
20305
|
+
}
|
|
20306
|
+
if (typeof changedCov === "number" && changedCov >= 80) {
|
|
20307
|
+
moderate.push(`changed-line coverage ${changedCov}%`);
|
|
20308
|
+
}
|
|
20309
|
+
if (isIntegration) moderate.push("integration-level test");
|
|
20310
|
+
if (moderate.length > 0) return { strength: "moderate", reasons: moderate };
|
|
20311
|
+
return {
|
|
20312
|
+
strength: "weak",
|
|
20313
|
+
reasons: [
|
|
20314
|
+
"passing test only \u2014 no corroborating evidence (add e2e proof, mutation score, or failing-first)"
|
|
20315
|
+
]
|
|
20316
|
+
};
|
|
20317
|
+
}
|
|
20318
|
+
function toClaim(testCase, changedSourcePaths) {
|
|
20319
|
+
const audience = deriveAudience(testCase.sourceFile, testCase.tags);
|
|
20320
|
+
const changeType = deriveChangeType(testCase.tags);
|
|
20321
|
+
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
20322
|
+
const key = testBaseKey(testCase.sourceFile);
|
|
20323
|
+
const coversFiles = changedSourcePaths.filter(
|
|
20324
|
+
(path11) => sourceBaseKey(path11) === key
|
|
20325
|
+
);
|
|
20326
|
+
return {
|
|
20327
|
+
id: testCase.id,
|
|
20328
|
+
scenario: testCase.story.scenario,
|
|
20329
|
+
sourceFile: testCase.sourceFile,
|
|
20330
|
+
sourceLine: testCase.sourceLine,
|
|
20331
|
+
status: testCase.status,
|
|
20332
|
+
audience,
|
|
20333
|
+
changeType,
|
|
20334
|
+
strength,
|
|
20335
|
+
strengthReasons: reasons,
|
|
20336
|
+
intent: extractIntent(testCase),
|
|
20337
|
+
coversFiles,
|
|
20338
|
+
testCase
|
|
20339
|
+
};
|
|
20340
|
+
}
|
|
20341
|
+
function bandFor(claims) {
|
|
20342
|
+
if (claims.length === 0) return "uncovered";
|
|
20343
|
+
const maxRank = Math.max(...claims.map((c) => STRENGTH_RANK[c.strength]));
|
|
20344
|
+
return maxRank >= STRENGTH_RANK.moderate ? "covered" : "weak";
|
|
20345
|
+
}
|
|
20346
|
+
var AUDIENCE_ORDER = {
|
|
20347
|
+
stakeholder: 0,
|
|
20348
|
+
engineer: 1
|
|
20349
|
+
};
|
|
20350
|
+
function buildReview(run, context = { changedFiles: [] }) {
|
|
20351
|
+
const changedSource = context.changedFiles.filter(
|
|
20352
|
+
(f) => isReviewableSource(f.path)
|
|
20353
|
+
);
|
|
20354
|
+
const changedSourcePaths = changedSource.map((f) => f.path);
|
|
20355
|
+
const claims = run.testCases.map((tc) => toClaim(tc, changedSourcePaths));
|
|
20356
|
+
const changedFiles = changedSource.map((file) => {
|
|
20357
|
+
const covering = claims.filter((c) => c.coversFiles.includes(file.path));
|
|
20358
|
+
return {
|
|
20359
|
+
path: file.path,
|
|
20360
|
+
changeKind: file.changeKind,
|
|
20361
|
+
band: bandFor(covering),
|
|
20362
|
+
claims: covering.map((c) => ({
|
|
20363
|
+
id: c.id,
|
|
20364
|
+
scenario: c.scenario,
|
|
20365
|
+
strength: c.strength
|
|
20366
|
+
}))
|
|
20367
|
+
};
|
|
20368
|
+
});
|
|
20369
|
+
const sortedClaims = [...claims].sort((a, b) => {
|
|
20370
|
+
if (AUDIENCE_ORDER[a.audience] !== AUDIENCE_ORDER[b.audience]) {
|
|
20371
|
+
return AUDIENCE_ORDER[a.audience] - AUDIENCE_ORDER[b.audience];
|
|
20372
|
+
}
|
|
20373
|
+
if (STRENGTH_RANK[a.strength] !== STRENGTH_RANK[b.strength]) {
|
|
20374
|
+
return STRENGTH_RANK[a.strength] - STRENGTH_RANK[b.strength];
|
|
20375
|
+
}
|
|
20376
|
+
if (a.sourceFile !== b.sourceFile) {
|
|
20377
|
+
return a.sourceFile.localeCompare(b.sourceFile);
|
|
20378
|
+
}
|
|
20379
|
+
return a.scenario.localeCompare(b.scenario);
|
|
20380
|
+
});
|
|
20381
|
+
const bandRank = { uncovered: 0, weak: 1, covered: 2 };
|
|
20382
|
+
const sortedFiles = [...changedFiles].sort((a, b) => {
|
|
20383
|
+
if (bandRank[a.band] !== bandRank[b.band]) {
|
|
20384
|
+
return bandRank[a.band] - bandRank[b.band];
|
|
20385
|
+
}
|
|
20386
|
+
return a.path.localeCompare(b.path);
|
|
20387
|
+
});
|
|
20388
|
+
const summary = buildSummary2(sortedClaims, sortedFiles);
|
|
20389
|
+
return {
|
|
20390
|
+
run,
|
|
20391
|
+
context,
|
|
20392
|
+
summary,
|
|
20393
|
+
claims: sortedClaims,
|
|
20394
|
+
changedFiles: sortedFiles
|
|
20395
|
+
};
|
|
20396
|
+
}
|
|
20397
|
+
function buildSummary2(claims, changedFiles) {
|
|
20398
|
+
const byAudience = {
|
|
20399
|
+
stakeholder: 0,
|
|
20400
|
+
engineer: 0
|
|
20401
|
+
};
|
|
20402
|
+
const byStrength = {
|
|
20403
|
+
none: 0,
|
|
20404
|
+
weak: 0,
|
|
20405
|
+
moderate: 0,
|
|
20406
|
+
strong: 0
|
|
20407
|
+
};
|
|
20408
|
+
for (const claim of claims) {
|
|
20409
|
+
byAudience[claim.audience] += 1;
|
|
20410
|
+
byStrength[claim.strength] += 1;
|
|
20411
|
+
}
|
|
20412
|
+
return {
|
|
20413
|
+
totalClaims: claims.length,
|
|
20414
|
+
byAudience,
|
|
20415
|
+
byStrength,
|
|
20416
|
+
changedSourceFiles: changedFiles.length,
|
|
20417
|
+
uncovered: changedFiles.filter((f) => f.band === "uncovered").length,
|
|
20418
|
+
weaklyCovered: changedFiles.filter((f) => f.band === "weak").length,
|
|
20419
|
+
covered: changedFiles.filter((f) => f.band === "covered").length
|
|
20420
|
+
};
|
|
20421
|
+
}
|
|
20422
|
+
|
|
20423
|
+
// src/formatters/review-markdown.ts
|
|
20424
|
+
var STRENGTH_BADGE = {
|
|
20425
|
+
strong: "\u{1F7E2} strong",
|
|
20426
|
+
moderate: "\u{1F7E1} moderate",
|
|
20427
|
+
weak: "\u{1F7E0} weak",
|
|
20428
|
+
none: "\u{1F534} none"
|
|
20429
|
+
};
|
|
20430
|
+
function statusIcon2(status) {
|
|
20431
|
+
switch (status) {
|
|
20432
|
+
case "passed":
|
|
20433
|
+
return "\u2705";
|
|
20434
|
+
case "failed":
|
|
20435
|
+
return "\u274C";
|
|
20436
|
+
case "skipped":
|
|
20437
|
+
return "\u2298";
|
|
20438
|
+
default:
|
|
20439
|
+
return "\u2022";
|
|
20440
|
+
}
|
|
20441
|
+
}
|
|
20442
|
+
function escapeCell2(value) {
|
|
20443
|
+
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
20444
|
+
}
|
|
20445
|
+
function intentSummary(intent) {
|
|
20446
|
+
const firstLine = intent.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
20447
|
+
const trimmed = firstLine.trim();
|
|
20448
|
+
return trimmed.length > 200 ? `${trimmed.slice(0, 197)}\u2026` : trimmed;
|
|
20449
|
+
}
|
|
20450
|
+
function renderTicket2(ticket) {
|
|
20451
|
+
return ticket.url ? `[${ticket.id}](${ticket.url})` : `\`${ticket.id}\``;
|
|
20452
|
+
}
|
|
20453
|
+
function renderUncoveredBand(lines, files) {
|
|
20454
|
+
const uncovered = files.filter((f) => f.band === "uncovered");
|
|
20455
|
+
if (uncovered.length === 0) return;
|
|
20456
|
+
lines.push(`## \u{1F534} Changed code with no evidence (${uncovered.length})`);
|
|
20457
|
+
lines.push("");
|
|
20458
|
+
lines.push("Start here \u2014 these changed source files have no claim or test behind them.");
|
|
20459
|
+
lines.push("");
|
|
20460
|
+
for (const file of uncovered) {
|
|
20461
|
+
lines.push(`- \`${file.path}\` _(${file.changeKind})_`);
|
|
20462
|
+
}
|
|
20463
|
+
lines.push("");
|
|
20464
|
+
}
|
|
20465
|
+
function renderWeakBand(lines, files) {
|
|
20466
|
+
const weak = files.filter((f) => f.band === "weak");
|
|
20467
|
+
if (weak.length === 0) return;
|
|
20468
|
+
lines.push(`## \u{1F7E1} Changed code with weak evidence (${weak.length})`);
|
|
20469
|
+
lines.push("");
|
|
20470
|
+
for (const file of weak) {
|
|
20471
|
+
const covered = file.claims.map((c) => `${escapeCell2(c.scenario)} (${c.strength})`).join(", ");
|
|
20472
|
+
lines.push(`- \`${file.path}\` _(${file.changeKind})_ \u2014 only: ${covered}`);
|
|
20473
|
+
}
|
|
20474
|
+
lines.push("");
|
|
20475
|
+
}
|
|
20476
|
+
function renderClaim(lines, claim) {
|
|
20477
|
+
lines.push(`### ${statusIcon2(claim.status)} ${claim.scenario}`);
|
|
20478
|
+
lines.push("");
|
|
20479
|
+
lines.push(`- File: \`${claim.sourceFile}:${claim.sourceLine}\``);
|
|
20480
|
+
if (claim.changeType !== "unknown") {
|
|
20481
|
+
lines.push(`- Change: \`${claim.changeType}\``);
|
|
20482
|
+
}
|
|
20483
|
+
const tickets = claim.testCase.story.tickets ?? [];
|
|
20484
|
+
if (tickets.length > 0) {
|
|
20485
|
+
lines.push(`- Tickets: ${tickets.map(renderTicket2).join(", ")}`);
|
|
20486
|
+
}
|
|
20487
|
+
lines.push(
|
|
20488
|
+
`- Evidence: ${STRENGTH_BADGE[claim.strength]} \u2014 ${claim.strengthReasons.join("; ")}`
|
|
20489
|
+
);
|
|
20490
|
+
if (claim.coversFiles.length > 0) {
|
|
20491
|
+
lines.push(
|
|
20492
|
+
`- Covers: ${claim.coversFiles.map((f) => `\`${f}\``).join(", ")}`
|
|
20493
|
+
);
|
|
20494
|
+
}
|
|
20495
|
+
if (claim.intent) {
|
|
20496
|
+
lines.push(`- Why: ${escapeCell2(intentSummary(claim.intent))}`);
|
|
20497
|
+
}
|
|
20498
|
+
lines.push("");
|
|
20499
|
+
}
|
|
20500
|
+
function renderAudienceSection(lines, title, claims) {
|
|
20501
|
+
if (claims.length === 0) return;
|
|
20502
|
+
lines.push(`## ${title} (${claims.length})`);
|
|
20503
|
+
lines.push("");
|
|
20504
|
+
for (const claim of claims) {
|
|
20505
|
+
renderClaim(lines, claim);
|
|
20506
|
+
}
|
|
20507
|
+
}
|
|
20508
|
+
var ReviewMarkdownFormatter = class {
|
|
20509
|
+
title;
|
|
20510
|
+
constructor(options = {}) {
|
|
20511
|
+
this.title = options.title ?? "Evidence Review";
|
|
20512
|
+
}
|
|
20513
|
+
format(review) {
|
|
20514
|
+
const lines = [];
|
|
20515
|
+
const { summary, context } = review;
|
|
20516
|
+
lines.push(`# ${this.title}`);
|
|
20517
|
+
lines.push("");
|
|
20518
|
+
if (context.baseRef || context.headRef) {
|
|
20519
|
+
lines.push(
|
|
20520
|
+
`Comparing \`${context.baseRef ?? "base"}\` \u2192 \`${context.headRef ?? "head"}\`.`
|
|
20521
|
+
);
|
|
20522
|
+
lines.push("");
|
|
20523
|
+
}
|
|
20524
|
+
lines.push("## Review priority");
|
|
20525
|
+
lines.push("");
|
|
20526
|
+
if (summary.changedSourceFiles === 0) {
|
|
20527
|
+
lines.push(
|
|
20528
|
+
"No changed source files supplied \u2014 showing claims and evidence only."
|
|
20529
|
+
);
|
|
20530
|
+
} else if (summary.uncovered > 0) {
|
|
20531
|
+
lines.push(
|
|
20532
|
+
`Review the ${summary.uncovered} unaccounted-for file(s) first: changed code with no evidence behind it.`
|
|
20533
|
+
);
|
|
20534
|
+
} else if (summary.weaklyCovered > 0) {
|
|
20535
|
+
lines.push(
|
|
20536
|
+
`No unaccounted-for changes. Review ${summary.weaklyCovered} weakly-covered file(s) next.`
|
|
20537
|
+
);
|
|
20538
|
+
} else {
|
|
20539
|
+
lines.push("Every changed source file is backed by at least moderate evidence.");
|
|
20540
|
+
}
|
|
20541
|
+
lines.push("");
|
|
20542
|
+
if (summary.changedSourceFiles > 0) {
|
|
20543
|
+
lines.push("| \u{1F534} Uncovered | \u{1F7E1} Weak | \u{1F7E2} Covered | Changed files |");
|
|
20544
|
+
lines.push("| ---: | ---: | ---: | ---: |");
|
|
20545
|
+
lines.push(
|
|
20546
|
+
`| ${summary.uncovered} | ${summary.weaklyCovered} | ${summary.covered} | ${summary.changedSourceFiles} |`
|
|
20547
|
+
);
|
|
20548
|
+
lines.push("");
|
|
20549
|
+
}
|
|
20550
|
+
lines.push("| Claims | Stakeholder | Engineer | Strong | Moderate | Weak | None |");
|
|
20551
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
20552
|
+
lines.push(
|
|
20553
|
+
`| ${summary.totalClaims} | ${summary.byAudience.stakeholder} | ${summary.byAudience.engineer} | ${summary.byStrength.strong} | ${summary.byStrength.moderate} | ${summary.byStrength.weak} | ${summary.byStrength.none} |`
|
|
20554
|
+
);
|
|
20555
|
+
lines.push("");
|
|
20556
|
+
renderUncoveredBand(lines, review.changedFiles);
|
|
20557
|
+
renderWeakBand(lines, review.changedFiles);
|
|
20558
|
+
renderAudienceSection(
|
|
20559
|
+
lines,
|
|
20560
|
+
"Stakeholder behaviour",
|
|
20561
|
+
review.claims.filter((c) => c.audience === "stakeholder")
|
|
20562
|
+
);
|
|
20563
|
+
renderAudienceSection(
|
|
20564
|
+
lines,
|
|
20565
|
+
"Engineer changes",
|
|
20566
|
+
review.claims.filter((c) => c.audience === "engineer")
|
|
20567
|
+
);
|
|
20568
|
+
return lines.join("\n").trimEnd();
|
|
20569
|
+
}
|
|
20570
|
+
};
|
|
20571
|
+
|
|
20572
|
+
// src/formatters/review-html.ts
|
|
20573
|
+
function escapeHtml3(value) {
|
|
20574
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20575
|
+
}
|
|
20576
|
+
var STRENGTH_LABEL = {
|
|
20577
|
+
strong: "Strong",
|
|
20578
|
+
moderate: "Moderate",
|
|
20579
|
+
weak: "Weak",
|
|
20580
|
+
none: "None"
|
|
20581
|
+
};
|
|
20582
|
+
function statusIcon3(status) {
|
|
20583
|
+
switch (status) {
|
|
20584
|
+
case "passed":
|
|
20585
|
+
return "\u2705";
|
|
20586
|
+
case "failed":
|
|
20587
|
+
return "\u274C";
|
|
20588
|
+
case "skipped":
|
|
20589
|
+
return "\u2298";
|
|
20590
|
+
default:
|
|
20591
|
+
return "\u2022";
|
|
20592
|
+
}
|
|
20593
|
+
}
|
|
20594
|
+
function formatStep3(step) {
|
|
20595
|
+
return `<li><strong>${escapeHtml3(step.keyword)}</strong> ${escapeHtml3(step.text)}</li>`;
|
|
20596
|
+
}
|
|
20597
|
+
function inlineDoc(doc) {
|
|
20598
|
+
switch (doc.kind) {
|
|
20599
|
+
case "note":
|
|
20600
|
+
return escapeHtml3(doc.text);
|
|
20601
|
+
case "section":
|
|
20602
|
+
return `<strong>${escapeHtml3(doc.title)}</strong>: ${escapeHtml3(doc.markdown)}`;
|
|
20603
|
+
case "kv":
|
|
20604
|
+
return `${escapeHtml3(doc.label)}: ${escapeHtml3(String(doc.value))}`;
|
|
20605
|
+
case "code":
|
|
20606
|
+
return `${escapeHtml3(doc.label)}: <code>${escapeHtml3(doc.content)}</code>`;
|
|
20607
|
+
case "link":
|
|
20608
|
+
return `${escapeHtml3(doc.label)}: ${escapeHtml3(doc.url)}`;
|
|
20609
|
+
default:
|
|
20610
|
+
return escapeHtml3(doc.kind);
|
|
20611
|
+
}
|
|
20612
|
+
}
|
|
20613
|
+
function renderEvidenceArtifacts(testCase) {
|
|
20614
|
+
const parts = [];
|
|
20615
|
+
for (const att of testCase.attachments) {
|
|
20616
|
+
if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
|
|
20617
|
+
parts.push(
|
|
20618
|
+
`<img class="shot" alt="${escapeHtml3(att.name)}" src="data:${escapeHtml3(att.mediaType)};base64,${att.body}" />`
|
|
20619
|
+
);
|
|
20620
|
+
}
|
|
20621
|
+
}
|
|
20622
|
+
if ((testCase.story.otelSpans?.length ?? 0) > 0) {
|
|
20623
|
+
parts.push(
|
|
20624
|
+
`<p class="trace-note">\u{1F4E1} ${testCase.story.otelSpans.length} OTEL span(s) captured</p>`
|
|
20625
|
+
);
|
|
20626
|
+
}
|
|
20627
|
+
return parts.length > 0 ? `<div class="artifacts">${parts.join("")}</div>` : "";
|
|
20628
|
+
}
|
|
20629
|
+
function renderTicketPills(claim) {
|
|
20630
|
+
const tickets = claim.testCase.story.tickets ?? [];
|
|
20631
|
+
if (tickets.length === 0) return "";
|
|
20632
|
+
return `<div class="ticket-row">${tickets.map((ticket) => {
|
|
20633
|
+
const label = escapeHtml3(ticket.id);
|
|
20634
|
+
if (ticket.url) {
|
|
20635
|
+
return `<a class="ticket-pill" href="${escapeHtml3(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
|
|
20636
|
+
}
|
|
20637
|
+
return `<span class="ticket-pill">${label}</span>`;
|
|
20638
|
+
}).join("")}</div>`;
|
|
20639
|
+
}
|
|
20640
|
+
function renderClaimCard(claim) {
|
|
20641
|
+
const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
|
|
20642
|
+
const search = escapeHtml3(
|
|
20643
|
+
`${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
|
|
20644
|
+
).toLowerCase();
|
|
20645
|
+
const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
|
|
20646
|
+
const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml3(r)}</li>`).join("")}</ul>`;
|
|
20647
|
+
const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml3(claim.intent)}</div>` : "";
|
|
20648
|
+
const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml3(f)}</code>`).join(", ")}</p>` : "";
|
|
20649
|
+
const docs = (claim.testCase.story.docs ?? []).filter(
|
|
20650
|
+
(d) => d.kind === "section" || d.kind === "note"
|
|
20651
|
+
);
|
|
20652
|
+
const extraDocs = docs.length > 0 && claim.intent === void 0 ? `<div class="intent">${docs.map(inlineDoc).join("<br>")}</div>` : "";
|
|
20653
|
+
return `
|
|
20654
|
+
<article class="claim-card" data-audience="${claim.audience}" data-strength="${claim.strength}" data-search="${search}">
|
|
20655
|
+
<header class="claim-header">
|
|
20656
|
+
<div>
|
|
20657
|
+
<span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
|
|
20658
|
+
${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml3(claim.changeType)}</span>` : ""}
|
|
20659
|
+
<h3>${statusIcon3(claim.status)} ${escapeHtml3(claim.scenario)}</h3>
|
|
20660
|
+
<p class="source">${escapeHtml3(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
|
|
20661
|
+
${renderTicketPills(claim)}
|
|
20662
|
+
</div>
|
|
20663
|
+
</header>
|
|
20664
|
+
${intent}${extraDocs}
|
|
20665
|
+
<div class="evidence-block">
|
|
20666
|
+
<span class="evidence-label">Evidence</span>
|
|
20667
|
+
${reasons}
|
|
20668
|
+
</div>
|
|
20669
|
+
${covers}
|
|
20670
|
+
${renderEvidenceArtifacts(claim.testCase)}
|
|
20671
|
+
${steps}
|
|
20672
|
+
</article>`;
|
|
20673
|
+
}
|
|
20674
|
+
function renderChangedFileRow(file) {
|
|
20675
|
+
const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml3(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
|
|
20676
|
+
return `<tr data-band="${file.band}">
|
|
20677
|
+
<td><span class="band-dot band-${file.band}"></span></td>
|
|
20678
|
+
<td><code>${escapeHtml3(file.path)}</code></td>
|
|
20679
|
+
<td>${escapeHtml3(file.changeKind)}</td>
|
|
20680
|
+
<td>${claims}</td>
|
|
20681
|
+
</tr>`;
|
|
20682
|
+
}
|
|
20683
|
+
function renderAudienceSection2(title, claims) {
|
|
20684
|
+
if (claims.length === 0) return "";
|
|
20685
|
+
return `<section class="audience-section">
|
|
20686
|
+
<h2>${escapeHtml3(title)} <span class="count">${claims.length}</span></h2>
|
|
20687
|
+
<div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
|
|
20688
|
+
</section>`;
|
|
20689
|
+
}
|
|
20690
|
+
var REVIEW_CSS = `
|
|
20691
|
+
* { box-sizing: border-box; }
|
|
20692
|
+
body { margin: 0; font-family: var(--font-sans, system-ui, sans-serif); background: var(--background); color: var(--foreground); }
|
|
20693
|
+
main { max-width: 1100px; margin: 0 auto; padding: 32px 20px 80px; }
|
|
20694
|
+
h1, h2, h3, p { margin: 0; }
|
|
20695
|
+
.review-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
|
20696
|
+
.subtle { color: var(--muted-foreground); margin-top: 6px; }
|
|
20697
|
+
.theme-toggle { background: var(--secondary); border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; cursor: pointer; font-size: 1.1rem; color: var(--foreground); }
|
|
20698
|
+
.card, .claim-card, .summary-card, .panel { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius, 16px); }
|
|
20699
|
+
.hero-card { padding: 24px; margin-bottom: 20px; }
|
|
20700
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin-bottom: 20px; }
|
|
20701
|
+
.summary-card { padding: 14px 16px; }
|
|
20702
|
+
.summary-card strong { display: block; font-size: 1.8rem; }
|
|
20703
|
+
.priority-banner { padding: 18px 20px; margin-bottom: 20px; background: linear-gradient(135deg, color-mix(in srgb, var(--destructive) 10%, transparent), var(--card)); }
|
|
20704
|
+
.panel { padding: 18px; margin-bottom: 24px; }
|
|
20705
|
+
table { width: 100%; border-collapse: collapse; }
|
|
20706
|
+
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
20707
|
+
th { color: var(--muted-foreground); font-weight: 600; }
|
|
20708
|
+
.band-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
|
|
20709
|
+
.band-uncovered { background: var(--destructive); }
|
|
20710
|
+
.band-weak { background: var(--warning, #b58900); }
|
|
20711
|
+
.band-covered { background: var(--success, #2e7d32); }
|
|
20712
|
+
.toolbar { position: sticky; top: 12px; z-index: 2; display: flex; flex-wrap: wrap; gap: 10px; padding: 14px; margin-bottom: 20px; }
|
|
20713
|
+
.toolbar input { flex: 1 1 240px; border: 1px solid var(--border); border-radius: 999px; padding: 10px 14px; font: inherit; background: var(--background); color: var(--foreground); }
|
|
20714
|
+
.toolbar button { border: 1px solid var(--border); background: var(--secondary); border-radius: 999px; padding: 10px 14px; font: inherit; cursor: pointer; color: var(--foreground); }
|
|
20715
|
+
.toolbar button.active { background: var(--foreground); color: var(--background); }
|
|
20716
|
+
.audience-section { margin-bottom: 28px; }
|
|
20717
|
+
.audience-section h2 { margin-bottom: 12px; }
|
|
20718
|
+
.count { color: var(--muted-foreground); font-weight: 400; }
|
|
20719
|
+
.claim-list { display: grid; gap: 14px; }
|
|
20720
|
+
.claim-card { padding: 18px; }
|
|
20721
|
+
.claim-header h3 { margin-top: 8px; }
|
|
20722
|
+
.source { color: var(--muted-foreground); font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.85rem; margin-top: 4px; }
|
|
20723
|
+
.ticket-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
20724
|
+
.ticket-pill { display: inline-flex; align-items: center; border: 1px solid var(--border); border-radius: 999px; padding: 3px 9px; color: var(--muted-foreground); background: var(--background); font-size: 0.78rem; text-decoration: none; }
|
|
20725
|
+
.ticket-pill:hover { color: var(--foreground); border-color: var(--muted-foreground); }
|
|
20726
|
+
.strength-badge, .change-pill { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 999px; font-size: 0.8rem; margin-right: 6px; }
|
|
20727
|
+
.change-pill { background: var(--secondary); }
|
|
20728
|
+
.strength-strong { background: color-mix(in srgb, var(--success, #2e7d32) 18%, transparent); color: var(--success, #2e7d32); }
|
|
20729
|
+
.strength-moderate { background: color-mix(in srgb, var(--warning, #b58900) 20%, transparent); color: var(--warning, #b58900); }
|
|
20730
|
+
.strength-weak { background: color-mix(in srgb, #d2691e 20%, transparent); color: #b5530a; }
|
|
20731
|
+
.strength-none { background: color-mix(in srgb, var(--destructive) 16%, transparent); color: var(--destructive); }
|
|
20732
|
+
.intent { margin: 12px 0; padding: 10px 12px; border-left: 3px solid var(--border); background: color-mix(in srgb, var(--card) 60%, var(--background)); border-radius: 6px; }
|
|
20733
|
+
.intent-label { font-weight: 700; margin-right: 6px; }
|
|
20734
|
+
.evidence-block { margin-top: 10px; }
|
|
20735
|
+
.evidence-label { font-weight: 600; color: var(--muted-foreground); }
|
|
20736
|
+
.reasons { margin: 6px 0 0; padding-left: 18px; }
|
|
20737
|
+
.covers { color: var(--muted-foreground); margin-top: 8px; font-size: 0.9rem; }
|
|
20738
|
+
.artifacts { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; }
|
|
20739
|
+
.shot { max-width: 280px; max-height: 200px; border: 1px solid var(--border); border-radius: 8px; }
|
|
20740
|
+
.trace-note { color: var(--muted-foreground); }
|
|
20741
|
+
.step-list { margin: 12px 0 0; padding-left: 18px; color: var(--muted-foreground); }
|
|
20742
|
+
`;
|
|
20743
|
+
var JS_THEME_TOGGLE2 = `
|
|
20744
|
+
function getSystemTheme() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }
|
|
20745
|
+
function getEffectiveTheme() { var s = localStorage.getItem('review-theme'); return (s === 'dark' || s === 'light') ? s : getSystemTheme(); }
|
|
20746
|
+
function toggleTheme() { var n = getEffectiveTheme() === 'dark' ? 'light' : 'dark'; localStorage.setItem('review-theme', n); applyTheme(n); }
|
|
20747
|
+
function applyTheme(t) {
|
|
20748
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
20749
|
+
var b = document.querySelector('.theme-toggle');
|
|
20750
|
+
if (b) { b.textContent = t === 'dark' ? '\\u2600\\ufe0f' : '\\ud83c\\udf19'; }
|
|
20751
|
+
}
|
|
20752
|
+
`;
|
|
20753
|
+
var ReviewHtmlFormatter = class {
|
|
20754
|
+
title;
|
|
20755
|
+
theme;
|
|
20756
|
+
darkMode;
|
|
20757
|
+
constructor(options = {}) {
|
|
20758
|
+
this.title = options.title ?? "Evidence Review";
|
|
20759
|
+
this.theme = resolveTheme(options.theme ?? "default");
|
|
20760
|
+
this.darkMode = options.darkMode ?? true;
|
|
20761
|
+
}
|
|
20762
|
+
format(review) {
|
|
20763
|
+
const { summary, context } = review;
|
|
20764
|
+
const priority = summary.changedSourceFiles === 0 ? "No changed source files supplied \u2014 showing claims and evidence only." : summary.uncovered > 0 ? `${summary.uncovered} changed file(s) have no evidence. Review them first.` : summary.weaklyCovered > 0 ? `No unaccounted-for changes. ${summary.weaklyCovered} file(s) are weakly covered.` : "Every changed source file is backed by at least moderate evidence.";
|
|
20765
|
+
const changedFilesPanel = summary.changedSourceFiles > 0 ? `<section class="panel">
|
|
20766
|
+
<h2>Changed files</h2>
|
|
20767
|
+
<table>
|
|
20768
|
+
<thead><tr><th></th><th>File</th><th>Change</th><th>Evidence</th></tr></thead>
|
|
20769
|
+
<tbody>${review.changedFiles.map(renderChangedFileRow).join("")}</tbody>
|
|
20770
|
+
</table>
|
|
20771
|
+
</section>` : "";
|
|
20772
|
+
const themeToggleHtml = this.darkMode ? `<button type="button" class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme"></button>` : "";
|
|
20773
|
+
const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
|
|
20774
|
+
applyTheme(getEffectiveTheme());` : "";
|
|
20775
|
+
const themeAttr = this.darkMode ? ' data-theme="light"' : "";
|
|
20776
|
+
const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml3(context.baseRef ?? "base")} \u2192 ${escapeHtml3(context.headRef ?? "head")}</p>` : "";
|
|
20777
|
+
return `<!doctype html>
|
|
20778
|
+
<html lang="en"${themeAttr}>
|
|
20779
|
+
<head>
|
|
20780
|
+
<meta charset="utf-8" />
|
|
20781
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
20782
|
+
<title>${escapeHtml3(this.title)}</title>
|
|
20783
|
+
<style>
|
|
20784
|
+
${this.theme.css}
|
|
20785
|
+
${REVIEW_CSS}
|
|
20786
|
+
</style>
|
|
20787
|
+
</head>
|
|
20788
|
+
<body>
|
|
20789
|
+
<main>
|
|
20790
|
+
<div class="hero-card card">
|
|
20791
|
+
<div class="review-header">
|
|
20792
|
+
<h1>${escapeHtml3(this.title)}</h1>
|
|
20793
|
+
${themeToggleHtml}
|
|
20794
|
+
</div>
|
|
20795
|
+
${refsLine}
|
|
20796
|
+
</div>
|
|
20797
|
+
<section class="summary-grid">
|
|
20798
|
+
<div class="summary-card"><strong>${summary.uncovered}</strong><span>\u{1F534} Uncovered</span></div>
|
|
20799
|
+
<div class="summary-card"><strong>${summary.weaklyCovered}</strong><span>\u{1F7E1} Weak</span></div>
|
|
20800
|
+
<div class="summary-card"><strong>${summary.covered}</strong><span>\u{1F7E2} Covered</span></div>
|
|
20801
|
+
<div class="summary-card"><strong>${summary.totalClaims}</strong><span>Claims</span></div>
|
|
20802
|
+
<div class="summary-card"><strong>${summary.byStrength.strong}</strong><span>Strong</span></div>
|
|
20803
|
+
<div class="summary-card"><strong>${summary.byStrength.weak + summary.byStrength.none}</strong><span>Weak/None</span></div>
|
|
20804
|
+
</section>
|
|
20805
|
+
<section class="card priority-banner">
|
|
20806
|
+
<h2>Review priority</h2>
|
|
20807
|
+
<p class="subtle">${escapeHtml3(priority)}</p>
|
|
20808
|
+
</section>
|
|
20809
|
+
${changedFilesPanel}
|
|
20810
|
+
<section class="toolbar">
|
|
20811
|
+
<input type="search" placeholder="Filter claims by scenario, file, change-type" aria-label="Filter claims" />
|
|
20812
|
+
<button type="button" class="active" data-filter="all">All</button>
|
|
20813
|
+
<button type="button" data-filter="stakeholder">Stakeholder</button>
|
|
20814
|
+
<button type="button" data-filter="engineer">Engineer</button>
|
|
20815
|
+
<button type="button" data-filter="weak">Weak/None</button>
|
|
20816
|
+
</section>
|
|
20817
|
+
${renderAudienceSection2("Stakeholder behaviour", review.claims.filter((c) => c.audience === "stakeholder"))}
|
|
20818
|
+
${renderAudienceSection2("Engineer changes", review.claims.filter((c) => c.audience === "engineer"))}
|
|
20819
|
+
</main>
|
|
20820
|
+
<script>
|
|
20821
|
+
${themeInitJs}
|
|
20822
|
+
const input = document.querySelector('input[type="search"]');
|
|
20823
|
+
const buttons = Array.from(document.querySelectorAll('[data-filter]'));
|
|
20824
|
+
const cards = Array.from(document.querySelectorAll('.claim-card'));
|
|
20825
|
+
let activeFilter = 'all';
|
|
20826
|
+
function applyFilters() {
|
|
20827
|
+
const query = (input.value || '').trim().toLowerCase();
|
|
20828
|
+
cards.forEach((card) => {
|
|
20829
|
+
const audience = card.getAttribute('data-audience');
|
|
20830
|
+
const strength = card.getAttribute('data-strength');
|
|
20831
|
+
const haystack = card.getAttribute('data-search') || '';
|
|
20832
|
+
let matchesFilter = activeFilter === 'all'
|
|
20833
|
+
|| audience === activeFilter
|
|
20834
|
+
|| (activeFilter === 'weak' && (strength === 'weak' || strength === 'none'));
|
|
20835
|
+
const matchesSearch = !query || haystack.includes(query);
|
|
20836
|
+
card.style.display = matchesFilter && matchesSearch ? '' : 'none';
|
|
20837
|
+
});
|
|
20838
|
+
}
|
|
20839
|
+
input.addEventListener('input', applyFilters);
|
|
20840
|
+
buttons.forEach((button) => {
|
|
20841
|
+
button.addEventListener('click', () => {
|
|
20842
|
+
activeFilter = button.getAttribute('data-filter');
|
|
20843
|
+
buttons.forEach((b) => b.classList.toggle('active', b === button));
|
|
20844
|
+
applyFilters();
|
|
20845
|
+
});
|
|
20846
|
+
});
|
|
20847
|
+
applyFilters();
|
|
20848
|
+
</script>
|
|
20849
|
+
</body>
|
|
20850
|
+
</html>`;
|
|
20851
|
+
}
|
|
20852
|
+
};
|
|
19750
20853
|
|
|
19751
20854
|
// src/index.ts
|
|
19752
20855
|
var FORMAT_EXTENSIONS = {
|
|
19753
20856
|
astro: ".md",
|
|
20857
|
+
"behavior-manifest-json": ".behavior-manifest.json",
|
|
19754
20858
|
markdown: ".md",
|
|
19755
20859
|
html: ".html",
|
|
19756
20860
|
"cucumber-html": ".cucumber.html",
|
|
@@ -19758,6 +20862,7 @@ var FORMAT_EXTENSIONS = {
|
|
|
19758
20862
|
"cucumber-json": ".cucumber.json",
|
|
19759
20863
|
"cucumber-messages": ".ndjson",
|
|
19760
20864
|
confluence: ".adf.json",
|
|
20865
|
+
"scenario-index-json": ".scenarios-index.json",
|
|
19761
20866
|
"story-report-json": ".story-report.json"
|
|
19762
20867
|
};
|
|
19763
20868
|
var TEST_EXTENSIONS = [
|
|
@@ -19785,11 +20890,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
19785
20890
|
const ext = FORMAT_EXTENSIONS[format];
|
|
19786
20891
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
19787
20892
|
if (mode === "aggregated") {
|
|
19788
|
-
return toPosix(
|
|
20893
|
+
return toPosix(path10.join(baseOutputDir, `${effectiveName}${ext}`));
|
|
19789
20894
|
}
|
|
19790
20895
|
const normalizedSource = toPosix(sourceFile);
|
|
19791
|
-
const dirOfSource =
|
|
19792
|
-
let baseName =
|
|
20896
|
+
const dirOfSource = path10.posix.dirname(normalizedSource);
|
|
20897
|
+
let baseName = path10.posix.basename(normalizedSource);
|
|
19793
20898
|
for (const testExt of TEST_EXTENSIONS) {
|
|
19794
20899
|
if (baseName.endsWith(testExt)) {
|
|
19795
20900
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -19798,9 +20903,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
19798
20903
|
}
|
|
19799
20904
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
19800
20905
|
if (colocatedStyle === "adjacent") {
|
|
19801
|
-
return toPosix(
|
|
20906
|
+
return toPosix(path10.posix.join(dirOfSource, fileName));
|
|
19802
20907
|
}
|
|
19803
|
-
return toPosix(
|
|
20908
|
+
return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
19804
20909
|
}
|
|
19805
20910
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
19806
20911
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -19886,6 +20991,12 @@ var ReportGenerator = class {
|
|
|
19886
20991
|
storyReportJson: {
|
|
19887
20992
|
pretty: options.storyReportJson?.pretty ?? true
|
|
19888
20993
|
},
|
|
20994
|
+
scenarioIndexJson: {
|
|
20995
|
+
pretty: options.scenarioIndexJson?.pretty ?? true
|
|
20996
|
+
},
|
|
20997
|
+
behaviorManifestJson: {
|
|
20998
|
+
pretty: options.behaviorManifestJson?.pretty ?? true
|
|
20999
|
+
},
|
|
19889
21000
|
cucumberMessages: {
|
|
19890
21001
|
uriStrategy: options.cucumberMessages?.uriStrategy ?? "sourceFile",
|
|
19891
21002
|
includeSynthetics: options.cucumberMessages?.includeSynthetics ?? true,
|
|
@@ -20001,8 +21112,8 @@ var ReportGenerator = class {
|
|
|
20001
21112
|
if (astroPaths) {
|
|
20002
21113
|
for (const mdPath of astroPaths) {
|
|
20003
21114
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
20004
|
-
const mdDir =
|
|
20005
|
-
const assetsDir =
|
|
21115
|
+
const mdDir = path10.dirname(mdPath);
|
|
21116
|
+
const assetsDir = path10.resolve(this.options.astro.assetsDir);
|
|
20006
21117
|
const result = copyMarkdownAssets({
|
|
20007
21118
|
markdown: content,
|
|
20008
21119
|
markdownDir: mdDir,
|
|
@@ -20033,9 +21144,9 @@ var ReportGenerator = class {
|
|
|
20033
21144
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
20034
21145
|
const ext = FORMAT_EXTENSIONS[format];
|
|
20035
21146
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
20036
|
-
const outputPath = toPosix(
|
|
21147
|
+
const outputPath = toPosix(path10.join(this.options.outputDir, `${effectiveName}${ext}`));
|
|
20037
21148
|
const content = await this.formatContent(run, format);
|
|
20038
|
-
const dir =
|
|
21149
|
+
const dir = path10.dirname(outputPath);
|
|
20039
21150
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
20040
21151
|
await this.deps.writeFile(outputPath, content);
|
|
20041
21152
|
return [outputPath];
|
|
@@ -20047,7 +21158,7 @@ var ReportGenerator = class {
|
|
|
20047
21158
|
testCases
|
|
20048
21159
|
};
|
|
20049
21160
|
const content = await this.formatContent(groupRun, format);
|
|
20050
|
-
const dir =
|
|
21161
|
+
const dir = path10.dirname(outputPath);
|
|
20051
21162
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
20052
21163
|
await this.deps.writeFile(outputPath, content);
|
|
20053
21164
|
writtenPaths.push(outputPath);
|
|
@@ -20160,6 +21271,18 @@ var ReportGenerator = class {
|
|
|
20160
21271
|
});
|
|
20161
21272
|
return formatter.format(run);
|
|
20162
21273
|
}
|
|
21274
|
+
case "scenario-index-json": {
|
|
21275
|
+
const formatter = new ScenarioIndexJsonFormatter({
|
|
21276
|
+
pretty: this.options.scenarioIndexJson.pretty
|
|
21277
|
+
});
|
|
21278
|
+
return formatter.format(run);
|
|
21279
|
+
}
|
|
21280
|
+
case "behavior-manifest-json": {
|
|
21281
|
+
const formatter = new BehaviorManifestJsonFormatter({
|
|
21282
|
+
pretty: this.options.behaviorManifestJson.pretty
|
|
21283
|
+
});
|
|
21284
|
+
return formatter.format(run);
|
|
21285
|
+
}
|
|
20163
21286
|
default:
|
|
20164
21287
|
throw new Error(`Unknown format: ${format}`);
|
|
20165
21288
|
}
|
|
@@ -20176,7 +21299,7 @@ async function generateRunComparison(args) {
|
|
|
20176
21299
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
20177
21300
|
for (const format of args.formats) {
|
|
20178
21301
|
const ext = format === "html" ? ".html" : ".md";
|
|
20179
|
-
const outputPath = toPosix(
|
|
21302
|
+
const outputPath = toPosix(path10.join(outputDir, `${outputName}${ext}`));
|
|
20180
21303
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
20181
21304
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
20182
21305
|
files.push(outputPath);
|
|
@@ -20198,6 +21321,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
20198
21321
|
// Annotate the CommonJS export names for ESM import in node:
|
|
20199
21322
|
0 && (module.exports = {
|
|
20200
21323
|
AstroFormatter,
|
|
21324
|
+
BehaviorManifestJsonFormatter,
|
|
20201
21325
|
ConfluenceFormatter,
|
|
20202
21326
|
CucumberHtmlFormatter,
|
|
20203
21327
|
CucumberJsonFormatter,
|
|
@@ -20211,29 +21335,37 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
20211
21335
|
MIN_PERF_SAMPLES,
|
|
20212
21336
|
MarkdownFormatter,
|
|
20213
21337
|
ReportGenerator,
|
|
21338
|
+
ReviewHtmlFormatter,
|
|
21339
|
+
ReviewMarkdownFormatter,
|
|
20214
21340
|
RunDiffHtmlFormatter,
|
|
20215
21341
|
RunDiffMarkdownFormatter,
|
|
20216
21342
|
STORY_META_KEY,
|
|
20217
21343
|
STORY_REPORT_SCHEMA_MAJOR,
|
|
20218
21344
|
STORY_REPORT_SCHEMA_VERSION,
|
|
21345
|
+
ScenarioIndexJsonFormatter,
|
|
20219
21346
|
StoryReportJsonFormatter,
|
|
20220
21347
|
adaptJestRun,
|
|
20221
21348
|
adaptPlaywrightRun,
|
|
20222
21349
|
adaptVitestRun,
|
|
20223
21350
|
assertValidRun,
|
|
21351
|
+
buildReview,
|
|
20224
21352
|
bundleAssets,
|
|
20225
21353
|
calculateFlakiness,
|
|
20226
21354
|
calculateStability,
|
|
20227
21355
|
canonicalizeRun,
|
|
21356
|
+
classifyStatusChange,
|
|
20228
21357
|
clearVersionCache,
|
|
20229
21358
|
computeTestMetrics,
|
|
20230
21359
|
copyMarkdownAssets,
|
|
20231
21360
|
createPrCommentSummary,
|
|
20232
21361
|
createReportGenerator,
|
|
21362
|
+
deriveAudience,
|
|
21363
|
+
deriveChangeType,
|
|
20233
21364
|
deriveStepResults,
|
|
20234
21365
|
detectCI,
|
|
20235
21366
|
detectPerformanceTrend,
|
|
20236
21367
|
diffRuns,
|
|
21368
|
+
diffStoryReports,
|
|
20237
21369
|
findGitDir,
|
|
20238
21370
|
formatDuration,
|
|
20239
21371
|
generateRunComparison,
|
|
@@ -20241,7 +21373,10 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
20241
21373
|
generateTestCaseId,
|
|
20242
21374
|
getAvailableThemes,
|
|
20243
21375
|
getCssOnlyThemes,
|
|
21376
|
+
gradeEvidence,
|
|
20244
21377
|
hasSufficientHistory,
|
|
21378
|
+
isReviewableSource,
|
|
21379
|
+
isTestFile,
|
|
20245
21380
|
listScenarios,
|
|
20246
21381
|
loadHistory,
|
|
20247
21382
|
mergeStepResults,
|
|
@@ -20258,21 +21393,26 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
20258
21393
|
readBranchName,
|
|
20259
21394
|
readGitSha,
|
|
20260
21395
|
readPackageVersion,
|
|
21396
|
+
regenerateArtifacts,
|
|
20261
21397
|
resolveAttachment,
|
|
20262
21398
|
resolveAttachments,
|
|
20263
21399
|
resolveTheme,
|
|
20264
21400
|
resolveTraceUrl,
|
|
20265
21401
|
rewriteAssetPaths,
|
|
20266
21402
|
saveHistory,
|
|
21403
|
+
scenariosCoveringPaths,
|
|
20267
21404
|
sendNotifications,
|
|
20268
21405
|
sendSlackNotification,
|
|
20269
21406
|
sendTeamsNotification,
|
|
20270
21407
|
sendWebhookNotification,
|
|
20271
21408
|
signBody,
|
|
20272
21409
|
slugify,
|
|
21410
|
+
startWatch,
|
|
20273
21411
|
stripAnsi,
|
|
21412
|
+
toBehaviorManifest,
|
|
20274
21413
|
toCIInfo,
|
|
20275
21414
|
toRawCIInfo,
|
|
21415
|
+
toScenarioIndex,
|
|
20276
21416
|
toStoryReport,
|
|
20277
21417
|
tryGetActiveOtelContext,
|
|
20278
21418
|
updateHistory,
|