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.
Files changed (37) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +240 -0
  3. package/bin/run.js +133 -0
  4. package/dist/constants/index.d.ts +72 -0
  5. package/dist/constants/index.d.ts.map +1 -0
  6. package/dist/constants/index.js +93 -0
  7. package/dist/constants/index.js.map +1 -0
  8. package/dist/core/OrderedExecution.d.ts +52 -0
  9. package/dist/core/OrderedExecution.d.ts.map +1 -0
  10. package/dist/core/OrderedExecution.js +253 -0
  11. package/dist/core/OrderedExecution.js.map +1 -0
  12. package/dist/core/OrderedReportParser.d.ts +38 -0
  13. package/dist/core/OrderedReportParser.d.ts.map +1 -0
  14. package/dist/core/OrderedReportParser.js +169 -0
  15. package/dist/core/OrderedReportParser.js.map +1 -0
  16. package/dist/core/OrderedSummary.d.ts +20 -0
  17. package/dist/core/OrderedSummary.d.ts.map +1 -0
  18. package/dist/core/OrderedSummary.js +747 -0
  19. package/dist/core/OrderedSummary.js.map +1 -0
  20. package/dist/fixtures/index.d.ts +30 -0
  21. package/dist/fixtures/index.d.ts.map +1 -0
  22. package/dist/fixtures/index.js +212 -0
  23. package/dist/fixtures/index.js.map +1 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +31 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/runner/TestOrderManager.d.ts +62 -0
  29. package/dist/runner/TestOrderManager.d.ts.map +1 -0
  30. package/dist/runner/TestOrderManager.js +490 -0
  31. package/dist/runner/TestOrderManager.js.map +1 -0
  32. package/dist/types/index.d.ts +215 -0
  33. package/dist/types/index.d.ts.map +1 -0
  34. package/dist/types/index.js +6 -0
  35. package/dist/types/index.js.map +1 -0
  36. package/package.json +65 -0
  37. 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, '&amp;')
61
+ .replace(/</g, '&lt;')
62
+ .replace(/>/g, '&gt;')
63
+ .replace(/"/g, '&quot;')
64
+ .replace(/'/g, '&#039;');
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 &nbsp;(${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)} &rarr; ${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 &amp; 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">&#9660;</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 &nbsp;&bull;&nbsp;
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