executable-stories-formatters 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -1
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +1132 -10
- package/dist/cli.js.map +1 -1
- package/dist/{index-DCJ0NvAp.d.cts → index-C4QO-SVT.d.cts} +58 -8
- package/dist/{index-DCJ0NvAp.d.ts → index-C4QO-SVT.d.ts} +58 -8
- package/dist/index.cjs +1224 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +403 -5
- package/dist/index.d.ts +403 -5
- package/dist/index.js +1205 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/README.md +11 -2
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,9 @@ __export(src_exports, {
|
|
|
35
35
|
CucumberMessagesFormatter: () => CucumberMessagesFormatter,
|
|
36
36
|
HtmlFormatter: () => HtmlFormatter,
|
|
37
37
|
JUnitFormatter: () => JUnitFormatter,
|
|
38
|
+
MIN_FLAKINESS_SAMPLES: () => MIN_FLAKINESS_SAMPLES,
|
|
39
|
+
MIN_METRIC_SAMPLES: () => MIN_METRIC_SAMPLES,
|
|
40
|
+
MIN_PERF_SAMPLES: () => MIN_PERF_SAMPLES,
|
|
38
41
|
MarkdownFormatter: () => MarkdownFormatter,
|
|
39
42
|
ReportGenerator: () => ReportGenerator,
|
|
40
43
|
STORY_META_KEY: () => STORY_META_KEY,
|
|
@@ -42,15 +45,21 @@ __export(src_exports, {
|
|
|
42
45
|
adaptPlaywrightRun: () => adaptPlaywrightRun,
|
|
43
46
|
adaptVitestRun: () => adaptVitestRun,
|
|
44
47
|
assertValidRun: () => assertValidRun,
|
|
48
|
+
calculateFlakiness: () => calculateFlakiness,
|
|
49
|
+
calculateStability: () => calculateStability,
|
|
45
50
|
canonicalizeRun: () => canonicalizeRun,
|
|
46
51
|
clearVersionCache: () => clearVersionCache,
|
|
52
|
+
computeTestMetrics: () => computeTestMetrics,
|
|
47
53
|
createReportGenerator: () => createReportGenerator,
|
|
48
54
|
deriveStepResults: () => deriveStepResults,
|
|
49
55
|
detectCI: () => detectCI4,
|
|
56
|
+
detectPerformanceTrend: () => detectPerformanceTrend,
|
|
50
57
|
findGitDir: () => findGitDir,
|
|
51
|
-
formatDuration: () =>
|
|
58
|
+
formatDuration: () => formatDuration2,
|
|
52
59
|
generateRunId: () => generateRunId,
|
|
53
60
|
generateTestCaseId: () => generateTestCaseId,
|
|
61
|
+
hasSufficientHistory: () => hasSufficientHistory,
|
|
62
|
+
loadHistory: () => loadHistory,
|
|
54
63
|
mergeStepResults: () => mergeStepResults,
|
|
55
64
|
msToNanoseconds: () => msToNanoseconds,
|
|
56
65
|
nanosecondsToMs: () => nanosecondsToMs,
|
|
@@ -66,8 +75,18 @@ __export(src_exports, {
|
|
|
66
75
|
resolveAttachment: () => resolveAttachment,
|
|
67
76
|
resolveAttachments: () => resolveAttachments,
|
|
68
77
|
resolveTraceUrl: () => resolveTraceUrl,
|
|
78
|
+
saveHistory: () => saveHistory,
|
|
79
|
+
sendNotifications: () => sendNotifications,
|
|
80
|
+
sendSlackNotification: () => sendSlackNotification,
|
|
81
|
+
sendTeamsNotification: () => sendTeamsNotification,
|
|
82
|
+
sendWebhookNotification: () => sendWebhookNotification,
|
|
83
|
+
signBody: () => signBody,
|
|
69
84
|
slugify: () => slugify,
|
|
85
|
+
stripAnsi: () => stripAnsi,
|
|
86
|
+
toCIInfo: () => toCIInfo,
|
|
87
|
+
toRawCIInfo: () => toRawCIInfo,
|
|
70
88
|
tryGetActiveOtelContext: () => tryGetActiveOtelContext,
|
|
89
|
+
updateHistory: () => updateHistory,
|
|
71
90
|
validateCanonicalRun: () => validateCanonicalRun
|
|
72
91
|
});
|
|
73
92
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -970,17 +989,17 @@ function initCollapse() {
|
|
|
970
989
|
}
|
|
971
990
|
|
|
972
991
|
function expandAll() {
|
|
973
|
-
document.querySelectorAll('.feature, .scenario').forEach(el => {
|
|
992
|
+
document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
|
|
974
993
|
el.classList.remove('collapsed');
|
|
975
|
-
const header = el.querySelector('.feature-header, .scenario-header');
|
|
994
|
+
const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
|
|
976
995
|
header?.setAttribute('aria-expanded', 'true');
|
|
977
996
|
});
|
|
978
997
|
}
|
|
979
998
|
|
|
980
999
|
function collapseAll() {
|
|
981
|
-
document.querySelectorAll('.feature, .scenario').forEach(el => {
|
|
1000
|
+
document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
|
|
982
1001
|
el.classList.add('collapsed');
|
|
983
|
-
const header = el.querySelector('.feature-header, .scenario-header');
|
|
1002
|
+
const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
|
|
984
1003
|
header?.setAttribute('aria-expanded', 'false');
|
|
985
1004
|
});
|
|
986
1005
|
}
|
|
@@ -2507,6 +2526,147 @@ body {
|
|
|
2507
2526
|
background: none;
|
|
2508
2527
|
}
|
|
2509
2528
|
|
|
2529
|
+
/* ============================================================================
|
|
2530
|
+
Trace View - OTel span waterfall
|
|
2531
|
+
============================================================================ */
|
|
2532
|
+
.trace-view {
|
|
2533
|
+
margin-top: 0.75rem;
|
|
2534
|
+
border: 1px solid var(--border);
|
|
2535
|
+
border-radius: calc(var(--radius) - 2px);
|
|
2536
|
+
overflow: hidden;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
.trace-view-header {
|
|
2540
|
+
display: flex;
|
|
2541
|
+
align-items: center;
|
|
2542
|
+
gap: 0.5rem;
|
|
2543
|
+
padding: 0.5rem 0.75rem;
|
|
2544
|
+
background: var(--card);
|
|
2545
|
+
cursor: pointer;
|
|
2546
|
+
user-select: none;
|
|
2547
|
+
font-size: 0.8125rem;
|
|
2548
|
+
font-weight: 500;
|
|
2549
|
+
color: var(--foreground);
|
|
2550
|
+
transition: background-color 0.15s ease;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
.trace-view-header:hover {
|
|
2554
|
+
background: var(--accordion-header-hover);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
.trace-view-count {
|
|
2558
|
+
font-size: 0.6875rem;
|
|
2559
|
+
font-weight: 500;
|
|
2560
|
+
padding: 0.125rem 0.5rem;
|
|
2561
|
+
background: var(--success-light);
|
|
2562
|
+
color: var(--success);
|
|
2563
|
+
border: 1px solid var(--success-border);
|
|
2564
|
+
border-radius: 9999px;
|
|
2565
|
+
font-family: var(--font-mono);
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
.trace-view-content {
|
|
2569
|
+
border-top: 1px solid var(--border);
|
|
2570
|
+
padding: 0.5rem 0.75rem;
|
|
2571
|
+
background: var(--accordion-content-bg);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
.trace-view.collapsed .trace-view-content {
|
|
2575
|
+
display: none;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
.trace-view-axis {
|
|
2579
|
+
display: flex;
|
|
2580
|
+
justify-content: space-between;
|
|
2581
|
+
font-size: 0.625rem;
|
|
2582
|
+
font-family: var(--font-mono);
|
|
2583
|
+
color: var(--muted-foreground);
|
|
2584
|
+
padding-bottom: 0.375rem;
|
|
2585
|
+
margin-bottom: 0.375rem;
|
|
2586
|
+
border-bottom: 1px solid var(--border);
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
.trace-view-row {
|
|
2590
|
+
display: flex;
|
|
2591
|
+
align-items: center;
|
|
2592
|
+
gap: 0.5rem;
|
|
2593
|
+
padding: 0.1875rem 0;
|
|
2594
|
+
font-size: 0.75rem;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
.trace-view-name {
|
|
2598
|
+
width: 35%;
|
|
2599
|
+
flex-shrink: 0;
|
|
2600
|
+
display: flex;
|
|
2601
|
+
align-items: center;
|
|
2602
|
+
gap: 0.375rem;
|
|
2603
|
+
font-family: var(--font-mono);
|
|
2604
|
+
white-space: nowrap;
|
|
2605
|
+
overflow: hidden;
|
|
2606
|
+
text-overflow: ellipsis;
|
|
2607
|
+
color: var(--foreground);
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
.trace-view-status-dot {
|
|
2611
|
+
width: 8px;
|
|
2612
|
+
height: 8px;
|
|
2613
|
+
border-radius: 50%;
|
|
2614
|
+
flex-shrink: 0;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
.trace-view-status-ok { background: var(--success); }
|
|
2618
|
+
.trace-view-status-error { background: var(--error); }
|
|
2619
|
+
.trace-view-status-unset { background: var(--muted-foreground); }
|
|
2620
|
+
|
|
2621
|
+
.trace-view-bar-container {
|
|
2622
|
+
flex: 1;
|
|
2623
|
+
position: relative;
|
|
2624
|
+
height: 1.25rem;
|
|
2625
|
+
background: var(--muted);
|
|
2626
|
+
border-radius: 2px;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
.trace-view-bar {
|
|
2630
|
+
position: absolute;
|
|
2631
|
+
top: 0;
|
|
2632
|
+
height: 100%;
|
|
2633
|
+
border-radius: 2px;
|
|
2634
|
+
min-width: 2px;
|
|
2635
|
+
display: flex;
|
|
2636
|
+
align-items: center;
|
|
2637
|
+
padding: 0 0.375rem;
|
|
2638
|
+
font-size: 0.625rem;
|
|
2639
|
+
font-family: var(--font-mono);
|
|
2640
|
+
color: white;
|
|
2641
|
+
white-space: nowrap;
|
|
2642
|
+
overflow: hidden;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
.trace-view-bar-ok { background: var(--success); }
|
|
2646
|
+
.trace-view-bar-error { background: var(--error); }
|
|
2647
|
+
.trace-view-bar-unset { background: var(--muted-foreground); }
|
|
2648
|
+
|
|
2649
|
+
@media print {
|
|
2650
|
+
.trace-view.collapsed .trace-view-content {
|
|
2651
|
+
display: block;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
/* ============================================================================
|
|
2656
|
+
History metric badges
|
|
2657
|
+
============================================================================ */
|
|
2658
|
+
.badge { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 0.75em; font-weight: 600; margin-left: 4px; vertical-align: middle; }
|
|
2659
|
+
.badge-grade { color: #fff; }
|
|
2660
|
+
.badge-grade-A { background: var(--success); }
|
|
2661
|
+
.badge-grade-B { background: #2196F3; }
|
|
2662
|
+
.badge-grade-C { background: #FF9800; }
|
|
2663
|
+
.badge-grade-D { background: #f44336; }
|
|
2664
|
+
.badge-grade-F { background: #9E0000; }
|
|
2665
|
+
.badge-flaky { background: #FF9800; color: #fff; }
|
|
2666
|
+
.badge-perf { font-size: 0.7em; }
|
|
2667
|
+
.badge-perf-improving { color: var(--success); }
|
|
2668
|
+
.badge-perf-regressing { color: var(--error); }
|
|
2669
|
+
|
|
2510
2670
|
`;
|
|
2511
2671
|
|
|
2512
2672
|
// src/formatters/html/renderers/status.ts
|
|
@@ -2540,7 +2700,22 @@ function renderMetaInfo(args, deps) {
|
|
|
2540
2700
|
items.push(`<dt>Git:</dt><dd>${deps.escapeHtml(shortSha)}</dd>`);
|
|
2541
2701
|
}
|
|
2542
2702
|
if (args.ciName) {
|
|
2543
|
-
|
|
2703
|
+
if (args.ciUrl && args.ciBuildNumber) {
|
|
2704
|
+
items.push(
|
|
2705
|
+
`<dt>CI:</dt><dd>${deps.escapeHtml(args.ciName)} <a href="${deps.escapeHtml(args.ciUrl)}">#${deps.escapeHtml(args.ciBuildNumber)}</a></dd>`
|
|
2706
|
+
);
|
|
2707
|
+
} else {
|
|
2708
|
+
items.push(`<dt>CI:</dt><dd>${deps.escapeHtml(args.ciName)}</dd>`);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
if (args.ciBranch) {
|
|
2712
|
+
items.push(`<dt>Branch:</dt><dd>${deps.escapeHtml(args.ciBranch)}</dd>`);
|
|
2713
|
+
}
|
|
2714
|
+
if (args.ciCommitSha) {
|
|
2715
|
+
const shortSha = args.ciCommitSha.length > 7 ? args.ciCommitSha.slice(0, 7) : args.ciCommitSha;
|
|
2716
|
+
items.push(
|
|
2717
|
+
`<dt>Commit:</dt><dd title="${deps.escapeHtml(args.ciCommitSha)}">${deps.escapeHtml(shortSha)}</dd>`
|
|
2718
|
+
);
|
|
2544
2719
|
}
|
|
2545
2720
|
return `<dl class="meta-info">${items.join("")}</dl>`;
|
|
2546
2721
|
}
|
|
@@ -2791,6 +2966,14 @@ function highlightStepParams(text, deps) {
|
|
|
2791
2966
|
return result;
|
|
2792
2967
|
}
|
|
2793
2968
|
|
|
2969
|
+
// src/history/sample-policy.ts
|
|
2970
|
+
var MIN_PERF_SAMPLES = 6;
|
|
2971
|
+
var MIN_METRIC_SAMPLES = 5;
|
|
2972
|
+
var MIN_FLAKINESS_SAMPLES = 3;
|
|
2973
|
+
function hasSufficientHistory(entries, min) {
|
|
2974
|
+
return entries.length >= min;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2794
2977
|
// src/formatters/html/renderers/scenario.ts
|
|
2795
2978
|
function renderScenario(args, deps) {
|
|
2796
2979
|
const { tc } = args;
|
|
@@ -2811,6 +2994,19 @@ function renderScenario(args, deps) {
|
|
|
2811
2994
|
traceBadge = `<span class="tag trace-tag" title="${deps.escapeHtml(otelMeta.traceId)}">${deps.escapeHtml(shortId)}\u2026</span>`;
|
|
2812
2995
|
}
|
|
2813
2996
|
}
|
|
2997
|
+
let metricBadges = "";
|
|
2998
|
+
const { metrics } = args;
|
|
2999
|
+
if (metrics && metrics.sampleSize >= MIN_METRIC_SAMPLES) {
|
|
3000
|
+
const grade = metrics.stabilityGrade;
|
|
3001
|
+
metricBadges += `<span class="badge badge-grade badge-grade-${grade}" title="Pass rate: ${(metrics.passRate * 100).toFixed(0)}% (${metrics.sampleSize} runs)">${grade}</span>`;
|
|
3002
|
+
if (metrics.flakinessLevel !== "stable") {
|
|
3003
|
+
metricBadges += `<span class="badge badge-flaky">${metrics.flakinessLevel}</span>`;
|
|
3004
|
+
}
|
|
3005
|
+
if (metrics.performanceTrend !== "stable") {
|
|
3006
|
+
const arrow = metrics.performanceTrend === "improving" ? "\u2191" : "\u2193";
|
|
3007
|
+
metricBadges += `<span class="badge badge-perf badge-perf-${metrics.performanceTrend}">${arrow} ${metrics.performanceTrend}</span>`;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
2814
3010
|
const storyDocs = deps.renderDocs(tc.story.docs, "story-docs");
|
|
2815
3011
|
const steps = deps.renderSteps(
|
|
2816
3012
|
{ steps: tc.story.steps, stepResults: tc.stepResults },
|
|
@@ -2831,6 +3027,10 @@ function renderScenario(args, deps) {
|
|
|
2831
3027
|
embedScreenshots: deps.embedScreenshots
|
|
2832
3028
|
}
|
|
2833
3029
|
);
|
|
3030
|
+
const traceView = deps.renderTraceView(
|
|
3031
|
+
{ spans: tc.story.otelSpans },
|
|
3032
|
+
{ escapeHtml: deps.escapeHtml }
|
|
3033
|
+
);
|
|
2834
3034
|
const collapsedClass = deps.startCollapsed ? " collapsed" : "";
|
|
2835
3035
|
const ariaExpanded = !deps.startCollapsed;
|
|
2836
3036
|
return `
|
|
@@ -2841,7 +3041,7 @@ function renderScenario(args, deps) {
|
|
|
2841
3041
|
<span class="status-icon ${statusClass}">${statusIcon}</span>
|
|
2842
3042
|
<span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
|
|
2843
3043
|
</div>
|
|
2844
|
-
<div class="scenario-meta">${tags}${traceBadge}</div>
|
|
3044
|
+
<div class="scenario-meta">${tags}${traceBadge}${metricBadges}</div>
|
|
2845
3045
|
</div>
|
|
2846
3046
|
<span class="scenario-duration">${duration}</span>
|
|
2847
3047
|
</div>
|
|
@@ -2850,6 +3050,193 @@ function renderScenario(args, deps) {
|
|
|
2850
3050
|
${steps}
|
|
2851
3051
|
${error}
|
|
2852
3052
|
${attachments}
|
|
3053
|
+
${traceView}
|
|
3054
|
+
</div>
|
|
3055
|
+
</div>`;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
// src/formatters/html/renderers/trace-view.ts
|
|
3059
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
|
|
3060
|
+
var TOOLTIP_MAX_LENGTH = 4096;
|
|
3061
|
+
function safeStatus(status) {
|
|
3062
|
+
return VALID_STATUSES.has(status) ? status : "unset";
|
|
3063
|
+
}
|
|
3064
|
+
function formatDuration(ms) {
|
|
3065
|
+
if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
|
|
3066
|
+
return `${ms.toFixed(1)}ms`;
|
|
3067
|
+
}
|
|
3068
|
+
function clamp(value, min, max) {
|
|
3069
|
+
return Math.min(max, Math.max(min, value));
|
|
3070
|
+
}
|
|
3071
|
+
function normalizeSpans(spans) {
|
|
3072
|
+
const result = [];
|
|
3073
|
+
for (const span of spans) {
|
|
3074
|
+
if (!span || typeof span !== "object") continue;
|
|
3075
|
+
if (typeof span.spanId !== "string" || typeof span.name !== "string") continue;
|
|
3076
|
+
let startTimeMs;
|
|
3077
|
+
let durationMs;
|
|
3078
|
+
if (span.startTimeMs != null && span.durationMs != null) {
|
|
3079
|
+
startTimeMs = span.startTimeMs;
|
|
3080
|
+
durationMs = span.durationMs;
|
|
3081
|
+
} else if (span.startTimeUnixNano != null && span.endTimeUnixNano != null) {
|
|
3082
|
+
startTimeMs = span.startTimeUnixNano / 1e6;
|
|
3083
|
+
durationMs = (span.endTimeUnixNano - span.startTimeUnixNano) / 1e6;
|
|
3084
|
+
} else {
|
|
3085
|
+
continue;
|
|
3086
|
+
}
|
|
3087
|
+
durationMs = Math.max(0, durationMs);
|
|
3088
|
+
if (!isFinite(startTimeMs) || !isFinite(durationMs)) continue;
|
|
3089
|
+
result.push({
|
|
3090
|
+
spanId: span.spanId,
|
|
3091
|
+
parentSpanId: span.parentSpanId,
|
|
3092
|
+
name: span.name,
|
|
3093
|
+
startTimeMs,
|
|
3094
|
+
durationMs,
|
|
3095
|
+
status: safeStatus(span.status),
|
|
3096
|
+
statusMessage: span.statusMessage,
|
|
3097
|
+
attributes: span.attributes
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
return result;
|
|
3101
|
+
}
|
|
3102
|
+
function buildTree(spans) {
|
|
3103
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3104
|
+
for (const span of spans) {
|
|
3105
|
+
let key = span.spanId;
|
|
3106
|
+
if (byId.has(key)) {
|
|
3107
|
+
let suffix = 2;
|
|
3108
|
+
while (byId.has(`${span.spanId}__dup${suffix}`)) suffix++;
|
|
3109
|
+
key = `${span.spanId}__dup${suffix}`;
|
|
3110
|
+
}
|
|
3111
|
+
byId.set(key, { span: { ...span, spanId: key }, children: [], depth: 0 });
|
|
3112
|
+
}
|
|
3113
|
+
const roots = [];
|
|
3114
|
+
for (const node of byId.values()) {
|
|
3115
|
+
const parentId = node.span.parentSpanId;
|
|
3116
|
+
const parent = parentId ? byId.get(parentId) : void 0;
|
|
3117
|
+
if (parent && parent !== node) {
|
|
3118
|
+
parent.children.push(node);
|
|
3119
|
+
} else {
|
|
3120
|
+
roots.push(node);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
for (const node of byId.values()) {
|
|
3124
|
+
node.children.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
3125
|
+
}
|
|
3126
|
+
roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
3127
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3128
|
+
function assignDepth(node, depth) {
|
|
3129
|
+
if (visited.has(node.span.spanId)) return;
|
|
3130
|
+
visited.add(node.span.spanId);
|
|
3131
|
+
node.depth = depth;
|
|
3132
|
+
for (const child of node.children) {
|
|
3133
|
+
assignDepth(child, depth + 1);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
for (const root of roots) {
|
|
3137
|
+
assignDepth(root, 0);
|
|
3138
|
+
}
|
|
3139
|
+
for (const node of byId.values()) {
|
|
3140
|
+
if (!visited.has(node.span.spanId)) {
|
|
3141
|
+
node.children = [];
|
|
3142
|
+
roots.push(node);
|
|
3143
|
+
assignDepth(node, 0);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
3147
|
+
return roots;
|
|
3148
|
+
}
|
|
3149
|
+
function flattenTree(roots) {
|
|
3150
|
+
const result = [];
|
|
3151
|
+
function walk(node) {
|
|
3152
|
+
result.push(node);
|
|
3153
|
+
for (const child of node.children) {
|
|
3154
|
+
walk(child);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
for (const root of roots) {
|
|
3158
|
+
walk(root);
|
|
3159
|
+
}
|
|
3160
|
+
return result;
|
|
3161
|
+
}
|
|
3162
|
+
function buildTooltip(span, escapeHtml2) {
|
|
3163
|
+
const parts = [];
|
|
3164
|
+
parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
|
|
3165
|
+
if (span.statusMessage) {
|
|
3166
|
+
parts.push(`Status: ${span.statusMessage}`);
|
|
3167
|
+
}
|
|
3168
|
+
if (span.attributes) {
|
|
3169
|
+
const keys = Object.keys(span.attributes).sort();
|
|
3170
|
+
for (const key of keys) {
|
|
3171
|
+
const val = span.attributes[key];
|
|
3172
|
+
const formatted = Array.isArray(val) ? `[${val.map((v) => String(v)).join(", ")}]` : String(val);
|
|
3173
|
+
parts.push(`${key}=${formatted}`);
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
let text = parts.join("\n");
|
|
3177
|
+
if (text.length > TOOLTIP_MAX_LENGTH) {
|
|
3178
|
+
text = text.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
|
|
3179
|
+
}
|
|
3180
|
+
return escapeHtml2(text);
|
|
3181
|
+
}
|
|
3182
|
+
function renderTraceView(args, deps) {
|
|
3183
|
+
if (!args.spans || args.spans.length === 0) return "";
|
|
3184
|
+
const normalized = normalizeSpans(args.spans);
|
|
3185
|
+
if (normalized.length === 0) return "";
|
|
3186
|
+
const roots = buildTree(normalized);
|
|
3187
|
+
const flat = flattenTree(roots);
|
|
3188
|
+
let minStart = Infinity;
|
|
3189
|
+
let maxEnd = -Infinity;
|
|
3190
|
+
for (const node of flat) {
|
|
3191
|
+
const s = node.span.startTimeMs;
|
|
3192
|
+
const e = s + node.span.durationMs;
|
|
3193
|
+
if (s < minStart) minStart = s;
|
|
3194
|
+
if (e > maxEnd) maxEnd = e;
|
|
3195
|
+
}
|
|
3196
|
+
let totalDuration = maxEnd - minStart;
|
|
3197
|
+
if (totalDuration <= 0) totalDuration = 1;
|
|
3198
|
+
const rows = flat.map((node) => {
|
|
3199
|
+
const { span, depth } = node;
|
|
3200
|
+
const indent = depth * 16;
|
|
3201
|
+
const minWidth = 0.5;
|
|
3202
|
+
let spanLeft = clamp(
|
|
3203
|
+
(span.startTimeMs - minStart) / totalDuration * 100,
|
|
3204
|
+
0,
|
|
3205
|
+
100
|
|
3206
|
+
);
|
|
3207
|
+
if (spanLeft + minWidth > 100) {
|
|
3208
|
+
spanLeft = 100 - minWidth;
|
|
3209
|
+
}
|
|
3210
|
+
const spanWidth = clamp(
|
|
3211
|
+
span.durationMs / totalDuration * 100,
|
|
3212
|
+
minWidth,
|
|
3213
|
+
100 - spanLeft
|
|
3214
|
+
);
|
|
3215
|
+
const tooltip = buildTooltip(span, deps.escapeHtml);
|
|
3216
|
+
const durationLabel = formatDuration(span.durationMs);
|
|
3217
|
+
return ` <div class="trace-view-row">
|
|
3218
|
+
<div class="trace-view-name" style="padding-left: ${indent}px" title="${deps.escapeHtml(span.name)}">
|
|
3219
|
+
<span class="trace-view-status-dot trace-view-status-${span.status}"></span>
|
|
3220
|
+
${deps.escapeHtml(span.name)}
|
|
3221
|
+
</div>
|
|
3222
|
+
<div class="trace-view-bar-container">
|
|
3223
|
+
<div class="trace-view-bar trace-view-bar-${span.status}" style="left: ${spanLeft.toFixed(2)}%; width: ${spanWidth.toFixed(2)}%" title="${tooltip}">${durationLabel}</div>
|
|
3224
|
+
</div>
|
|
3225
|
+
</div>`;
|
|
3226
|
+
}).join("\n");
|
|
3227
|
+
const axisEnd = formatDuration(maxEnd - minStart);
|
|
3228
|
+
return `<div class="trace-view collapsed">
|
|
3229
|
+
<div class="trace-view-header" role="button" tabindex="0" aria-expanded="false">
|
|
3230
|
+
<span>Spans</span>
|
|
3231
|
+
<span class="trace-view-count">${flat.length}</span>
|
|
3232
|
+
<span class="chevron">▼</span>
|
|
3233
|
+
</div>
|
|
3234
|
+
<div class="trace-view-content">
|
|
3235
|
+
<div class="trace-view-axis">
|
|
3236
|
+
<span>0ms</span>
|
|
3237
|
+
<span>${axisEnd}</span>
|
|
3238
|
+
</div>
|
|
3239
|
+
${rows}
|
|
2853
3240
|
</div>
|
|
2854
3241
|
</div>`;
|
|
2855
3242
|
}
|
|
@@ -2866,7 +3253,12 @@ function renderFeature(args, deps) {
|
|
|
2866
3253
|
const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
|
|
2867
3254
|
const collapsedClass = deps.startCollapsed ? " collapsed" : "";
|
|
2868
3255
|
const ariaExpanded = !deps.startCollapsed;
|
|
2869
|
-
const scenarios = testCases.map(
|
|
3256
|
+
const scenarios = testCases.map(
|
|
3257
|
+
(tc) => deps.renderScenario(
|
|
3258
|
+
{ tc, metrics: args.metricsMap?.get(tc.id) },
|
|
3259
|
+
deps.scenarioDeps
|
|
3260
|
+
)
|
|
3261
|
+
).join("\n");
|
|
2870
3262
|
return `
|
|
2871
3263
|
<div class="feature${collapsedClass}">
|
|
2872
3264
|
<div class="feature-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
|
|
@@ -2911,7 +3303,11 @@ function buildBody(args, deps) {
|
|
|
2911
3303
|
durationMs: run.durationMs,
|
|
2912
3304
|
packageVersion: run.packageVersion,
|
|
2913
3305
|
gitSha: run.gitSha,
|
|
2914
|
-
ciName: run.ci?.name
|
|
3306
|
+
ciName: run.ci?.name,
|
|
3307
|
+
ciBranch: run.ci?.branch,
|
|
3308
|
+
ciUrl: run.ci?.url,
|
|
3309
|
+
ciCommitSha: run.ci?.commitSha,
|
|
3310
|
+
ciBuildNumber: run.ci?.buildNumber
|
|
2915
3311
|
},
|
|
2916
3312
|
deps.metaDeps
|
|
2917
3313
|
)
|
|
@@ -2940,7 +3336,10 @@ function buildBody(args, deps) {
|
|
|
2940
3336
|
const byFile = groupBy(run.testCases, (tc) => tc.sourceFile);
|
|
2941
3337
|
for (const [file, testCases] of byFile) {
|
|
2942
3338
|
parts.push(
|
|
2943
|
-
deps.renderFeature(
|
|
3339
|
+
deps.renderFeature(
|
|
3340
|
+
{ file, testCases, metricsMap: args.metricsMap },
|
|
3341
|
+
deps.featureDeps
|
|
3342
|
+
)
|
|
2944
3343
|
);
|
|
2945
3344
|
}
|
|
2946
3345
|
return parts.join("\n");
|
|
@@ -2986,6 +3385,7 @@ function createHtmlFormatter(options = {}) {
|
|
|
2986
3385
|
renderDocs,
|
|
2987
3386
|
renderErrorBox: (args, d) => renderErrorBox(args, d),
|
|
2988
3387
|
renderAttachments: (args, d) => renderAttachments(args, d),
|
|
3388
|
+
renderTraceView: (args, d) => renderTraceView(args, d),
|
|
2989
3389
|
embedScreenshots: opts.embedScreenshots
|
|
2990
3390
|
};
|
|
2991
3391
|
const featureDeps = {
|
|
@@ -5351,7 +5751,7 @@ function readBranchName(cwd = process.cwd()) {
|
|
|
5351
5751
|
}
|
|
5352
5752
|
|
|
5353
5753
|
// src/utils/duration.ts
|
|
5354
|
-
function
|
|
5754
|
+
function formatDuration2(ms) {
|
|
5355
5755
|
if (ms < 1e3) {
|
|
5356
5756
|
return `${Math.round(ms)} ms`;
|
|
5357
5757
|
}
|
|
@@ -5411,45 +5811,102 @@ function clearVersionCache() {
|
|
|
5411
5811
|
|
|
5412
5812
|
// src/utils/ci-detect.ts
|
|
5413
5813
|
function detectCI4(env = process.env) {
|
|
5814
|
+
if (env.TF_BUILD === "True") {
|
|
5815
|
+
const branch = env.BUILD_SOURCEBRANCH?.replace(/^refs\/heads\//, "");
|
|
5816
|
+
const serverUri = env.SYSTEM_TEAMFOUNDATIONSERVERURI;
|
|
5817
|
+
const teamProject = env.SYSTEM_TEAMPROJECT;
|
|
5818
|
+
const buildId = env.BUILD_BUILDID;
|
|
5819
|
+
const url = serverUri && teamProject && buildId ? `${serverUri}${teamProject}/_build/results?buildId=${buildId}` : void 0;
|
|
5820
|
+
return {
|
|
5821
|
+
name: "azure",
|
|
5822
|
+
provider: "azure",
|
|
5823
|
+
buildNumber: buildId,
|
|
5824
|
+
url,
|
|
5825
|
+
branch,
|
|
5826
|
+
commitSha: env.BUILD_SOURCEVERSION,
|
|
5827
|
+
prNumber: env.SYSTEM_PULLREQUEST_PULLREQUESTID
|
|
5828
|
+
};
|
|
5829
|
+
}
|
|
5830
|
+
if (env.BUILDKITE === "true") {
|
|
5831
|
+
const prRaw = env.BUILDKITE_PULL_REQUEST;
|
|
5832
|
+
const prNumber = prRaw && prRaw !== "false" ? prRaw : void 0;
|
|
5833
|
+
return {
|
|
5834
|
+
name: "buildkite",
|
|
5835
|
+
provider: "buildkite",
|
|
5836
|
+
buildNumber: env.BUILDKITE_BUILD_NUMBER,
|
|
5837
|
+
url: env.BUILDKITE_BUILD_URL,
|
|
5838
|
+
branch: env.BUILDKITE_BRANCH,
|
|
5839
|
+
commitSha: env.BUILDKITE_COMMIT,
|
|
5840
|
+
prNumber
|
|
5841
|
+
};
|
|
5842
|
+
}
|
|
5414
5843
|
if (env.GITHUB_ACTIONS === "true") {
|
|
5415
5844
|
const url = env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : void 0;
|
|
5845
|
+
const branch = env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME;
|
|
5846
|
+
const prMatch = env.GITHUB_REF?.match(/^refs\/pull\/(\d+)\/(merge|head)$/);
|
|
5847
|
+
const prNumber = prMatch ? prMatch[1] : void 0;
|
|
5416
5848
|
return {
|
|
5417
5849
|
name: "github",
|
|
5850
|
+
provider: "github",
|
|
5418
5851
|
buildNumber: env.GITHUB_RUN_NUMBER,
|
|
5419
|
-
url
|
|
5852
|
+
url,
|
|
5853
|
+
branch,
|
|
5854
|
+
commitSha: env.GITHUB_SHA,
|
|
5855
|
+
prNumber
|
|
5856
|
+
};
|
|
5857
|
+
}
|
|
5858
|
+
if (env.GITLAB_CI === "true") {
|
|
5859
|
+
return {
|
|
5860
|
+
name: "gitlab",
|
|
5861
|
+
provider: "gitlab",
|
|
5862
|
+
buildNumber: env.CI_PIPELINE_IID,
|
|
5863
|
+
url: env.CI_PIPELINE_URL,
|
|
5864
|
+
branch: env.CI_COMMIT_REF_NAME,
|
|
5865
|
+
commitSha: env.CI_COMMIT_SHA,
|
|
5866
|
+
prNumber: env.CI_MERGE_REQUEST_IID
|
|
5420
5867
|
};
|
|
5421
5868
|
}
|
|
5422
5869
|
if (env.CIRCLECI === "true") {
|
|
5870
|
+
const prUrl = env.CIRCLE_PULL_REQUEST;
|
|
5871
|
+
const prMatch = prUrl?.match(/\/(\d+)$/);
|
|
5872
|
+
const prNumber = prMatch ? prMatch[1] : void 0;
|
|
5423
5873
|
return {
|
|
5424
5874
|
name: "circleci",
|
|
5875
|
+
provider: "circleci",
|
|
5425
5876
|
buildNumber: env.CIRCLE_BUILD_NUM,
|
|
5426
|
-
url: env.CIRCLE_BUILD_URL
|
|
5877
|
+
url: env.CIRCLE_BUILD_URL,
|
|
5878
|
+
branch: env.CIRCLE_BRANCH,
|
|
5879
|
+
commitSha: env.CIRCLE_SHA1,
|
|
5880
|
+
prNumber
|
|
5427
5881
|
};
|
|
5428
5882
|
}
|
|
5429
5883
|
if (env.JENKINS_URL !== void 0) {
|
|
5430
5884
|
return {
|
|
5431
5885
|
name: "jenkins",
|
|
5886
|
+
provider: "jenkins",
|
|
5432
5887
|
buildNumber: env.BUILD_NUMBER,
|
|
5433
|
-
url: env.BUILD_URL
|
|
5888
|
+
url: env.BUILD_URL,
|
|
5889
|
+
branch: env.GIT_BRANCH,
|
|
5890
|
+
commitSha: env.GIT_COMMIT
|
|
5434
5891
|
};
|
|
5435
5892
|
}
|
|
5436
5893
|
if (env.TRAVIS === "true") {
|
|
5894
|
+
const prRaw = env.TRAVIS_PULL_REQUEST;
|
|
5895
|
+
const prNumber = prRaw && prRaw !== "false" ? prRaw : void 0;
|
|
5437
5896
|
return {
|
|
5438
5897
|
name: "travis",
|
|
5898
|
+
provider: "travis",
|
|
5439
5899
|
buildNumber: env.TRAVIS_BUILD_NUMBER,
|
|
5440
|
-
url: env.TRAVIS_BUILD_WEB_URL
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
return {
|
|
5445
|
-
name: "gitlab",
|
|
5446
|
-
buildNumber: env.CI_PIPELINE_IID,
|
|
5447
|
-
url: env.CI_PIPELINE_URL
|
|
5900
|
+
url: env.TRAVIS_BUILD_WEB_URL,
|
|
5901
|
+
branch: env.TRAVIS_BRANCH,
|
|
5902
|
+
commitSha: env.TRAVIS_COMMIT,
|
|
5903
|
+
prNumber
|
|
5448
5904
|
};
|
|
5449
5905
|
}
|
|
5450
5906
|
if (env.CI === "true") {
|
|
5451
5907
|
return {
|
|
5452
|
-
name: "ci"
|
|
5908
|
+
name: "ci",
|
|
5909
|
+
provider: "unknown"
|
|
5453
5910
|
};
|
|
5454
5911
|
}
|
|
5455
5912
|
return void 0;
|
|
@@ -5481,6 +5938,731 @@ function resolveTraceUrl(template, traceId) {
|
|
|
5481
5938
|
return template.replace(/\{traceId\}/g, traceId);
|
|
5482
5939
|
}
|
|
5483
5940
|
|
|
5941
|
+
// src/notifiers/ansi-strip.ts
|
|
5942
|
+
function stripAnsi(text) {
|
|
5943
|
+
return text.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
|
|
5944
|
+
}
|
|
5945
|
+
|
|
5946
|
+
// src/notifiers/slack.ts
|
|
5947
|
+
function truncate(text, maxLen) {
|
|
5948
|
+
if (text.length <= maxLen) return text;
|
|
5949
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
5950
|
+
}
|
|
5951
|
+
function formatDuration3(ms) {
|
|
5952
|
+
const seconds = ms / 1e3;
|
|
5953
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
5954
|
+
const minutes = Math.floor(seconds / 60);
|
|
5955
|
+
const remainingSeconds = seconds % 60;
|
|
5956
|
+
return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
|
|
5957
|
+
}
|
|
5958
|
+
function buildSlackPayload(summary, maxFailedTests) {
|
|
5959
|
+
const allPassed = summary.failed === 0;
|
|
5960
|
+
const emoji = allPassed ? ":white_check_mark:" : ":x:";
|
|
5961
|
+
const statusText = allPassed ? "Passed" : "Failed";
|
|
5962
|
+
const blocks = [];
|
|
5963
|
+
blocks.push({
|
|
5964
|
+
type: "header",
|
|
5965
|
+
text: {
|
|
5966
|
+
type: "plain_text",
|
|
5967
|
+
text: `${emoji} Test Results: ${summary.passed} passed, ${summary.failed} failed`,
|
|
5968
|
+
emoji: true
|
|
5969
|
+
}
|
|
5970
|
+
});
|
|
5971
|
+
blocks.push({
|
|
5972
|
+
type: "section",
|
|
5973
|
+
fields: [
|
|
5974
|
+
{ type: "mrkdwn", text: `*Total:* ${summary.total}` },
|
|
5975
|
+
{ type: "mrkdwn", text: `*Passed:* ${summary.passed}` },
|
|
5976
|
+
{ type: "mrkdwn", text: `*Failed:* ${summary.failed}` },
|
|
5977
|
+
{ type: "mrkdwn", text: `*Skipped:* ${summary.skipped}` },
|
|
5978
|
+
{ type: "mrkdwn", text: `*Duration:* ${formatDuration3(summary.durationMs)}` },
|
|
5979
|
+
{ type: "mrkdwn", text: `*Status:* ${statusText}` }
|
|
5980
|
+
]
|
|
5981
|
+
});
|
|
5982
|
+
if (summary.failedTests.length > 0) {
|
|
5983
|
+
const displayedTests = summary.failedTests.slice(0, maxFailedTests);
|
|
5984
|
+
const lines = displayedTests.map((t) => {
|
|
5985
|
+
const name = t.name;
|
|
5986
|
+
if (t.error) {
|
|
5987
|
+
const cleanError = truncate(stripAnsi(t.error), 500);
|
|
5988
|
+
return `*${name}*
|
|
5989
|
+
\`\`\`${cleanError}\`\`\``;
|
|
5990
|
+
}
|
|
5991
|
+
return `*${name}*`;
|
|
5992
|
+
});
|
|
5993
|
+
let text = lines.join("\n\n");
|
|
5994
|
+
if (summary.failedTests.length > maxFailedTests) {
|
|
5995
|
+
text += `
|
|
5996
|
+
|
|
5997
|
+
_...and ${summary.failedTests.length - maxFailedTests} more_`;
|
|
5998
|
+
}
|
|
5999
|
+
blocks.push({
|
|
6000
|
+
type: "section",
|
|
6001
|
+
text: {
|
|
6002
|
+
type: "mrkdwn",
|
|
6003
|
+
text
|
|
6004
|
+
}
|
|
6005
|
+
});
|
|
6006
|
+
}
|
|
6007
|
+
if (summary.ci) {
|
|
6008
|
+
const elements = [];
|
|
6009
|
+
if (summary.ci.displayName) {
|
|
6010
|
+
elements.push({ type: "mrkdwn", text: `*CI:* ${summary.ci.displayName}` });
|
|
6011
|
+
}
|
|
6012
|
+
if (summary.ci.branch) {
|
|
6013
|
+
elements.push({ type: "mrkdwn", text: `*Branch:* ${summary.ci.branch}` });
|
|
6014
|
+
}
|
|
6015
|
+
if (summary.ci.commitSha) {
|
|
6016
|
+
elements.push({ type: "mrkdwn", text: `*Commit:* ${summary.ci.commitSha.slice(0, 7)}` });
|
|
6017
|
+
}
|
|
6018
|
+
if (summary.ci.buildNumber) {
|
|
6019
|
+
elements.push({ type: "mrkdwn", text: `*Build:* #${summary.ci.buildNumber}` });
|
|
6020
|
+
}
|
|
6021
|
+
if (elements.length > 0) {
|
|
6022
|
+
blocks.push({
|
|
6023
|
+
type: "context",
|
|
6024
|
+
elements
|
|
6025
|
+
});
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
if (summary.reportUrl) {
|
|
6029
|
+
blocks.push({
|
|
6030
|
+
type: "actions",
|
|
6031
|
+
elements: [
|
|
6032
|
+
{
|
|
6033
|
+
type: "button",
|
|
6034
|
+
text: {
|
|
6035
|
+
type: "plain_text",
|
|
6036
|
+
text: "View Report",
|
|
6037
|
+
emoji: true
|
|
6038
|
+
},
|
|
6039
|
+
url: summary.reportUrl,
|
|
6040
|
+
action_id: "view_report"
|
|
6041
|
+
}
|
|
6042
|
+
]
|
|
6043
|
+
});
|
|
6044
|
+
}
|
|
6045
|
+
return { blocks };
|
|
6046
|
+
}
|
|
6047
|
+
async function sendSlackNotification(args, deps) {
|
|
6048
|
+
const { summary, webhookUrl, maxFailedTests = 5 } = args;
|
|
6049
|
+
const { fetch, logger } = deps;
|
|
6050
|
+
const payload = buildSlackPayload(summary, maxFailedTests);
|
|
6051
|
+
try {
|
|
6052
|
+
const response = await fetch(webhookUrl, {
|
|
6053
|
+
method: "POST",
|
|
6054
|
+
headers: { "Content-Type": "application/json" },
|
|
6055
|
+
body: JSON.stringify(payload)
|
|
6056
|
+
});
|
|
6057
|
+
if (!response.ok) {
|
|
6058
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
6059
|
+
let bodyText = "";
|
|
6060
|
+
try {
|
|
6061
|
+
bodyText = await response.text();
|
|
6062
|
+
} catch {
|
|
6063
|
+
}
|
|
6064
|
+
const truncatedBody = truncate(bodyText, 200);
|
|
6065
|
+
const idPart = requestId ? ` x-request-id=${requestId}` : "";
|
|
6066
|
+
const errorMsg = `Slack notifier failed: HTTP ${response.status}${idPart} ${truncatedBody}`;
|
|
6067
|
+
logger.warn(errorMsg);
|
|
6068
|
+
return { ok: false, error: errorMsg };
|
|
6069
|
+
}
|
|
6070
|
+
return { ok: true };
|
|
6071
|
+
} catch (err) {
|
|
6072
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6073
|
+
const errorMsg = `Slack notifier failed: ${msg}`;
|
|
6074
|
+
logger.warn(errorMsg);
|
|
6075
|
+
return { ok: false, error: errorMsg };
|
|
6076
|
+
}
|
|
6077
|
+
}
|
|
6078
|
+
|
|
6079
|
+
// src/notifiers/teams.ts
|
|
6080
|
+
function truncate2(text, maxLen) {
|
|
6081
|
+
if (text.length <= maxLen) return text;
|
|
6082
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
6083
|
+
}
|
|
6084
|
+
function formatDuration4(ms) {
|
|
6085
|
+
const seconds = ms / 1e3;
|
|
6086
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
6087
|
+
const minutes = Math.floor(seconds / 60);
|
|
6088
|
+
const remainingSeconds = seconds % 60;
|
|
6089
|
+
return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
|
|
6090
|
+
}
|
|
6091
|
+
function buildTeamsPayload(summary, maxFailedTests) {
|
|
6092
|
+
const allPassed = summary.failed === 0;
|
|
6093
|
+
const statusEmoji = allPassed ? "\u2705" : "\u274C";
|
|
6094
|
+
const statusColor = allPassed ? "good" : "attention";
|
|
6095
|
+
const bodyItems = [];
|
|
6096
|
+
bodyItems.push({
|
|
6097
|
+
type: "TextBlock",
|
|
6098
|
+
size: "Large",
|
|
6099
|
+
weight: "Bolder",
|
|
6100
|
+
text: `${statusEmoji} Test Results`,
|
|
6101
|
+
color: statusColor
|
|
6102
|
+
});
|
|
6103
|
+
bodyItems.push({
|
|
6104
|
+
type: "FactSet",
|
|
6105
|
+
facts: [
|
|
6106
|
+
{ title: "Total", value: String(summary.total) },
|
|
6107
|
+
{ title: "Passed", value: String(summary.passed) },
|
|
6108
|
+
{ title: "Failed", value: String(summary.failed) },
|
|
6109
|
+
{ title: "Skipped", value: String(summary.skipped) },
|
|
6110
|
+
{ title: "Duration", value: formatDuration4(summary.durationMs) }
|
|
6111
|
+
]
|
|
6112
|
+
});
|
|
6113
|
+
if (summary.failedTests.length > 0) {
|
|
6114
|
+
const displayedTests = summary.failedTests.slice(0, maxFailedTests);
|
|
6115
|
+
const failedItems = [
|
|
6116
|
+
{
|
|
6117
|
+
type: "TextBlock",
|
|
6118
|
+
text: "Failed Tests",
|
|
6119
|
+
weight: "Bolder",
|
|
6120
|
+
spacing: "Medium"
|
|
6121
|
+
}
|
|
6122
|
+
];
|
|
6123
|
+
for (const t of displayedTests) {
|
|
6124
|
+
failedItems.push({
|
|
6125
|
+
type: "TextBlock",
|
|
6126
|
+
text: `**${t.name}**`,
|
|
6127
|
+
wrap: true
|
|
6128
|
+
});
|
|
6129
|
+
if (t.error) {
|
|
6130
|
+
const cleanError = truncate2(stripAnsi(t.error), 500);
|
|
6131
|
+
failedItems.push({
|
|
6132
|
+
type: "TextBlock",
|
|
6133
|
+
text: cleanError,
|
|
6134
|
+
wrap: true,
|
|
6135
|
+
fontType: "Monospace",
|
|
6136
|
+
size: "Small",
|
|
6137
|
+
color: "Attention"
|
|
6138
|
+
});
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
if (summary.failedTests.length > maxFailedTests) {
|
|
6142
|
+
failedItems.push({
|
|
6143
|
+
type: "TextBlock",
|
|
6144
|
+
text: `...and ${summary.failedTests.length - maxFailedTests} more`,
|
|
6145
|
+
isSubtle: true,
|
|
6146
|
+
spacing: "Small"
|
|
6147
|
+
});
|
|
6148
|
+
}
|
|
6149
|
+
bodyItems.push({
|
|
6150
|
+
type: "Container",
|
|
6151
|
+
items: failedItems
|
|
6152
|
+
});
|
|
6153
|
+
}
|
|
6154
|
+
if (summary.ci) {
|
|
6155
|
+
const ciFacts = [];
|
|
6156
|
+
if (summary.ci.displayName) {
|
|
6157
|
+
ciFacts.push({ title: "CI", value: summary.ci.displayName });
|
|
6158
|
+
}
|
|
6159
|
+
if (summary.ci.branch) {
|
|
6160
|
+
ciFacts.push({ title: "Branch", value: summary.ci.branch });
|
|
6161
|
+
}
|
|
6162
|
+
if (summary.ci.commitSha) {
|
|
6163
|
+
ciFacts.push({ title: "Commit", value: summary.ci.commitSha.slice(0, 7) });
|
|
6164
|
+
}
|
|
6165
|
+
if (summary.ci.buildNumber) {
|
|
6166
|
+
ciFacts.push({ title: "Build", value: `#${summary.ci.buildNumber}` });
|
|
6167
|
+
}
|
|
6168
|
+
if (ciFacts.length > 0) {
|
|
6169
|
+
bodyItems.push({
|
|
6170
|
+
type: "FactSet",
|
|
6171
|
+
facts: ciFacts,
|
|
6172
|
+
separator: true
|
|
6173
|
+
});
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6176
|
+
const card = {
|
|
6177
|
+
type: "AdaptiveCard",
|
|
6178
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
6179
|
+
version: "1.4",
|
|
6180
|
+
body: bodyItems
|
|
6181
|
+
};
|
|
6182
|
+
if (summary.reportUrl) {
|
|
6183
|
+
card.actions = [
|
|
6184
|
+
{
|
|
6185
|
+
type: "Action.OpenUrl",
|
|
6186
|
+
title: "View Report",
|
|
6187
|
+
url: summary.reportUrl
|
|
6188
|
+
}
|
|
6189
|
+
];
|
|
6190
|
+
}
|
|
6191
|
+
return {
|
|
6192
|
+
type: "message",
|
|
6193
|
+
attachments: [
|
|
6194
|
+
{
|
|
6195
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
6196
|
+
content: card
|
|
6197
|
+
}
|
|
6198
|
+
]
|
|
6199
|
+
};
|
|
6200
|
+
}
|
|
6201
|
+
async function sendTeamsNotification(args, deps) {
|
|
6202
|
+
const { summary, webhookUrl, maxFailedTests = 5 } = args;
|
|
6203
|
+
const { fetch, logger } = deps;
|
|
6204
|
+
const payload = buildTeamsPayload(summary, maxFailedTests);
|
|
6205
|
+
try {
|
|
6206
|
+
const response = await fetch(webhookUrl, {
|
|
6207
|
+
method: "POST",
|
|
6208
|
+
headers: { "Content-Type": "application/json" },
|
|
6209
|
+
body: JSON.stringify(payload)
|
|
6210
|
+
});
|
|
6211
|
+
if (!response.ok) {
|
|
6212
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
6213
|
+
let bodyText = "";
|
|
6214
|
+
try {
|
|
6215
|
+
bodyText = await response.text();
|
|
6216
|
+
} catch {
|
|
6217
|
+
}
|
|
6218
|
+
const truncatedBody = truncate2(bodyText, 200);
|
|
6219
|
+
const idPart = requestId ? ` x-request-id=${requestId}` : "";
|
|
6220
|
+
const errorMsg = `Teams notifier failed: HTTP ${response.status}${idPart} ${truncatedBody}`;
|
|
6221
|
+
logger.warn(errorMsg);
|
|
6222
|
+
return { ok: false, error: errorMsg };
|
|
6223
|
+
}
|
|
6224
|
+
return { ok: true };
|
|
6225
|
+
} catch (err) {
|
|
6226
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6227
|
+
const errorMsg = `Teams notifier failed: ${msg}`;
|
|
6228
|
+
logger.warn(errorMsg);
|
|
6229
|
+
return { ok: false, error: errorMsg };
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6232
|
+
|
|
6233
|
+
// src/notifiers/hmac.ts
|
|
6234
|
+
var import_node_crypto3 = require("crypto");
|
|
6235
|
+
function signBody(args) {
|
|
6236
|
+
let input;
|
|
6237
|
+
let timestamp;
|
|
6238
|
+
if (args.includeTimestamp) {
|
|
6239
|
+
timestamp = args.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
6240
|
+
input = `${timestamp}.${args.body}`;
|
|
6241
|
+
} else {
|
|
6242
|
+
input = args.body;
|
|
6243
|
+
}
|
|
6244
|
+
const hex = (0, import_node_crypto3.createHmac)("sha256", args.secret).update(input, "utf8").digest("hex");
|
|
6245
|
+
return {
|
|
6246
|
+
signature: `sha256=${hex}`,
|
|
6247
|
+
timestamp
|
|
6248
|
+
};
|
|
6249
|
+
}
|
|
6250
|
+
|
|
6251
|
+
// src/notifiers/webhook.ts
|
|
6252
|
+
async function sendWebhookNotification(args, deps) {
|
|
6253
|
+
const { summary, options } = args;
|
|
6254
|
+
const { fetch, logger } = deps;
|
|
6255
|
+
const payload = {
|
|
6256
|
+
schemaVersion: 1,
|
|
6257
|
+
event: "test_run_finished",
|
|
6258
|
+
summary
|
|
6259
|
+
};
|
|
6260
|
+
const body = JSON.stringify(payload);
|
|
6261
|
+
const headers = { "Content-Type": "application/json" };
|
|
6262
|
+
if (options.headers) {
|
|
6263
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
6264
|
+
headers[key] = value;
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
6267
|
+
if (options.signer) {
|
|
6268
|
+
const { secret, header, includeTimestamp, timestampHeader } = options.signer;
|
|
6269
|
+
const result = signBody({ body, secret, includeTimestamp });
|
|
6270
|
+
headers[header] = result.signature;
|
|
6271
|
+
if (result.timestamp) {
|
|
6272
|
+
headers[timestampHeader ?? "X-Timestamp"] = result.timestamp;
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
try {
|
|
6276
|
+
const response = await fetch(options.url, {
|
|
6277
|
+
method: options.method ?? "POST",
|
|
6278
|
+
headers,
|
|
6279
|
+
body
|
|
6280
|
+
});
|
|
6281
|
+
if (!response.ok) {
|
|
6282
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
6283
|
+
let snippet = "";
|
|
6284
|
+
try {
|
|
6285
|
+
snippet = (await response.text()).slice(0, 200);
|
|
6286
|
+
} catch {
|
|
6287
|
+
}
|
|
6288
|
+
const idPart = requestId ? ` x-request-id=${requestId}` : "";
|
|
6289
|
+
const errorMsg = `webhook: HTTP ${response.status}${idPart} ${snippet}`;
|
|
6290
|
+
logger.warn(errorMsg);
|
|
6291
|
+
return { ok: false, error: errorMsg };
|
|
6292
|
+
}
|
|
6293
|
+
return { ok: true };
|
|
6294
|
+
} catch (err) {
|
|
6295
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6296
|
+
const errorMsg = `webhook: ${msg}`;
|
|
6297
|
+
logger.warn(errorMsg);
|
|
6298
|
+
return { ok: false, error: errorMsg };
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6301
|
+
|
|
6302
|
+
// src/notifiers/index.ts
|
|
6303
|
+
function buildSummary(run, reportUrl, toCIInfo2) {
|
|
6304
|
+
let passed = 0;
|
|
6305
|
+
let failed = 0;
|
|
6306
|
+
let skipped = 0;
|
|
6307
|
+
const failedTests = [];
|
|
6308
|
+
for (const tc of run.testCases) {
|
|
6309
|
+
switch (tc.status) {
|
|
6310
|
+
case "passed":
|
|
6311
|
+
passed++;
|
|
6312
|
+
break;
|
|
6313
|
+
case "failed":
|
|
6314
|
+
failed++;
|
|
6315
|
+
failedTests.push({
|
|
6316
|
+
testId: tc.id,
|
|
6317
|
+
name: tc.story.scenario,
|
|
6318
|
+
error: tc.errorMessage
|
|
6319
|
+
});
|
|
6320
|
+
break;
|
|
6321
|
+
case "skipped":
|
|
6322
|
+
case "pending":
|
|
6323
|
+
skipped++;
|
|
6324
|
+
break;
|
|
6325
|
+
}
|
|
6326
|
+
}
|
|
6327
|
+
let ci;
|
|
6328
|
+
if (run.ci) {
|
|
6329
|
+
ci = toCIInfo2(run.ci);
|
|
6330
|
+
}
|
|
6331
|
+
return {
|
|
6332
|
+
total: run.testCases.length,
|
|
6333
|
+
passed,
|
|
6334
|
+
failed,
|
|
6335
|
+
skipped,
|
|
6336
|
+
durationMs: run.durationMs,
|
|
6337
|
+
failedTests,
|
|
6338
|
+
ci,
|
|
6339
|
+
reportUrl
|
|
6340
|
+
};
|
|
6341
|
+
}
|
|
6342
|
+
function shouldNotify(condition, failedCount) {
|
|
6343
|
+
if (condition === "never") return false;
|
|
6344
|
+
if (condition === "on-failure" && failedCount === 0) return false;
|
|
6345
|
+
return true;
|
|
6346
|
+
}
|
|
6347
|
+
async function sendNotifications(args, deps) {
|
|
6348
|
+
const { run, notification } = args;
|
|
6349
|
+
const { logger, toCIInfo: toCIInfo2 } = deps;
|
|
6350
|
+
const env = deps.env ?? process.env;
|
|
6351
|
+
if (!deps.fetch) {
|
|
6352
|
+
logger.warn("notifications: skipped (fetch unavailable)");
|
|
6353
|
+
return;
|
|
6354
|
+
}
|
|
6355
|
+
const fetch = deps.fetch;
|
|
6356
|
+
const slackWebhookUrl = notification?.slackWebhookUrl ?? env.SLACK_WEBHOOK_URL;
|
|
6357
|
+
const teamsWebhookUrl = notification?.teamsWebhookUrl ?? env.TEAMS_WEBHOOK_URL;
|
|
6358
|
+
const globalCondition = notification?.condition ?? "on-failure";
|
|
6359
|
+
const reportUrl = notification?.reportUrl;
|
|
6360
|
+
const maxFailedTests = notification?.maxFailedTests ?? 5;
|
|
6361
|
+
const webhooks = notification?.webhooks ?? [];
|
|
6362
|
+
if (!slackWebhookUrl && !teamsWebhookUrl && webhooks.length === 0) {
|
|
6363
|
+
return;
|
|
6364
|
+
}
|
|
6365
|
+
const summary = buildSummary(run, reportUrl, toCIInfo2);
|
|
6366
|
+
const promises = [];
|
|
6367
|
+
if (slackWebhookUrl && shouldNotify(globalCondition, summary.failed)) {
|
|
6368
|
+
promises.push(
|
|
6369
|
+
sendSlackNotification(
|
|
6370
|
+
{ summary, webhookUrl: slackWebhookUrl, maxFailedTests },
|
|
6371
|
+
{ fetch, logger }
|
|
6372
|
+
).then(() => void 0)
|
|
6373
|
+
);
|
|
6374
|
+
}
|
|
6375
|
+
if (teamsWebhookUrl && shouldNotify(globalCondition, summary.failed)) {
|
|
6376
|
+
promises.push(
|
|
6377
|
+
sendTeamsNotification(
|
|
6378
|
+
{ summary, webhookUrl: teamsWebhookUrl, maxFailedTests },
|
|
6379
|
+
{ fetch, logger }
|
|
6380
|
+
).then(() => void 0)
|
|
6381
|
+
);
|
|
6382
|
+
}
|
|
6383
|
+
for (const webhook of webhooks) {
|
|
6384
|
+
const effectiveCondition = webhook.condition ?? globalCondition;
|
|
6385
|
+
if (!shouldNotify(effectiveCondition, summary.failed)) continue;
|
|
6386
|
+
promises.push(
|
|
6387
|
+
sendWebhookNotification(
|
|
6388
|
+
{ summary, options: webhook, maxFailedTests },
|
|
6389
|
+
{ fetch, logger }
|
|
6390
|
+
).then(() => void 0)
|
|
6391
|
+
);
|
|
6392
|
+
}
|
|
6393
|
+
await Promise.allSettled(promises);
|
|
6394
|
+
}
|
|
6395
|
+
|
|
6396
|
+
// src/types/ci.ts
|
|
6397
|
+
var DISPLAY_NAMES = {
|
|
6398
|
+
github: "GitHub Actions",
|
|
6399
|
+
gitlab: "GitLab CI",
|
|
6400
|
+
circleci: "CircleCI",
|
|
6401
|
+
jenkins: "Jenkins",
|
|
6402
|
+
azure: "Azure DevOps",
|
|
6403
|
+
buildkite: "Buildkite",
|
|
6404
|
+
travis: "Travis CI",
|
|
6405
|
+
unknown: "CI"
|
|
6406
|
+
};
|
|
6407
|
+
var NAME_TO_PROVIDER = {
|
|
6408
|
+
github: "github",
|
|
6409
|
+
gitlab: "gitlab",
|
|
6410
|
+
circleci: "circleci",
|
|
6411
|
+
jenkins: "jenkins",
|
|
6412
|
+
azure: "azure",
|
|
6413
|
+
buildkite: "buildkite",
|
|
6414
|
+
travis: "travis",
|
|
6415
|
+
ci: "unknown"
|
|
6416
|
+
};
|
|
6417
|
+
function toCIInfo(raw) {
|
|
6418
|
+
if (!raw) return void 0;
|
|
6419
|
+
const provider = raw.provider ?? NAME_TO_PROVIDER[raw.name] ?? "unknown";
|
|
6420
|
+
return {
|
|
6421
|
+
provider,
|
|
6422
|
+
displayName: DISPLAY_NAMES[provider],
|
|
6423
|
+
url: raw.url,
|
|
6424
|
+
buildNumber: raw.buildNumber,
|
|
6425
|
+
branch: raw.branch,
|
|
6426
|
+
commitSha: raw.commitSha,
|
|
6427
|
+
prNumber: raw.prNumber
|
|
6428
|
+
};
|
|
6429
|
+
}
|
|
6430
|
+
function toRawCIInfo(ci) {
|
|
6431
|
+
if (!ci) return void 0;
|
|
6432
|
+
return {
|
|
6433
|
+
name: ci.provider === "unknown" ? "ci" : ci.provider,
|
|
6434
|
+
provider: ci.provider,
|
|
6435
|
+
url: ci.url,
|
|
6436
|
+
buildNumber: ci.buildNumber,
|
|
6437
|
+
branch: ci.branch,
|
|
6438
|
+
commitSha: ci.commitSha,
|
|
6439
|
+
prNumber: ci.prNumber
|
|
6440
|
+
};
|
|
6441
|
+
}
|
|
6442
|
+
|
|
6443
|
+
// src/history/history-store.ts
|
|
6444
|
+
function emptyStore() {
|
|
6445
|
+
return { version: 1, maxRuns: 10, tests: {}, lastUpdated: 0 };
|
|
6446
|
+
}
|
|
6447
|
+
function loadHistory(args, deps) {
|
|
6448
|
+
const content = deps.readFile(args.filePath);
|
|
6449
|
+
if (content === void 0) {
|
|
6450
|
+
return emptyStore();
|
|
6451
|
+
}
|
|
6452
|
+
let parsed;
|
|
6453
|
+
try {
|
|
6454
|
+
parsed = JSON.parse(content);
|
|
6455
|
+
} catch {
|
|
6456
|
+
deps.logger.warn(`Failed to parse history file: ${args.filePath}`);
|
|
6457
|
+
return emptyStore();
|
|
6458
|
+
}
|
|
6459
|
+
if (typeof parsed !== "object" || parsed === null || parsed.version !== 1) {
|
|
6460
|
+
deps.logger.warn(
|
|
6461
|
+
`Unknown history version in ${args.filePath}, expected version 1`
|
|
6462
|
+
);
|
|
6463
|
+
return emptyStore();
|
|
6464
|
+
}
|
|
6465
|
+
const obj = parsed;
|
|
6466
|
+
if (typeof obj.tests !== "object" || obj.tests === null || Array.isArray(obj.tests)) {
|
|
6467
|
+
deps.logger.warn(
|
|
6468
|
+
`Malformed history store in ${args.filePath}: tests must be a non-null object`
|
|
6469
|
+
);
|
|
6470
|
+
return emptyStore();
|
|
6471
|
+
}
|
|
6472
|
+
return parsed;
|
|
6473
|
+
}
|
|
6474
|
+
function saveHistory(args, deps) {
|
|
6475
|
+
deps.writeFile(args.filePath, JSON.stringify(args.store, null, 2));
|
|
6476
|
+
}
|
|
6477
|
+
function updateHistory(args) {
|
|
6478
|
+
const { store, run, maxRuns } = args;
|
|
6479
|
+
const newTests = { ...store.tests };
|
|
6480
|
+
for (const tc of run.testCases) {
|
|
6481
|
+
const entry = {
|
|
6482
|
+
runId: run.runId,
|
|
6483
|
+
timestamp: run.startedAtMs,
|
|
6484
|
+
status: tc.status,
|
|
6485
|
+
durationMs: tc.durationMs,
|
|
6486
|
+
ci: run.ci ? {
|
|
6487
|
+
provider: void 0,
|
|
6488
|
+
branch: run.ci.branch,
|
|
6489
|
+
commitSha: run.ci.commitSha
|
|
6490
|
+
} : void 0
|
|
6491
|
+
};
|
|
6492
|
+
const existing = newTests[tc.id];
|
|
6493
|
+
if (existing) {
|
|
6494
|
+
const updatedEntries = [...existing.entries, entry];
|
|
6495
|
+
const trimmed = updatedEntries.length > maxRuns ? updatedEntries.slice(updatedEntries.length - maxRuns) : updatedEntries;
|
|
6496
|
+
newTests[tc.id] = {
|
|
6497
|
+
...existing,
|
|
6498
|
+
testName: tc.story.scenario,
|
|
6499
|
+
sourceFile: tc.sourceFile,
|
|
6500
|
+
sourceLine: tc.sourceLine,
|
|
6501
|
+
entries: trimmed
|
|
6502
|
+
};
|
|
6503
|
+
} else {
|
|
6504
|
+
newTests[tc.id] = {
|
|
6505
|
+
testId: tc.id,
|
|
6506
|
+
testName: tc.story.scenario,
|
|
6507
|
+
sourceFile: tc.sourceFile,
|
|
6508
|
+
sourceLine: tc.sourceLine,
|
|
6509
|
+
entries: [entry]
|
|
6510
|
+
};
|
|
6511
|
+
}
|
|
6512
|
+
}
|
|
6513
|
+
return {
|
|
6514
|
+
version: 1,
|
|
6515
|
+
maxRuns,
|
|
6516
|
+
tests: newTests,
|
|
6517
|
+
lastUpdated: Date.now()
|
|
6518
|
+
};
|
|
6519
|
+
}
|
|
6520
|
+
|
|
6521
|
+
// src/history/flakiness.ts
|
|
6522
|
+
function calculateFlakiness(args) {
|
|
6523
|
+
const { entries } = args;
|
|
6524
|
+
const countable = entries.filter(
|
|
6525
|
+
(e) => e.status === "passed" || e.status === "failed"
|
|
6526
|
+
);
|
|
6527
|
+
if (countable.length < MIN_FLAKINESS_SAMPLES) {
|
|
6528
|
+
return {
|
|
6529
|
+
flakinessLevel: "stable",
|
|
6530
|
+
flakinessScore: 0,
|
|
6531
|
+
failureRate: 0,
|
|
6532
|
+
longestPassStreak: countable.length,
|
|
6533
|
+
longestFailStreak: 0
|
|
6534
|
+
};
|
|
6535
|
+
}
|
|
6536
|
+
let transitions = 0;
|
|
6537
|
+
for (let i = 1; i < countable.length; i++) {
|
|
6538
|
+
if (countable[i].status !== countable[i - 1].status) {
|
|
6539
|
+
transitions++;
|
|
6540
|
+
}
|
|
6541
|
+
}
|
|
6542
|
+
const transitionScore = transitions / (countable.length - 1);
|
|
6543
|
+
const failures = countable.filter((e) => e.status === "failed").length;
|
|
6544
|
+
const failureRate = failures / countable.length;
|
|
6545
|
+
let longestPassStreak = 0;
|
|
6546
|
+
let longestFailStreak = 0;
|
|
6547
|
+
let currentPassStreak = 0;
|
|
6548
|
+
let currentFailStreak = 0;
|
|
6549
|
+
for (const e of countable) {
|
|
6550
|
+
if (e.status === "passed") {
|
|
6551
|
+
currentPassStreak++;
|
|
6552
|
+
currentFailStreak = 0;
|
|
6553
|
+
if (currentPassStreak > longestPassStreak) {
|
|
6554
|
+
longestPassStreak = currentPassStreak;
|
|
6555
|
+
}
|
|
6556
|
+
} else {
|
|
6557
|
+
currentFailStreak++;
|
|
6558
|
+
currentPassStreak = 0;
|
|
6559
|
+
if (currentFailStreak > longestFailStreak) {
|
|
6560
|
+
longestFailStreak = currentFailStreak;
|
|
6561
|
+
}
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
6564
|
+
let flakinessLevel;
|
|
6565
|
+
if (transitionScore > 0.5 || transitionScore > 0.3 && failureRate > 0.2) {
|
|
6566
|
+
flakinessLevel = "flaky";
|
|
6567
|
+
} else if (transitionScore > 0.2 || failureRate > 0.3) {
|
|
6568
|
+
flakinessLevel = "unstable";
|
|
6569
|
+
} else {
|
|
6570
|
+
flakinessLevel = "stable";
|
|
6571
|
+
}
|
|
6572
|
+
return {
|
|
6573
|
+
flakinessLevel,
|
|
6574
|
+
flakinessScore: transitionScore,
|
|
6575
|
+
failureRate,
|
|
6576
|
+
longestPassStreak,
|
|
6577
|
+
longestFailStreak
|
|
6578
|
+
};
|
|
6579
|
+
}
|
|
6580
|
+
|
|
6581
|
+
// src/history/performance.ts
|
|
6582
|
+
function detectPerformanceTrend(args) {
|
|
6583
|
+
const { entries } = args;
|
|
6584
|
+
const countable = entries.filter(
|
|
6585
|
+
(e) => e.status !== "skipped" && e.status !== "pending"
|
|
6586
|
+
);
|
|
6587
|
+
if (countable.length === 0) {
|
|
6588
|
+
return { trend: "stable", avgDurationMs: 0 };
|
|
6589
|
+
}
|
|
6590
|
+
const avgAll = countable.reduce((sum, e) => sum + e.durationMs, 0) / countable.length;
|
|
6591
|
+
if (countable.length < MIN_PERF_SAMPLES) {
|
|
6592
|
+
return { trend: "stable", avgDurationMs: avgAll };
|
|
6593
|
+
}
|
|
6594
|
+
const mid = Math.floor(countable.length / 2);
|
|
6595
|
+
const earlier = countable.slice(0, mid);
|
|
6596
|
+
const recent = countable.slice(mid);
|
|
6597
|
+
const earlierAvg = earlier.reduce((sum, e) => sum + e.durationMs, 0) / earlier.length;
|
|
6598
|
+
const recentAvg = recent.reduce((sum, e) => sum + e.durationMs, 0) / recent.length;
|
|
6599
|
+
let trend;
|
|
6600
|
+
if (earlierAvg === 0) {
|
|
6601
|
+
trend = "stable";
|
|
6602
|
+
} else {
|
|
6603
|
+
const change = (recentAvg - earlierAvg) / earlierAvg;
|
|
6604
|
+
if (change > 0.1) {
|
|
6605
|
+
trend = "regressing";
|
|
6606
|
+
} else if (change < -0.1) {
|
|
6607
|
+
trend = "improving";
|
|
6608
|
+
} else {
|
|
6609
|
+
trend = "stable";
|
|
6610
|
+
}
|
|
6611
|
+
}
|
|
6612
|
+
return { trend, avgDurationMs: avgAll };
|
|
6613
|
+
}
|
|
6614
|
+
|
|
6615
|
+
// src/history/stability.ts
|
|
6616
|
+
function calculateStability(args) {
|
|
6617
|
+
const { passRate, flakinessScore, longestPassStreak, sampleSize } = args;
|
|
6618
|
+
const inverseFlakiness = 1 - flakinessScore;
|
|
6619
|
+
const streakNorm = longestPassStreak / Math.min(sampleSize, 10);
|
|
6620
|
+
const score = passRate * 0.6 + inverseFlakiness * 0.3 + streakNorm * 0.1;
|
|
6621
|
+
if (score >= 0.95) return "A";
|
|
6622
|
+
if (score >= 0.85) return "B";
|
|
6623
|
+
if (score >= 0.7) return "C";
|
|
6624
|
+
if (score >= 0.5) return "D";
|
|
6625
|
+
return "F";
|
|
6626
|
+
}
|
|
6627
|
+
|
|
6628
|
+
// src/history/metrics.ts
|
|
6629
|
+
function computeTestMetrics(args) {
|
|
6630
|
+
const { testId, entries } = args;
|
|
6631
|
+
const flakiness = calculateFlakiness({ entries });
|
|
6632
|
+
const perf = detectPerformanceTrend({ entries });
|
|
6633
|
+
const countable = entries.filter(
|
|
6634
|
+
(e) => e.status === "passed" || e.status === "failed"
|
|
6635
|
+
);
|
|
6636
|
+
const passRate = countable.length > 0 ? countable.filter((e) => e.status === "passed").length / countable.length : 1;
|
|
6637
|
+
const stabilityGrade = calculateStability({
|
|
6638
|
+
passRate,
|
|
6639
|
+
flakinessScore: flakiness.flakinessScore,
|
|
6640
|
+
longestPassStreak: flakiness.longestPassStreak,
|
|
6641
|
+
sampleSize: entries.length
|
|
6642
|
+
});
|
|
6643
|
+
let consecutiveFailures = 0;
|
|
6644
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
6645
|
+
if (entries[i].status === "failed") {
|
|
6646
|
+
consecutiveFailures++;
|
|
6647
|
+
} else {
|
|
6648
|
+
break;
|
|
6649
|
+
}
|
|
6650
|
+
}
|
|
6651
|
+
return {
|
|
6652
|
+
testId,
|
|
6653
|
+
flakinessLevel: flakiness.flakinessLevel,
|
|
6654
|
+
flakinessScore: flakiness.flakinessScore,
|
|
6655
|
+
failureRate: flakiness.failureRate,
|
|
6656
|
+
stabilityGrade,
|
|
6657
|
+
performanceTrend: perf.trend,
|
|
6658
|
+
avgDurationMs: perf.avgDurationMs,
|
|
6659
|
+
passRate,
|
|
6660
|
+
longestPassStreak: flakiness.longestPassStreak,
|
|
6661
|
+
consecutiveFailures,
|
|
6662
|
+
sampleSize: entries.length
|
|
6663
|
+
};
|
|
6664
|
+
}
|
|
6665
|
+
|
|
5484
6666
|
// src/index.ts
|
|
5485
6667
|
var FORMAT_EXTENSIONS = {
|
|
5486
6668
|
markdown: ".md",
|
|
@@ -5830,6 +7012,9 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
5830
7012
|
CucumberMessagesFormatter,
|
|
5831
7013
|
HtmlFormatter,
|
|
5832
7014
|
JUnitFormatter,
|
|
7015
|
+
MIN_FLAKINESS_SAMPLES,
|
|
7016
|
+
MIN_METRIC_SAMPLES,
|
|
7017
|
+
MIN_PERF_SAMPLES,
|
|
5833
7018
|
MarkdownFormatter,
|
|
5834
7019
|
ReportGenerator,
|
|
5835
7020
|
STORY_META_KEY,
|
|
@@ -5837,15 +7022,21 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
5837
7022
|
adaptPlaywrightRun,
|
|
5838
7023
|
adaptVitestRun,
|
|
5839
7024
|
assertValidRun,
|
|
7025
|
+
calculateFlakiness,
|
|
7026
|
+
calculateStability,
|
|
5840
7027
|
canonicalizeRun,
|
|
5841
7028
|
clearVersionCache,
|
|
7029
|
+
computeTestMetrics,
|
|
5842
7030
|
createReportGenerator,
|
|
5843
7031
|
deriveStepResults,
|
|
5844
7032
|
detectCI,
|
|
7033
|
+
detectPerformanceTrend,
|
|
5845
7034
|
findGitDir,
|
|
5846
7035
|
formatDuration,
|
|
5847
7036
|
generateRunId,
|
|
5848
7037
|
generateTestCaseId,
|
|
7038
|
+
hasSufficientHistory,
|
|
7039
|
+
loadHistory,
|
|
5849
7040
|
mergeStepResults,
|
|
5850
7041
|
msToNanoseconds,
|
|
5851
7042
|
nanosecondsToMs,
|
|
@@ -5861,8 +7052,18 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
5861
7052
|
resolveAttachment,
|
|
5862
7053
|
resolveAttachments,
|
|
5863
7054
|
resolveTraceUrl,
|
|
7055
|
+
saveHistory,
|
|
7056
|
+
sendNotifications,
|
|
7057
|
+
sendSlackNotification,
|
|
7058
|
+
sendTeamsNotification,
|
|
7059
|
+
sendWebhookNotification,
|
|
7060
|
+
signBody,
|
|
5864
7061
|
slugify,
|
|
7062
|
+
stripAnsi,
|
|
7063
|
+
toCIInfo,
|
|
7064
|
+
toRawCIInfo,
|
|
5865
7065
|
tryGetActiveOtelContext,
|
|
7066
|
+
updateHistory,
|
|
5866
7067
|
validateCanonicalRun
|
|
5867
7068
|
});
|
|
5868
7069
|
//# sourceMappingURL=index.cjs.map
|