donobu 5.41.4 → 5.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/reporter/buildReport.js +4 -0
- package/dist/esm/reporter/html.d.ts +11 -0
- package/dist/esm/reporter/html.js +55 -0
- package/dist/esm/reporter/render.d.ts +15 -0
- package/dist/esm/reporter/render.js +315 -24
- package/dist/reporter/buildReport.js +4 -0
- package/dist/reporter/html.d.ts +11 -0
- package/dist/reporter/html.js +55 -0
- package/dist/reporter/render.d.ts +15 -0
- package/dist/reporter/render.js +315 -24
- package/package.json +1 -1
|
@@ -48,6 +48,10 @@ function buildDonobuReport(resultsByTest, rootDir) {
|
|
|
48
48
|
const testEntries = tests.map((test) => {
|
|
49
49
|
const results = resultsByTest.get(test) ?? [];
|
|
50
50
|
return {
|
|
51
|
+
// Playwright's stable per-(file, project, title) ID — used by the
|
|
52
|
+
// merge step's `byId` index and by the HTML renderer for permalink
|
|
53
|
+
// anchors (`index.html#?testId=<id>`) and the per-test redirect stubs.
|
|
54
|
+
testId: test.id,
|
|
51
55
|
annotations: test.annotations,
|
|
52
56
|
tags: test.tags,
|
|
53
57
|
projectName: getProjectName(test),
|
|
@@ -51,5 +51,16 @@ export default class DonobuHtmlReporter implements Reporter {
|
|
|
51
51
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
52
52
|
onEnd(_result: FullResult): Promise<void>;
|
|
53
53
|
printsToStdio(): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Drop a tiny redirect `index.html` into each Playwright-managed per-test
|
|
56
|
+
* directory under `outputDir`. The stub points back at the combined report's
|
|
57
|
+
* `#?testId=<id>` deep link, giving every test a stable URL inside its own
|
|
58
|
+
* directory without duplicating any of the rendered HTML.
|
|
59
|
+
*
|
|
60
|
+
* Directories are discovered from `dirname(attachment.path)` — Donobu does
|
|
61
|
+
* not invent any directory naming or layout. Tests with no attachments (and
|
|
62
|
+
* therefore no Playwright-created directory) get no stub.
|
|
63
|
+
*/
|
|
64
|
+
private writePerTestStubs;
|
|
54
65
|
}
|
|
55
66
|
//# sourceMappingURL=html.d.ts.map
|
|
@@ -82,10 +82,65 @@ class DonobuHtmlReporter {
|
|
|
82
82
|
(0, fs_1.mkdirSync)(outputDir, { recursive: true });
|
|
83
83
|
(0, fs_1.writeFileSync)(outputFile, html, 'utf8');
|
|
84
84
|
Logger_1.appLogger.info(`Donobu report written to ${outputFile}`);
|
|
85
|
+
this.writePerTestStubs(outputFile, outputDir);
|
|
85
86
|
}
|
|
86
87
|
printsToStdio() {
|
|
87
88
|
return false;
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Drop a tiny redirect `index.html` into each Playwright-managed per-test
|
|
92
|
+
* directory under `outputDir`. The stub points back at the combined report's
|
|
93
|
+
* `#?testId=<id>` deep link, giving every test a stable URL inside its own
|
|
94
|
+
* directory without duplicating any of the rendered HTML.
|
|
95
|
+
*
|
|
96
|
+
* Directories are discovered from `dirname(attachment.path)` — Donobu does
|
|
97
|
+
* not invent any directory naming or layout. Tests with no attachments (and
|
|
98
|
+
* therefore no Playwright-created directory) get no stub.
|
|
99
|
+
*/
|
|
100
|
+
writePerTestStubs(outputFile, outputDir) {
|
|
101
|
+
for (const [test, results] of this.resultsByTest) {
|
|
102
|
+
if (!test.id) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const testDirs = new Set();
|
|
106
|
+
for (const result of results) {
|
|
107
|
+
for (const att of result.attachments) {
|
|
108
|
+
if (!att.path) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const attDir = (0, path_1.dirname)((0, path_1.resolve)(att.path));
|
|
112
|
+
const rel = (0, path_1.relative)(outputDir, attDir);
|
|
113
|
+
if (!rel || rel.startsWith('..')) {
|
|
114
|
+
// Attachment lives outside the report's output directory — skip;
|
|
115
|
+
// we shouldn't be writing into arbitrary paths.
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
testDirs.add(attDir);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (testDirs.size === 0) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const fileBase = test.location.file.split('/').pop() ?? test.location.file;
|
|
125
|
+
const title = `${fileBase} › ${test.title}`;
|
|
126
|
+
for (const testDir of testDirs) {
|
|
127
|
+
const stubPath = (0, path_1.resolve)(testDir, 'index.html');
|
|
128
|
+
const relPathToReport = (0, path_1.relative)(testDir, outputFile);
|
|
129
|
+
const stub = (0, render_1.renderPerTestStub)({
|
|
130
|
+
testId: test.id,
|
|
131
|
+
title,
|
|
132
|
+
relPathToReport,
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
(0, fs_1.mkdirSync)(testDir, { recursive: true });
|
|
136
|
+
(0, fs_1.writeFileSync)(stubPath, stub, 'utf8');
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
Logger_1.appLogger.warn(`Failed to write per-test redirect stub at ${stubPath}: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
89
144
|
}
|
|
90
145
|
exports.default = DonobuHtmlReporter;
|
|
91
146
|
//# sourceMappingURL=html.js.map
|
|
@@ -140,5 +140,20 @@ export interface TriageData {
|
|
|
140
140
|
}
|
|
141
141
|
export declare function loadTriageData(triageDir: string): TriageData;
|
|
142
142
|
export declare function renderHtml(report: DonobuReport, triage: TriageData, outputDir: string | null): string;
|
|
143
|
+
/**
|
|
144
|
+
* Render the tiny redirect HTML that Donobu drops into each Playwright-managed
|
|
145
|
+
* per-test directory under `test-results/`. The stub bounces straight to the
|
|
146
|
+
* combined report's `#?testId=<id>` deep link — meta-refresh + JS replace +
|
|
147
|
+
* visible fallback link, so it works with or without JS, online or `file://`.
|
|
148
|
+
*
|
|
149
|
+
* Strictly additive: Donobu does not create or rename Playwright's per-test
|
|
150
|
+
* directories — the caller in `html.ts` only writes this file into directories
|
|
151
|
+
* Playwright already created for the test's attachments.
|
|
152
|
+
*/
|
|
153
|
+
export declare function renderPerTestStub(params: {
|
|
154
|
+
testId: string;
|
|
155
|
+
title: string;
|
|
156
|
+
relPathToReport: string;
|
|
157
|
+
}): string;
|
|
143
158
|
export {};
|
|
144
159
|
//# sourceMappingURL=render.d.ts.map
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.loadTriageData = loadTriageData;
|
|
12
12
|
exports.renderHtml = renderHtml;
|
|
13
|
+
exports.renderPerTestStub = renderPerTestStub;
|
|
13
14
|
const fs_1 = require("fs");
|
|
14
15
|
const path_1 = require("path");
|
|
15
16
|
const ansi_1 = require("../utils/ansi");
|
|
@@ -366,6 +367,7 @@ function extractTests(jsonData) {
|
|
|
366
367
|
tests.push({
|
|
367
368
|
file: suite.file,
|
|
368
369
|
specTitle: spec.title,
|
|
370
|
+
testId: typeof test.testId === 'string' ? test.testId : '',
|
|
369
371
|
status,
|
|
370
372
|
isSelfHealed,
|
|
371
373
|
objective: objectiveAnnotation?.description ?? null,
|
|
@@ -722,7 +724,7 @@ function renderAiInvocation(inv, childrenHtml) {
|
|
|
722
724
|
miss: 'cache · miss',
|
|
723
725
|
};
|
|
724
726
|
const cacheTitle = {
|
|
725
|
-
hit: 'Replayed from the page-AI cache
|
|
727
|
+
hit: 'Replayed from the page-AI cache; no AI used for this step.',
|
|
726
728
|
stored: 'Live AI run; the resulting locators/steps were recorded to the page-AI cache. The next run can replay them without calling the AI.',
|
|
727
729
|
miss: "Live AI run; nothing was recorded to the page-AI cache. The next run will hit the AI again. For asserts, this typically means the AI's structured Playwright locators didn't reproduce its screenshot verdict.",
|
|
728
730
|
};
|
|
@@ -1530,8 +1532,10 @@ function renderHtml(report, triage, outputDir) {
|
|
|
1530
1532
|
// Group by file
|
|
1531
1533
|
const uniqueFiles = new Set(tests.map((t) => t.file));
|
|
1532
1534
|
// --- Build HTML sections ---
|
|
1533
|
-
//
|
|
1534
|
-
|
|
1535
|
+
// Per-card IDs used by the test-detail div, bar-block `data-target`, and the
|
|
1536
|
+
// outer `.test-card` anchor that `#?testId=<id>` deep links target. Sourced
|
|
1537
|
+
// from Playwright's stable `TestCase.id`.
|
|
1538
|
+
const testIds = tests.map((t) => `test-${t.testId}`);
|
|
1535
1539
|
// Build test bar blocks (one square per test, ordered by status, clickable)
|
|
1536
1540
|
const statusOrder = [
|
|
1537
1541
|
'passed',
|
|
@@ -1674,7 +1678,7 @@ function renderHtml(report, triage, outputDir) {
|
|
|
1674
1678
|
? `<div class="flow-id-detail"><span class="detail-label">Flow ID</span><span class="flow-id-value">${esc(test.flowId)}<button class="copy-flow-id" data-flow-id="${esc(test.flowId)}" title="Copy flow ID"><svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button></span></div>`
|
|
1675
1679
|
: '';
|
|
1676
1680
|
testSectionsHtml += `
|
|
1677
|
-
<div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" data-status="${test.status}" ${hasDetails ? `data-detail="${testId}"` : ''}>
|
|
1681
|
+
<div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" id="${testId}" data-status="${test.status}" data-tags="${esc(JSON.stringify(test.tags))}"${test.plan ? ` data-reason="${esc(test.plan.plan.failureReason)}"` : ''} ${hasDetails ? `data-detail="${testId}"` : ''}>
|
|
1678
1682
|
<div class="test-summary">
|
|
1679
1683
|
${chevron}
|
|
1680
1684
|
<span class="status-dot" style="background:${sc.color}" title="${sc.label}"></span>
|
|
@@ -1686,7 +1690,7 @@ function renderHtml(report, triage, outputDir) {
|
|
|
1686
1690
|
${totalStepCount > 0 ? `<span class="test-step-count" title="${totalStepCount} steps">${totalStepCount} steps</span>` : ''}
|
|
1687
1691
|
<span class="test-duration">${fmtDuration(totalTestDuration)}</span>
|
|
1688
1692
|
</div>
|
|
1689
|
-
${hasDetails ? `<div class="test-detail" id="
|
|
1693
|
+
${hasDetails ? `<div class="test-detail" id="detail-${testId}">${flowIdDetailHtml}${detailsHtml}</div>` : ''}
|
|
1690
1694
|
</div>`;
|
|
1691
1695
|
}
|
|
1692
1696
|
const mergedBanner = isMergedReport
|
|
@@ -1768,6 +1772,34 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
|
|
|
1768
1772
|
.clear-filter:hover{background:var(--surface-raised);border-color:var(--text-dim);color:var(--text)}
|
|
1769
1773
|
.clear-filter.visible{display:flex}
|
|
1770
1774
|
|
|
1775
|
+
/* Tag filter controls: + button opens a menu of available tags; selecting one
|
|
1776
|
+
* adds a removable chip. Multiple chips combine with logical AND. */
|
|
1777
|
+
.tag-filter-controls{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
1778
|
+
.tag-filter-trigger-wrap{position:relative;display:inline-flex}
|
|
1779
|
+
.add-tag-filter{background:var(--surface);border:1px solid var(--border);color:var(--text-muted);height:28px;padding:0 12px;border-radius:var(--radius);cursor:pointer;font-size:12px;font-weight:600;font-family:inherit;display:inline-flex;align-items:center;gap:4px;flex-shrink:0;transition:all .2s;line-height:1}
|
|
1780
|
+
.add-tag-filter .add-tag-plus{font-size:15px;line-height:1}
|
|
1781
|
+
.add-tag-filter:hover{background:var(--surface-raised);border-color:var(--text-dim);color:var(--text)}
|
|
1782
|
+
.add-tag-filter.active{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
1783
|
+
.tag-menu{position:absolute;top:calc(100% + 6px);left:0;min-width:200px;max-width:320px;max-height:280px;overflow-y:auto;background:var(--surface-raised);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 8px 24px rgba(0,0,0,.4);z-index:20;padding:4px;display:none}
|
|
1784
|
+
.tag-menu:not([hidden]){display:block}
|
|
1785
|
+
.tag-menu-item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;font-size:12px;font-family:var(--mono);color:var(--text);background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;width:100%;transition:background .15s}
|
|
1786
|
+
.tag-menu-item:hover{background:var(--surface)}
|
|
1787
|
+
.tag-menu-item .tag-menu-count{color:var(--text-muted);font-size:11px;font-family:var(--mono)}
|
|
1788
|
+
.tag-menu-empty{padding:8px 10px;font-size:12px;color:var(--text-muted);font-style:italic}
|
|
1789
|
+
.tag-menu-section{padding:8px 10px 4px;font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--text-dim);font-family:inherit}
|
|
1790
|
+
.tag-menu-section:not(:first-child){margin-top:4px;border-top:1px solid var(--border)}
|
|
1791
|
+
.active-tag-filters{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
1792
|
+
.tag-chip{display:inline-flex;align-items:center;gap:6px;background:rgba(255,127,58,.12);border:1px solid rgba(255,127,58,.3);color:var(--accent);font-size:11px;font-family:var(--mono);padding:3px 4px 3px 8px;border-radius:4px}
|
|
1793
|
+
.tag-chip-remove{background:transparent;border:none;color:inherit;cursor:pointer;font-size:14px;line-height:1;padding:0 4px;font-family:inherit;opacity:.7;transition:opacity .15s}
|
|
1794
|
+
.tag-chip-remove:hover{opacity:1}
|
|
1795
|
+
|
|
1796
|
+
/* Total of cards visible under the currently composed filters (status + tags).
|
|
1797
|
+
* The stat-pill counts always reflect totals; this disambiguates when filters
|
|
1798
|
+
* intersect. Hidden until any filter is active. */
|
|
1799
|
+
.match-count{display:none;align-items:center;font-size:12px;color:var(--text-muted);font-family:var(--mono);padding:0 8px;height:28px;flex-shrink:0}
|
|
1800
|
+
.match-count.visible{display:inline-flex}
|
|
1801
|
+
.match-count-value{color:var(--text);font-weight:600;margin-left:6px}
|
|
1802
|
+
|
|
1771
1803
|
/* Test cards */
|
|
1772
1804
|
.test-card{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);margin-bottom:10px;overflow:hidden}
|
|
1773
1805
|
.test-card.failed,.test-card.timedout,.test-card.interrupted{border-color:rgba(239,68,68,.2)}
|
|
@@ -2104,7 +2136,15 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
|
|
|
2104
2136
|
<div class="test-bar">${testBarHtml}</div>
|
|
2105
2137
|
<div class="summary-stats">
|
|
2106
2138
|
<div class="stat-pills">${statPillsHtml}</div>
|
|
2107
|
-
<
|
|
2139
|
+
<div class="tag-filter-controls" data-tag-filter-controls hidden>
|
|
2140
|
+
<div class="tag-filter-trigger-wrap">
|
|
2141
|
+
<button class="add-tag-filter" data-add-tag-filter title="Filter by tag or diagnosis"><span class="add-tag-plus">+</span> Filter</button>
|
|
2142
|
+
<div class="tag-menu" data-tag-menu hidden></div>
|
|
2143
|
+
</div>
|
|
2144
|
+
<div class="active-tag-filters" data-active-tag-filters></div>
|
|
2145
|
+
</div>
|
|
2146
|
+
<span class="match-count" data-match-count>Matches:<span class="match-count-value" data-match-count-value>0</span></span>
|
|
2147
|
+
<button class="clear-filter" data-clear-filter>✖ Clear Filters</button>
|
|
2108
2148
|
</div>
|
|
2109
2149
|
</div>
|
|
2110
2150
|
|
|
@@ -2122,20 +2162,173 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
|
|
|
2122
2162
|
|
|
2123
2163
|
<script>
|
|
2124
2164
|
(function(){
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2165
|
+
// Filters compose across three dimensions:
|
|
2166
|
+
// status — single-select; card.data-status must match (if set).
|
|
2167
|
+
// tags — multi-select AND; card must carry every active tag.
|
|
2168
|
+
// reasons — multi-select OR; card.data-reason must match any active reason
|
|
2169
|
+
// (a card has at most one diagnosis, so AND would always be 0/1).
|
|
2170
|
+
// "Clear Filters" wipes all three.
|
|
2171
|
+
var activeStatus=null;
|
|
2172
|
+
var activeTags=new Set();
|
|
2173
|
+
var activeReasons=new Set();
|
|
2174
|
+
var allTags=[];
|
|
2175
|
+
var allReasons=[]; // ordered list of REASON keys present in the report
|
|
2176
|
+
var REASON_LABELS=${JSON.stringify(REASON_LABELS)};
|
|
2177
|
+
|
|
2178
|
+
function cardTags(card){var raw=card.getAttribute('data-tags');if(!raw)return [];try{var v=JSON.parse(raw);return Array.isArray(v)?v:[]}catch(_){return []}}
|
|
2179
|
+
function tagCount(t){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(cardTags(c).indexOf(t)!==-1)n++});return n}
|
|
2180
|
+
|
|
2181
|
+
function applyFilters(){
|
|
2182
|
+
var anyActive=activeStatus!==null||activeTags.size>0||activeReasons.size>0;
|
|
2183
|
+
document.querySelector('.clear-filter').classList.toggle('visible',anyActive);
|
|
2184
|
+
var visible=0;
|
|
2185
|
+
document.querySelectorAll('.test-card').forEach(function(card){
|
|
2186
|
+
var statusOk=activeStatus===null||card.getAttribute('data-status')===activeStatus;
|
|
2187
|
+
var tagsOk=true;
|
|
2188
|
+
if(activeTags.size>0){
|
|
2189
|
+
var t=cardTags(card);
|
|
2190
|
+
activeTags.forEach(function(want){if(t.indexOf(want)===-1)tagsOk=false});
|
|
2191
|
+
}
|
|
2192
|
+
var reasonOk=true;
|
|
2193
|
+
if(activeReasons.size>0){
|
|
2194
|
+
var r=card.getAttribute('data-reason')||'';
|
|
2195
|
+
reasonOk=activeReasons.has(r);
|
|
2196
|
+
}
|
|
2197
|
+
var hide=!(statusOk&&tagsOk&&reasonOk);
|
|
2198
|
+
card.classList.toggle('hidden-by-filter',hide);
|
|
2199
|
+
if(!hide)visible++;
|
|
2200
|
+
});
|
|
2201
|
+
var mc=document.querySelector('[data-match-count]');
|
|
2202
|
+
if(mc){
|
|
2203
|
+
mc.classList.toggle('visible',anyActive);
|
|
2204
|
+
var mv=mc.querySelector('[data-match-count-value]');
|
|
2205
|
+
if(mv)mv.textContent=visible;
|
|
2206
|
+
}
|
|
2207
|
+
syncFiltersToUrl();
|
|
2132
2208
|
}
|
|
2133
|
-
|
|
2134
|
-
|
|
2209
|
+
|
|
2210
|
+
// Reflect the active filter state in the URL query string so the current
|
|
2211
|
+
// view is shareable. replaceState (not pushState) keeps the back button
|
|
2212
|
+
// useful; the existing #?testId=<id> hash is preserved.
|
|
2213
|
+
function syncFiltersToUrl(){
|
|
2214
|
+
var p=new URLSearchParams();
|
|
2215
|
+
if(activeStatus)p.set('status',activeStatus);
|
|
2216
|
+
activeTags.forEach(function(t){p.append('tag',t)});
|
|
2217
|
+
activeReasons.forEach(function(r){p.append('reason',r)});
|
|
2218
|
+
var qs=p.toString();
|
|
2219
|
+
var next=location.pathname+(qs?'?'+qs:'')+(location.hash||'');
|
|
2220
|
+
if(next!==location.pathname+location.search+location.hash){
|
|
2221
|
+
history.replaceState(null,'',next);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
function toggleStatus(s){
|
|
2226
|
+
activeStatus=(activeStatus===s)?null:s;
|
|
2227
|
+
document.querySelectorAll('.stat-pill').forEach(function(p){p.classList.toggle('active',p.getAttribute('data-filter')===activeStatus)});
|
|
2228
|
+
applyFilters();
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// Convert "#rrggbb" or "rgb(...)" into a low-alpha background for tinted
|
|
2232
|
+
// diagnosis chips. Falls back to the accent tint if parsing fails.
|
|
2233
|
+
function hexToRgba(hex,a){
|
|
2234
|
+
if(!hex||hex.charAt(0)!=='#'||(hex.length!==4&&hex.length!==7))return 'rgba(255,127,58,'+a+')';
|
|
2235
|
+
var s=hex.length===4?('#'+hex[1]+hex[1]+hex[2]+hex[2]+hex[3]+hex[3]):hex;
|
|
2236
|
+
var r=parseInt(s.slice(1,3),16),g=parseInt(s.slice(3,5),16),b=parseInt(s.slice(5,7),16);
|
|
2237
|
+
return 'rgba('+r+','+g+','+b+','+a+')';
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
function renderActiveChips(){
|
|
2241
|
+
var c=document.querySelector('[data-active-tag-filters]');
|
|
2242
|
+
if(!c)return;
|
|
2243
|
+
c.innerHTML='';
|
|
2244
|
+
activeTags.forEach(function(t){
|
|
2245
|
+
var chip=document.createElement('span');chip.className='tag-chip';
|
|
2246
|
+
var label=document.createElement('span');label.textContent=t;
|
|
2247
|
+
var btn=document.createElement('button');btn.className='tag-chip-remove';btn.setAttribute('data-remove-tag',t);btn.setAttribute('title','Remove filter');btn.textContent='×';
|
|
2248
|
+
chip.appendChild(label);chip.appendChild(btn);
|
|
2249
|
+
c.appendChild(chip);
|
|
2250
|
+
});
|
|
2251
|
+
activeReasons.forEach(function(r){
|
|
2252
|
+
var meta=REASON_LABELS[r]||REASON_LABELS['UNKNOWN'];
|
|
2253
|
+
var chip=document.createElement('span');chip.className='tag-chip reason-chip';
|
|
2254
|
+
chip.style.background=hexToRgba(meta.color,0.14);
|
|
2255
|
+
chip.style.borderColor=hexToRgba(meta.color,0.4);
|
|
2256
|
+
chip.style.color=meta.color;
|
|
2257
|
+
var label=document.createElement('span');label.textContent=meta.label;
|
|
2258
|
+
var btn=document.createElement('button');btn.className='tag-chip-remove';btn.setAttribute('data-remove-reason',r);btn.setAttribute('title','Remove filter');btn.textContent='×';
|
|
2259
|
+
chip.appendChild(label);chip.appendChild(btn);
|
|
2260
|
+
c.appendChild(chip);
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
function addTag(t){if(!t||activeTags.has(t))return;activeTags.add(t);renderActiveChips();applyFilters()}
|
|
2264
|
+
function removeTag(t){if(!activeTags.delete(t))return;renderActiveChips();applyFilters()}
|
|
2265
|
+
function addReason(r){if(!r||activeReasons.has(r))return;activeReasons.add(r);renderActiveChips();applyFilters()}
|
|
2266
|
+
function removeReason(r){if(!activeReasons.delete(r))return;renderActiveChips();applyFilters()}
|
|
2267
|
+
|
|
2268
|
+
function reasonCount(r){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(c.getAttribute('data-reason')===r)n++});return n}
|
|
2269
|
+
|
|
2270
|
+
function openTagMenu(){
|
|
2271
|
+
var menu=document.querySelector('[data-tag-menu]');
|
|
2272
|
+
if(!menu)return;
|
|
2273
|
+
var trigger=document.querySelector('[data-add-tag-filter]');
|
|
2274
|
+
menu.innerHTML='';
|
|
2275
|
+
var availTags=allTags.filter(function(t){return !activeTags.has(t)});
|
|
2276
|
+
var availReasons=allReasons.filter(function(r){return !activeReasons.has(r)});
|
|
2277
|
+
var added=false;
|
|
2278
|
+
if(allTags.length>0){
|
|
2279
|
+
var hT=document.createElement('div');hT.className='tag-menu-section';hT.textContent='Tags';menu.appendChild(hT);
|
|
2280
|
+
if(availTags.length===0){
|
|
2281
|
+
var emptyT=document.createElement('div');emptyT.className='tag-menu-empty';emptyT.textContent='All tags selected';menu.appendChild(emptyT);
|
|
2282
|
+
}else{
|
|
2283
|
+
availTags.forEach(function(t){
|
|
2284
|
+
var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-tag-menu-item',t);
|
|
2285
|
+
var label=document.createElement('span');label.textContent=t;
|
|
2286
|
+
var count=document.createElement('span');count.className='tag-menu-count';count.textContent=tagCount(t);
|
|
2287
|
+
item.appendChild(label);item.appendChild(count);
|
|
2288
|
+
menu.appendChild(item);
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
added=true;
|
|
2292
|
+
}
|
|
2293
|
+
if(allReasons.length>0){
|
|
2294
|
+
var hR=document.createElement('div');hR.className='tag-menu-section';hR.textContent='Diagnoses';menu.appendChild(hR);
|
|
2295
|
+
if(availReasons.length===0){
|
|
2296
|
+
var emptyR=document.createElement('div');emptyR.className='tag-menu-empty';emptyR.textContent='All diagnoses selected';menu.appendChild(emptyR);
|
|
2297
|
+
}else{
|
|
2298
|
+
availReasons.forEach(function(r){
|
|
2299
|
+
var meta=REASON_LABELS[r]||REASON_LABELS['UNKNOWN'];
|
|
2300
|
+
var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-reason-menu-item',r);
|
|
2301
|
+
var label=document.createElement('span');label.textContent=meta.label;label.style.color=meta.color;
|
|
2302
|
+
var count=document.createElement('span');count.className='tag-menu-count';count.textContent=reasonCount(r);
|
|
2303
|
+
item.appendChild(label);item.appendChild(count);
|
|
2304
|
+
menu.appendChild(item);
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
added=true;
|
|
2308
|
+
}
|
|
2309
|
+
if(!added){
|
|
2310
|
+
var empty=document.createElement('div');empty.className='tag-menu-empty';empty.textContent='No filters available';menu.appendChild(empty);
|
|
2311
|
+
}
|
|
2312
|
+
menu.hidden=false;
|
|
2313
|
+
if(trigger)trigger.classList.add('active');
|
|
2314
|
+
}
|
|
2315
|
+
function closeTagMenu(){
|
|
2316
|
+
var menu=document.querySelector('[data-tag-menu]');var trigger=document.querySelector('[data-add-tag-filter]');
|
|
2317
|
+
if(menu)menu.hidden=true;
|
|
2318
|
+
if(trigger)trigger.classList.remove('active');
|
|
2319
|
+
}
|
|
2320
|
+
function tagMenuOpen(){var m=document.querySelector('[data-tag-menu]');return !!(m&&!m.hidden)}
|
|
2321
|
+
|
|
2322
|
+
function clearAllFilters(){
|
|
2323
|
+
activeStatus=null;
|
|
2324
|
+
activeTags.clear();
|
|
2325
|
+
activeReasons.clear();
|
|
2135
2326
|
document.querySelectorAll('.stat-pill').forEach(function(p){p.classList.remove('active')});
|
|
2136
|
-
|
|
2137
|
-
|
|
2327
|
+
renderActiveChips();
|
|
2328
|
+
closeTagMenu();
|
|
2329
|
+
applyFilters();
|
|
2138
2330
|
}
|
|
2331
|
+
|
|
2139
2332
|
function closeLightbox(){document.getElementById('lightbox').classList.remove('open')}
|
|
2140
2333
|
|
|
2141
2334
|
document.addEventListener('click',function(e){
|
|
@@ -2165,14 +2358,32 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
|
|
|
2165
2358
|
if(flowBtn){e.stopPropagation();navigator.clipboard.writeText(flowBtn.getAttribute('data-flow-id'));var copySvg=flowBtn.innerHTML;flowBtn.innerHTML='<svg class="check-icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';setTimeout(function(){flowBtn.innerHTML=copySvg},2000);return}
|
|
2166
2359
|
var jsonBtn=e.target.closest('.copy-json');
|
|
2167
2360
|
if(jsonBtn){e.stopPropagation();var pre=jsonBtn.closest('.step-json-wrap').querySelector('.step-json');navigator.clipboard.writeText(pre.textContent);var s=jsonBtn.innerHTML;jsonBtn.innerHTML='<svg class="check-icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';setTimeout(function(){jsonBtn.innerHTML=s},2000);return}
|
|
2361
|
+
// Tag filter: add (+ button toggles the menu) / select (menu item) /
|
|
2362
|
+
// remove (chip × button). Outside-clicks close the menu via the catch-all
|
|
2363
|
+
// at the end of this handler.
|
|
2364
|
+
var addTagBtn=e.target.closest('[data-add-tag-filter]');
|
|
2365
|
+
if(addTagBtn){if(tagMenuOpen()){closeTagMenu()}else{openTagMenu()}return}
|
|
2366
|
+
var tagItem=e.target.closest('[data-tag-menu-item]');
|
|
2367
|
+
if(tagItem){addTag(tagItem.getAttribute('data-tag-menu-item'));closeTagMenu();return}
|
|
2368
|
+
var reasonItem=e.target.closest('[data-reason-menu-item]');
|
|
2369
|
+
if(reasonItem){addReason(reasonItem.getAttribute('data-reason-menu-item'));closeTagMenu();return}
|
|
2370
|
+
var tagRemove=e.target.closest('[data-remove-tag]');
|
|
2371
|
+
if(tagRemove){removeTag(tagRemove.getAttribute('data-remove-tag'));return}
|
|
2372
|
+
var reasonRemove=e.target.closest('[data-remove-reason]');
|
|
2373
|
+
if(reasonRemove){removeReason(reasonRemove.getAttribute('data-remove-reason'));return}
|
|
2168
2374
|
// Stat pill filter
|
|
2169
2375
|
var pill=e.target.closest('.stat-pill[data-filter]');
|
|
2170
|
-
if(pill){
|
|
2376
|
+
if(pill){toggleStatus(pill.getAttribute('data-filter'));return}
|
|
2171
2377
|
// Clear filter
|
|
2172
|
-
if(e.target.closest('[data-clear-filter]')){
|
|
2173
|
-
//
|
|
2378
|
+
if(e.target.closest('[data-clear-filter]')){clearAllFilters();return}
|
|
2379
|
+
// Any other click closes the tag menu (outside-click dismiss).
|
|
2380
|
+
if(tagMenuOpen()&&!e.target.closest('[data-tag-menu]')){closeTagMenu()}
|
|
2381
|
+
// Test bar block click — scroll to and highlight the target test card,
|
|
2382
|
+
// and reflect the selection in location.hash so the URL is shareable.
|
|
2383
|
+
// replaceState (not pushState) keeps the back button useful after the
|
|
2384
|
+
// user has clicked through several bar blocks.
|
|
2174
2385
|
var barBlock=e.target.closest('.test-bar-block[data-target]');
|
|
2175
|
-
if(barBlock){var tid=barBlock.getAttribute('data-target');var card=document.getElementById(tid);if(card){var tc=card.closest('.test-card');if(tc){tc.classList.add('expanded');tc.scrollIntoView({behavior:'smooth',block:'center'});tc.style.outline='2px solid var(--accent)';setTimeout(function(){tc.style.outline=''},1500)}}return}
|
|
2386
|
+
if(barBlock){var tid=barBlock.getAttribute('data-target');var card=document.getElementById(tid);if(card){var tc=card.closest('.test-card');if(tc){tc.classList.add('expanded');tc.scrollIntoView({behavior:'smooth',block:'center'});tc.style.outline='2px solid var(--accent)';setTimeout(function(){tc.style.outline=''},1500)}}if(tid&&tid.indexOf('test-')===0){history.replaceState(null,'','#?testId='+encodeURIComponent(tid.slice(5)))}return}
|
|
2176
2387
|
// Filmstrip step expand (skip if clicking a link inside).
|
|
2177
2388
|
// <div>-based steps: toggle the open class on any click in the wrapper.
|
|
2178
2389
|
// <details>-based steps: <summary> clicks are handled natively; padding
|
|
@@ -2192,18 +2403,98 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
|
|
|
2192
2403
|
// Audit check row expand
|
|
2193
2404
|
var auditCheck=e.target.closest('.audit-check.expandable');
|
|
2194
2405
|
if(auditCheck){auditCheck.classList.toggle('open');return}
|
|
2195
|
-
// Test card expand
|
|
2406
|
+
// Test card expand — also reflect the selection in location.hash on
|
|
2407
|
+
// open so the URL is shareable. Same replaceState rationale as the bar
|
|
2408
|
+
// block handler above. Collapses leave the URL alone.
|
|
2196
2409
|
var row=e.target.closest('.test-card[data-detail]');
|
|
2197
|
-
if(row&&!e.target.closest('.test-detail')){row.classList.toggle('expanded');return}
|
|
2410
|
+
if(row&&!e.target.closest('.test-detail')){var nowOpen=row.classList.toggle('expanded');if(nowOpen&&row.id&&row.id.indexOf('test-')===0){history.replaceState(null,'','#?testId='+encodeURIComponent(row.id.slice(5)))}return}
|
|
2198
2411
|
});
|
|
2199
2412
|
|
|
2200
|
-
document.addEventListener('keydown',function(e){if(e.key==='Escape'){closeLightbox();
|
|
2413
|
+
document.addEventListener('keydown',function(e){if(e.key==='Escape'){closeLightbox();closeTagMenu();clearAllFilters()}});
|
|
2201
2414
|
|
|
2202
2415
|
// Auto-expand failed/timedout/interrupted/healed tests
|
|
2203
2416
|
document.querySelectorAll('.test-card.failed,.test-card.timedout,.test-card.interrupted,.test-card.healed').forEach(function(r){r.classList.add('expanded')});
|
|
2417
|
+
|
|
2418
|
+
// Collect the cumulative set of tags + diagnosis reasons present across all
|
|
2419
|
+
// test cards. Reveal the +filter controls when at least one of either exists.
|
|
2420
|
+
(function(){
|
|
2421
|
+
var seenTags=Object.create(null);
|
|
2422
|
+
var seenReasons=Object.create(null);
|
|
2423
|
+
document.querySelectorAll('.test-card').forEach(function(card){
|
|
2424
|
+
var raw=card.getAttribute('data-tags');
|
|
2425
|
+
if(raw){try{var tags=JSON.parse(raw);if(Array.isArray(tags)){tags.forEach(function(t){if(typeof t==='string'&&t)seenTags[t]=true})}}catch(_){}}
|
|
2426
|
+
var r=card.getAttribute('data-reason');
|
|
2427
|
+
if(r)seenReasons[r]=true;
|
|
2428
|
+
});
|
|
2429
|
+
allTags=Object.keys(seenTags).sort();
|
|
2430
|
+
// Preserve the REASON_LABELS declaration order rather than alphabetical —
|
|
2431
|
+
// they're already arranged from most-frequent/specific to UNKNOWN catch-all.
|
|
2432
|
+
allReasons=Object.keys(REASON_LABELS).filter(function(r){return seenReasons[r]});
|
|
2433
|
+
var controls=document.querySelector('[data-tag-filter-controls]');
|
|
2434
|
+
if(controls&&(allTags.length>0||allReasons.length>0))controls.hidden=false;
|
|
2435
|
+
})();
|
|
2436
|
+
|
|
2437
|
+
// Seed filter state from ?status=...&tag=...&reason=... so shared URLs
|
|
2438
|
+
// restore the view. Status values not in the known stat-pill set are
|
|
2439
|
+
// ignored; tag and reason values not present in this report are dropped
|
|
2440
|
+
// so a stale URL can't poison the state.
|
|
2441
|
+
(function(){
|
|
2442
|
+
var p=new URLSearchParams(location.search);
|
|
2443
|
+
var s=p.get('status');
|
|
2444
|
+
var validStatuses={};
|
|
2445
|
+
document.querySelectorAll('.stat-pill[data-filter]').forEach(function(el){validStatuses[el.getAttribute('data-filter')]=true});
|
|
2446
|
+
if(s&&validStatuses[s]){
|
|
2447
|
+
activeStatus=s;
|
|
2448
|
+
document.querySelectorAll('.stat-pill').forEach(function(el){el.classList.toggle('active',el.getAttribute('data-filter')===s)});
|
|
2449
|
+
}
|
|
2450
|
+
var tagSet={};allTags.forEach(function(t){tagSet[t]=true});
|
|
2451
|
+
p.getAll('tag').forEach(function(t){if(tagSet[t])activeTags.add(t)});
|
|
2452
|
+
var reasonSet={};allReasons.forEach(function(r){reasonSet[r]=true});
|
|
2453
|
+
p.getAll('reason').forEach(function(r){if(reasonSet[r])activeReasons.add(r)});
|
|
2454
|
+
if(activeTags.size>0||activeReasons.size>0)renderActiveChips();
|
|
2455
|
+
if(activeStatus!==null||activeTags.size>0||activeReasons.size>0)applyFilters();
|
|
2456
|
+
})();
|
|
2457
|
+
|
|
2458
|
+
// Open #?testId=<id> deep links to the matching test card. Used by the
|
|
2459
|
+
// per-test redirect stubs (one per Playwright test-results directory) and
|
|
2460
|
+
// by any external link that wants to permalink a specific test.
|
|
2461
|
+
function focusTestFromHash(){
|
|
2462
|
+
var h=location.hash||'';
|
|
2463
|
+
if(h.indexOf('#?')!==0)return;
|
|
2464
|
+
var params=new URLSearchParams(h.slice(2));
|
|
2465
|
+
var id=params.get('testId');
|
|
2466
|
+
if(!id)return;
|
|
2467
|
+
var card=document.getElementById('test-'+id);
|
|
2468
|
+
if(!card||!card.classList.contains('test-card'))return;
|
|
2469
|
+
card.classList.add('expanded');
|
|
2470
|
+
card.scrollIntoView({behavior:'smooth',block:'center'});
|
|
2471
|
+
}
|
|
2472
|
+
focusTestFromHash();
|
|
2473
|
+
window.addEventListener('hashchange',focusTestFromHash);
|
|
2204
2474
|
})();
|
|
2205
2475
|
</script>
|
|
2206
2476
|
</body>
|
|
2207
2477
|
</html>`;
|
|
2208
2478
|
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Render the tiny redirect HTML that Donobu drops into each Playwright-managed
|
|
2481
|
+
* per-test directory under `test-results/`. The stub bounces straight to the
|
|
2482
|
+
* combined report's `#?testId=<id>` deep link — meta-refresh + JS replace +
|
|
2483
|
+
* visible fallback link, so it works with or without JS, online or `file://`.
|
|
2484
|
+
*
|
|
2485
|
+
* Strictly additive: Donobu does not create or rename Playwright's per-test
|
|
2486
|
+
* directories — the caller in `html.ts` only writes this file into directories
|
|
2487
|
+
* Playwright already created for the test's attachments.
|
|
2488
|
+
*/
|
|
2489
|
+
function renderPerTestStub(params) {
|
|
2490
|
+
const { testId, title, relPathToReport } = params;
|
|
2491
|
+
const href = `${relPathToReport}#?testId=${encodeURIComponent(testId)}`;
|
|
2492
|
+
return `<!DOCTYPE html>
|
|
2493
|
+
<meta charset="UTF-8">
|
|
2494
|
+
<title>Donobu — ${esc(title)}</title>
|
|
2495
|
+
<meta http-equiv="refresh" content="0; url=${esc(href)}">
|
|
2496
|
+
<script>location.replace(${JSON.stringify(href)});</script>
|
|
2497
|
+
<p>Redirecting to <a href="${esc(href)}">test report</a>…</p>
|
|
2498
|
+
`;
|
|
2499
|
+
}
|
|
2209
2500
|
//# sourceMappingURL=render.js.map
|
|
@@ -48,6 +48,10 @@ function buildDonobuReport(resultsByTest, rootDir) {
|
|
|
48
48
|
const testEntries = tests.map((test) => {
|
|
49
49
|
const results = resultsByTest.get(test) ?? [];
|
|
50
50
|
return {
|
|
51
|
+
// Playwright's stable per-(file, project, title) ID — used by the
|
|
52
|
+
// merge step's `byId` index and by the HTML renderer for permalink
|
|
53
|
+
// anchors (`index.html#?testId=<id>`) and the per-test redirect stubs.
|
|
54
|
+
testId: test.id,
|
|
51
55
|
annotations: test.annotations,
|
|
52
56
|
tags: test.tags,
|
|
53
57
|
projectName: getProjectName(test),
|
package/dist/reporter/html.d.ts
CHANGED
|
@@ -51,5 +51,16 @@ export default class DonobuHtmlReporter implements Reporter {
|
|
|
51
51
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
52
52
|
onEnd(_result: FullResult): Promise<void>;
|
|
53
53
|
printsToStdio(): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Drop a tiny redirect `index.html` into each Playwright-managed per-test
|
|
56
|
+
* directory under `outputDir`. The stub points back at the combined report's
|
|
57
|
+
* `#?testId=<id>` deep link, giving every test a stable URL inside its own
|
|
58
|
+
* directory without duplicating any of the rendered HTML.
|
|
59
|
+
*
|
|
60
|
+
* Directories are discovered from `dirname(attachment.path)` — Donobu does
|
|
61
|
+
* not invent any directory naming or layout. Tests with no attachments (and
|
|
62
|
+
* therefore no Playwright-created directory) get no stub.
|
|
63
|
+
*/
|
|
64
|
+
private writePerTestStubs;
|
|
54
65
|
}
|
|
55
66
|
//# sourceMappingURL=html.d.ts.map
|
package/dist/reporter/html.js
CHANGED
|
@@ -82,10 +82,65 @@ class DonobuHtmlReporter {
|
|
|
82
82
|
(0, fs_1.mkdirSync)(outputDir, { recursive: true });
|
|
83
83
|
(0, fs_1.writeFileSync)(outputFile, html, 'utf8');
|
|
84
84
|
Logger_1.appLogger.info(`Donobu report written to ${outputFile}`);
|
|
85
|
+
this.writePerTestStubs(outputFile, outputDir);
|
|
85
86
|
}
|
|
86
87
|
printsToStdio() {
|
|
87
88
|
return false;
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Drop a tiny redirect `index.html` into each Playwright-managed per-test
|
|
92
|
+
* directory under `outputDir`. The stub points back at the combined report's
|
|
93
|
+
* `#?testId=<id>` deep link, giving every test a stable URL inside its own
|
|
94
|
+
* directory without duplicating any of the rendered HTML.
|
|
95
|
+
*
|
|
96
|
+
* Directories are discovered from `dirname(attachment.path)` — Donobu does
|
|
97
|
+
* not invent any directory naming or layout. Tests with no attachments (and
|
|
98
|
+
* therefore no Playwright-created directory) get no stub.
|
|
99
|
+
*/
|
|
100
|
+
writePerTestStubs(outputFile, outputDir) {
|
|
101
|
+
for (const [test, results] of this.resultsByTest) {
|
|
102
|
+
if (!test.id) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const testDirs = new Set();
|
|
106
|
+
for (const result of results) {
|
|
107
|
+
for (const att of result.attachments) {
|
|
108
|
+
if (!att.path) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const attDir = (0, path_1.dirname)((0, path_1.resolve)(att.path));
|
|
112
|
+
const rel = (0, path_1.relative)(outputDir, attDir);
|
|
113
|
+
if (!rel || rel.startsWith('..')) {
|
|
114
|
+
// Attachment lives outside the report's output directory — skip;
|
|
115
|
+
// we shouldn't be writing into arbitrary paths.
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
testDirs.add(attDir);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (testDirs.size === 0) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const fileBase = test.location.file.split('/').pop() ?? test.location.file;
|
|
125
|
+
const title = `${fileBase} › ${test.title}`;
|
|
126
|
+
for (const testDir of testDirs) {
|
|
127
|
+
const stubPath = (0, path_1.resolve)(testDir, 'index.html');
|
|
128
|
+
const relPathToReport = (0, path_1.relative)(testDir, outputFile);
|
|
129
|
+
const stub = (0, render_1.renderPerTestStub)({
|
|
130
|
+
testId: test.id,
|
|
131
|
+
title,
|
|
132
|
+
relPathToReport,
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
(0, fs_1.mkdirSync)(testDir, { recursive: true });
|
|
136
|
+
(0, fs_1.writeFileSync)(stubPath, stub, 'utf8');
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
Logger_1.appLogger.warn(`Failed to write per-test redirect stub at ${stubPath}: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
89
144
|
}
|
|
90
145
|
exports.default = DonobuHtmlReporter;
|
|
91
146
|
//# sourceMappingURL=html.js.map
|
|
@@ -140,5 +140,20 @@ export interface TriageData {
|
|
|
140
140
|
}
|
|
141
141
|
export declare function loadTriageData(triageDir: string): TriageData;
|
|
142
142
|
export declare function renderHtml(report: DonobuReport, triage: TriageData, outputDir: string | null): string;
|
|
143
|
+
/**
|
|
144
|
+
* Render the tiny redirect HTML that Donobu drops into each Playwright-managed
|
|
145
|
+
* per-test directory under `test-results/`. The stub bounces straight to the
|
|
146
|
+
* combined report's `#?testId=<id>` deep link — meta-refresh + JS replace +
|
|
147
|
+
* visible fallback link, so it works with or without JS, online or `file://`.
|
|
148
|
+
*
|
|
149
|
+
* Strictly additive: Donobu does not create or rename Playwright's per-test
|
|
150
|
+
* directories — the caller in `html.ts` only writes this file into directories
|
|
151
|
+
* Playwright already created for the test's attachments.
|
|
152
|
+
*/
|
|
153
|
+
export declare function renderPerTestStub(params: {
|
|
154
|
+
testId: string;
|
|
155
|
+
title: string;
|
|
156
|
+
relPathToReport: string;
|
|
157
|
+
}): string;
|
|
143
158
|
export {};
|
|
144
159
|
//# sourceMappingURL=render.d.ts.map
|
package/dist/reporter/render.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.loadTriageData = loadTriageData;
|
|
12
12
|
exports.renderHtml = renderHtml;
|
|
13
|
+
exports.renderPerTestStub = renderPerTestStub;
|
|
13
14
|
const fs_1 = require("fs");
|
|
14
15
|
const path_1 = require("path");
|
|
15
16
|
const ansi_1 = require("../utils/ansi");
|
|
@@ -366,6 +367,7 @@ function extractTests(jsonData) {
|
|
|
366
367
|
tests.push({
|
|
367
368
|
file: suite.file,
|
|
368
369
|
specTitle: spec.title,
|
|
370
|
+
testId: typeof test.testId === 'string' ? test.testId : '',
|
|
369
371
|
status,
|
|
370
372
|
isSelfHealed,
|
|
371
373
|
objective: objectiveAnnotation?.description ?? null,
|
|
@@ -722,7 +724,7 @@ function renderAiInvocation(inv, childrenHtml) {
|
|
|
722
724
|
miss: 'cache · miss',
|
|
723
725
|
};
|
|
724
726
|
const cacheTitle = {
|
|
725
|
-
hit: 'Replayed from the page-AI cache
|
|
727
|
+
hit: 'Replayed from the page-AI cache; no AI used for this step.',
|
|
726
728
|
stored: 'Live AI run; the resulting locators/steps were recorded to the page-AI cache. The next run can replay them without calling the AI.',
|
|
727
729
|
miss: "Live AI run; nothing was recorded to the page-AI cache. The next run will hit the AI again. For asserts, this typically means the AI's structured Playwright locators didn't reproduce its screenshot verdict.",
|
|
728
730
|
};
|
|
@@ -1530,8 +1532,10 @@ function renderHtml(report, triage, outputDir) {
|
|
|
1530
1532
|
// Group by file
|
|
1531
1533
|
const uniqueFiles = new Set(tests.map((t) => t.file));
|
|
1532
1534
|
// --- Build HTML sections ---
|
|
1533
|
-
//
|
|
1534
|
-
|
|
1535
|
+
// Per-card IDs used by the test-detail div, bar-block `data-target`, and the
|
|
1536
|
+
// outer `.test-card` anchor that `#?testId=<id>` deep links target. Sourced
|
|
1537
|
+
// from Playwright's stable `TestCase.id`.
|
|
1538
|
+
const testIds = tests.map((t) => `test-${t.testId}`);
|
|
1535
1539
|
// Build test bar blocks (one square per test, ordered by status, clickable)
|
|
1536
1540
|
const statusOrder = [
|
|
1537
1541
|
'passed',
|
|
@@ -1674,7 +1678,7 @@ function renderHtml(report, triage, outputDir) {
|
|
|
1674
1678
|
? `<div class="flow-id-detail"><span class="detail-label">Flow ID</span><span class="flow-id-value">${esc(test.flowId)}<button class="copy-flow-id" data-flow-id="${esc(test.flowId)}" title="Copy flow ID"><svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button></span></div>`
|
|
1675
1679
|
: '';
|
|
1676
1680
|
testSectionsHtml += `
|
|
1677
|
-
<div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" data-status="${test.status}" ${hasDetails ? `data-detail="${testId}"` : ''}>
|
|
1681
|
+
<div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" id="${testId}" data-status="${test.status}" data-tags="${esc(JSON.stringify(test.tags))}"${test.plan ? ` data-reason="${esc(test.plan.plan.failureReason)}"` : ''} ${hasDetails ? `data-detail="${testId}"` : ''}>
|
|
1678
1682
|
<div class="test-summary">
|
|
1679
1683
|
${chevron}
|
|
1680
1684
|
<span class="status-dot" style="background:${sc.color}" title="${sc.label}"></span>
|
|
@@ -1686,7 +1690,7 @@ function renderHtml(report, triage, outputDir) {
|
|
|
1686
1690
|
${totalStepCount > 0 ? `<span class="test-step-count" title="${totalStepCount} steps">${totalStepCount} steps</span>` : ''}
|
|
1687
1691
|
<span class="test-duration">${fmtDuration(totalTestDuration)}</span>
|
|
1688
1692
|
</div>
|
|
1689
|
-
${hasDetails ? `<div class="test-detail" id="
|
|
1693
|
+
${hasDetails ? `<div class="test-detail" id="detail-${testId}">${flowIdDetailHtml}${detailsHtml}</div>` : ''}
|
|
1690
1694
|
</div>`;
|
|
1691
1695
|
}
|
|
1692
1696
|
const mergedBanner = isMergedReport
|
|
@@ -1768,6 +1772,34 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
|
|
|
1768
1772
|
.clear-filter:hover{background:var(--surface-raised);border-color:var(--text-dim);color:var(--text)}
|
|
1769
1773
|
.clear-filter.visible{display:flex}
|
|
1770
1774
|
|
|
1775
|
+
/* Tag filter controls: + button opens a menu of available tags; selecting one
|
|
1776
|
+
* adds a removable chip. Multiple chips combine with logical AND. */
|
|
1777
|
+
.tag-filter-controls{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
1778
|
+
.tag-filter-trigger-wrap{position:relative;display:inline-flex}
|
|
1779
|
+
.add-tag-filter{background:var(--surface);border:1px solid var(--border);color:var(--text-muted);height:28px;padding:0 12px;border-radius:var(--radius);cursor:pointer;font-size:12px;font-weight:600;font-family:inherit;display:inline-flex;align-items:center;gap:4px;flex-shrink:0;transition:all .2s;line-height:1}
|
|
1780
|
+
.add-tag-filter .add-tag-plus{font-size:15px;line-height:1}
|
|
1781
|
+
.add-tag-filter:hover{background:var(--surface-raised);border-color:var(--text-dim);color:var(--text)}
|
|
1782
|
+
.add-tag-filter.active{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
1783
|
+
.tag-menu{position:absolute;top:calc(100% + 6px);left:0;min-width:200px;max-width:320px;max-height:280px;overflow-y:auto;background:var(--surface-raised);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 8px 24px rgba(0,0,0,.4);z-index:20;padding:4px;display:none}
|
|
1784
|
+
.tag-menu:not([hidden]){display:block}
|
|
1785
|
+
.tag-menu-item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;font-size:12px;font-family:var(--mono);color:var(--text);background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;width:100%;transition:background .15s}
|
|
1786
|
+
.tag-menu-item:hover{background:var(--surface)}
|
|
1787
|
+
.tag-menu-item .tag-menu-count{color:var(--text-muted);font-size:11px;font-family:var(--mono)}
|
|
1788
|
+
.tag-menu-empty{padding:8px 10px;font-size:12px;color:var(--text-muted);font-style:italic}
|
|
1789
|
+
.tag-menu-section{padding:8px 10px 4px;font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--text-dim);font-family:inherit}
|
|
1790
|
+
.tag-menu-section:not(:first-child){margin-top:4px;border-top:1px solid var(--border)}
|
|
1791
|
+
.active-tag-filters{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
1792
|
+
.tag-chip{display:inline-flex;align-items:center;gap:6px;background:rgba(255,127,58,.12);border:1px solid rgba(255,127,58,.3);color:var(--accent);font-size:11px;font-family:var(--mono);padding:3px 4px 3px 8px;border-radius:4px}
|
|
1793
|
+
.tag-chip-remove{background:transparent;border:none;color:inherit;cursor:pointer;font-size:14px;line-height:1;padding:0 4px;font-family:inherit;opacity:.7;transition:opacity .15s}
|
|
1794
|
+
.tag-chip-remove:hover{opacity:1}
|
|
1795
|
+
|
|
1796
|
+
/* Total of cards visible under the currently composed filters (status + tags).
|
|
1797
|
+
* The stat-pill counts always reflect totals; this disambiguates when filters
|
|
1798
|
+
* intersect. Hidden until any filter is active. */
|
|
1799
|
+
.match-count{display:none;align-items:center;font-size:12px;color:var(--text-muted);font-family:var(--mono);padding:0 8px;height:28px;flex-shrink:0}
|
|
1800
|
+
.match-count.visible{display:inline-flex}
|
|
1801
|
+
.match-count-value{color:var(--text);font-weight:600;margin-left:6px}
|
|
1802
|
+
|
|
1771
1803
|
/* Test cards */
|
|
1772
1804
|
.test-card{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);margin-bottom:10px;overflow:hidden}
|
|
1773
1805
|
.test-card.failed,.test-card.timedout,.test-card.interrupted{border-color:rgba(239,68,68,.2)}
|
|
@@ -2104,7 +2136,15 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
|
|
|
2104
2136
|
<div class="test-bar">${testBarHtml}</div>
|
|
2105
2137
|
<div class="summary-stats">
|
|
2106
2138
|
<div class="stat-pills">${statPillsHtml}</div>
|
|
2107
|
-
<
|
|
2139
|
+
<div class="tag-filter-controls" data-tag-filter-controls hidden>
|
|
2140
|
+
<div class="tag-filter-trigger-wrap">
|
|
2141
|
+
<button class="add-tag-filter" data-add-tag-filter title="Filter by tag or diagnosis"><span class="add-tag-plus">+</span> Filter</button>
|
|
2142
|
+
<div class="tag-menu" data-tag-menu hidden></div>
|
|
2143
|
+
</div>
|
|
2144
|
+
<div class="active-tag-filters" data-active-tag-filters></div>
|
|
2145
|
+
</div>
|
|
2146
|
+
<span class="match-count" data-match-count>Matches:<span class="match-count-value" data-match-count-value>0</span></span>
|
|
2147
|
+
<button class="clear-filter" data-clear-filter>✖ Clear Filters</button>
|
|
2108
2148
|
</div>
|
|
2109
2149
|
</div>
|
|
2110
2150
|
|
|
@@ -2122,20 +2162,173 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
|
|
|
2122
2162
|
|
|
2123
2163
|
<script>
|
|
2124
2164
|
(function(){
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2165
|
+
// Filters compose across three dimensions:
|
|
2166
|
+
// status — single-select; card.data-status must match (if set).
|
|
2167
|
+
// tags — multi-select AND; card must carry every active tag.
|
|
2168
|
+
// reasons — multi-select OR; card.data-reason must match any active reason
|
|
2169
|
+
// (a card has at most one diagnosis, so AND would always be 0/1).
|
|
2170
|
+
// "Clear Filters" wipes all three.
|
|
2171
|
+
var activeStatus=null;
|
|
2172
|
+
var activeTags=new Set();
|
|
2173
|
+
var activeReasons=new Set();
|
|
2174
|
+
var allTags=[];
|
|
2175
|
+
var allReasons=[]; // ordered list of REASON keys present in the report
|
|
2176
|
+
var REASON_LABELS=${JSON.stringify(REASON_LABELS)};
|
|
2177
|
+
|
|
2178
|
+
function cardTags(card){var raw=card.getAttribute('data-tags');if(!raw)return [];try{var v=JSON.parse(raw);return Array.isArray(v)?v:[]}catch(_){return []}}
|
|
2179
|
+
function tagCount(t){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(cardTags(c).indexOf(t)!==-1)n++});return n}
|
|
2180
|
+
|
|
2181
|
+
function applyFilters(){
|
|
2182
|
+
var anyActive=activeStatus!==null||activeTags.size>0||activeReasons.size>0;
|
|
2183
|
+
document.querySelector('.clear-filter').classList.toggle('visible',anyActive);
|
|
2184
|
+
var visible=0;
|
|
2185
|
+
document.querySelectorAll('.test-card').forEach(function(card){
|
|
2186
|
+
var statusOk=activeStatus===null||card.getAttribute('data-status')===activeStatus;
|
|
2187
|
+
var tagsOk=true;
|
|
2188
|
+
if(activeTags.size>0){
|
|
2189
|
+
var t=cardTags(card);
|
|
2190
|
+
activeTags.forEach(function(want){if(t.indexOf(want)===-1)tagsOk=false});
|
|
2191
|
+
}
|
|
2192
|
+
var reasonOk=true;
|
|
2193
|
+
if(activeReasons.size>0){
|
|
2194
|
+
var r=card.getAttribute('data-reason')||'';
|
|
2195
|
+
reasonOk=activeReasons.has(r);
|
|
2196
|
+
}
|
|
2197
|
+
var hide=!(statusOk&&tagsOk&&reasonOk);
|
|
2198
|
+
card.classList.toggle('hidden-by-filter',hide);
|
|
2199
|
+
if(!hide)visible++;
|
|
2200
|
+
});
|
|
2201
|
+
var mc=document.querySelector('[data-match-count]');
|
|
2202
|
+
if(mc){
|
|
2203
|
+
mc.classList.toggle('visible',anyActive);
|
|
2204
|
+
var mv=mc.querySelector('[data-match-count-value]');
|
|
2205
|
+
if(mv)mv.textContent=visible;
|
|
2206
|
+
}
|
|
2207
|
+
syncFiltersToUrl();
|
|
2132
2208
|
}
|
|
2133
|
-
|
|
2134
|
-
|
|
2209
|
+
|
|
2210
|
+
// Reflect the active filter state in the URL query string so the current
|
|
2211
|
+
// view is shareable. replaceState (not pushState) keeps the back button
|
|
2212
|
+
// useful; the existing #?testId=<id> hash is preserved.
|
|
2213
|
+
function syncFiltersToUrl(){
|
|
2214
|
+
var p=new URLSearchParams();
|
|
2215
|
+
if(activeStatus)p.set('status',activeStatus);
|
|
2216
|
+
activeTags.forEach(function(t){p.append('tag',t)});
|
|
2217
|
+
activeReasons.forEach(function(r){p.append('reason',r)});
|
|
2218
|
+
var qs=p.toString();
|
|
2219
|
+
var next=location.pathname+(qs?'?'+qs:'')+(location.hash||'');
|
|
2220
|
+
if(next!==location.pathname+location.search+location.hash){
|
|
2221
|
+
history.replaceState(null,'',next);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
function toggleStatus(s){
|
|
2226
|
+
activeStatus=(activeStatus===s)?null:s;
|
|
2227
|
+
document.querySelectorAll('.stat-pill').forEach(function(p){p.classList.toggle('active',p.getAttribute('data-filter')===activeStatus)});
|
|
2228
|
+
applyFilters();
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// Convert "#rrggbb" or "rgb(...)" into a low-alpha background for tinted
|
|
2232
|
+
// diagnosis chips. Falls back to the accent tint if parsing fails.
|
|
2233
|
+
function hexToRgba(hex,a){
|
|
2234
|
+
if(!hex||hex.charAt(0)!=='#'||(hex.length!==4&&hex.length!==7))return 'rgba(255,127,58,'+a+')';
|
|
2235
|
+
var s=hex.length===4?('#'+hex[1]+hex[1]+hex[2]+hex[2]+hex[3]+hex[3]):hex;
|
|
2236
|
+
var r=parseInt(s.slice(1,3),16),g=parseInt(s.slice(3,5),16),b=parseInt(s.slice(5,7),16);
|
|
2237
|
+
return 'rgba('+r+','+g+','+b+','+a+')';
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
function renderActiveChips(){
|
|
2241
|
+
var c=document.querySelector('[data-active-tag-filters]');
|
|
2242
|
+
if(!c)return;
|
|
2243
|
+
c.innerHTML='';
|
|
2244
|
+
activeTags.forEach(function(t){
|
|
2245
|
+
var chip=document.createElement('span');chip.className='tag-chip';
|
|
2246
|
+
var label=document.createElement('span');label.textContent=t;
|
|
2247
|
+
var btn=document.createElement('button');btn.className='tag-chip-remove';btn.setAttribute('data-remove-tag',t);btn.setAttribute('title','Remove filter');btn.textContent='×';
|
|
2248
|
+
chip.appendChild(label);chip.appendChild(btn);
|
|
2249
|
+
c.appendChild(chip);
|
|
2250
|
+
});
|
|
2251
|
+
activeReasons.forEach(function(r){
|
|
2252
|
+
var meta=REASON_LABELS[r]||REASON_LABELS['UNKNOWN'];
|
|
2253
|
+
var chip=document.createElement('span');chip.className='tag-chip reason-chip';
|
|
2254
|
+
chip.style.background=hexToRgba(meta.color,0.14);
|
|
2255
|
+
chip.style.borderColor=hexToRgba(meta.color,0.4);
|
|
2256
|
+
chip.style.color=meta.color;
|
|
2257
|
+
var label=document.createElement('span');label.textContent=meta.label;
|
|
2258
|
+
var btn=document.createElement('button');btn.className='tag-chip-remove';btn.setAttribute('data-remove-reason',r);btn.setAttribute('title','Remove filter');btn.textContent='×';
|
|
2259
|
+
chip.appendChild(label);chip.appendChild(btn);
|
|
2260
|
+
c.appendChild(chip);
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
function addTag(t){if(!t||activeTags.has(t))return;activeTags.add(t);renderActiveChips();applyFilters()}
|
|
2264
|
+
function removeTag(t){if(!activeTags.delete(t))return;renderActiveChips();applyFilters()}
|
|
2265
|
+
function addReason(r){if(!r||activeReasons.has(r))return;activeReasons.add(r);renderActiveChips();applyFilters()}
|
|
2266
|
+
function removeReason(r){if(!activeReasons.delete(r))return;renderActiveChips();applyFilters()}
|
|
2267
|
+
|
|
2268
|
+
function reasonCount(r){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(c.getAttribute('data-reason')===r)n++});return n}
|
|
2269
|
+
|
|
2270
|
+
function openTagMenu(){
|
|
2271
|
+
var menu=document.querySelector('[data-tag-menu]');
|
|
2272
|
+
if(!menu)return;
|
|
2273
|
+
var trigger=document.querySelector('[data-add-tag-filter]');
|
|
2274
|
+
menu.innerHTML='';
|
|
2275
|
+
var availTags=allTags.filter(function(t){return !activeTags.has(t)});
|
|
2276
|
+
var availReasons=allReasons.filter(function(r){return !activeReasons.has(r)});
|
|
2277
|
+
var added=false;
|
|
2278
|
+
if(allTags.length>0){
|
|
2279
|
+
var hT=document.createElement('div');hT.className='tag-menu-section';hT.textContent='Tags';menu.appendChild(hT);
|
|
2280
|
+
if(availTags.length===0){
|
|
2281
|
+
var emptyT=document.createElement('div');emptyT.className='tag-menu-empty';emptyT.textContent='All tags selected';menu.appendChild(emptyT);
|
|
2282
|
+
}else{
|
|
2283
|
+
availTags.forEach(function(t){
|
|
2284
|
+
var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-tag-menu-item',t);
|
|
2285
|
+
var label=document.createElement('span');label.textContent=t;
|
|
2286
|
+
var count=document.createElement('span');count.className='tag-menu-count';count.textContent=tagCount(t);
|
|
2287
|
+
item.appendChild(label);item.appendChild(count);
|
|
2288
|
+
menu.appendChild(item);
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
added=true;
|
|
2292
|
+
}
|
|
2293
|
+
if(allReasons.length>0){
|
|
2294
|
+
var hR=document.createElement('div');hR.className='tag-menu-section';hR.textContent='Diagnoses';menu.appendChild(hR);
|
|
2295
|
+
if(availReasons.length===0){
|
|
2296
|
+
var emptyR=document.createElement('div');emptyR.className='tag-menu-empty';emptyR.textContent='All diagnoses selected';menu.appendChild(emptyR);
|
|
2297
|
+
}else{
|
|
2298
|
+
availReasons.forEach(function(r){
|
|
2299
|
+
var meta=REASON_LABELS[r]||REASON_LABELS['UNKNOWN'];
|
|
2300
|
+
var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-reason-menu-item',r);
|
|
2301
|
+
var label=document.createElement('span');label.textContent=meta.label;label.style.color=meta.color;
|
|
2302
|
+
var count=document.createElement('span');count.className='tag-menu-count';count.textContent=reasonCount(r);
|
|
2303
|
+
item.appendChild(label);item.appendChild(count);
|
|
2304
|
+
menu.appendChild(item);
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
added=true;
|
|
2308
|
+
}
|
|
2309
|
+
if(!added){
|
|
2310
|
+
var empty=document.createElement('div');empty.className='tag-menu-empty';empty.textContent='No filters available';menu.appendChild(empty);
|
|
2311
|
+
}
|
|
2312
|
+
menu.hidden=false;
|
|
2313
|
+
if(trigger)trigger.classList.add('active');
|
|
2314
|
+
}
|
|
2315
|
+
function closeTagMenu(){
|
|
2316
|
+
var menu=document.querySelector('[data-tag-menu]');var trigger=document.querySelector('[data-add-tag-filter]');
|
|
2317
|
+
if(menu)menu.hidden=true;
|
|
2318
|
+
if(trigger)trigger.classList.remove('active');
|
|
2319
|
+
}
|
|
2320
|
+
function tagMenuOpen(){var m=document.querySelector('[data-tag-menu]');return !!(m&&!m.hidden)}
|
|
2321
|
+
|
|
2322
|
+
function clearAllFilters(){
|
|
2323
|
+
activeStatus=null;
|
|
2324
|
+
activeTags.clear();
|
|
2325
|
+
activeReasons.clear();
|
|
2135
2326
|
document.querySelectorAll('.stat-pill').forEach(function(p){p.classList.remove('active')});
|
|
2136
|
-
|
|
2137
|
-
|
|
2327
|
+
renderActiveChips();
|
|
2328
|
+
closeTagMenu();
|
|
2329
|
+
applyFilters();
|
|
2138
2330
|
}
|
|
2331
|
+
|
|
2139
2332
|
function closeLightbox(){document.getElementById('lightbox').classList.remove('open')}
|
|
2140
2333
|
|
|
2141
2334
|
document.addEventListener('click',function(e){
|
|
@@ -2165,14 +2358,32 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
|
|
|
2165
2358
|
if(flowBtn){e.stopPropagation();navigator.clipboard.writeText(flowBtn.getAttribute('data-flow-id'));var copySvg=flowBtn.innerHTML;flowBtn.innerHTML='<svg class="check-icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';setTimeout(function(){flowBtn.innerHTML=copySvg},2000);return}
|
|
2166
2359
|
var jsonBtn=e.target.closest('.copy-json');
|
|
2167
2360
|
if(jsonBtn){e.stopPropagation();var pre=jsonBtn.closest('.step-json-wrap').querySelector('.step-json');navigator.clipboard.writeText(pre.textContent);var s=jsonBtn.innerHTML;jsonBtn.innerHTML='<svg class="check-icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';setTimeout(function(){jsonBtn.innerHTML=s},2000);return}
|
|
2361
|
+
// Tag filter: add (+ button toggles the menu) / select (menu item) /
|
|
2362
|
+
// remove (chip × button). Outside-clicks close the menu via the catch-all
|
|
2363
|
+
// at the end of this handler.
|
|
2364
|
+
var addTagBtn=e.target.closest('[data-add-tag-filter]');
|
|
2365
|
+
if(addTagBtn){if(tagMenuOpen()){closeTagMenu()}else{openTagMenu()}return}
|
|
2366
|
+
var tagItem=e.target.closest('[data-tag-menu-item]');
|
|
2367
|
+
if(tagItem){addTag(tagItem.getAttribute('data-tag-menu-item'));closeTagMenu();return}
|
|
2368
|
+
var reasonItem=e.target.closest('[data-reason-menu-item]');
|
|
2369
|
+
if(reasonItem){addReason(reasonItem.getAttribute('data-reason-menu-item'));closeTagMenu();return}
|
|
2370
|
+
var tagRemove=e.target.closest('[data-remove-tag]');
|
|
2371
|
+
if(tagRemove){removeTag(tagRemove.getAttribute('data-remove-tag'));return}
|
|
2372
|
+
var reasonRemove=e.target.closest('[data-remove-reason]');
|
|
2373
|
+
if(reasonRemove){removeReason(reasonRemove.getAttribute('data-remove-reason'));return}
|
|
2168
2374
|
// Stat pill filter
|
|
2169
2375
|
var pill=e.target.closest('.stat-pill[data-filter]');
|
|
2170
|
-
if(pill){
|
|
2376
|
+
if(pill){toggleStatus(pill.getAttribute('data-filter'));return}
|
|
2171
2377
|
// Clear filter
|
|
2172
|
-
if(e.target.closest('[data-clear-filter]')){
|
|
2173
|
-
//
|
|
2378
|
+
if(e.target.closest('[data-clear-filter]')){clearAllFilters();return}
|
|
2379
|
+
// Any other click closes the tag menu (outside-click dismiss).
|
|
2380
|
+
if(tagMenuOpen()&&!e.target.closest('[data-tag-menu]')){closeTagMenu()}
|
|
2381
|
+
// Test bar block click — scroll to and highlight the target test card,
|
|
2382
|
+
// and reflect the selection in location.hash so the URL is shareable.
|
|
2383
|
+
// replaceState (not pushState) keeps the back button useful after the
|
|
2384
|
+
// user has clicked through several bar blocks.
|
|
2174
2385
|
var barBlock=e.target.closest('.test-bar-block[data-target]');
|
|
2175
|
-
if(barBlock){var tid=barBlock.getAttribute('data-target');var card=document.getElementById(tid);if(card){var tc=card.closest('.test-card');if(tc){tc.classList.add('expanded');tc.scrollIntoView({behavior:'smooth',block:'center'});tc.style.outline='2px solid var(--accent)';setTimeout(function(){tc.style.outline=''},1500)}}return}
|
|
2386
|
+
if(barBlock){var tid=barBlock.getAttribute('data-target');var card=document.getElementById(tid);if(card){var tc=card.closest('.test-card');if(tc){tc.classList.add('expanded');tc.scrollIntoView({behavior:'smooth',block:'center'});tc.style.outline='2px solid var(--accent)';setTimeout(function(){tc.style.outline=''},1500)}}if(tid&&tid.indexOf('test-')===0){history.replaceState(null,'','#?testId='+encodeURIComponent(tid.slice(5)))}return}
|
|
2176
2387
|
// Filmstrip step expand (skip if clicking a link inside).
|
|
2177
2388
|
// <div>-based steps: toggle the open class on any click in the wrapper.
|
|
2178
2389
|
// <details>-based steps: <summary> clicks are handled natively; padding
|
|
@@ -2192,18 +2403,98 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
|
|
|
2192
2403
|
// Audit check row expand
|
|
2193
2404
|
var auditCheck=e.target.closest('.audit-check.expandable');
|
|
2194
2405
|
if(auditCheck){auditCheck.classList.toggle('open');return}
|
|
2195
|
-
// Test card expand
|
|
2406
|
+
// Test card expand — also reflect the selection in location.hash on
|
|
2407
|
+
// open so the URL is shareable. Same replaceState rationale as the bar
|
|
2408
|
+
// block handler above. Collapses leave the URL alone.
|
|
2196
2409
|
var row=e.target.closest('.test-card[data-detail]');
|
|
2197
|
-
if(row&&!e.target.closest('.test-detail')){row.classList.toggle('expanded');return}
|
|
2410
|
+
if(row&&!e.target.closest('.test-detail')){var nowOpen=row.classList.toggle('expanded');if(nowOpen&&row.id&&row.id.indexOf('test-')===0){history.replaceState(null,'','#?testId='+encodeURIComponent(row.id.slice(5)))}return}
|
|
2198
2411
|
});
|
|
2199
2412
|
|
|
2200
|
-
document.addEventListener('keydown',function(e){if(e.key==='Escape'){closeLightbox();
|
|
2413
|
+
document.addEventListener('keydown',function(e){if(e.key==='Escape'){closeLightbox();closeTagMenu();clearAllFilters()}});
|
|
2201
2414
|
|
|
2202
2415
|
// Auto-expand failed/timedout/interrupted/healed tests
|
|
2203
2416
|
document.querySelectorAll('.test-card.failed,.test-card.timedout,.test-card.interrupted,.test-card.healed').forEach(function(r){r.classList.add('expanded')});
|
|
2417
|
+
|
|
2418
|
+
// Collect the cumulative set of tags + diagnosis reasons present across all
|
|
2419
|
+
// test cards. Reveal the +filter controls when at least one of either exists.
|
|
2420
|
+
(function(){
|
|
2421
|
+
var seenTags=Object.create(null);
|
|
2422
|
+
var seenReasons=Object.create(null);
|
|
2423
|
+
document.querySelectorAll('.test-card').forEach(function(card){
|
|
2424
|
+
var raw=card.getAttribute('data-tags');
|
|
2425
|
+
if(raw){try{var tags=JSON.parse(raw);if(Array.isArray(tags)){tags.forEach(function(t){if(typeof t==='string'&&t)seenTags[t]=true})}}catch(_){}}
|
|
2426
|
+
var r=card.getAttribute('data-reason');
|
|
2427
|
+
if(r)seenReasons[r]=true;
|
|
2428
|
+
});
|
|
2429
|
+
allTags=Object.keys(seenTags).sort();
|
|
2430
|
+
// Preserve the REASON_LABELS declaration order rather than alphabetical —
|
|
2431
|
+
// they're already arranged from most-frequent/specific to UNKNOWN catch-all.
|
|
2432
|
+
allReasons=Object.keys(REASON_LABELS).filter(function(r){return seenReasons[r]});
|
|
2433
|
+
var controls=document.querySelector('[data-tag-filter-controls]');
|
|
2434
|
+
if(controls&&(allTags.length>0||allReasons.length>0))controls.hidden=false;
|
|
2435
|
+
})();
|
|
2436
|
+
|
|
2437
|
+
// Seed filter state from ?status=...&tag=...&reason=... so shared URLs
|
|
2438
|
+
// restore the view. Status values not in the known stat-pill set are
|
|
2439
|
+
// ignored; tag and reason values not present in this report are dropped
|
|
2440
|
+
// so a stale URL can't poison the state.
|
|
2441
|
+
(function(){
|
|
2442
|
+
var p=new URLSearchParams(location.search);
|
|
2443
|
+
var s=p.get('status');
|
|
2444
|
+
var validStatuses={};
|
|
2445
|
+
document.querySelectorAll('.stat-pill[data-filter]').forEach(function(el){validStatuses[el.getAttribute('data-filter')]=true});
|
|
2446
|
+
if(s&&validStatuses[s]){
|
|
2447
|
+
activeStatus=s;
|
|
2448
|
+
document.querySelectorAll('.stat-pill').forEach(function(el){el.classList.toggle('active',el.getAttribute('data-filter')===s)});
|
|
2449
|
+
}
|
|
2450
|
+
var tagSet={};allTags.forEach(function(t){tagSet[t]=true});
|
|
2451
|
+
p.getAll('tag').forEach(function(t){if(tagSet[t])activeTags.add(t)});
|
|
2452
|
+
var reasonSet={};allReasons.forEach(function(r){reasonSet[r]=true});
|
|
2453
|
+
p.getAll('reason').forEach(function(r){if(reasonSet[r])activeReasons.add(r)});
|
|
2454
|
+
if(activeTags.size>0||activeReasons.size>0)renderActiveChips();
|
|
2455
|
+
if(activeStatus!==null||activeTags.size>0||activeReasons.size>0)applyFilters();
|
|
2456
|
+
})();
|
|
2457
|
+
|
|
2458
|
+
// Open #?testId=<id> deep links to the matching test card. Used by the
|
|
2459
|
+
// per-test redirect stubs (one per Playwright test-results directory) and
|
|
2460
|
+
// by any external link that wants to permalink a specific test.
|
|
2461
|
+
function focusTestFromHash(){
|
|
2462
|
+
var h=location.hash||'';
|
|
2463
|
+
if(h.indexOf('#?')!==0)return;
|
|
2464
|
+
var params=new URLSearchParams(h.slice(2));
|
|
2465
|
+
var id=params.get('testId');
|
|
2466
|
+
if(!id)return;
|
|
2467
|
+
var card=document.getElementById('test-'+id);
|
|
2468
|
+
if(!card||!card.classList.contains('test-card'))return;
|
|
2469
|
+
card.classList.add('expanded');
|
|
2470
|
+
card.scrollIntoView({behavior:'smooth',block:'center'});
|
|
2471
|
+
}
|
|
2472
|
+
focusTestFromHash();
|
|
2473
|
+
window.addEventListener('hashchange',focusTestFromHash);
|
|
2204
2474
|
})();
|
|
2205
2475
|
</script>
|
|
2206
2476
|
</body>
|
|
2207
2477
|
</html>`;
|
|
2208
2478
|
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Render the tiny redirect HTML that Donobu drops into each Playwright-managed
|
|
2481
|
+
* per-test directory under `test-results/`. The stub bounces straight to the
|
|
2482
|
+
* combined report's `#?testId=<id>` deep link — meta-refresh + JS replace +
|
|
2483
|
+
* visible fallback link, so it works with or without JS, online or `file://`.
|
|
2484
|
+
*
|
|
2485
|
+
* Strictly additive: Donobu does not create or rename Playwright's per-test
|
|
2486
|
+
* directories — the caller in `html.ts` only writes this file into directories
|
|
2487
|
+
* Playwright already created for the test's attachments.
|
|
2488
|
+
*/
|
|
2489
|
+
function renderPerTestStub(params) {
|
|
2490
|
+
const { testId, title, relPathToReport } = params;
|
|
2491
|
+
const href = `${relPathToReport}#?testId=${encodeURIComponent(testId)}`;
|
|
2492
|
+
return `<!DOCTYPE html>
|
|
2493
|
+
<meta charset="UTF-8">
|
|
2494
|
+
<title>Donobu — ${esc(title)}</title>
|
|
2495
|
+
<meta http-equiv="refresh" content="0; url=${esc(href)}">
|
|
2496
|
+
<script>location.replace(${JSON.stringify(href)});</script>
|
|
2497
|
+
<p>Redirecting to <a href="${esc(href)}">test report</a>…</p>
|
|
2498
|
+
`;
|
|
2499
|
+
}
|
|
2209
2500
|
//# sourceMappingURL=render.js.map
|