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.js
CHANGED
|
@@ -898,17 +898,17 @@ function initCollapse() {
|
|
|
898
898
|
}
|
|
899
899
|
|
|
900
900
|
function expandAll() {
|
|
901
|
-
document.querySelectorAll('.feature, .scenario').forEach(el => {
|
|
901
|
+
document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
|
|
902
902
|
el.classList.remove('collapsed');
|
|
903
|
-
const header = el.querySelector('.feature-header, .scenario-header');
|
|
903
|
+
const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
|
|
904
904
|
header?.setAttribute('aria-expanded', 'true');
|
|
905
905
|
});
|
|
906
906
|
}
|
|
907
907
|
|
|
908
908
|
function collapseAll() {
|
|
909
|
-
document.querySelectorAll('.feature, .scenario').forEach(el => {
|
|
909
|
+
document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
|
|
910
910
|
el.classList.add('collapsed');
|
|
911
|
-
const header = el.querySelector('.feature-header, .scenario-header');
|
|
911
|
+
const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
|
|
912
912
|
header?.setAttribute('aria-expanded', 'false');
|
|
913
913
|
});
|
|
914
914
|
}
|
|
@@ -2435,6 +2435,147 @@ body {
|
|
|
2435
2435
|
background: none;
|
|
2436
2436
|
}
|
|
2437
2437
|
|
|
2438
|
+
/* ============================================================================
|
|
2439
|
+
Trace View - OTel span waterfall
|
|
2440
|
+
============================================================================ */
|
|
2441
|
+
.trace-view {
|
|
2442
|
+
margin-top: 0.75rem;
|
|
2443
|
+
border: 1px solid var(--border);
|
|
2444
|
+
border-radius: calc(var(--radius) - 2px);
|
|
2445
|
+
overflow: hidden;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
.trace-view-header {
|
|
2449
|
+
display: flex;
|
|
2450
|
+
align-items: center;
|
|
2451
|
+
gap: 0.5rem;
|
|
2452
|
+
padding: 0.5rem 0.75rem;
|
|
2453
|
+
background: var(--card);
|
|
2454
|
+
cursor: pointer;
|
|
2455
|
+
user-select: none;
|
|
2456
|
+
font-size: 0.8125rem;
|
|
2457
|
+
font-weight: 500;
|
|
2458
|
+
color: var(--foreground);
|
|
2459
|
+
transition: background-color 0.15s ease;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
.trace-view-header:hover {
|
|
2463
|
+
background: var(--accordion-header-hover);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
.trace-view-count {
|
|
2467
|
+
font-size: 0.6875rem;
|
|
2468
|
+
font-weight: 500;
|
|
2469
|
+
padding: 0.125rem 0.5rem;
|
|
2470
|
+
background: var(--success-light);
|
|
2471
|
+
color: var(--success);
|
|
2472
|
+
border: 1px solid var(--success-border);
|
|
2473
|
+
border-radius: 9999px;
|
|
2474
|
+
font-family: var(--font-mono);
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
.trace-view-content {
|
|
2478
|
+
border-top: 1px solid var(--border);
|
|
2479
|
+
padding: 0.5rem 0.75rem;
|
|
2480
|
+
background: var(--accordion-content-bg);
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
.trace-view.collapsed .trace-view-content {
|
|
2484
|
+
display: none;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
.trace-view-axis {
|
|
2488
|
+
display: flex;
|
|
2489
|
+
justify-content: space-between;
|
|
2490
|
+
font-size: 0.625rem;
|
|
2491
|
+
font-family: var(--font-mono);
|
|
2492
|
+
color: var(--muted-foreground);
|
|
2493
|
+
padding-bottom: 0.375rem;
|
|
2494
|
+
margin-bottom: 0.375rem;
|
|
2495
|
+
border-bottom: 1px solid var(--border);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
.trace-view-row {
|
|
2499
|
+
display: flex;
|
|
2500
|
+
align-items: center;
|
|
2501
|
+
gap: 0.5rem;
|
|
2502
|
+
padding: 0.1875rem 0;
|
|
2503
|
+
font-size: 0.75rem;
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
.trace-view-name {
|
|
2507
|
+
width: 35%;
|
|
2508
|
+
flex-shrink: 0;
|
|
2509
|
+
display: flex;
|
|
2510
|
+
align-items: center;
|
|
2511
|
+
gap: 0.375rem;
|
|
2512
|
+
font-family: var(--font-mono);
|
|
2513
|
+
white-space: nowrap;
|
|
2514
|
+
overflow: hidden;
|
|
2515
|
+
text-overflow: ellipsis;
|
|
2516
|
+
color: var(--foreground);
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
.trace-view-status-dot {
|
|
2520
|
+
width: 8px;
|
|
2521
|
+
height: 8px;
|
|
2522
|
+
border-radius: 50%;
|
|
2523
|
+
flex-shrink: 0;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
.trace-view-status-ok { background: var(--success); }
|
|
2527
|
+
.trace-view-status-error { background: var(--error); }
|
|
2528
|
+
.trace-view-status-unset { background: var(--muted-foreground); }
|
|
2529
|
+
|
|
2530
|
+
.trace-view-bar-container {
|
|
2531
|
+
flex: 1;
|
|
2532
|
+
position: relative;
|
|
2533
|
+
height: 1.25rem;
|
|
2534
|
+
background: var(--muted);
|
|
2535
|
+
border-radius: 2px;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
.trace-view-bar {
|
|
2539
|
+
position: absolute;
|
|
2540
|
+
top: 0;
|
|
2541
|
+
height: 100%;
|
|
2542
|
+
border-radius: 2px;
|
|
2543
|
+
min-width: 2px;
|
|
2544
|
+
display: flex;
|
|
2545
|
+
align-items: center;
|
|
2546
|
+
padding: 0 0.375rem;
|
|
2547
|
+
font-size: 0.625rem;
|
|
2548
|
+
font-family: var(--font-mono);
|
|
2549
|
+
color: white;
|
|
2550
|
+
white-space: nowrap;
|
|
2551
|
+
overflow: hidden;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
.trace-view-bar-ok { background: var(--success); }
|
|
2555
|
+
.trace-view-bar-error { background: var(--error); }
|
|
2556
|
+
.trace-view-bar-unset { background: var(--muted-foreground); }
|
|
2557
|
+
|
|
2558
|
+
@media print {
|
|
2559
|
+
.trace-view.collapsed .trace-view-content {
|
|
2560
|
+
display: block;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
/* ============================================================================
|
|
2565
|
+
History metric badges
|
|
2566
|
+
============================================================================ */
|
|
2567
|
+
.badge { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 0.75em; font-weight: 600; margin-left: 4px; vertical-align: middle; }
|
|
2568
|
+
.badge-grade { color: #fff; }
|
|
2569
|
+
.badge-grade-A { background: var(--success); }
|
|
2570
|
+
.badge-grade-B { background: #2196F3; }
|
|
2571
|
+
.badge-grade-C { background: #FF9800; }
|
|
2572
|
+
.badge-grade-D { background: #f44336; }
|
|
2573
|
+
.badge-grade-F { background: #9E0000; }
|
|
2574
|
+
.badge-flaky { background: #FF9800; color: #fff; }
|
|
2575
|
+
.badge-perf { font-size: 0.7em; }
|
|
2576
|
+
.badge-perf-improving { color: var(--success); }
|
|
2577
|
+
.badge-perf-regressing { color: var(--error); }
|
|
2578
|
+
|
|
2438
2579
|
`;
|
|
2439
2580
|
|
|
2440
2581
|
// src/formatters/html/renderers/status.ts
|
|
@@ -2468,7 +2609,22 @@ function renderMetaInfo(args, deps) {
|
|
|
2468
2609
|
items.push(`<dt>Git:</dt><dd>${deps.escapeHtml(shortSha)}</dd>`);
|
|
2469
2610
|
}
|
|
2470
2611
|
if (args.ciName) {
|
|
2471
|
-
|
|
2612
|
+
if (args.ciUrl && args.ciBuildNumber) {
|
|
2613
|
+
items.push(
|
|
2614
|
+
`<dt>CI:</dt><dd>${deps.escapeHtml(args.ciName)} <a href="${deps.escapeHtml(args.ciUrl)}">#${deps.escapeHtml(args.ciBuildNumber)}</a></dd>`
|
|
2615
|
+
);
|
|
2616
|
+
} else {
|
|
2617
|
+
items.push(`<dt>CI:</dt><dd>${deps.escapeHtml(args.ciName)}</dd>`);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
if (args.ciBranch) {
|
|
2621
|
+
items.push(`<dt>Branch:</dt><dd>${deps.escapeHtml(args.ciBranch)}</dd>`);
|
|
2622
|
+
}
|
|
2623
|
+
if (args.ciCommitSha) {
|
|
2624
|
+
const shortSha = args.ciCommitSha.length > 7 ? args.ciCommitSha.slice(0, 7) : args.ciCommitSha;
|
|
2625
|
+
items.push(
|
|
2626
|
+
`<dt>Commit:</dt><dd title="${deps.escapeHtml(args.ciCommitSha)}">${deps.escapeHtml(shortSha)}</dd>`
|
|
2627
|
+
);
|
|
2472
2628
|
}
|
|
2473
2629
|
return `<dl class="meta-info">${items.join("")}</dl>`;
|
|
2474
2630
|
}
|
|
@@ -2719,6 +2875,14 @@ function highlightStepParams(text, deps) {
|
|
|
2719
2875
|
return result;
|
|
2720
2876
|
}
|
|
2721
2877
|
|
|
2878
|
+
// src/history/sample-policy.ts
|
|
2879
|
+
var MIN_PERF_SAMPLES = 6;
|
|
2880
|
+
var MIN_METRIC_SAMPLES = 5;
|
|
2881
|
+
var MIN_FLAKINESS_SAMPLES = 3;
|
|
2882
|
+
function hasSufficientHistory(entries, min) {
|
|
2883
|
+
return entries.length >= min;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2722
2886
|
// src/formatters/html/renderers/scenario.ts
|
|
2723
2887
|
function renderScenario(args, deps) {
|
|
2724
2888
|
const { tc } = args;
|
|
@@ -2739,6 +2903,19 @@ function renderScenario(args, deps) {
|
|
|
2739
2903
|
traceBadge = `<span class="tag trace-tag" title="${deps.escapeHtml(otelMeta.traceId)}">${deps.escapeHtml(shortId)}\u2026</span>`;
|
|
2740
2904
|
}
|
|
2741
2905
|
}
|
|
2906
|
+
let metricBadges = "";
|
|
2907
|
+
const { metrics } = args;
|
|
2908
|
+
if (metrics && metrics.sampleSize >= MIN_METRIC_SAMPLES) {
|
|
2909
|
+
const grade = metrics.stabilityGrade;
|
|
2910
|
+
metricBadges += `<span class="badge badge-grade badge-grade-${grade}" title="Pass rate: ${(metrics.passRate * 100).toFixed(0)}% (${metrics.sampleSize} runs)">${grade}</span>`;
|
|
2911
|
+
if (metrics.flakinessLevel !== "stable") {
|
|
2912
|
+
metricBadges += `<span class="badge badge-flaky">${metrics.flakinessLevel}</span>`;
|
|
2913
|
+
}
|
|
2914
|
+
if (metrics.performanceTrend !== "stable") {
|
|
2915
|
+
const arrow = metrics.performanceTrend === "improving" ? "\u2191" : "\u2193";
|
|
2916
|
+
metricBadges += `<span class="badge badge-perf badge-perf-${metrics.performanceTrend}">${arrow} ${metrics.performanceTrend}</span>`;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2742
2919
|
const storyDocs = deps.renderDocs(tc.story.docs, "story-docs");
|
|
2743
2920
|
const steps = deps.renderSteps(
|
|
2744
2921
|
{ steps: tc.story.steps, stepResults: tc.stepResults },
|
|
@@ -2759,6 +2936,10 @@ function renderScenario(args, deps) {
|
|
|
2759
2936
|
embedScreenshots: deps.embedScreenshots
|
|
2760
2937
|
}
|
|
2761
2938
|
);
|
|
2939
|
+
const traceView = deps.renderTraceView(
|
|
2940
|
+
{ spans: tc.story.otelSpans },
|
|
2941
|
+
{ escapeHtml: deps.escapeHtml }
|
|
2942
|
+
);
|
|
2762
2943
|
const collapsedClass = deps.startCollapsed ? " collapsed" : "";
|
|
2763
2944
|
const ariaExpanded = !deps.startCollapsed;
|
|
2764
2945
|
return `
|
|
@@ -2769,7 +2950,7 @@ function renderScenario(args, deps) {
|
|
|
2769
2950
|
<span class="status-icon ${statusClass}">${statusIcon}</span>
|
|
2770
2951
|
<span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
|
|
2771
2952
|
</div>
|
|
2772
|
-
<div class="scenario-meta">${tags}${traceBadge}</div>
|
|
2953
|
+
<div class="scenario-meta">${tags}${traceBadge}${metricBadges}</div>
|
|
2773
2954
|
</div>
|
|
2774
2955
|
<span class="scenario-duration">${duration}</span>
|
|
2775
2956
|
</div>
|
|
@@ -2778,6 +2959,193 @@ function renderScenario(args, deps) {
|
|
|
2778
2959
|
${steps}
|
|
2779
2960
|
${error}
|
|
2780
2961
|
${attachments}
|
|
2962
|
+
${traceView}
|
|
2963
|
+
</div>
|
|
2964
|
+
</div>`;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// src/formatters/html/renderers/trace-view.ts
|
|
2968
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
|
|
2969
|
+
var TOOLTIP_MAX_LENGTH = 4096;
|
|
2970
|
+
function safeStatus(status) {
|
|
2971
|
+
return VALID_STATUSES.has(status) ? status : "unset";
|
|
2972
|
+
}
|
|
2973
|
+
function formatDuration(ms) {
|
|
2974
|
+
if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
|
|
2975
|
+
return `${ms.toFixed(1)}ms`;
|
|
2976
|
+
}
|
|
2977
|
+
function clamp(value, min, max) {
|
|
2978
|
+
return Math.min(max, Math.max(min, value));
|
|
2979
|
+
}
|
|
2980
|
+
function normalizeSpans(spans) {
|
|
2981
|
+
const result = [];
|
|
2982
|
+
for (const span of spans) {
|
|
2983
|
+
if (!span || typeof span !== "object") continue;
|
|
2984
|
+
if (typeof span.spanId !== "string" || typeof span.name !== "string") continue;
|
|
2985
|
+
let startTimeMs;
|
|
2986
|
+
let durationMs;
|
|
2987
|
+
if (span.startTimeMs != null && span.durationMs != null) {
|
|
2988
|
+
startTimeMs = span.startTimeMs;
|
|
2989
|
+
durationMs = span.durationMs;
|
|
2990
|
+
} else if (span.startTimeUnixNano != null && span.endTimeUnixNano != null) {
|
|
2991
|
+
startTimeMs = span.startTimeUnixNano / 1e6;
|
|
2992
|
+
durationMs = (span.endTimeUnixNano - span.startTimeUnixNano) / 1e6;
|
|
2993
|
+
} else {
|
|
2994
|
+
continue;
|
|
2995
|
+
}
|
|
2996
|
+
durationMs = Math.max(0, durationMs);
|
|
2997
|
+
if (!isFinite(startTimeMs) || !isFinite(durationMs)) continue;
|
|
2998
|
+
result.push({
|
|
2999
|
+
spanId: span.spanId,
|
|
3000
|
+
parentSpanId: span.parentSpanId,
|
|
3001
|
+
name: span.name,
|
|
3002
|
+
startTimeMs,
|
|
3003
|
+
durationMs,
|
|
3004
|
+
status: safeStatus(span.status),
|
|
3005
|
+
statusMessage: span.statusMessage,
|
|
3006
|
+
attributes: span.attributes
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
return result;
|
|
3010
|
+
}
|
|
3011
|
+
function buildTree(spans) {
|
|
3012
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3013
|
+
for (const span of spans) {
|
|
3014
|
+
let key = span.spanId;
|
|
3015
|
+
if (byId.has(key)) {
|
|
3016
|
+
let suffix = 2;
|
|
3017
|
+
while (byId.has(`${span.spanId}__dup${suffix}`)) suffix++;
|
|
3018
|
+
key = `${span.spanId}__dup${suffix}`;
|
|
3019
|
+
}
|
|
3020
|
+
byId.set(key, { span: { ...span, spanId: key }, children: [], depth: 0 });
|
|
3021
|
+
}
|
|
3022
|
+
const roots = [];
|
|
3023
|
+
for (const node of byId.values()) {
|
|
3024
|
+
const parentId = node.span.parentSpanId;
|
|
3025
|
+
const parent = parentId ? byId.get(parentId) : void 0;
|
|
3026
|
+
if (parent && parent !== node) {
|
|
3027
|
+
parent.children.push(node);
|
|
3028
|
+
} else {
|
|
3029
|
+
roots.push(node);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
for (const node of byId.values()) {
|
|
3033
|
+
node.children.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
3034
|
+
}
|
|
3035
|
+
roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
3036
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3037
|
+
function assignDepth(node, depth) {
|
|
3038
|
+
if (visited.has(node.span.spanId)) return;
|
|
3039
|
+
visited.add(node.span.spanId);
|
|
3040
|
+
node.depth = depth;
|
|
3041
|
+
for (const child of node.children) {
|
|
3042
|
+
assignDepth(child, depth + 1);
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
for (const root of roots) {
|
|
3046
|
+
assignDepth(root, 0);
|
|
3047
|
+
}
|
|
3048
|
+
for (const node of byId.values()) {
|
|
3049
|
+
if (!visited.has(node.span.spanId)) {
|
|
3050
|
+
node.children = [];
|
|
3051
|
+
roots.push(node);
|
|
3052
|
+
assignDepth(node, 0);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
3056
|
+
return roots;
|
|
3057
|
+
}
|
|
3058
|
+
function flattenTree(roots) {
|
|
3059
|
+
const result = [];
|
|
3060
|
+
function walk(node) {
|
|
3061
|
+
result.push(node);
|
|
3062
|
+
for (const child of node.children) {
|
|
3063
|
+
walk(child);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
for (const root of roots) {
|
|
3067
|
+
walk(root);
|
|
3068
|
+
}
|
|
3069
|
+
return result;
|
|
3070
|
+
}
|
|
3071
|
+
function buildTooltip(span, escapeHtml2) {
|
|
3072
|
+
const parts = [];
|
|
3073
|
+
parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
|
|
3074
|
+
if (span.statusMessage) {
|
|
3075
|
+
parts.push(`Status: ${span.statusMessage}`);
|
|
3076
|
+
}
|
|
3077
|
+
if (span.attributes) {
|
|
3078
|
+
const keys = Object.keys(span.attributes).sort();
|
|
3079
|
+
for (const key of keys) {
|
|
3080
|
+
const val = span.attributes[key];
|
|
3081
|
+
const formatted = Array.isArray(val) ? `[${val.map((v) => String(v)).join(", ")}]` : String(val);
|
|
3082
|
+
parts.push(`${key}=${formatted}`);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
let text = parts.join("\n");
|
|
3086
|
+
if (text.length > TOOLTIP_MAX_LENGTH) {
|
|
3087
|
+
text = text.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
|
|
3088
|
+
}
|
|
3089
|
+
return escapeHtml2(text);
|
|
3090
|
+
}
|
|
3091
|
+
function renderTraceView(args, deps) {
|
|
3092
|
+
if (!args.spans || args.spans.length === 0) return "";
|
|
3093
|
+
const normalized = normalizeSpans(args.spans);
|
|
3094
|
+
if (normalized.length === 0) return "";
|
|
3095
|
+
const roots = buildTree(normalized);
|
|
3096
|
+
const flat = flattenTree(roots);
|
|
3097
|
+
let minStart = Infinity;
|
|
3098
|
+
let maxEnd = -Infinity;
|
|
3099
|
+
for (const node of flat) {
|
|
3100
|
+
const s = node.span.startTimeMs;
|
|
3101
|
+
const e = s + node.span.durationMs;
|
|
3102
|
+
if (s < minStart) minStart = s;
|
|
3103
|
+
if (e > maxEnd) maxEnd = e;
|
|
3104
|
+
}
|
|
3105
|
+
let totalDuration = maxEnd - minStart;
|
|
3106
|
+
if (totalDuration <= 0) totalDuration = 1;
|
|
3107
|
+
const rows = flat.map((node) => {
|
|
3108
|
+
const { span, depth } = node;
|
|
3109
|
+
const indent = depth * 16;
|
|
3110
|
+
const minWidth = 0.5;
|
|
3111
|
+
let spanLeft = clamp(
|
|
3112
|
+
(span.startTimeMs - minStart) / totalDuration * 100,
|
|
3113
|
+
0,
|
|
3114
|
+
100
|
|
3115
|
+
);
|
|
3116
|
+
if (spanLeft + minWidth > 100) {
|
|
3117
|
+
spanLeft = 100 - minWidth;
|
|
3118
|
+
}
|
|
3119
|
+
const spanWidth = clamp(
|
|
3120
|
+
span.durationMs / totalDuration * 100,
|
|
3121
|
+
minWidth,
|
|
3122
|
+
100 - spanLeft
|
|
3123
|
+
);
|
|
3124
|
+
const tooltip = buildTooltip(span, deps.escapeHtml);
|
|
3125
|
+
const durationLabel = formatDuration(span.durationMs);
|
|
3126
|
+
return ` <div class="trace-view-row">
|
|
3127
|
+
<div class="trace-view-name" style="padding-left: ${indent}px" title="${deps.escapeHtml(span.name)}">
|
|
3128
|
+
<span class="trace-view-status-dot trace-view-status-${span.status}"></span>
|
|
3129
|
+
${deps.escapeHtml(span.name)}
|
|
3130
|
+
</div>
|
|
3131
|
+
<div class="trace-view-bar-container">
|
|
3132
|
+
<div class="trace-view-bar trace-view-bar-${span.status}" style="left: ${spanLeft.toFixed(2)}%; width: ${spanWidth.toFixed(2)}%" title="${tooltip}">${durationLabel}</div>
|
|
3133
|
+
</div>
|
|
3134
|
+
</div>`;
|
|
3135
|
+
}).join("\n");
|
|
3136
|
+
const axisEnd = formatDuration(maxEnd - minStart);
|
|
3137
|
+
return `<div class="trace-view collapsed">
|
|
3138
|
+
<div class="trace-view-header" role="button" tabindex="0" aria-expanded="false">
|
|
3139
|
+
<span>Spans</span>
|
|
3140
|
+
<span class="trace-view-count">${flat.length}</span>
|
|
3141
|
+
<span class="chevron">▼</span>
|
|
3142
|
+
</div>
|
|
3143
|
+
<div class="trace-view-content">
|
|
3144
|
+
<div class="trace-view-axis">
|
|
3145
|
+
<span>0ms</span>
|
|
3146
|
+
<span>${axisEnd}</span>
|
|
3147
|
+
</div>
|
|
3148
|
+
${rows}
|
|
2781
3149
|
</div>
|
|
2782
3150
|
</div>`;
|
|
2783
3151
|
}
|
|
@@ -2794,7 +3162,12 @@ function renderFeature(args, deps) {
|
|
|
2794
3162
|
const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
|
|
2795
3163
|
const collapsedClass = deps.startCollapsed ? " collapsed" : "";
|
|
2796
3164
|
const ariaExpanded = !deps.startCollapsed;
|
|
2797
|
-
const scenarios = testCases.map(
|
|
3165
|
+
const scenarios = testCases.map(
|
|
3166
|
+
(tc) => deps.renderScenario(
|
|
3167
|
+
{ tc, metrics: args.metricsMap?.get(tc.id) },
|
|
3168
|
+
deps.scenarioDeps
|
|
3169
|
+
)
|
|
3170
|
+
).join("\n");
|
|
2798
3171
|
return `
|
|
2799
3172
|
<div class="feature${collapsedClass}">
|
|
2800
3173
|
<div class="feature-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
|
|
@@ -2839,7 +3212,11 @@ function buildBody(args, deps) {
|
|
|
2839
3212
|
durationMs: run.durationMs,
|
|
2840
3213
|
packageVersion: run.packageVersion,
|
|
2841
3214
|
gitSha: run.gitSha,
|
|
2842
|
-
ciName: run.ci?.name
|
|
3215
|
+
ciName: run.ci?.name,
|
|
3216
|
+
ciBranch: run.ci?.branch,
|
|
3217
|
+
ciUrl: run.ci?.url,
|
|
3218
|
+
ciCommitSha: run.ci?.commitSha,
|
|
3219
|
+
ciBuildNumber: run.ci?.buildNumber
|
|
2843
3220
|
},
|
|
2844
3221
|
deps.metaDeps
|
|
2845
3222
|
)
|
|
@@ -2868,7 +3245,10 @@ function buildBody(args, deps) {
|
|
|
2868
3245
|
const byFile = groupBy(run.testCases, (tc) => tc.sourceFile);
|
|
2869
3246
|
for (const [file, testCases] of byFile) {
|
|
2870
3247
|
parts.push(
|
|
2871
|
-
deps.renderFeature(
|
|
3248
|
+
deps.renderFeature(
|
|
3249
|
+
{ file, testCases, metricsMap: args.metricsMap },
|
|
3250
|
+
deps.featureDeps
|
|
3251
|
+
)
|
|
2872
3252
|
);
|
|
2873
3253
|
}
|
|
2874
3254
|
return parts.join("\n");
|
|
@@ -2914,6 +3294,7 @@ function createHtmlFormatter(options = {}) {
|
|
|
2914
3294
|
renderDocs,
|
|
2915
3295
|
renderErrorBox: (args, d) => renderErrorBox(args, d),
|
|
2916
3296
|
renderAttachments: (args, d) => renderAttachments(args, d),
|
|
3297
|
+
renderTraceView: (args, d) => renderTraceView(args, d),
|
|
2917
3298
|
embedScreenshots: opts.embedScreenshots
|
|
2918
3299
|
};
|
|
2919
3300
|
const featureDeps = {
|
|
@@ -5279,7 +5660,7 @@ function readBranchName(cwd = process.cwd()) {
|
|
|
5279
5660
|
}
|
|
5280
5661
|
|
|
5281
5662
|
// src/utils/duration.ts
|
|
5282
|
-
function
|
|
5663
|
+
function formatDuration2(ms) {
|
|
5283
5664
|
if (ms < 1e3) {
|
|
5284
5665
|
return `${Math.round(ms)} ms`;
|
|
5285
5666
|
}
|
|
@@ -5339,45 +5720,102 @@ function clearVersionCache() {
|
|
|
5339
5720
|
|
|
5340
5721
|
// src/utils/ci-detect.ts
|
|
5341
5722
|
function detectCI4(env = process.env) {
|
|
5723
|
+
if (env.TF_BUILD === "True") {
|
|
5724
|
+
const branch = env.BUILD_SOURCEBRANCH?.replace(/^refs\/heads\//, "");
|
|
5725
|
+
const serverUri = env.SYSTEM_TEAMFOUNDATIONSERVERURI;
|
|
5726
|
+
const teamProject = env.SYSTEM_TEAMPROJECT;
|
|
5727
|
+
const buildId = env.BUILD_BUILDID;
|
|
5728
|
+
const url = serverUri && teamProject && buildId ? `${serverUri}${teamProject}/_build/results?buildId=${buildId}` : void 0;
|
|
5729
|
+
return {
|
|
5730
|
+
name: "azure",
|
|
5731
|
+
provider: "azure",
|
|
5732
|
+
buildNumber: buildId,
|
|
5733
|
+
url,
|
|
5734
|
+
branch,
|
|
5735
|
+
commitSha: env.BUILD_SOURCEVERSION,
|
|
5736
|
+
prNumber: env.SYSTEM_PULLREQUEST_PULLREQUESTID
|
|
5737
|
+
};
|
|
5738
|
+
}
|
|
5739
|
+
if (env.BUILDKITE === "true") {
|
|
5740
|
+
const prRaw = env.BUILDKITE_PULL_REQUEST;
|
|
5741
|
+
const prNumber = prRaw && prRaw !== "false" ? prRaw : void 0;
|
|
5742
|
+
return {
|
|
5743
|
+
name: "buildkite",
|
|
5744
|
+
provider: "buildkite",
|
|
5745
|
+
buildNumber: env.BUILDKITE_BUILD_NUMBER,
|
|
5746
|
+
url: env.BUILDKITE_BUILD_URL,
|
|
5747
|
+
branch: env.BUILDKITE_BRANCH,
|
|
5748
|
+
commitSha: env.BUILDKITE_COMMIT,
|
|
5749
|
+
prNumber
|
|
5750
|
+
};
|
|
5751
|
+
}
|
|
5342
5752
|
if (env.GITHUB_ACTIONS === "true") {
|
|
5343
5753
|
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;
|
|
5754
|
+
const branch = env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME;
|
|
5755
|
+
const prMatch = env.GITHUB_REF?.match(/^refs\/pull\/(\d+)\/(merge|head)$/);
|
|
5756
|
+
const prNumber = prMatch ? prMatch[1] : void 0;
|
|
5344
5757
|
return {
|
|
5345
5758
|
name: "github",
|
|
5759
|
+
provider: "github",
|
|
5346
5760
|
buildNumber: env.GITHUB_RUN_NUMBER,
|
|
5347
|
-
url
|
|
5761
|
+
url,
|
|
5762
|
+
branch,
|
|
5763
|
+
commitSha: env.GITHUB_SHA,
|
|
5764
|
+
prNumber
|
|
5765
|
+
};
|
|
5766
|
+
}
|
|
5767
|
+
if (env.GITLAB_CI === "true") {
|
|
5768
|
+
return {
|
|
5769
|
+
name: "gitlab",
|
|
5770
|
+
provider: "gitlab",
|
|
5771
|
+
buildNumber: env.CI_PIPELINE_IID,
|
|
5772
|
+
url: env.CI_PIPELINE_URL,
|
|
5773
|
+
branch: env.CI_COMMIT_REF_NAME,
|
|
5774
|
+
commitSha: env.CI_COMMIT_SHA,
|
|
5775
|
+
prNumber: env.CI_MERGE_REQUEST_IID
|
|
5348
5776
|
};
|
|
5349
5777
|
}
|
|
5350
5778
|
if (env.CIRCLECI === "true") {
|
|
5779
|
+
const prUrl = env.CIRCLE_PULL_REQUEST;
|
|
5780
|
+
const prMatch = prUrl?.match(/\/(\d+)$/);
|
|
5781
|
+
const prNumber = prMatch ? prMatch[1] : void 0;
|
|
5351
5782
|
return {
|
|
5352
5783
|
name: "circleci",
|
|
5784
|
+
provider: "circleci",
|
|
5353
5785
|
buildNumber: env.CIRCLE_BUILD_NUM,
|
|
5354
|
-
url: env.CIRCLE_BUILD_URL
|
|
5786
|
+
url: env.CIRCLE_BUILD_URL,
|
|
5787
|
+
branch: env.CIRCLE_BRANCH,
|
|
5788
|
+
commitSha: env.CIRCLE_SHA1,
|
|
5789
|
+
prNumber
|
|
5355
5790
|
};
|
|
5356
5791
|
}
|
|
5357
5792
|
if (env.JENKINS_URL !== void 0) {
|
|
5358
5793
|
return {
|
|
5359
5794
|
name: "jenkins",
|
|
5795
|
+
provider: "jenkins",
|
|
5360
5796
|
buildNumber: env.BUILD_NUMBER,
|
|
5361
|
-
url: env.BUILD_URL
|
|
5797
|
+
url: env.BUILD_URL,
|
|
5798
|
+
branch: env.GIT_BRANCH,
|
|
5799
|
+
commitSha: env.GIT_COMMIT
|
|
5362
5800
|
};
|
|
5363
5801
|
}
|
|
5364
5802
|
if (env.TRAVIS === "true") {
|
|
5803
|
+
const prRaw = env.TRAVIS_PULL_REQUEST;
|
|
5804
|
+
const prNumber = prRaw && prRaw !== "false" ? prRaw : void 0;
|
|
5365
5805
|
return {
|
|
5366
5806
|
name: "travis",
|
|
5807
|
+
provider: "travis",
|
|
5367
5808
|
buildNumber: env.TRAVIS_BUILD_NUMBER,
|
|
5368
|
-
url: env.TRAVIS_BUILD_WEB_URL
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
return {
|
|
5373
|
-
name: "gitlab",
|
|
5374
|
-
buildNumber: env.CI_PIPELINE_IID,
|
|
5375
|
-
url: env.CI_PIPELINE_URL
|
|
5809
|
+
url: env.TRAVIS_BUILD_WEB_URL,
|
|
5810
|
+
branch: env.TRAVIS_BRANCH,
|
|
5811
|
+
commitSha: env.TRAVIS_COMMIT,
|
|
5812
|
+
prNumber
|
|
5376
5813
|
};
|
|
5377
5814
|
}
|
|
5378
5815
|
if (env.CI === "true") {
|
|
5379
5816
|
return {
|
|
5380
|
-
name: "ci"
|
|
5817
|
+
name: "ci",
|
|
5818
|
+
provider: "unknown"
|
|
5381
5819
|
};
|
|
5382
5820
|
}
|
|
5383
5821
|
return void 0;
|
|
@@ -5408,6 +5846,731 @@ function resolveTraceUrl(template, traceId) {
|
|
|
5408
5846
|
return template.replace(/\{traceId\}/g, traceId);
|
|
5409
5847
|
}
|
|
5410
5848
|
|
|
5849
|
+
// src/notifiers/ansi-strip.ts
|
|
5850
|
+
function stripAnsi(text) {
|
|
5851
|
+
return text.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
|
|
5852
|
+
}
|
|
5853
|
+
|
|
5854
|
+
// src/notifiers/slack.ts
|
|
5855
|
+
function truncate(text, maxLen) {
|
|
5856
|
+
if (text.length <= maxLen) return text;
|
|
5857
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
5858
|
+
}
|
|
5859
|
+
function formatDuration3(ms) {
|
|
5860
|
+
const seconds = ms / 1e3;
|
|
5861
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
5862
|
+
const minutes = Math.floor(seconds / 60);
|
|
5863
|
+
const remainingSeconds = seconds % 60;
|
|
5864
|
+
return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
|
|
5865
|
+
}
|
|
5866
|
+
function buildSlackPayload(summary, maxFailedTests) {
|
|
5867
|
+
const allPassed = summary.failed === 0;
|
|
5868
|
+
const emoji = allPassed ? ":white_check_mark:" : ":x:";
|
|
5869
|
+
const statusText = allPassed ? "Passed" : "Failed";
|
|
5870
|
+
const blocks = [];
|
|
5871
|
+
blocks.push({
|
|
5872
|
+
type: "header",
|
|
5873
|
+
text: {
|
|
5874
|
+
type: "plain_text",
|
|
5875
|
+
text: `${emoji} Test Results: ${summary.passed} passed, ${summary.failed} failed`,
|
|
5876
|
+
emoji: true
|
|
5877
|
+
}
|
|
5878
|
+
});
|
|
5879
|
+
blocks.push({
|
|
5880
|
+
type: "section",
|
|
5881
|
+
fields: [
|
|
5882
|
+
{ type: "mrkdwn", text: `*Total:* ${summary.total}` },
|
|
5883
|
+
{ type: "mrkdwn", text: `*Passed:* ${summary.passed}` },
|
|
5884
|
+
{ type: "mrkdwn", text: `*Failed:* ${summary.failed}` },
|
|
5885
|
+
{ type: "mrkdwn", text: `*Skipped:* ${summary.skipped}` },
|
|
5886
|
+
{ type: "mrkdwn", text: `*Duration:* ${formatDuration3(summary.durationMs)}` },
|
|
5887
|
+
{ type: "mrkdwn", text: `*Status:* ${statusText}` }
|
|
5888
|
+
]
|
|
5889
|
+
});
|
|
5890
|
+
if (summary.failedTests.length > 0) {
|
|
5891
|
+
const displayedTests = summary.failedTests.slice(0, maxFailedTests);
|
|
5892
|
+
const lines = displayedTests.map((t) => {
|
|
5893
|
+
const name = t.name;
|
|
5894
|
+
if (t.error) {
|
|
5895
|
+
const cleanError = truncate(stripAnsi(t.error), 500);
|
|
5896
|
+
return `*${name}*
|
|
5897
|
+
\`\`\`${cleanError}\`\`\``;
|
|
5898
|
+
}
|
|
5899
|
+
return `*${name}*`;
|
|
5900
|
+
});
|
|
5901
|
+
let text = lines.join("\n\n");
|
|
5902
|
+
if (summary.failedTests.length > maxFailedTests) {
|
|
5903
|
+
text += `
|
|
5904
|
+
|
|
5905
|
+
_...and ${summary.failedTests.length - maxFailedTests} more_`;
|
|
5906
|
+
}
|
|
5907
|
+
blocks.push({
|
|
5908
|
+
type: "section",
|
|
5909
|
+
text: {
|
|
5910
|
+
type: "mrkdwn",
|
|
5911
|
+
text
|
|
5912
|
+
}
|
|
5913
|
+
});
|
|
5914
|
+
}
|
|
5915
|
+
if (summary.ci) {
|
|
5916
|
+
const elements = [];
|
|
5917
|
+
if (summary.ci.displayName) {
|
|
5918
|
+
elements.push({ type: "mrkdwn", text: `*CI:* ${summary.ci.displayName}` });
|
|
5919
|
+
}
|
|
5920
|
+
if (summary.ci.branch) {
|
|
5921
|
+
elements.push({ type: "mrkdwn", text: `*Branch:* ${summary.ci.branch}` });
|
|
5922
|
+
}
|
|
5923
|
+
if (summary.ci.commitSha) {
|
|
5924
|
+
elements.push({ type: "mrkdwn", text: `*Commit:* ${summary.ci.commitSha.slice(0, 7)}` });
|
|
5925
|
+
}
|
|
5926
|
+
if (summary.ci.buildNumber) {
|
|
5927
|
+
elements.push({ type: "mrkdwn", text: `*Build:* #${summary.ci.buildNumber}` });
|
|
5928
|
+
}
|
|
5929
|
+
if (elements.length > 0) {
|
|
5930
|
+
blocks.push({
|
|
5931
|
+
type: "context",
|
|
5932
|
+
elements
|
|
5933
|
+
});
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
if (summary.reportUrl) {
|
|
5937
|
+
blocks.push({
|
|
5938
|
+
type: "actions",
|
|
5939
|
+
elements: [
|
|
5940
|
+
{
|
|
5941
|
+
type: "button",
|
|
5942
|
+
text: {
|
|
5943
|
+
type: "plain_text",
|
|
5944
|
+
text: "View Report",
|
|
5945
|
+
emoji: true
|
|
5946
|
+
},
|
|
5947
|
+
url: summary.reportUrl,
|
|
5948
|
+
action_id: "view_report"
|
|
5949
|
+
}
|
|
5950
|
+
]
|
|
5951
|
+
});
|
|
5952
|
+
}
|
|
5953
|
+
return { blocks };
|
|
5954
|
+
}
|
|
5955
|
+
async function sendSlackNotification(args, deps) {
|
|
5956
|
+
const { summary, webhookUrl, maxFailedTests = 5 } = args;
|
|
5957
|
+
const { fetch, logger } = deps;
|
|
5958
|
+
const payload = buildSlackPayload(summary, maxFailedTests);
|
|
5959
|
+
try {
|
|
5960
|
+
const response = await fetch(webhookUrl, {
|
|
5961
|
+
method: "POST",
|
|
5962
|
+
headers: { "Content-Type": "application/json" },
|
|
5963
|
+
body: JSON.stringify(payload)
|
|
5964
|
+
});
|
|
5965
|
+
if (!response.ok) {
|
|
5966
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
5967
|
+
let bodyText = "";
|
|
5968
|
+
try {
|
|
5969
|
+
bodyText = await response.text();
|
|
5970
|
+
} catch {
|
|
5971
|
+
}
|
|
5972
|
+
const truncatedBody = truncate(bodyText, 200);
|
|
5973
|
+
const idPart = requestId ? ` x-request-id=${requestId}` : "";
|
|
5974
|
+
const errorMsg = `Slack notifier failed: HTTP ${response.status}${idPart} ${truncatedBody}`;
|
|
5975
|
+
logger.warn(errorMsg);
|
|
5976
|
+
return { ok: false, error: errorMsg };
|
|
5977
|
+
}
|
|
5978
|
+
return { ok: true };
|
|
5979
|
+
} catch (err) {
|
|
5980
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5981
|
+
const errorMsg = `Slack notifier failed: ${msg}`;
|
|
5982
|
+
logger.warn(errorMsg);
|
|
5983
|
+
return { ok: false, error: errorMsg };
|
|
5984
|
+
}
|
|
5985
|
+
}
|
|
5986
|
+
|
|
5987
|
+
// src/notifiers/teams.ts
|
|
5988
|
+
function truncate2(text, maxLen) {
|
|
5989
|
+
if (text.length <= maxLen) return text;
|
|
5990
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
5991
|
+
}
|
|
5992
|
+
function formatDuration4(ms) {
|
|
5993
|
+
const seconds = ms / 1e3;
|
|
5994
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
5995
|
+
const minutes = Math.floor(seconds / 60);
|
|
5996
|
+
const remainingSeconds = seconds % 60;
|
|
5997
|
+
return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
|
|
5998
|
+
}
|
|
5999
|
+
function buildTeamsPayload(summary, maxFailedTests) {
|
|
6000
|
+
const allPassed = summary.failed === 0;
|
|
6001
|
+
const statusEmoji = allPassed ? "\u2705" : "\u274C";
|
|
6002
|
+
const statusColor = allPassed ? "good" : "attention";
|
|
6003
|
+
const bodyItems = [];
|
|
6004
|
+
bodyItems.push({
|
|
6005
|
+
type: "TextBlock",
|
|
6006
|
+
size: "Large",
|
|
6007
|
+
weight: "Bolder",
|
|
6008
|
+
text: `${statusEmoji} Test Results`,
|
|
6009
|
+
color: statusColor
|
|
6010
|
+
});
|
|
6011
|
+
bodyItems.push({
|
|
6012
|
+
type: "FactSet",
|
|
6013
|
+
facts: [
|
|
6014
|
+
{ title: "Total", value: String(summary.total) },
|
|
6015
|
+
{ title: "Passed", value: String(summary.passed) },
|
|
6016
|
+
{ title: "Failed", value: String(summary.failed) },
|
|
6017
|
+
{ title: "Skipped", value: String(summary.skipped) },
|
|
6018
|
+
{ title: "Duration", value: formatDuration4(summary.durationMs) }
|
|
6019
|
+
]
|
|
6020
|
+
});
|
|
6021
|
+
if (summary.failedTests.length > 0) {
|
|
6022
|
+
const displayedTests = summary.failedTests.slice(0, maxFailedTests);
|
|
6023
|
+
const failedItems = [
|
|
6024
|
+
{
|
|
6025
|
+
type: "TextBlock",
|
|
6026
|
+
text: "Failed Tests",
|
|
6027
|
+
weight: "Bolder",
|
|
6028
|
+
spacing: "Medium"
|
|
6029
|
+
}
|
|
6030
|
+
];
|
|
6031
|
+
for (const t of displayedTests) {
|
|
6032
|
+
failedItems.push({
|
|
6033
|
+
type: "TextBlock",
|
|
6034
|
+
text: `**${t.name}**`,
|
|
6035
|
+
wrap: true
|
|
6036
|
+
});
|
|
6037
|
+
if (t.error) {
|
|
6038
|
+
const cleanError = truncate2(stripAnsi(t.error), 500);
|
|
6039
|
+
failedItems.push({
|
|
6040
|
+
type: "TextBlock",
|
|
6041
|
+
text: cleanError,
|
|
6042
|
+
wrap: true,
|
|
6043
|
+
fontType: "Monospace",
|
|
6044
|
+
size: "Small",
|
|
6045
|
+
color: "Attention"
|
|
6046
|
+
});
|
|
6047
|
+
}
|
|
6048
|
+
}
|
|
6049
|
+
if (summary.failedTests.length > maxFailedTests) {
|
|
6050
|
+
failedItems.push({
|
|
6051
|
+
type: "TextBlock",
|
|
6052
|
+
text: `...and ${summary.failedTests.length - maxFailedTests} more`,
|
|
6053
|
+
isSubtle: true,
|
|
6054
|
+
spacing: "Small"
|
|
6055
|
+
});
|
|
6056
|
+
}
|
|
6057
|
+
bodyItems.push({
|
|
6058
|
+
type: "Container",
|
|
6059
|
+
items: failedItems
|
|
6060
|
+
});
|
|
6061
|
+
}
|
|
6062
|
+
if (summary.ci) {
|
|
6063
|
+
const ciFacts = [];
|
|
6064
|
+
if (summary.ci.displayName) {
|
|
6065
|
+
ciFacts.push({ title: "CI", value: summary.ci.displayName });
|
|
6066
|
+
}
|
|
6067
|
+
if (summary.ci.branch) {
|
|
6068
|
+
ciFacts.push({ title: "Branch", value: summary.ci.branch });
|
|
6069
|
+
}
|
|
6070
|
+
if (summary.ci.commitSha) {
|
|
6071
|
+
ciFacts.push({ title: "Commit", value: summary.ci.commitSha.slice(0, 7) });
|
|
6072
|
+
}
|
|
6073
|
+
if (summary.ci.buildNumber) {
|
|
6074
|
+
ciFacts.push({ title: "Build", value: `#${summary.ci.buildNumber}` });
|
|
6075
|
+
}
|
|
6076
|
+
if (ciFacts.length > 0) {
|
|
6077
|
+
bodyItems.push({
|
|
6078
|
+
type: "FactSet",
|
|
6079
|
+
facts: ciFacts,
|
|
6080
|
+
separator: true
|
|
6081
|
+
});
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
const card = {
|
|
6085
|
+
type: "AdaptiveCard",
|
|
6086
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
6087
|
+
version: "1.4",
|
|
6088
|
+
body: bodyItems
|
|
6089
|
+
};
|
|
6090
|
+
if (summary.reportUrl) {
|
|
6091
|
+
card.actions = [
|
|
6092
|
+
{
|
|
6093
|
+
type: "Action.OpenUrl",
|
|
6094
|
+
title: "View Report",
|
|
6095
|
+
url: summary.reportUrl
|
|
6096
|
+
}
|
|
6097
|
+
];
|
|
6098
|
+
}
|
|
6099
|
+
return {
|
|
6100
|
+
type: "message",
|
|
6101
|
+
attachments: [
|
|
6102
|
+
{
|
|
6103
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
6104
|
+
content: card
|
|
6105
|
+
}
|
|
6106
|
+
]
|
|
6107
|
+
};
|
|
6108
|
+
}
|
|
6109
|
+
async function sendTeamsNotification(args, deps) {
|
|
6110
|
+
const { summary, webhookUrl, maxFailedTests = 5 } = args;
|
|
6111
|
+
const { fetch, logger } = deps;
|
|
6112
|
+
const payload = buildTeamsPayload(summary, maxFailedTests);
|
|
6113
|
+
try {
|
|
6114
|
+
const response = await fetch(webhookUrl, {
|
|
6115
|
+
method: "POST",
|
|
6116
|
+
headers: { "Content-Type": "application/json" },
|
|
6117
|
+
body: JSON.stringify(payload)
|
|
6118
|
+
});
|
|
6119
|
+
if (!response.ok) {
|
|
6120
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
6121
|
+
let bodyText = "";
|
|
6122
|
+
try {
|
|
6123
|
+
bodyText = await response.text();
|
|
6124
|
+
} catch {
|
|
6125
|
+
}
|
|
6126
|
+
const truncatedBody = truncate2(bodyText, 200);
|
|
6127
|
+
const idPart = requestId ? ` x-request-id=${requestId}` : "";
|
|
6128
|
+
const errorMsg = `Teams notifier failed: HTTP ${response.status}${idPart} ${truncatedBody}`;
|
|
6129
|
+
logger.warn(errorMsg);
|
|
6130
|
+
return { ok: false, error: errorMsg };
|
|
6131
|
+
}
|
|
6132
|
+
return { ok: true };
|
|
6133
|
+
} catch (err) {
|
|
6134
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6135
|
+
const errorMsg = `Teams notifier failed: ${msg}`;
|
|
6136
|
+
logger.warn(errorMsg);
|
|
6137
|
+
return { ok: false, error: errorMsg };
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
|
|
6141
|
+
// src/notifiers/hmac.ts
|
|
6142
|
+
import { createHmac } from "crypto";
|
|
6143
|
+
function signBody(args) {
|
|
6144
|
+
let input;
|
|
6145
|
+
let timestamp;
|
|
6146
|
+
if (args.includeTimestamp) {
|
|
6147
|
+
timestamp = args.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
6148
|
+
input = `${timestamp}.${args.body}`;
|
|
6149
|
+
} else {
|
|
6150
|
+
input = args.body;
|
|
6151
|
+
}
|
|
6152
|
+
const hex = createHmac("sha256", args.secret).update(input, "utf8").digest("hex");
|
|
6153
|
+
return {
|
|
6154
|
+
signature: `sha256=${hex}`,
|
|
6155
|
+
timestamp
|
|
6156
|
+
};
|
|
6157
|
+
}
|
|
6158
|
+
|
|
6159
|
+
// src/notifiers/webhook.ts
|
|
6160
|
+
async function sendWebhookNotification(args, deps) {
|
|
6161
|
+
const { summary, options } = args;
|
|
6162
|
+
const { fetch, logger } = deps;
|
|
6163
|
+
const payload = {
|
|
6164
|
+
schemaVersion: 1,
|
|
6165
|
+
event: "test_run_finished",
|
|
6166
|
+
summary
|
|
6167
|
+
};
|
|
6168
|
+
const body = JSON.stringify(payload);
|
|
6169
|
+
const headers = { "Content-Type": "application/json" };
|
|
6170
|
+
if (options.headers) {
|
|
6171
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
6172
|
+
headers[key] = value;
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
if (options.signer) {
|
|
6176
|
+
const { secret, header, includeTimestamp, timestampHeader } = options.signer;
|
|
6177
|
+
const result = signBody({ body, secret, includeTimestamp });
|
|
6178
|
+
headers[header] = result.signature;
|
|
6179
|
+
if (result.timestamp) {
|
|
6180
|
+
headers[timestampHeader ?? "X-Timestamp"] = result.timestamp;
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
try {
|
|
6184
|
+
const response = await fetch(options.url, {
|
|
6185
|
+
method: options.method ?? "POST",
|
|
6186
|
+
headers,
|
|
6187
|
+
body
|
|
6188
|
+
});
|
|
6189
|
+
if (!response.ok) {
|
|
6190
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
6191
|
+
let snippet = "";
|
|
6192
|
+
try {
|
|
6193
|
+
snippet = (await response.text()).slice(0, 200);
|
|
6194
|
+
} catch {
|
|
6195
|
+
}
|
|
6196
|
+
const idPart = requestId ? ` x-request-id=${requestId}` : "";
|
|
6197
|
+
const errorMsg = `webhook: HTTP ${response.status}${idPart} ${snippet}`;
|
|
6198
|
+
logger.warn(errorMsg);
|
|
6199
|
+
return { ok: false, error: errorMsg };
|
|
6200
|
+
}
|
|
6201
|
+
return { ok: true };
|
|
6202
|
+
} catch (err) {
|
|
6203
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6204
|
+
const errorMsg = `webhook: ${msg}`;
|
|
6205
|
+
logger.warn(errorMsg);
|
|
6206
|
+
return { ok: false, error: errorMsg };
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6209
|
+
|
|
6210
|
+
// src/notifiers/index.ts
|
|
6211
|
+
function buildSummary(run, reportUrl, toCIInfo2) {
|
|
6212
|
+
let passed = 0;
|
|
6213
|
+
let failed = 0;
|
|
6214
|
+
let skipped = 0;
|
|
6215
|
+
const failedTests = [];
|
|
6216
|
+
for (const tc of run.testCases) {
|
|
6217
|
+
switch (tc.status) {
|
|
6218
|
+
case "passed":
|
|
6219
|
+
passed++;
|
|
6220
|
+
break;
|
|
6221
|
+
case "failed":
|
|
6222
|
+
failed++;
|
|
6223
|
+
failedTests.push({
|
|
6224
|
+
testId: tc.id,
|
|
6225
|
+
name: tc.story.scenario,
|
|
6226
|
+
error: tc.errorMessage
|
|
6227
|
+
});
|
|
6228
|
+
break;
|
|
6229
|
+
case "skipped":
|
|
6230
|
+
case "pending":
|
|
6231
|
+
skipped++;
|
|
6232
|
+
break;
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
let ci;
|
|
6236
|
+
if (run.ci) {
|
|
6237
|
+
ci = toCIInfo2(run.ci);
|
|
6238
|
+
}
|
|
6239
|
+
return {
|
|
6240
|
+
total: run.testCases.length,
|
|
6241
|
+
passed,
|
|
6242
|
+
failed,
|
|
6243
|
+
skipped,
|
|
6244
|
+
durationMs: run.durationMs,
|
|
6245
|
+
failedTests,
|
|
6246
|
+
ci,
|
|
6247
|
+
reportUrl
|
|
6248
|
+
};
|
|
6249
|
+
}
|
|
6250
|
+
function shouldNotify(condition, failedCount) {
|
|
6251
|
+
if (condition === "never") return false;
|
|
6252
|
+
if (condition === "on-failure" && failedCount === 0) return false;
|
|
6253
|
+
return true;
|
|
6254
|
+
}
|
|
6255
|
+
async function sendNotifications(args, deps) {
|
|
6256
|
+
const { run, notification } = args;
|
|
6257
|
+
const { logger, toCIInfo: toCIInfo2 } = deps;
|
|
6258
|
+
const env = deps.env ?? process.env;
|
|
6259
|
+
if (!deps.fetch) {
|
|
6260
|
+
logger.warn("notifications: skipped (fetch unavailable)");
|
|
6261
|
+
return;
|
|
6262
|
+
}
|
|
6263
|
+
const fetch = deps.fetch;
|
|
6264
|
+
const slackWebhookUrl = notification?.slackWebhookUrl ?? env.SLACK_WEBHOOK_URL;
|
|
6265
|
+
const teamsWebhookUrl = notification?.teamsWebhookUrl ?? env.TEAMS_WEBHOOK_URL;
|
|
6266
|
+
const globalCondition = notification?.condition ?? "on-failure";
|
|
6267
|
+
const reportUrl = notification?.reportUrl;
|
|
6268
|
+
const maxFailedTests = notification?.maxFailedTests ?? 5;
|
|
6269
|
+
const webhooks = notification?.webhooks ?? [];
|
|
6270
|
+
if (!slackWebhookUrl && !teamsWebhookUrl && webhooks.length === 0) {
|
|
6271
|
+
return;
|
|
6272
|
+
}
|
|
6273
|
+
const summary = buildSummary(run, reportUrl, toCIInfo2);
|
|
6274
|
+
const promises = [];
|
|
6275
|
+
if (slackWebhookUrl && shouldNotify(globalCondition, summary.failed)) {
|
|
6276
|
+
promises.push(
|
|
6277
|
+
sendSlackNotification(
|
|
6278
|
+
{ summary, webhookUrl: slackWebhookUrl, maxFailedTests },
|
|
6279
|
+
{ fetch, logger }
|
|
6280
|
+
).then(() => void 0)
|
|
6281
|
+
);
|
|
6282
|
+
}
|
|
6283
|
+
if (teamsWebhookUrl && shouldNotify(globalCondition, summary.failed)) {
|
|
6284
|
+
promises.push(
|
|
6285
|
+
sendTeamsNotification(
|
|
6286
|
+
{ summary, webhookUrl: teamsWebhookUrl, maxFailedTests },
|
|
6287
|
+
{ fetch, logger }
|
|
6288
|
+
).then(() => void 0)
|
|
6289
|
+
);
|
|
6290
|
+
}
|
|
6291
|
+
for (const webhook of webhooks) {
|
|
6292
|
+
const effectiveCondition = webhook.condition ?? globalCondition;
|
|
6293
|
+
if (!shouldNotify(effectiveCondition, summary.failed)) continue;
|
|
6294
|
+
promises.push(
|
|
6295
|
+
sendWebhookNotification(
|
|
6296
|
+
{ summary, options: webhook, maxFailedTests },
|
|
6297
|
+
{ fetch, logger }
|
|
6298
|
+
).then(() => void 0)
|
|
6299
|
+
);
|
|
6300
|
+
}
|
|
6301
|
+
await Promise.allSettled(promises);
|
|
6302
|
+
}
|
|
6303
|
+
|
|
6304
|
+
// src/types/ci.ts
|
|
6305
|
+
var DISPLAY_NAMES = {
|
|
6306
|
+
github: "GitHub Actions",
|
|
6307
|
+
gitlab: "GitLab CI",
|
|
6308
|
+
circleci: "CircleCI",
|
|
6309
|
+
jenkins: "Jenkins",
|
|
6310
|
+
azure: "Azure DevOps",
|
|
6311
|
+
buildkite: "Buildkite",
|
|
6312
|
+
travis: "Travis CI",
|
|
6313
|
+
unknown: "CI"
|
|
6314
|
+
};
|
|
6315
|
+
var NAME_TO_PROVIDER = {
|
|
6316
|
+
github: "github",
|
|
6317
|
+
gitlab: "gitlab",
|
|
6318
|
+
circleci: "circleci",
|
|
6319
|
+
jenkins: "jenkins",
|
|
6320
|
+
azure: "azure",
|
|
6321
|
+
buildkite: "buildkite",
|
|
6322
|
+
travis: "travis",
|
|
6323
|
+
ci: "unknown"
|
|
6324
|
+
};
|
|
6325
|
+
function toCIInfo(raw) {
|
|
6326
|
+
if (!raw) return void 0;
|
|
6327
|
+
const provider = raw.provider ?? NAME_TO_PROVIDER[raw.name] ?? "unknown";
|
|
6328
|
+
return {
|
|
6329
|
+
provider,
|
|
6330
|
+
displayName: DISPLAY_NAMES[provider],
|
|
6331
|
+
url: raw.url,
|
|
6332
|
+
buildNumber: raw.buildNumber,
|
|
6333
|
+
branch: raw.branch,
|
|
6334
|
+
commitSha: raw.commitSha,
|
|
6335
|
+
prNumber: raw.prNumber
|
|
6336
|
+
};
|
|
6337
|
+
}
|
|
6338
|
+
function toRawCIInfo(ci) {
|
|
6339
|
+
if (!ci) return void 0;
|
|
6340
|
+
return {
|
|
6341
|
+
name: ci.provider === "unknown" ? "ci" : ci.provider,
|
|
6342
|
+
provider: ci.provider,
|
|
6343
|
+
url: ci.url,
|
|
6344
|
+
buildNumber: ci.buildNumber,
|
|
6345
|
+
branch: ci.branch,
|
|
6346
|
+
commitSha: ci.commitSha,
|
|
6347
|
+
prNumber: ci.prNumber
|
|
6348
|
+
};
|
|
6349
|
+
}
|
|
6350
|
+
|
|
6351
|
+
// src/history/history-store.ts
|
|
6352
|
+
function emptyStore() {
|
|
6353
|
+
return { version: 1, maxRuns: 10, tests: {}, lastUpdated: 0 };
|
|
6354
|
+
}
|
|
6355
|
+
function loadHistory(args, deps) {
|
|
6356
|
+
const content = deps.readFile(args.filePath);
|
|
6357
|
+
if (content === void 0) {
|
|
6358
|
+
return emptyStore();
|
|
6359
|
+
}
|
|
6360
|
+
let parsed;
|
|
6361
|
+
try {
|
|
6362
|
+
parsed = JSON.parse(content);
|
|
6363
|
+
} catch {
|
|
6364
|
+
deps.logger.warn(`Failed to parse history file: ${args.filePath}`);
|
|
6365
|
+
return emptyStore();
|
|
6366
|
+
}
|
|
6367
|
+
if (typeof parsed !== "object" || parsed === null || parsed.version !== 1) {
|
|
6368
|
+
deps.logger.warn(
|
|
6369
|
+
`Unknown history version in ${args.filePath}, expected version 1`
|
|
6370
|
+
);
|
|
6371
|
+
return emptyStore();
|
|
6372
|
+
}
|
|
6373
|
+
const obj = parsed;
|
|
6374
|
+
if (typeof obj.tests !== "object" || obj.tests === null || Array.isArray(obj.tests)) {
|
|
6375
|
+
deps.logger.warn(
|
|
6376
|
+
`Malformed history store in ${args.filePath}: tests must be a non-null object`
|
|
6377
|
+
);
|
|
6378
|
+
return emptyStore();
|
|
6379
|
+
}
|
|
6380
|
+
return parsed;
|
|
6381
|
+
}
|
|
6382
|
+
function saveHistory(args, deps) {
|
|
6383
|
+
deps.writeFile(args.filePath, JSON.stringify(args.store, null, 2));
|
|
6384
|
+
}
|
|
6385
|
+
function updateHistory(args) {
|
|
6386
|
+
const { store, run, maxRuns } = args;
|
|
6387
|
+
const newTests = { ...store.tests };
|
|
6388
|
+
for (const tc of run.testCases) {
|
|
6389
|
+
const entry = {
|
|
6390
|
+
runId: run.runId,
|
|
6391
|
+
timestamp: run.startedAtMs,
|
|
6392
|
+
status: tc.status,
|
|
6393
|
+
durationMs: tc.durationMs,
|
|
6394
|
+
ci: run.ci ? {
|
|
6395
|
+
provider: void 0,
|
|
6396
|
+
branch: run.ci.branch,
|
|
6397
|
+
commitSha: run.ci.commitSha
|
|
6398
|
+
} : void 0
|
|
6399
|
+
};
|
|
6400
|
+
const existing = newTests[tc.id];
|
|
6401
|
+
if (existing) {
|
|
6402
|
+
const updatedEntries = [...existing.entries, entry];
|
|
6403
|
+
const trimmed = updatedEntries.length > maxRuns ? updatedEntries.slice(updatedEntries.length - maxRuns) : updatedEntries;
|
|
6404
|
+
newTests[tc.id] = {
|
|
6405
|
+
...existing,
|
|
6406
|
+
testName: tc.story.scenario,
|
|
6407
|
+
sourceFile: tc.sourceFile,
|
|
6408
|
+
sourceLine: tc.sourceLine,
|
|
6409
|
+
entries: trimmed
|
|
6410
|
+
};
|
|
6411
|
+
} else {
|
|
6412
|
+
newTests[tc.id] = {
|
|
6413
|
+
testId: tc.id,
|
|
6414
|
+
testName: tc.story.scenario,
|
|
6415
|
+
sourceFile: tc.sourceFile,
|
|
6416
|
+
sourceLine: tc.sourceLine,
|
|
6417
|
+
entries: [entry]
|
|
6418
|
+
};
|
|
6419
|
+
}
|
|
6420
|
+
}
|
|
6421
|
+
return {
|
|
6422
|
+
version: 1,
|
|
6423
|
+
maxRuns,
|
|
6424
|
+
tests: newTests,
|
|
6425
|
+
lastUpdated: Date.now()
|
|
6426
|
+
};
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6429
|
+
// src/history/flakiness.ts
|
|
6430
|
+
function calculateFlakiness(args) {
|
|
6431
|
+
const { entries } = args;
|
|
6432
|
+
const countable = entries.filter(
|
|
6433
|
+
(e) => e.status === "passed" || e.status === "failed"
|
|
6434
|
+
);
|
|
6435
|
+
if (countable.length < MIN_FLAKINESS_SAMPLES) {
|
|
6436
|
+
return {
|
|
6437
|
+
flakinessLevel: "stable",
|
|
6438
|
+
flakinessScore: 0,
|
|
6439
|
+
failureRate: 0,
|
|
6440
|
+
longestPassStreak: countable.length,
|
|
6441
|
+
longestFailStreak: 0
|
|
6442
|
+
};
|
|
6443
|
+
}
|
|
6444
|
+
let transitions = 0;
|
|
6445
|
+
for (let i = 1; i < countable.length; i++) {
|
|
6446
|
+
if (countable[i].status !== countable[i - 1].status) {
|
|
6447
|
+
transitions++;
|
|
6448
|
+
}
|
|
6449
|
+
}
|
|
6450
|
+
const transitionScore = transitions / (countable.length - 1);
|
|
6451
|
+
const failures = countable.filter((e) => e.status === "failed").length;
|
|
6452
|
+
const failureRate = failures / countable.length;
|
|
6453
|
+
let longestPassStreak = 0;
|
|
6454
|
+
let longestFailStreak = 0;
|
|
6455
|
+
let currentPassStreak = 0;
|
|
6456
|
+
let currentFailStreak = 0;
|
|
6457
|
+
for (const e of countable) {
|
|
6458
|
+
if (e.status === "passed") {
|
|
6459
|
+
currentPassStreak++;
|
|
6460
|
+
currentFailStreak = 0;
|
|
6461
|
+
if (currentPassStreak > longestPassStreak) {
|
|
6462
|
+
longestPassStreak = currentPassStreak;
|
|
6463
|
+
}
|
|
6464
|
+
} else {
|
|
6465
|
+
currentFailStreak++;
|
|
6466
|
+
currentPassStreak = 0;
|
|
6467
|
+
if (currentFailStreak > longestFailStreak) {
|
|
6468
|
+
longestFailStreak = currentFailStreak;
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6471
|
+
}
|
|
6472
|
+
let flakinessLevel;
|
|
6473
|
+
if (transitionScore > 0.5 || transitionScore > 0.3 && failureRate > 0.2) {
|
|
6474
|
+
flakinessLevel = "flaky";
|
|
6475
|
+
} else if (transitionScore > 0.2 || failureRate > 0.3) {
|
|
6476
|
+
flakinessLevel = "unstable";
|
|
6477
|
+
} else {
|
|
6478
|
+
flakinessLevel = "stable";
|
|
6479
|
+
}
|
|
6480
|
+
return {
|
|
6481
|
+
flakinessLevel,
|
|
6482
|
+
flakinessScore: transitionScore,
|
|
6483
|
+
failureRate,
|
|
6484
|
+
longestPassStreak,
|
|
6485
|
+
longestFailStreak
|
|
6486
|
+
};
|
|
6487
|
+
}
|
|
6488
|
+
|
|
6489
|
+
// src/history/performance.ts
|
|
6490
|
+
function detectPerformanceTrend(args) {
|
|
6491
|
+
const { entries } = args;
|
|
6492
|
+
const countable = entries.filter(
|
|
6493
|
+
(e) => e.status !== "skipped" && e.status !== "pending"
|
|
6494
|
+
);
|
|
6495
|
+
if (countable.length === 0) {
|
|
6496
|
+
return { trend: "stable", avgDurationMs: 0 };
|
|
6497
|
+
}
|
|
6498
|
+
const avgAll = countable.reduce((sum, e) => sum + e.durationMs, 0) / countable.length;
|
|
6499
|
+
if (countable.length < MIN_PERF_SAMPLES) {
|
|
6500
|
+
return { trend: "stable", avgDurationMs: avgAll };
|
|
6501
|
+
}
|
|
6502
|
+
const mid = Math.floor(countable.length / 2);
|
|
6503
|
+
const earlier = countable.slice(0, mid);
|
|
6504
|
+
const recent = countable.slice(mid);
|
|
6505
|
+
const earlierAvg = earlier.reduce((sum, e) => sum + e.durationMs, 0) / earlier.length;
|
|
6506
|
+
const recentAvg = recent.reduce((sum, e) => sum + e.durationMs, 0) / recent.length;
|
|
6507
|
+
let trend;
|
|
6508
|
+
if (earlierAvg === 0) {
|
|
6509
|
+
trend = "stable";
|
|
6510
|
+
} else {
|
|
6511
|
+
const change = (recentAvg - earlierAvg) / earlierAvg;
|
|
6512
|
+
if (change > 0.1) {
|
|
6513
|
+
trend = "regressing";
|
|
6514
|
+
} else if (change < -0.1) {
|
|
6515
|
+
trend = "improving";
|
|
6516
|
+
} else {
|
|
6517
|
+
trend = "stable";
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6520
|
+
return { trend, avgDurationMs: avgAll };
|
|
6521
|
+
}
|
|
6522
|
+
|
|
6523
|
+
// src/history/stability.ts
|
|
6524
|
+
function calculateStability(args) {
|
|
6525
|
+
const { passRate, flakinessScore, longestPassStreak, sampleSize } = args;
|
|
6526
|
+
const inverseFlakiness = 1 - flakinessScore;
|
|
6527
|
+
const streakNorm = longestPassStreak / Math.min(sampleSize, 10);
|
|
6528
|
+
const score = passRate * 0.6 + inverseFlakiness * 0.3 + streakNorm * 0.1;
|
|
6529
|
+
if (score >= 0.95) return "A";
|
|
6530
|
+
if (score >= 0.85) return "B";
|
|
6531
|
+
if (score >= 0.7) return "C";
|
|
6532
|
+
if (score >= 0.5) return "D";
|
|
6533
|
+
return "F";
|
|
6534
|
+
}
|
|
6535
|
+
|
|
6536
|
+
// src/history/metrics.ts
|
|
6537
|
+
function computeTestMetrics(args) {
|
|
6538
|
+
const { testId, entries } = args;
|
|
6539
|
+
const flakiness = calculateFlakiness({ entries });
|
|
6540
|
+
const perf = detectPerformanceTrend({ entries });
|
|
6541
|
+
const countable = entries.filter(
|
|
6542
|
+
(e) => e.status === "passed" || e.status === "failed"
|
|
6543
|
+
);
|
|
6544
|
+
const passRate = countable.length > 0 ? countable.filter((e) => e.status === "passed").length / countable.length : 1;
|
|
6545
|
+
const stabilityGrade = calculateStability({
|
|
6546
|
+
passRate,
|
|
6547
|
+
flakinessScore: flakiness.flakinessScore,
|
|
6548
|
+
longestPassStreak: flakiness.longestPassStreak,
|
|
6549
|
+
sampleSize: entries.length
|
|
6550
|
+
});
|
|
6551
|
+
let consecutiveFailures = 0;
|
|
6552
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
6553
|
+
if (entries[i].status === "failed") {
|
|
6554
|
+
consecutiveFailures++;
|
|
6555
|
+
} else {
|
|
6556
|
+
break;
|
|
6557
|
+
}
|
|
6558
|
+
}
|
|
6559
|
+
return {
|
|
6560
|
+
testId,
|
|
6561
|
+
flakinessLevel: flakiness.flakinessLevel,
|
|
6562
|
+
flakinessScore: flakiness.flakinessScore,
|
|
6563
|
+
failureRate: flakiness.failureRate,
|
|
6564
|
+
stabilityGrade,
|
|
6565
|
+
performanceTrend: perf.trend,
|
|
6566
|
+
avgDurationMs: perf.avgDurationMs,
|
|
6567
|
+
passRate,
|
|
6568
|
+
longestPassStreak: flakiness.longestPassStreak,
|
|
6569
|
+
consecutiveFailures,
|
|
6570
|
+
sampleSize: entries.length
|
|
6571
|
+
};
|
|
6572
|
+
}
|
|
6573
|
+
|
|
5411
6574
|
// src/index.ts
|
|
5412
6575
|
var FORMAT_EXTENSIONS = {
|
|
5413
6576
|
markdown: ".md",
|
|
@@ -5756,6 +6919,9 @@ export {
|
|
|
5756
6919
|
CucumberMessagesFormatter,
|
|
5757
6920
|
HtmlFormatter,
|
|
5758
6921
|
JUnitFormatter,
|
|
6922
|
+
MIN_FLAKINESS_SAMPLES,
|
|
6923
|
+
MIN_METRIC_SAMPLES,
|
|
6924
|
+
MIN_PERF_SAMPLES,
|
|
5759
6925
|
MarkdownFormatter,
|
|
5760
6926
|
ReportGenerator,
|
|
5761
6927
|
STORY_META_KEY,
|
|
@@ -5763,15 +6929,21 @@ export {
|
|
|
5763
6929
|
adaptPlaywrightRun,
|
|
5764
6930
|
adaptVitestRun,
|
|
5765
6931
|
assertValidRun,
|
|
6932
|
+
calculateFlakiness,
|
|
6933
|
+
calculateStability,
|
|
5766
6934
|
canonicalizeRun,
|
|
5767
6935
|
clearVersionCache,
|
|
6936
|
+
computeTestMetrics,
|
|
5768
6937
|
createReportGenerator,
|
|
5769
6938
|
deriveStepResults,
|
|
5770
6939
|
detectCI4 as detectCI,
|
|
6940
|
+
detectPerformanceTrend,
|
|
5771
6941
|
findGitDir,
|
|
5772
|
-
formatDuration,
|
|
6942
|
+
formatDuration2 as formatDuration,
|
|
5773
6943
|
generateRunId,
|
|
5774
6944
|
generateTestCaseId,
|
|
6945
|
+
hasSufficientHistory,
|
|
6946
|
+
loadHistory,
|
|
5775
6947
|
mergeStepResults,
|
|
5776
6948
|
msToNanoseconds,
|
|
5777
6949
|
nanosecondsToMs,
|
|
@@ -5787,8 +6959,18 @@ export {
|
|
|
5787
6959
|
resolveAttachment,
|
|
5788
6960
|
resolveAttachments,
|
|
5789
6961
|
resolveTraceUrl,
|
|
6962
|
+
saveHistory,
|
|
6963
|
+
sendNotifications,
|
|
6964
|
+
sendSlackNotification,
|
|
6965
|
+
sendTeamsNotification,
|
|
6966
|
+
sendWebhookNotification,
|
|
6967
|
+
signBody,
|
|
5790
6968
|
slugify,
|
|
6969
|
+
stripAnsi,
|
|
6970
|
+
toCIInfo,
|
|
6971
|
+
toRawCIInfo,
|
|
5791
6972
|
tryGetActiveOtelContext,
|
|
6973
|
+
updateHistory,
|
|
5792
6974
|
validateCanonicalRun
|
|
5793
6975
|
};
|
|
5794
6976
|
//# sourceMappingURL=index.js.map
|