playwright-order-manager 0.1.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/CHANGELOG.md +20 -0
- package/README.md +240 -0
- package/bin/run.js +133 -0
- package/dist/constants/index.d.ts +72 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +93 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/core/OrderedExecution.d.ts +52 -0
- package/dist/core/OrderedExecution.d.ts.map +1 -0
- package/dist/core/OrderedExecution.js +253 -0
- package/dist/core/OrderedExecution.js.map +1 -0
- package/dist/core/OrderedReportParser.d.ts +38 -0
- package/dist/core/OrderedReportParser.d.ts.map +1 -0
- package/dist/core/OrderedReportParser.js +169 -0
- package/dist/core/OrderedReportParser.js.map +1 -0
- package/dist/core/OrderedSummary.d.ts +20 -0
- package/dist/core/OrderedSummary.d.ts.map +1 -0
- package/dist/core/OrderedSummary.js +747 -0
- package/dist/core/OrderedSummary.js.map +1 -0
- package/dist/fixtures/index.d.ts +30 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +212 -0
- package/dist/fixtures/index.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/runner/TestOrderManager.d.ts +62 -0
- package/dist/runner/TestOrderManager.d.ts.map +1 -0
- package/dist/runner/TestOrderManager.js +490 -0
- package/dist/runner/TestOrderManager.js.map +1 -0
- package/dist/types/index.d.ts +215 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +65 -0
- package/templates/playwright.merge.config.ts +57 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.OrderedSummary = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const constants_1 = require("../constants");
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// INTERNAL HELPERS
|
|
42
|
+
// =============================================================================
|
|
43
|
+
function ensureDir(dirPath) {
|
|
44
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
function formatDuration(ms) {
|
|
47
|
+
if (ms < 1000)
|
|
48
|
+
return `${ms}ms`;
|
|
49
|
+
const seconds = Math.floor(ms / 1000);
|
|
50
|
+
const minutes = Math.floor(seconds / 60);
|
|
51
|
+
const hours = Math.floor(minutes / 60);
|
|
52
|
+
if (hours > 0)
|
|
53
|
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
54
|
+
if (minutes > 0)
|
|
55
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
56
|
+
return `${seconds}s`;
|
|
57
|
+
}
|
|
58
|
+
function escapeHtml(str) {
|
|
59
|
+
return str
|
|
60
|
+
.replace(/&/g, '&')
|
|
61
|
+
.replace(/</g, '<')
|
|
62
|
+
.replace(/>/g, '>')
|
|
63
|
+
.replace(/"/g, '"')
|
|
64
|
+
.replace(/'/g, ''');
|
|
65
|
+
}
|
|
66
|
+
function formatTime(iso) {
|
|
67
|
+
try {
|
|
68
|
+
return new Date(iso).toLocaleTimeString([], {
|
|
69
|
+
hour: '2-digit',
|
|
70
|
+
minute: '2-digit',
|
|
71
|
+
second: '2-digit',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return iso;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A test is "flaky" if it ultimately passed but only after one or more retries.
|
|
80
|
+
*/
|
|
81
|
+
function isFlaky(result) {
|
|
82
|
+
return result.status === 'passed' && result.retries > 0;
|
|
83
|
+
}
|
|
84
|
+
// =============================================================================
|
|
85
|
+
// HTML GENERATION — PIECES
|
|
86
|
+
// =============================================================================
|
|
87
|
+
/**
|
|
88
|
+
* Generates the stacked progress bar showing pass / fail / skip proportions.
|
|
89
|
+
*/
|
|
90
|
+
function generateProgressBar(summary) {
|
|
91
|
+
const total = summary.totals.tests;
|
|
92
|
+
if (total === 0)
|
|
93
|
+
return '';
|
|
94
|
+
const passedPct = Math.round((summary.totals.passed / total) * 100);
|
|
95
|
+
const failedPct = Math.round((summary.totals.failed / total) * 100);
|
|
96
|
+
const skippedPct = Math.round((summary.totals.skipped / total) * 100);
|
|
97
|
+
return `
|
|
98
|
+
<div class="progress-wrap">
|
|
99
|
+
<div class="progress-labels">
|
|
100
|
+
<span class="pl-rate">${passedPct}% pass rate (${summary.totals.passed}/${total} tests)</span>
|
|
101
|
+
<span class="pl-fail">${summary.totals.failed > 0 ? `${summary.totals.failed} failed` : ''}</span>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="progress-bar" title="${passedPct}% passed, ${failedPct}% failed, ${skippedPct}% skipped">
|
|
104
|
+
<div class="pb-pass" style="width:${passedPct}%"></div>
|
|
105
|
+
<div class="pb-fail" style="width:${failedPct}%"></div>
|
|
106
|
+
<div class="pb-skip" style="width:${skippedPct}%"></div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Generates the bucket navigation pill bar at the top.
|
|
113
|
+
* Each pill shows bucket number, label, test count, and pass/fail status.
|
|
114
|
+
* Clicking scrolls to the matching bucket section.
|
|
115
|
+
*/
|
|
116
|
+
function generateBucketNav(buckets) {
|
|
117
|
+
const pills = buckets.map((bucket, index) => {
|
|
118
|
+
const num = index + 1;
|
|
119
|
+
const cls = bucket.status === 'failed' ? 'pill-fail'
|
|
120
|
+
: bucket.status === 'skipped' ? 'pill-skip'
|
|
121
|
+
: 'pill-pass';
|
|
122
|
+
const label = escapeHtml(bucket.label);
|
|
123
|
+
const count = bucket.totalTests;
|
|
124
|
+
return `<a href="#bucket-${num}" class="nav-pill ${cls}">#${num} ${label} (${count})</a>`;
|
|
125
|
+
}).join('');
|
|
126
|
+
return `<nav class="bucket-nav" aria-label="Jump to bucket">${pills}</nav>`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Generates the tag pills shown on each test row.
|
|
130
|
+
*/
|
|
131
|
+
function generateTagPills(tags) {
|
|
132
|
+
if (!tags || tags.length === 0)
|
|
133
|
+
return '<span class="no-tags">—</span>';
|
|
134
|
+
return tags
|
|
135
|
+
.map(tag => `<span class="tag-pill">${escapeHtml(tag)}</span>`)
|
|
136
|
+
.join(' ');
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Generates the rows for a single bucket's test table.
|
|
140
|
+
* Each row: status (with flaky badge) | test title + file:line | tags | duration | retries | error preview
|
|
141
|
+
*/
|
|
142
|
+
function generateTestRows(results) {
|
|
143
|
+
return results.map((result) => {
|
|
144
|
+
const flaky = isFlaky(result);
|
|
145
|
+
const rowCls = result.status === 'failed' ? 'row-fail'
|
|
146
|
+
: result.status === 'skipped' ? 'row-skip'
|
|
147
|
+
: result.status === 'timedOut' ? 'row-timeout'
|
|
148
|
+
: '';
|
|
149
|
+
// Status cell — dot + label + optional flaky badge
|
|
150
|
+
const statusCell = `
|
|
151
|
+
<div class="td-status">
|
|
152
|
+
<span class="s-dot s-${result.status}"></span>
|
|
153
|
+
<span class="s-lbl">${result.status}</span>
|
|
154
|
+
${flaky ? '<span class="badge-flaky">flaky</span>' : ''}
|
|
155
|
+
</div>`;
|
|
156
|
+
// Title + file:line cell
|
|
157
|
+
const fileDisplay = result.file
|
|
158
|
+
? `<div class="td-file">${escapeHtml(result.file)}</div>`
|
|
159
|
+
: '';
|
|
160
|
+
const titleCell = `
|
|
161
|
+
<div class="td-title">${escapeHtml(result.title)}</div>
|
|
162
|
+
${fileDisplay}`;
|
|
163
|
+
// Tags cell
|
|
164
|
+
const tagsCell = generateTagPills(result.tags);
|
|
165
|
+
// Duration cell
|
|
166
|
+
const durCell = `<span class="td-dur">${formatDuration(result.duration)}</span>`;
|
|
167
|
+
// Retries cell
|
|
168
|
+
const retryCell = result.retries > 0
|
|
169
|
+
? `<span class="td-retry">${result.retries} retry${result.retries > 1 ? 's' : ''}</span>`
|
|
170
|
+
: '<span class="no-tags">—</span>';
|
|
171
|
+
// Error cell — first line inline, full detail on expand
|
|
172
|
+
let errorCell = '<span class="no-tags">—</span>';
|
|
173
|
+
if (result.errorMessage) {
|
|
174
|
+
const firstLine = escapeHtml(result.errorMessage.split('\n')[0].slice(0, 120));
|
|
175
|
+
const fullMsg = escapeHtml(result.errorMessage);
|
|
176
|
+
errorCell = `
|
|
177
|
+
<details class="err-details">
|
|
178
|
+
<summary class="err-summary">${firstLine}</summary>
|
|
179
|
+
<pre class="err-pre">${fullMsg}</pre>
|
|
180
|
+
</details>`;
|
|
181
|
+
}
|
|
182
|
+
return `
|
|
183
|
+
<tr class="${rowCls}">
|
|
184
|
+
<td>${statusCell}</td>
|
|
185
|
+
<td>${titleCell}</td>
|
|
186
|
+
<td>${tagsCell}</td>
|
|
187
|
+
<td>${durCell}</td>
|
|
188
|
+
<td>${retryCell}</td>
|
|
189
|
+
<td>${errorCell}</td>
|
|
190
|
+
</tr>`;
|
|
191
|
+
}).join('');
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Generates the full HTML section for one bucket.
|
|
195
|
+
* Includes: collapsible header with sequence number, timestamps, stats, and test table.
|
|
196
|
+
*/
|
|
197
|
+
function generateBucketSection(bucket, index) {
|
|
198
|
+
const num = index + 1;
|
|
199
|
+
const statusCls = bucket.status === 'failed' ? 'bkt-fail'
|
|
200
|
+
: bucket.status === 'skipped' ? 'bkt-skip'
|
|
201
|
+
: 'bkt-pass';
|
|
202
|
+
// Timestamp range — only shown if the runner recorded them
|
|
203
|
+
const timestampHtml = (bucket.startedAt && bucket.finishedAt)
|
|
204
|
+
? `<span class="bkt-timing">
|
|
205
|
+
${formatTime(bucket.startedAt)} → ${formatTime(bucket.finishedAt)}
|
|
206
|
+
</span>`
|
|
207
|
+
: '';
|
|
208
|
+
const criticalBadge = bucket.critical
|
|
209
|
+
? '<span class="badge-critical">critical</span>'
|
|
210
|
+
: '';
|
|
211
|
+
const statsHtml = `
|
|
212
|
+
<span class="bkt-stat">
|
|
213
|
+
<span class="stat-pass">${bucket.passed}</span> passed
|
|
214
|
+
</span>
|
|
215
|
+
<span class="bkt-stat">
|
|
216
|
+
<span class="stat-fail">${bucket.failed}</span> failed
|
|
217
|
+
</span>
|
|
218
|
+
<span class="bkt-stat">
|
|
219
|
+
<span class="stat-skip">${bucket.skipped}</span> skipped
|
|
220
|
+
</span>
|
|
221
|
+
<span class="bkt-dur">${formatDuration(bucket.duration)}</span>`;
|
|
222
|
+
const tableHtml = `
|
|
223
|
+
<table class="test-table">
|
|
224
|
+
<thead>
|
|
225
|
+
<tr>
|
|
226
|
+
<th style="width:110px">Status</th>
|
|
227
|
+
<th>Test & file</th>
|
|
228
|
+
<th style="width:160px">Tags</th>
|
|
229
|
+
<th style="width:80px">Duration</th>
|
|
230
|
+
<th style="width:80px">Retries</th>
|
|
231
|
+
<th>Error</th>
|
|
232
|
+
</tr>
|
|
233
|
+
</thead>
|
|
234
|
+
<tbody>
|
|
235
|
+
${generateTestRows(bucket.results)}
|
|
236
|
+
</tbody>
|
|
237
|
+
</table>`;
|
|
238
|
+
return `
|
|
239
|
+
<section class="bucket ${statusCls}" id="bucket-${num}">
|
|
240
|
+
<button
|
|
241
|
+
class="bkt-header"
|
|
242
|
+
aria-expanded="true"
|
|
243
|
+
aria-controls="bkt-body-${num}"
|
|
244
|
+
onclick="toggleBucket(this)"
|
|
245
|
+
>
|
|
246
|
+
<div class="bkt-left">
|
|
247
|
+
<span class="bkt-seq">#${num}</span>
|
|
248
|
+
<span class="bkt-dot"></span>
|
|
249
|
+
<span class="bkt-name">${escapeHtml(bucket.label)}</span>
|
|
250
|
+
${criticalBadge}
|
|
251
|
+
</div>
|
|
252
|
+
<div class="bkt-right">
|
|
253
|
+
${timestampHtml}
|
|
254
|
+
${statsHtml}
|
|
255
|
+
<span class="bkt-chevron" aria-hidden="true">▼</span>
|
|
256
|
+
</div>
|
|
257
|
+
</button>
|
|
258
|
+
<div class="bkt-body" id="bkt-body-${num}">
|
|
259
|
+
${tableHtml}
|
|
260
|
+
</div>
|
|
261
|
+
</section>`;
|
|
262
|
+
}
|
|
263
|
+
// =============================================================================
|
|
264
|
+
// FULL HTML DOCUMENT
|
|
265
|
+
// =============================================================================
|
|
266
|
+
function generateHtml(summary) {
|
|
267
|
+
const overallCls = summary.success ? 'overall-pass' : 'overall-fail';
|
|
268
|
+
const overallLabel = summary.success ? 'Run passed' : 'Run failed';
|
|
269
|
+
const bucketSections = summary.buckets
|
|
270
|
+
.map((b, i) => generateBucketSection(b, i))
|
|
271
|
+
.join('\n');
|
|
272
|
+
return `<!DOCTYPE html>
|
|
273
|
+
<html lang="en">
|
|
274
|
+
<head>
|
|
275
|
+
<meta charset="UTF-8" />
|
|
276
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
277
|
+
<title>Ordered Test Report</title>
|
|
278
|
+
<style>
|
|
279
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
280
|
+
|
|
281
|
+
body {
|
|
282
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
283
|
+
background: #0d0f1a;
|
|
284
|
+
color: #e2e8f0;
|
|
285
|
+
padding: 1.5rem 2rem 4rem;
|
|
286
|
+
line-height: 1.5;
|
|
287
|
+
font-size: 14px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
a { color: inherit; text-decoration: none; }
|
|
291
|
+
|
|
292
|
+
/* ── Header ──────────────────────────────────────────────── */
|
|
293
|
+
.header {
|
|
294
|
+
background: #131625;
|
|
295
|
+
border: 1px solid #1e2235;
|
|
296
|
+
border-radius: 10px;
|
|
297
|
+
padding: 1.5rem;
|
|
298
|
+
margin-bottom: 1rem;
|
|
299
|
+
}
|
|
300
|
+
.header-top {
|
|
301
|
+
display: flex;
|
|
302
|
+
align-items: center;
|
|
303
|
+
justify-content: space-between;
|
|
304
|
+
margin-bottom: 1rem;
|
|
305
|
+
flex-wrap: wrap;
|
|
306
|
+
gap: .75rem;
|
|
307
|
+
}
|
|
308
|
+
.overall-status {
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: center;
|
|
311
|
+
gap: 10px;
|
|
312
|
+
font-size: 18px;
|
|
313
|
+
font-weight: 600;
|
|
314
|
+
}
|
|
315
|
+
.status-dot {
|
|
316
|
+
width: 12px; height: 12px;
|
|
317
|
+
border-radius: 50%;
|
|
318
|
+
flex-shrink: 0;
|
|
319
|
+
}
|
|
320
|
+
.overall-pass .status-dot { background: #4ade80; }
|
|
321
|
+
.overall-fail .status-dot { background: #f87171; }
|
|
322
|
+
.overall-pass .status-text { color: #4ade80; }
|
|
323
|
+
.overall-fail .status-text { color: #f87171; }
|
|
324
|
+
|
|
325
|
+
.run-meta {
|
|
326
|
+
display: flex; gap: 1.5rem; flex-wrap: wrap;
|
|
327
|
+
font-size: 12px; color: #94a3b8;
|
|
328
|
+
}
|
|
329
|
+
.run-meta strong { color: #e2e8f0; font-weight: 500; }
|
|
330
|
+
|
|
331
|
+
/* progress bar */
|
|
332
|
+
.progress-wrap { margin-top: 1rem; }
|
|
333
|
+
.progress-labels {
|
|
334
|
+
display: flex; justify-content: space-between;
|
|
335
|
+
font-size: 12px; color: #94a3b8; margin-bottom: 5px;
|
|
336
|
+
}
|
|
337
|
+
.pl-fail { color: #f87171; }
|
|
338
|
+
.progress-bar {
|
|
339
|
+
height: 6px; border-radius: 999px;
|
|
340
|
+
background: #1e2235;
|
|
341
|
+
display: flex; overflow: hidden;
|
|
342
|
+
}
|
|
343
|
+
.pb-pass { background: #4ade80; transition: width .3s; }
|
|
344
|
+
.pb-fail { background: #f87171; transition: width .3s; }
|
|
345
|
+
.pb-skip { background: #facc15; transition: width .3s; }
|
|
346
|
+
|
|
347
|
+
/* ── Summary cards ───────────────────────────────────────── */
|
|
348
|
+
.summary-cards {
|
|
349
|
+
display: grid;
|
|
350
|
+
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
351
|
+
gap: 8px;
|
|
352
|
+
margin-bottom: 1rem;
|
|
353
|
+
}
|
|
354
|
+
.card {
|
|
355
|
+
background: #131625;
|
|
356
|
+
border: 1px solid #1e2235;
|
|
357
|
+
border-radius: 8px;
|
|
358
|
+
padding: 1rem;
|
|
359
|
+
text-align: center;
|
|
360
|
+
}
|
|
361
|
+
.card-val {
|
|
362
|
+
font-size: 28px; font-weight: 600;
|
|
363
|
+
line-height: 1; margin-bottom: 4px;
|
|
364
|
+
}
|
|
365
|
+
.card-lbl {
|
|
366
|
+
font-size: 11px; color: #64748b;
|
|
367
|
+
text-transform: uppercase; letter-spacing: .05em;
|
|
368
|
+
}
|
|
369
|
+
.cv-total { color: #60a5fa; }
|
|
370
|
+
.cv-pass { color: #4ade80; }
|
|
371
|
+
.cv-fail { color: #f87171; }
|
|
372
|
+
.cv-skip { color: #facc15; }
|
|
373
|
+
.cv-rate { color: #e2e8f0; }
|
|
374
|
+
|
|
375
|
+
/* ── Bucket nav ──────────────────────────────────────────── */
|
|
376
|
+
.bucket-nav {
|
|
377
|
+
display: flex; flex-wrap: wrap; gap: 6px;
|
|
378
|
+
margin-bottom: 1.25rem;
|
|
379
|
+
}
|
|
380
|
+
.nav-pill {
|
|
381
|
+
font-size: 11px;
|
|
382
|
+
padding: 4px 10px;
|
|
383
|
+
border-radius: 999px;
|
|
384
|
+
border: 1px solid #1e2235;
|
|
385
|
+
cursor: pointer;
|
|
386
|
+
white-space: nowrap;
|
|
387
|
+
transition: opacity .15s;
|
|
388
|
+
}
|
|
389
|
+
.nav-pill:hover { opacity: .8; }
|
|
390
|
+
.pill-pass {
|
|
391
|
+
color: #4ade80;
|
|
392
|
+
background: rgba(74,222,128,.08);
|
|
393
|
+
border-color: rgba(74,222,128,.3);
|
|
394
|
+
}
|
|
395
|
+
.pill-fail {
|
|
396
|
+
color: #f87171;
|
|
397
|
+
background: rgba(248,113,113,.08);
|
|
398
|
+
border-color: rgba(248,113,113,.3);
|
|
399
|
+
}
|
|
400
|
+
.pill-skip {
|
|
401
|
+
color: #facc15;
|
|
402
|
+
background: rgba(250,204,21,.08);
|
|
403
|
+
border-color: rgba(250,204,21,.3);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* ── Bucket sections ─────────────────────────────────────── */
|
|
407
|
+
.bucket {
|
|
408
|
+
background: #131625;
|
|
409
|
+
border: 1px solid #1e2235;
|
|
410
|
+
border-radius: 10px;
|
|
411
|
+
margin-bottom: .875rem;
|
|
412
|
+
overflow: hidden;
|
|
413
|
+
}
|
|
414
|
+
.bucket.bkt-fail { border-color: rgba(248,113,113,.4); }
|
|
415
|
+
.bucket.bkt-pass { border-color: rgba(74,222,128,.2); }
|
|
416
|
+
.bucket.bkt-skip { border-color: rgba(250,204,21,.2); }
|
|
417
|
+
|
|
418
|
+
.bkt-header {
|
|
419
|
+
width: 100%;
|
|
420
|
+
display: flex;
|
|
421
|
+
align-items: center;
|
|
422
|
+
justify-content: space-between;
|
|
423
|
+
padding: .875rem 1.25rem;
|
|
424
|
+
background: #0d0f1a;
|
|
425
|
+
border: none;
|
|
426
|
+
cursor: pointer;
|
|
427
|
+
color: #e2e8f0;
|
|
428
|
+
text-align: left;
|
|
429
|
+
flex-wrap: wrap;
|
|
430
|
+
gap: .5rem;
|
|
431
|
+
}
|
|
432
|
+
.bkt-header:hover { background: #131625; }
|
|
433
|
+
|
|
434
|
+
.bkt-left {
|
|
435
|
+
display: flex; align-items: center; gap: 10px;
|
|
436
|
+
}
|
|
437
|
+
.bkt-seq {
|
|
438
|
+
font-size: 11px; color: #475569;
|
|
439
|
+
font-weight: 600; min-width: 22px;
|
|
440
|
+
font-variant-numeric: tabular-nums;
|
|
441
|
+
}
|
|
442
|
+
.bkt-dot {
|
|
443
|
+
width: 8px; height: 8px;
|
|
444
|
+
border-radius: 50%; flex-shrink: 0;
|
|
445
|
+
}
|
|
446
|
+
.bkt-pass .bkt-dot { background: #4ade80; }
|
|
447
|
+
.bkt-fail .bkt-dot { background: #f87171; }
|
|
448
|
+
.bkt-skip .bkt-dot { background: #facc15; }
|
|
449
|
+
|
|
450
|
+
.bkt-name {
|
|
451
|
+
font-size: 13px; font-weight: 600;
|
|
452
|
+
}
|
|
453
|
+
.badge-critical {
|
|
454
|
+
font-size: 10px; font-weight: 600;
|
|
455
|
+
padding: 2px 7px; border-radius: 999px;
|
|
456
|
+
background: rgba(248,113,113,.15);
|
|
457
|
+
color: #fca5a5;
|
|
458
|
+
border: 1px solid rgba(248,113,113,.3);
|
|
459
|
+
text-transform: uppercase; letter-spacing: .04em;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.bkt-right {
|
|
463
|
+
display: flex; align-items: center;
|
|
464
|
+
gap: 1.25rem; flex-wrap: wrap;
|
|
465
|
+
}
|
|
466
|
+
.bkt-timing {
|
|
467
|
+
font-size: 11px; color: #475569;
|
|
468
|
+
font-variant-numeric: tabular-nums;
|
|
469
|
+
}
|
|
470
|
+
.bkt-stat { font-size: 12px; color: #94a3b8; }
|
|
471
|
+
.stat-pass { color: #4ade80; font-weight: 600; }
|
|
472
|
+
.stat-fail { color: #f87171; font-weight: 600; }
|
|
473
|
+
.stat-skip { color: #facc15; font-weight: 600; }
|
|
474
|
+
.bkt-dur { font-size: 12px; color: #94a3b8; }
|
|
475
|
+
|
|
476
|
+
.bkt-chevron {
|
|
477
|
+
font-size: 12px; color: #475569;
|
|
478
|
+
transition: transform .2s;
|
|
479
|
+
flex-shrink: 0;
|
|
480
|
+
}
|
|
481
|
+
.bkt-header[aria-expanded="false"] .bkt-chevron {
|
|
482
|
+
transform: rotate(-90deg);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* collapsible body */
|
|
486
|
+
.bkt-body { padding: 0 1.25rem 1rem; }
|
|
487
|
+
.bkt-body.collapsed { display: none; }
|
|
488
|
+
|
|
489
|
+
/* ── Test table ──────────────────────────────────────────── */
|
|
490
|
+
.test-table {
|
|
491
|
+
width: 100%;
|
|
492
|
+
border-collapse: collapse;
|
|
493
|
+
font-size: 12px;
|
|
494
|
+
margin-top: .75rem;
|
|
495
|
+
table-layout: fixed;
|
|
496
|
+
}
|
|
497
|
+
.test-table th {
|
|
498
|
+
font-size: 10px; text-transform: uppercase;
|
|
499
|
+
letter-spacing: .05em; color: #475569;
|
|
500
|
+
padding: 7px 10px; text-align: left;
|
|
501
|
+
border-bottom: 1px solid #1e2235;
|
|
502
|
+
background: #0d0f1a;
|
|
503
|
+
}
|
|
504
|
+
.test-table td {
|
|
505
|
+
padding: 8px 10px;
|
|
506
|
+
border-bottom: 1px solid #131625;
|
|
507
|
+
vertical-align: top;
|
|
508
|
+
}
|
|
509
|
+
.test-table tr:last-child td { border-bottom: none; }
|
|
510
|
+
.test-table tr:hover td { background: rgba(255,255,255,.02); }
|
|
511
|
+
|
|
512
|
+
/* row tints */
|
|
513
|
+
tr.row-fail td { background: rgba(248,113,113,.05); }
|
|
514
|
+
tr.row-skip td { background: rgba(250,204,21, .04); }
|
|
515
|
+
tr.row-timeout td { background: rgba(251,146,60, .05); }
|
|
516
|
+
|
|
517
|
+
/* status cell */
|
|
518
|
+
.td-status {
|
|
519
|
+
display: flex; align-items: center; gap: 6px;
|
|
520
|
+
white-space: nowrap;
|
|
521
|
+
}
|
|
522
|
+
.s-dot {
|
|
523
|
+
width: 6px; height: 6px;
|
|
524
|
+
border-radius: 50%; flex-shrink: 0;
|
|
525
|
+
}
|
|
526
|
+
.s-passed { background: #4ade80; }
|
|
527
|
+
.s-failed { background: #f87171; }
|
|
528
|
+
.s-skipped { background: #facc15; }
|
|
529
|
+
.s-timedOut { background: #fb923c; }
|
|
530
|
+
.s-interrupted { background: #a78bfa; }
|
|
531
|
+
.s-lbl { font-size: 11px; color: #94a3b8; }
|
|
532
|
+
|
|
533
|
+
.badge-flaky {
|
|
534
|
+
font-size: 10px; padding: 1px 6px;
|
|
535
|
+
border-radius: 999px;
|
|
536
|
+
background: rgba(250,204,21,.12);
|
|
537
|
+
color: #facc15;
|
|
538
|
+
border: 1px solid rgba(250,204,21,.3);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/* title + file cell */
|
|
542
|
+
.td-title { color: #e2e8f0; word-break: break-word; }
|
|
543
|
+
.td-file {
|
|
544
|
+
font-size: 11px; color: #475569;
|
|
545
|
+
font-family: 'SFMono-Regular', Consolas, monospace;
|
|
546
|
+
margin-top: 2px; word-break: break-all;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/* tag pills */
|
|
550
|
+
.tag-pill {
|
|
551
|
+
display: inline-block;
|
|
552
|
+
font-size: 10px; padding: 1px 7px;
|
|
553
|
+
border-radius: 999px;
|
|
554
|
+
background: rgba(96,165,250,.1);
|
|
555
|
+
color: #93c5fd;
|
|
556
|
+
border: 1px solid rgba(96,165,250,.25);
|
|
557
|
+
margin-right: 3px; margin-bottom: 2px;
|
|
558
|
+
white-space: nowrap;
|
|
559
|
+
}
|
|
560
|
+
.no-tags { color: #334155; }
|
|
561
|
+
|
|
562
|
+
.td-dur { color: #64748b; white-space: nowrap; }
|
|
563
|
+
.td-retry { color: #facc15; font-size: 11px; }
|
|
564
|
+
|
|
565
|
+
/* error cell */
|
|
566
|
+
.err-details { cursor: pointer; }
|
|
567
|
+
.err-summary {
|
|
568
|
+
color: #f87171; font-size: 11px;
|
|
569
|
+
list-style: none; cursor: pointer;
|
|
570
|
+
overflow: hidden; text-overflow: ellipsis;
|
|
571
|
+
white-space: nowrap; max-width: 260px;
|
|
572
|
+
display: block;
|
|
573
|
+
}
|
|
574
|
+
.err-summary::-webkit-details-marker { display: none; }
|
|
575
|
+
.err-summary::before {
|
|
576
|
+
content: '▸ ';
|
|
577
|
+
font-size: 9px; color: #f87171;
|
|
578
|
+
}
|
|
579
|
+
details[open] .err-summary::before { content: '▾ '; }
|
|
580
|
+
.err-pre {
|
|
581
|
+
margin-top: .5rem; padding: .625rem;
|
|
582
|
+
background: #0d0f1a;
|
|
583
|
+
border-radius: 4px; font-size: 11px;
|
|
584
|
+
overflow-x: auto; white-space: pre-wrap;
|
|
585
|
+
word-break: break-word; color: #fca5a5;
|
|
586
|
+
border: 1px solid #1e2235;
|
|
587
|
+
max-height: 200px; overflow-y: auto;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/* ── Footer ──────────────────────────────────────────────── */
|
|
591
|
+
.footer {
|
|
592
|
+
text-align: center; margin-top: 3rem;
|
|
593
|
+
font-size: 11px; color: #334155;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/* ── Scroll offset for anchor links ─────────────────────── */
|
|
597
|
+
.bucket { scroll-margin-top: 1rem; }
|
|
598
|
+
</style>
|
|
599
|
+
</head>
|
|
600
|
+
<body>
|
|
601
|
+
|
|
602
|
+
<!-- ── Header ── -->
|
|
603
|
+
<div class="header">
|
|
604
|
+
<div class="header-top">
|
|
605
|
+
<div class="overall-status ${overallCls}">
|
|
606
|
+
<span class="status-dot"></span>
|
|
607
|
+
<span class="status-text">${overallLabel}</span>
|
|
608
|
+
</div>
|
|
609
|
+
<div class="run-meta">
|
|
610
|
+
<div><strong>Mode:</strong> ${escapeHtml(summary.orderMode)}</div>
|
|
611
|
+
<div><strong>Policy:</strong> ${escapeHtml(summary.failurePolicy)}</div>
|
|
612
|
+
<div><strong>Started:</strong> ${new Date(summary.startedAt).toLocaleString()}</div>
|
|
613
|
+
<div><strong>Finished:</strong> ${new Date(summary.finishedAt).toLocaleString()}</div>
|
|
614
|
+
<div><strong>Duration:</strong> ${formatDuration(summary.totalDuration)}</div>
|
|
615
|
+
<div><strong>Buckets:</strong> ${summary.totals.buckets}</div>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
${generateProgressBar(summary)}
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
<!-- ── Summary cards ── -->
|
|
622
|
+
<div class="summary-cards">
|
|
623
|
+
<div class="card">
|
|
624
|
+
<div class="card-val cv-total">${summary.totals.tests}</div>
|
|
625
|
+
<div class="card-lbl">Total</div>
|
|
626
|
+
</div>
|
|
627
|
+
<div class="card">
|
|
628
|
+
<div class="card-val cv-pass">${summary.totals.passed}</div>
|
|
629
|
+
<div class="card-lbl">Passed</div>
|
|
630
|
+
</div>
|
|
631
|
+
<div class="card">
|
|
632
|
+
<div class="card-val cv-fail">${summary.totals.failed}</div>
|
|
633
|
+
<div class="card-lbl">Failed</div>
|
|
634
|
+
</div>
|
|
635
|
+
<div class="card">
|
|
636
|
+
<div class="card-val cv-skip">${summary.totals.skipped}</div>
|
|
637
|
+
<div class="card-lbl">Skipped</div>
|
|
638
|
+
</div>
|
|
639
|
+
<div class="card">
|
|
640
|
+
<div class="card-val cv-rate">${summary.totals.tests > 0
|
|
641
|
+
? Math.round((summary.totals.passed / summary.totals.tests) * 100)
|
|
642
|
+
: 0}%</div>
|
|
643
|
+
<div class="card-lbl">Pass rate</div>
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
|
|
647
|
+
<!-- ── Bucket navigation ── -->
|
|
648
|
+
${generateBucketNav(summary.buckets)}
|
|
649
|
+
|
|
650
|
+
<!-- ── Bucket sections ── -->
|
|
651
|
+
${bucketSections}
|
|
652
|
+
|
|
653
|
+
<div class="footer">
|
|
654
|
+
Generated by playwright-order-manager •
|
|
655
|
+
${new Date(summary.finishedAt).toLocaleString()}
|
|
656
|
+
</div>
|
|
657
|
+
|
|
658
|
+
<script>
|
|
659
|
+
function toggleBucket(btn) {
|
|
660
|
+
var expanded = btn.getAttribute('aria-expanded') === 'true';
|
|
661
|
+
btn.setAttribute('aria-expanded', String(!expanded));
|
|
662
|
+
var bodyId = btn.getAttribute('aria-controls');
|
|
663
|
+
var body = document.getElementById(bodyId);
|
|
664
|
+
if (body) {
|
|
665
|
+
body.classList.toggle('collapsed', expanded);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// On page load — auto-collapse all passing buckets
|
|
670
|
+
// if there are any failing ones, so failures are immediately visible
|
|
671
|
+
window.addEventListener('DOMContentLoaded', function() {
|
|
672
|
+
var hasFailing = document.querySelector('.bucket.bkt-fail');
|
|
673
|
+
if (!hasFailing) return;
|
|
674
|
+
|
|
675
|
+
document.querySelectorAll('.bucket.bkt-pass .bkt-header').forEach(function(btn) {
|
|
676
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
677
|
+
var bodyId = btn.getAttribute('aria-controls');
|
|
678
|
+
var body = document.getElementById(bodyId);
|
|
679
|
+
if (body) body.classList.add('collapsed');
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
</script>
|
|
683
|
+
|
|
684
|
+
</body>
|
|
685
|
+
</html>`;
|
|
686
|
+
}
|
|
687
|
+
// =============================================================================
|
|
688
|
+
// PUBLIC API
|
|
689
|
+
// =============================================================================
|
|
690
|
+
class OrderedSummary {
|
|
691
|
+
/**
|
|
692
|
+
* Writes the ordered run summary to disk as both JSON and HTML.
|
|
693
|
+
*
|
|
694
|
+
* @param summary - The complete run summary to write
|
|
695
|
+
* @param reportRoot - Directory to write into. Defaults to RunnerConstants.DEFAULTS.REPORT_ROOT
|
|
696
|
+
* @returns Absolute paths of the files written: { jsonPath, htmlPath }
|
|
697
|
+
*/
|
|
698
|
+
static write(summary, reportRoot = constants_1.RunnerConstants.DEFAULTS.REPORT_ROOT) {
|
|
699
|
+
ensureDir(reportRoot);
|
|
700
|
+
const jsonPath = path.join(reportRoot, constants_1.RunnerConstants.DEFAULTS.SUMMARY_FILENAME);
|
|
701
|
+
const htmlPath = path.join(reportRoot, constants_1.RunnerConstants.DEFAULTS.REPORT_FILENAME);
|
|
702
|
+
try {
|
|
703
|
+
fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), 'utf8');
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
throw new Error(`OrderedSummary.write: failed to write JSON to ${jsonPath}\n` +
|
|
707
|
+
`Cause: ${err.message}`);
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
fs.writeFileSync(htmlPath, generateHtml(summary), 'utf8');
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
throw new Error(`OrderedSummary.write: failed to write HTML to ${htmlPath}\n` +
|
|
714
|
+
`Cause: ${err.message}`);
|
|
715
|
+
}
|
|
716
|
+
return { jsonPath, htmlPath };
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Builds an OrderedRunSummary from raw bucket execution records.
|
|
720
|
+
* Call this after all buckets finish, then pass the result to write().
|
|
721
|
+
*/
|
|
722
|
+
static buildSummary(buckets, startedAt, orderMode, failurePolicy) {
|
|
723
|
+
const finishedAt = new Date().toISOString();
|
|
724
|
+
const startMs = new Date(startedAt).getTime();
|
|
725
|
+
const endMs = new Date(finishedAt).getTime();
|
|
726
|
+
const totals = buckets.reduce((acc, bucket) => ({
|
|
727
|
+
tests: acc.tests + bucket.totalTests,
|
|
728
|
+
passed: acc.passed + bucket.passed,
|
|
729
|
+
failed: acc.failed + bucket.failed,
|
|
730
|
+
skipped: acc.skipped + bucket.skipped,
|
|
731
|
+
buckets: acc.buckets + 1,
|
|
732
|
+
}), { tests: 0, passed: 0, failed: 0, skipped: 0, buckets: 0 });
|
|
733
|
+
const success = buckets.every((b) => b.status !== 'failed');
|
|
734
|
+
return {
|
|
735
|
+
startedAt,
|
|
736
|
+
finishedAt,
|
|
737
|
+
totalDuration: endMs - startMs,
|
|
738
|
+
orderMode: orderMode,
|
|
739
|
+
failurePolicy: failurePolicy,
|
|
740
|
+
totals,
|
|
741
|
+
success,
|
|
742
|
+
buckets,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
exports.OrderedSummary = OrderedSummary;
|
|
747
|
+
//# sourceMappingURL=OrderedSummary.js.map
|