playwright-reborn 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/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/fixture.d.ts +4 -0
- package/dist/fixture.js +53 -0
- package/dist/fixture.js.map +1 -0
- package/dist/format.d.ts +16 -0
- package/dist/format.js +113 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/model.d.ts +102 -0
- package/dist/model.js +3 -0
- package/dist/model.js.map +1 -0
- package/dist/open.d.ts +1 -0
- package/dist/open.js +19 -0
- package/dist/open.js.map +1 -0
- package/dist/options.d.ts +26 -0
- package/dist/options.js +54 -0
- package/dist/options.js.map +1 -0
- package/dist/paths.d.ts +3 -0
- package/dist/paths.js +71 -0
- package/dist/paths.js.map +1 -0
- package/dist/render.d.ts +3 -0
- package/dist/render.js +112 -0
- package/dist/render.js.map +1 -0
- package/dist/serialize.d.ts +87 -0
- package/dist/serialize.js +230 -0
- package/dist/serialize.js.map +1 -0
- package/dist/status.d.ts +7 -0
- package/dist/status.js +69 -0
- package/dist/status.js.map +1 -0
- package/dist/ui/fonts/Geist-OFL.txt +93 -0
- package/dist/ui/fonts/JetBrainsMono-OFL.txt +93 -0
- package/dist/ui/fonts/geist-latin-400-normal.woff2 +0 -0
- package/dist/ui/fonts/geist-latin-500-normal.woff2 +0 -0
- package/dist/ui/fonts/geist-latin-600-normal.woff2 +0 -0
- package/dist/ui/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/dist/ui/report.css +1147 -0
- package/dist/ui/report.js +1349 -0
- package/package.json +60 -0
|
@@ -0,0 +1,1349 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
3
|
+
var app = document.getElementById('app');
|
|
4
|
+
var raw = document.getElementById('reborn-data');
|
|
5
|
+
var data;
|
|
6
|
+
try {
|
|
7
|
+
data = JSON.parse((raw && raw.textContent) || '');
|
|
8
|
+
} catch (error) {
|
|
9
|
+
app.textContent = 'This report could not be read.';
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (!data || !Array.isArray(data.tests)) {
|
|
13
|
+
app.textContent = 'This report could not be read.';
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
var TABS = ['overview', 'tests', 'screenshots'];
|
|
18
|
+
if (data.showLogs !== false) TABS.push('logs');
|
|
19
|
+
if (data.showFiles !== false) TABS.push('files');
|
|
20
|
+
var route = readRoute();
|
|
21
|
+
var state = {
|
|
22
|
+
tab: route.tab,
|
|
23
|
+
filter: 'all',
|
|
24
|
+
query: '',
|
|
25
|
+
selectedId: route.id || initialFailure(),
|
|
26
|
+
attempt: 0,
|
|
27
|
+
showApi: false,
|
|
28
|
+
testPane: 'steps',
|
|
29
|
+
listLimit: 200,
|
|
30
|
+
shotFilter: 'all',
|
|
31
|
+
logQuery: '',
|
|
32
|
+
overview: data.overview === 'timeline' ? 'timeline' : 'chart',
|
|
33
|
+
order: 'grouped',
|
|
34
|
+
band: 'all',
|
|
35
|
+
};
|
|
36
|
+
var selected = testById(state.selectedId);
|
|
37
|
+
if (selected) {
|
|
38
|
+
state.attempt = failedAttempt(selected);
|
|
39
|
+
state.testPane = defaultPane(selected);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var built = { tests: false, screenshots: false, logs: false, files: false };
|
|
43
|
+
var findTimer = 0;
|
|
44
|
+
|
|
45
|
+
app.innerHTML = [
|
|
46
|
+
'<div class="atmosphere" aria-hidden="true"><canvas class="bands"></canvas><div class="grain"></div><div class="veil"></div></div>',
|
|
47
|
+
'<div class="shell">',
|
|
48
|
+
'<aside class="rail">',
|
|
49
|
+
'<div class="tabbar" role="tablist" aria-label="Report">',
|
|
50
|
+
'<span class="tab-glow" id="tab-glow"></span>',
|
|
51
|
+
TABS.map(function (id) {
|
|
52
|
+
return tabButton(id, id.charAt(0).toUpperCase() + id.slice(1));
|
|
53
|
+
}).join(''),
|
|
54
|
+
'</div>',
|
|
55
|
+
'<p class="meta" id="meta"></p>',
|
|
56
|
+
'</aside>',
|
|
57
|
+
'<div class="stage">',
|
|
58
|
+
'<div id="panel-overview" role="tabpanel"></div>',
|
|
59
|
+
'<div id="panel-tests" role="tabpanel" hidden></div>',
|
|
60
|
+
'<div id="panel-screenshots" role="tabpanel" hidden></div>',
|
|
61
|
+
'<div id="panel-logs" role="tabpanel" hidden></div>',
|
|
62
|
+
'<div id="panel-files" role="tabpanel" hidden></div>',
|
|
63
|
+
'<p class="foot" id="foot"></p>',
|
|
64
|
+
'</div>',
|
|
65
|
+
'</div>',
|
|
66
|
+
].join('');
|
|
67
|
+
|
|
68
|
+
document.body.dataset.accent = accentName();
|
|
69
|
+
document.title = companyName() + (data.summary ? ' · ' + data.summary : '');
|
|
70
|
+
fill(document.getElementById('meta'), metaBits().map(function (bit) { return el('span', null, [bit]); }));
|
|
71
|
+
fill(document.getElementById('foot'), [
|
|
72
|
+
el('span', null, ['Open source by World of Z']),
|
|
73
|
+
el('span', null, [formatWhen(data.generatedAt)]),
|
|
74
|
+
el('span', null, ['Keys 1 to 5 switch tabs. Press / to find a test.']),
|
|
75
|
+
]);
|
|
76
|
+
var BANDS = [
|
|
77
|
+
['under1', 'Under 1 minute'],
|
|
78
|
+
['1to3', '1 to 3 minutes'],
|
|
79
|
+
['3to5', '3 to 5 minutes'],
|
|
80
|
+
['over5', 'Over 5 minutes'],
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
paintOverview();
|
|
84
|
+
showTab(state.tab, true);
|
|
85
|
+
bind();
|
|
86
|
+
watchHeader();
|
|
87
|
+
startCurtain();
|
|
88
|
+
|
|
89
|
+
function startCurtain() {
|
|
90
|
+
var canvas = document.querySelector('.atmosphere .bands');
|
|
91
|
+
if (!canvas || !canvas.getContext) return;
|
|
92
|
+
var ctx = canvas.getContext('2d');
|
|
93
|
+
var ratio = 1;
|
|
94
|
+
var columns = [];
|
|
95
|
+
var count = 22;
|
|
96
|
+
for (var i = 0; i < count; i += 1) {
|
|
97
|
+
var n = Math.sin(210 + i * 2.17);
|
|
98
|
+
columns.push({
|
|
99
|
+
at: (i + 0.35 + n * 0.15) / count,
|
|
100
|
+
width: 0.012 + (0.5 + n * 0.5) * 0.018,
|
|
101
|
+
speed: 0.22 + (i % 6) * 0.05,
|
|
102
|
+
phase: i * 1.37,
|
|
103
|
+
depth: i % 3,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function resize() {
|
|
107
|
+
ratio = Math.min(window.devicePixelRatio || 1, 2);
|
|
108
|
+
var rect = canvas.getBoundingClientRect();
|
|
109
|
+
canvas.width = Math.max(1, Math.floor(rect.width * ratio));
|
|
110
|
+
canvas.height = Math.max(1, Math.floor(rect.height * ratio));
|
|
111
|
+
}
|
|
112
|
+
var rgb = accentParts();
|
|
113
|
+
function draw(now) {
|
|
114
|
+
var w = canvas.width;
|
|
115
|
+
var h = canvas.height;
|
|
116
|
+
var t = reduce ? 8 : now / 1000;
|
|
117
|
+
ctx.clearRect(0, 0, w, h);
|
|
118
|
+
var wash = ctx.createLinearGradient(0, 0, 0, h);
|
|
119
|
+
wash.addColorStop(0, '#123d24');
|
|
120
|
+
wash.addColorStop(0.42, '#0a2416');
|
|
121
|
+
wash.addColorStop(1, '#050806');
|
|
122
|
+
ctx.fillStyle = wash;
|
|
123
|
+
ctx.fillRect(0, 0, w, h);
|
|
124
|
+
columns.forEach(function (column) {
|
|
125
|
+
var sway = Math.sin(t * column.speed + column.phase) * 0.02;
|
|
126
|
+
var breath = 0.4 + 0.6 * (0.5 + 0.5 * Math.sin(t * column.speed * 1.4 + column.phase));
|
|
127
|
+
var x = (column.at + sway) * w;
|
|
128
|
+
var spread = Math.max(10, column.width * w * (0.55 + breath * 0.35));
|
|
129
|
+
var gradient = ctx.createLinearGradient(x - spread, 0, x + spread, 0);
|
|
130
|
+
var alpha = 0.28 + 0.62 * breath;
|
|
131
|
+
gradient.addColorStop(0, 'rgba(' + rgb + ', 0)');
|
|
132
|
+
gradient.addColorStop(0.42, 'rgba(' + rgb + ', ' + (alpha * 0.45).toFixed(3) + ')');
|
|
133
|
+
gradient.addColorStop(0.5, 'rgba(' + rgb + ', ' + alpha.toFixed(3) + ')');
|
|
134
|
+
gradient.addColorStop(0.58, 'rgba(' + rgb + ', ' + (alpha * 0.45).toFixed(3) + ')');
|
|
135
|
+
gradient.addColorStop(1, 'rgba(' + rgb + ', 0)');
|
|
136
|
+
ctx.fillStyle = gradient;
|
|
137
|
+
ctx.fillRect(x - spread, 0, spread * 2, h * 0.92);
|
|
138
|
+
});
|
|
139
|
+
if (!reduce) requestAnimationFrame(draw);
|
|
140
|
+
}
|
|
141
|
+
resize();
|
|
142
|
+
window.addEventListener('resize', resize);
|
|
143
|
+
requestAnimationFrame(function (now) {
|
|
144
|
+
if (!canvas.width) resize();
|
|
145
|
+
draw(now);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function watchHeader() {
|
|
150
|
+
var rail = document.querySelector('.rail');
|
|
151
|
+
if (!rail) return;
|
|
152
|
+
var lastY = window.scrollY;
|
|
153
|
+
window.addEventListener('scroll', function () {
|
|
154
|
+
var y = window.scrollY;
|
|
155
|
+
var down = y > lastY + 6;
|
|
156
|
+
var up = y < lastY - 6;
|
|
157
|
+
if (y < 24) rail.classList.remove('is-hidden');
|
|
158
|
+
else if (down) rail.classList.add('is-hidden');
|
|
159
|
+
else if (up) rail.classList.remove('is-hidden');
|
|
160
|
+
lastY = y;
|
|
161
|
+
}, { passive: true });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function companyName() {
|
|
165
|
+
var name = typeof data.company === 'string' ? data.company.trim() : '';
|
|
166
|
+
return name || 'Sandata';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function accentName() {
|
|
170
|
+
var allowed = { green: 1, red: 1, blue: 1, amber: 1, violet: 1 };
|
|
171
|
+
return allowed[data.accent] ? data.accent : 'green';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function accentParts() {
|
|
175
|
+
var probe = document.createElement('span');
|
|
176
|
+
probe.style.color = 'var(--accent)';
|
|
177
|
+
document.body.appendChild(probe);
|
|
178
|
+
var color = getComputedStyle(probe).color;
|
|
179
|
+
probe.remove();
|
|
180
|
+
var parts = color.match(/\d+/g) || ['0', '255', '136'];
|
|
181
|
+
return parts.slice(0, 3).join(', ');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function tabButton(id, label) {
|
|
185
|
+
return '<button class="tab" role="tab" type="button" data-tab="' + id + '" aria-selected="false" aria-controls="panel-' + id + '">' + label + '</button>';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function readRoute() {
|
|
189
|
+
var hash = '';
|
|
190
|
+
try { hash = decodeURIComponent(location.hash.slice(1)); } catch (error) { hash = ''; }
|
|
191
|
+
if (!hash || hash === 'overview') return { tab: 'overview' };
|
|
192
|
+
if (TABS.indexOf(hash) !== -1) return { tab: hash };
|
|
193
|
+
if (hash.indexOf('tests/') === 0) return { tab: 'tests', id: decodeURIComponent(hash.slice(6)) };
|
|
194
|
+
if (data.tests.some(function (test) { return test.id === hash; })) return { tab: 'tests', id: hash };
|
|
195
|
+
return { tab: 'overview' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function initialFailure() {
|
|
199
|
+
var problem = data.tests.find(isProblem);
|
|
200
|
+
return problem ? problem.id : (data.tests[0] && data.tests[0].id) || null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function showTab(tab, quiet) {
|
|
204
|
+
if (TABS.indexOf(tab) === -1) tab = 'overview';
|
|
205
|
+
state.tab = tab;
|
|
206
|
+
document.querySelectorAll('[role="tab"]').forEach(function (button) {
|
|
207
|
+
button.setAttribute('aria-selected', button.dataset.tab === tab ? 'true' : 'false');
|
|
208
|
+
});
|
|
209
|
+
TABS.forEach(function (name) {
|
|
210
|
+
var panel = document.getElementById('panel-' + name);
|
|
211
|
+
var on = name === tab;
|
|
212
|
+
panel.hidden = !on;
|
|
213
|
+
panel.classList.toggle('panel', true);
|
|
214
|
+
if (on && !quiet && !reduce) {
|
|
215
|
+
panel.classList.remove('panel-enter');
|
|
216
|
+
void panel.offsetWidth;
|
|
217
|
+
panel.classList.add('panel-enter');
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
if (tab === 'tests') ensureTests();
|
|
221
|
+
if (tab === 'screenshots') ensureShots();
|
|
222
|
+
if (tab === 'logs') ensureLogs();
|
|
223
|
+
if (tab === 'files') ensureFiles();
|
|
224
|
+
writeHash();
|
|
225
|
+
placeTabGlow();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function writeHash() {
|
|
229
|
+
var next = state.tab === 'overview' ? 'overview' : state.tab;
|
|
230
|
+
if (state.tab === 'tests' && state.selectedId) next = 'tests/' + encodeURIComponent(state.selectedId);
|
|
231
|
+
if (window.history && history.replaceState) history.replaceState(null, '', '#' + next);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function placeTabGlow() {
|
|
235
|
+
var bar = document.querySelector('.tabbar');
|
|
236
|
+
var glow = document.getElementById('tab-glow');
|
|
237
|
+
var active = document.querySelector('.tab[aria-selected="true"]');
|
|
238
|
+
if (!bar || !glow || !active) return;
|
|
239
|
+
var barBox = bar.getBoundingClientRect();
|
|
240
|
+
var box = active.getBoundingClientRect();
|
|
241
|
+
glow.style.width = box.width + 'px';
|
|
242
|
+
glow.style.transform = 'translateX(' + (box.left - barBox.left - 4) + 'px)';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function paintOverview() {
|
|
246
|
+
var host = document.getElementById('panel-overview');
|
|
247
|
+
host.innerHTML = [
|
|
248
|
+
'<header class="hero">',
|
|
249
|
+
'<div><h1 class="word" id="word"></h1><p class="credit">Open source by World of Z</p><p class="summary" id="summary"></p></div>',
|
|
250
|
+
'<p class="time" id="time"></p>',
|
|
251
|
+
'</header>',
|
|
252
|
+
'<div id="errors"></div>',
|
|
253
|
+
'<section class="console">',
|
|
254
|
+
'<div class="console-body">',
|
|
255
|
+
'<p class="section-label">Results</p>',
|
|
256
|
+
'<div class="legend" id="legend"></div>',
|
|
257
|
+
'<div class="range-head">',
|
|
258
|
+
'<p class="section-label" id="range-label">Duration</p>',
|
|
259
|
+
'<div class="views" role="group" aria-label="Overview shape">',
|
|
260
|
+
'<button type="button" class="view" data-overview="chart">Chart</button>',
|
|
261
|
+
'<button type="button" class="view" data-overview="timeline">Timeline</button>',
|
|
262
|
+
'</div>',
|
|
263
|
+
'</div>',
|
|
264
|
+
'<p class="section-note" id="range-note"></p>',
|
|
265
|
+
'<div class="legend" id="duration"></div>',
|
|
266
|
+
'<div class="strip-window" id="strip-window"><div class="strip-scroll"><div class="strip" id="strip" role="group" aria-label="Tests by time"></div></div></div>',
|
|
267
|
+
'</div>',
|
|
268
|
+
'</section>',
|
|
269
|
+
].join('');
|
|
270
|
+
var word = document.getElementById('word');
|
|
271
|
+
word.textContent = companyName();
|
|
272
|
+
document.getElementById('summary').textContent = data.summary || '';
|
|
273
|
+
countUp(document.getElementById('time'), data.duration || 0, formatDuration);
|
|
274
|
+
paintErrors();
|
|
275
|
+
paintCounts(document.getElementById('legend'), false);
|
|
276
|
+
paintDuration();
|
|
277
|
+
paintStrip();
|
|
278
|
+
applyOverview();
|
|
279
|
+
showNow(state.selectedId);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function bandLabel(id) {
|
|
283
|
+
var found = BANDS.find(function (band) { return band[0] === id; });
|
|
284
|
+
return found ? found[1] : '';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Same cuts as durationBand in src/format.ts: 1:00, 3:00, and 5:00.
|
|
288
|
+
function durationBand(ms) {
|
|
289
|
+
if (!isFinite(ms) || ms < 0) ms = 0;
|
|
290
|
+
if (ms < 60000) return 'under1';
|
|
291
|
+
if (ms < 180000) return '1to3';
|
|
292
|
+
if (ms < 300000) return '3to5';
|
|
293
|
+
return 'over5';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function countedTests() {
|
|
297
|
+
return data.tests.filter(function (test) { return test.status !== 'skipped'; });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function paintDuration() {
|
|
301
|
+
var host = document.getElementById('duration');
|
|
302
|
+
if (!host) return;
|
|
303
|
+
var counts = { under1: 0, '1to3': 0, '3to5': 0, over5: 0 };
|
|
304
|
+
countedTests().forEach(function (test) {
|
|
305
|
+
counts[durationBand(test.duration)] += 1;
|
|
306
|
+
});
|
|
307
|
+
var max = 1;
|
|
308
|
+
BANDS.forEach(function (band) { max = Math.max(max, counts[band[0]]); });
|
|
309
|
+
fill(host, BANDS.map(function (band) {
|
|
310
|
+
var fillBar = el('span', { class: 'band-fill' });
|
|
311
|
+
fillBar.style.width = Math.round((counts[band[0]] / max) * 100) + '%';
|
|
312
|
+
return el('button', {
|
|
313
|
+
class: 'band',
|
|
314
|
+
type: 'button',
|
|
315
|
+
'data-band': band[0],
|
|
316
|
+
'aria-pressed': state.band === band[0] ? 'true' : 'false',
|
|
317
|
+
}, [
|
|
318
|
+
el('span', { class: 'n' }, [String(counts[band[0]])]),
|
|
319
|
+
el('span', { class: 'band-label' }, [band[1]]),
|
|
320
|
+
el('span', { class: 'band-track' }, [fillBar]),
|
|
321
|
+
]);
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function applyOverview() {
|
|
326
|
+
var chart = state.overview !== 'timeline';
|
|
327
|
+
var duration = document.getElementById('duration');
|
|
328
|
+
var strip = document.getElementById('strip-window');
|
|
329
|
+
var label = document.getElementById('range-label');
|
|
330
|
+
var note = document.getElementById('range-note');
|
|
331
|
+
if (duration) duration.hidden = !chart;
|
|
332
|
+
if (strip) strip.hidden = chart;
|
|
333
|
+
if (label) label.textContent = chart ? 'Duration' : 'Timeline';
|
|
334
|
+
if (note) {
|
|
335
|
+
var text = chart
|
|
336
|
+
? 'How many tests landed in each range. The time includes retries.'
|
|
337
|
+
: 'Tests in the order they started. The bar is how long each one took.';
|
|
338
|
+
if (chart && data.counts && data.counts.skipped) text += ' Skipped tests are not in these counts.';
|
|
339
|
+
note.textContent = text;
|
|
340
|
+
}
|
|
341
|
+
document.querySelectorAll('.view').forEach(function (button) {
|
|
342
|
+
button.setAttribute('aria-pressed', button.dataset.overview === state.overview ? 'true' : 'false');
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function paintErrors() {
|
|
347
|
+
var host = document.getElementById('errors');
|
|
348
|
+
var items = [];
|
|
349
|
+
(data.errors || []).forEach(function (error) {
|
|
350
|
+
items.push(el('div', { class: 'banner' }, [el('pre', null, [error.message || 'Runner error'])]));
|
|
351
|
+
});
|
|
352
|
+
(data.warnings || []).forEach(function (warning) {
|
|
353
|
+
items.push(el('div', { class: 'banner' }, [el('p', null, [warning])]));
|
|
354
|
+
});
|
|
355
|
+
fill(host, items);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function paintCounts(host, withFind) {
|
|
359
|
+
var counts = data.counts || {};
|
|
360
|
+
var filters = [
|
|
361
|
+
['all', 'ran', counts.total || 0],
|
|
362
|
+
['passed', 'passed', counts.passed || 0],
|
|
363
|
+
['failed', 'failed', counts.failed || 0],
|
|
364
|
+
['flaky', 'flaky', counts.flaky || 0],
|
|
365
|
+
['skipped', 'skipped', counts.skipped || 0],
|
|
366
|
+
];
|
|
367
|
+
if (counts.timedOut) filters.push(['timedOut', 'timed out', counts.timedOut]);
|
|
368
|
+
if (counts.interrupted) filters.push(['interrupted', 'interrupted', counts.interrupted]);
|
|
369
|
+
var buttons = filters.map(function (filter) {
|
|
370
|
+
return el('button', { class: 'count', type: 'button', 'data-status': filter[0], 'aria-pressed': 'false' }, [
|
|
371
|
+
el('span', { class: 'n' }, [String(filter[2])]),
|
|
372
|
+
filter[1],
|
|
373
|
+
]);
|
|
374
|
+
});
|
|
375
|
+
if (host.id === 'legend') {
|
|
376
|
+
buttons.unshift(el('div', { class: 'count count-static' }, [
|
|
377
|
+
el('span', { class: 'n' }, [formatDuration(data.duration || 0)]),
|
|
378
|
+
'total',
|
|
379
|
+
]));
|
|
380
|
+
buttons.forEach(function (button) {
|
|
381
|
+
if (button.classList.contains('count-static')) return;
|
|
382
|
+
var node = button.querySelector('.n');
|
|
383
|
+
var value = Number(node.textContent);
|
|
384
|
+
node.textContent = '0';
|
|
385
|
+
countUp(node, value, function (next) { return String(Math.round(next)); });
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
var children = buttons;
|
|
389
|
+
if (withFind) {
|
|
390
|
+
children = buttons.concat([el('div', { class: 'find-wrap' }, [
|
|
391
|
+
el('input', { id: 'find', class: 'find', type: 'search', placeholder: 'Find a test', 'aria-label': 'Find a test', autocomplete: 'off' }),
|
|
392
|
+
])]);
|
|
393
|
+
}
|
|
394
|
+
fill(host, children);
|
|
395
|
+
syncPressed();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function syncPressed() {
|
|
399
|
+
document.querySelectorAll('.count').forEach(function (button) {
|
|
400
|
+
button.setAttribute('aria-pressed', button.dataset.status === state.filter ? 'true' : 'false');
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function spanOf(test) {
|
|
405
|
+
var start = Infinity;
|
|
406
|
+
var end = 0;
|
|
407
|
+
(test.attempts || []).forEach(function (attempt) {
|
|
408
|
+
var began = new Date(attempt.startTime).getTime();
|
|
409
|
+
if (!isFinite(began)) return;
|
|
410
|
+
start = Math.min(start, began);
|
|
411
|
+
end = Math.max(end, began + (attempt.duration || 0));
|
|
412
|
+
});
|
|
413
|
+
if (!isFinite(start) || start === Infinity) return null;
|
|
414
|
+
return { start: start, end: Math.max(end, start + 1) };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function paintStrip() {
|
|
418
|
+
var strip = document.getElementById('strip');
|
|
419
|
+
if (!data.tests.length) {
|
|
420
|
+
fill(strip, []);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
var max = data.tests.reduce(function (hi, test) { return Math.max(hi, test.duration || 0); }, 1);
|
|
424
|
+
var ordered = data.tests.slice().sort(function (a, b) {
|
|
425
|
+
return (spanOf(a) ? spanOf(a).start : Infinity) - (spanOf(b) ? spanOf(b).start : Infinity);
|
|
426
|
+
});
|
|
427
|
+
strip.className = 'run-list';
|
|
428
|
+
strip.style.height = '';
|
|
429
|
+
fill(strip, ordered.map(function (test) {
|
|
430
|
+
var fillBar = el('span', { class: 'run-fill', 'data-status': test.status });
|
|
431
|
+
fillBar.style.width = Math.max(2, Math.round(((test.duration || 0) / max) * 100)) + '%';
|
|
432
|
+
return el('button', {
|
|
433
|
+
class: 'run-row',
|
|
434
|
+
type: 'button',
|
|
435
|
+
'data-id': test.id,
|
|
436
|
+
'data-status': test.status,
|
|
437
|
+
'aria-label': statusLabel(test) + ', ' + test.title + ', ' + formatDuration(test.duration),
|
|
438
|
+
}, [
|
|
439
|
+
el('span', { class: 'sw', 'data-status': test.status }, [statusLabel(test)]),
|
|
440
|
+
el('span', { class: 'run-name' }, [test.title]),
|
|
441
|
+
el('span', { class: 'run-track' }, [fillBar]),
|
|
442
|
+
el('span', { class: 'run-time' }, [formatDuration(test.duration)]),
|
|
443
|
+
]);
|
|
444
|
+
}));
|
|
445
|
+
markStrip();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function paintCanvasStrip(host) {
|
|
449
|
+
var canvas = el('canvas', { class: 'strip-canvas', 'aria-label': 'Tests by duration' });
|
|
450
|
+
fill(host, [canvas]);
|
|
451
|
+
var hits = [];
|
|
452
|
+
function draw() {
|
|
453
|
+
var width = host.clientWidth || 600;
|
|
454
|
+
var height = 52;
|
|
455
|
+
var ratio = window.devicePixelRatio || 1;
|
|
456
|
+
canvas.width = Math.floor(width * ratio);
|
|
457
|
+
canvas.height = Math.floor(height * ratio);
|
|
458
|
+
canvas.style.width = width + 'px';
|
|
459
|
+
canvas.style.height = height + 'px';
|
|
460
|
+
var ctx = canvas.getContext('2d');
|
|
461
|
+
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
462
|
+
ctx.clearRect(0, 0, width, height);
|
|
463
|
+
var total = data.tests.reduce(function (sum, test) { return sum + Math.max(test.duration || 0, 1); }, 0);
|
|
464
|
+
var x = 0;
|
|
465
|
+
hits = [];
|
|
466
|
+
data.tests.forEach(function (test) {
|
|
467
|
+
var w = Math.max(2, (Math.max(test.duration || 0, 1) / total) * width);
|
|
468
|
+
ctx.fillStyle = canvasColor(test.status);
|
|
469
|
+
ctx.fillRect(x, 8, Math.max(1, w - 2), 36);
|
|
470
|
+
hits.push({ x: x, w: w, id: test.id });
|
|
471
|
+
x += w;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
draw();
|
|
475
|
+
window.addEventListener('resize', draw);
|
|
476
|
+
canvas.addEventListener('mousemove', function (event) {
|
|
477
|
+
var rect = canvas.getBoundingClientRect();
|
|
478
|
+
var x = event.clientX - rect.left;
|
|
479
|
+
var hit = hits.find(function (item) { return x >= item.x && x < item.x + item.w; });
|
|
480
|
+
if (hit) showNow(hit.id);
|
|
481
|
+
});
|
|
482
|
+
canvas.addEventListener('mouseleave', function () { showNow(state.selectedId); });
|
|
483
|
+
canvas.addEventListener('click', function (event) {
|
|
484
|
+
var rect = canvas.getBoundingClientRect();
|
|
485
|
+
var x = event.clientX - rect.left;
|
|
486
|
+
var hit = hits.find(function (item) { return x >= item.x && x < item.x + item.w; });
|
|
487
|
+
if (hit) choose(hit.id, true);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function canvasColor(status) {
|
|
492
|
+
var accent = getComputedStyle(document.body).getPropertyValue('--accent').trim() || '#00ff88';
|
|
493
|
+
if (status === 'failed' || status === 'timedOut' || status === 'flaky') return accent;
|
|
494
|
+
if (status === 'passed') return 'rgba(255, 255, 255, 0.22)';
|
|
495
|
+
return '#1a1a1a';
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function markStrip() {
|
|
499
|
+
var visible = {};
|
|
500
|
+
filteredTests().forEach(function (test) { visible[test.id] = true; });
|
|
501
|
+
document.querySelectorAll('.run-row').forEach(function (segment) {
|
|
502
|
+
segment.classList.toggle('is-dim', !visible[segment.dataset.id]);
|
|
503
|
+
if (segment.dataset.id === state.selectedId) segment.setAttribute('aria-current', 'true');
|
|
504
|
+
else segment.removeAttribute('aria-current');
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function sortTests(tests) {
|
|
509
|
+
var copy = tests.slice();
|
|
510
|
+
if (state.order === 'fastest') {
|
|
511
|
+
copy.sort(function (a, b) { return (a.duration || 0) - (b.duration || 0) || a.title.localeCompare(b.title); });
|
|
512
|
+
} else if (state.order === 'started') {
|
|
513
|
+
copy.sort(function (a, b) {
|
|
514
|
+
return (spanOf(a) ? spanOf(a).start : Infinity) - (spanOf(b) ? spanOf(b).start : Infinity);
|
|
515
|
+
});
|
|
516
|
+
} else if (state.order === 'slowest') {
|
|
517
|
+
copy.sort(function (a, b) { return (b.duration || 0) - (a.duration || 0) || a.title.localeCompare(b.title); });
|
|
518
|
+
}
|
|
519
|
+
return copy;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function paintOrder() {
|
|
523
|
+
var host = document.getElementById('order');
|
|
524
|
+
if (!host) return;
|
|
525
|
+
var orders = [['grouped', 'Grouped'], ['slowest', 'Slowest'], ['fastest', 'Fastest'], ['started', 'Started']];
|
|
526
|
+
var nodes = [el('span', { class: 'order-label' }, ['Order'])].concat(orders.map(function (order) {
|
|
527
|
+
return el('button', {
|
|
528
|
+
class: 'order-btn',
|
|
529
|
+
type: 'button',
|
|
530
|
+
'data-order': order[0],
|
|
531
|
+
'aria-pressed': state.order === order[0] ? 'true' : 'false',
|
|
532
|
+
}, [order[1]]);
|
|
533
|
+
}));
|
|
534
|
+
if (state.band !== 'all') {
|
|
535
|
+
nodes.push(el('button', { class: 'band-clear', type: 'button', 'aria-pressed': 'true' }, [bandLabel(state.band)]));
|
|
536
|
+
}
|
|
537
|
+
fill(host, nodes);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function ensureTests() {
|
|
541
|
+
if (built.tests) return;
|
|
542
|
+
built.tests = true;
|
|
543
|
+
var host = document.getElementById('panel-tests');
|
|
544
|
+
host.innerHTML = '<div class="legend" id="test-tools"></div><div class="order" id="order"></div><div class="split"><div id="tests"></div><section class="detail" id="detail"></section></div>';
|
|
545
|
+
paintCounts(document.getElementById('test-tools'), true);
|
|
546
|
+
paintOrder();
|
|
547
|
+
paintList();
|
|
548
|
+
paintDetail();
|
|
549
|
+
markStrip();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function paintList() {
|
|
553
|
+
var host = document.getElementById('tests');
|
|
554
|
+
if (!host) return;
|
|
555
|
+
var tests = filteredTests();
|
|
556
|
+
if (!tests.length) {
|
|
557
|
+
fill(host, [el('p', { class: 'empty' }, [data.tests.length ? 'No tests match.' : 'No tests ran.'])]);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (state.order !== 'grouped') {
|
|
561
|
+
var sorted = sortTests(tests);
|
|
562
|
+
var shown = sorted.slice(0, state.listLimit);
|
|
563
|
+
var flat = shown.map(function (test) { return rowButton(test, true); });
|
|
564
|
+
if (shown.length < sorted.length) {
|
|
565
|
+
flat.push(el('button', { class: 'more', type: 'button', id: 'show-rest' }, ['Show the rest']));
|
|
566
|
+
}
|
|
567
|
+
fill(host, flat);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
var groups = [];
|
|
571
|
+
var problemIds = {};
|
|
572
|
+
var problems = tests.filter(isProblem);
|
|
573
|
+
if (state.filter === 'all' && !state.query && problems.length && problems.length !== tests.length) {
|
|
574
|
+
problems.forEach(function (test) { problemIds[test.id] = true; });
|
|
575
|
+
groups.push({ name: 'Problems', tests: problems });
|
|
576
|
+
}
|
|
577
|
+
var byFile = [];
|
|
578
|
+
var index = {};
|
|
579
|
+
tests.forEach(function (test) {
|
|
580
|
+
if (problemIds[test.id]) return;
|
|
581
|
+
if (index[test.file] == null) {
|
|
582
|
+
index[test.file] = byFile.length;
|
|
583
|
+
byFile.push({ name: test.file, tests: [] });
|
|
584
|
+
}
|
|
585
|
+
byFile[index[test.file]].tests.push(test);
|
|
586
|
+
});
|
|
587
|
+
var rendered = 0;
|
|
588
|
+
var truncated = false;
|
|
589
|
+
var nodes = groups.concat(byFile).map(function (group) {
|
|
590
|
+
var rows = [];
|
|
591
|
+
group.tests.forEach(function (test) {
|
|
592
|
+
if (rendered >= state.listLimit) {
|
|
593
|
+
truncated = true;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
rendered += 1;
|
|
597
|
+
rows.push(rowButton(test, group.name === 'Problems'));
|
|
598
|
+
});
|
|
599
|
+
if (!rows.length) return null;
|
|
600
|
+
return el('div', { class: 'group' }, [el('p', { class: 'file-name' }, [group.name])].concat(rows));
|
|
601
|
+
}).filter(Boolean);
|
|
602
|
+
if (truncated) {
|
|
603
|
+
nodes.push(el('button', { class: 'more', type: 'button', id: 'show-rest' }, ['Show the rest']));
|
|
604
|
+
}
|
|
605
|
+
fill(host, nodes);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function rowButton(test, showFile) {
|
|
609
|
+
var meta = [el('span', { class: 'sw', 'data-status': test.status }, [statusLabel(test)])];
|
|
610
|
+
if (showFile) meta.push(el('span', null, [test.file]));
|
|
611
|
+
if ((test.attempts || []).length > 1) meta.push(el('span', null, [test.attempts.length + ' attempts']));
|
|
612
|
+
var button = el('button', { class: 'row', type: 'button', 'data-id': test.id, 'data-status': test.status }, [
|
|
613
|
+
el('span', { class: 'row-title' }, [test.title]),
|
|
614
|
+
el('span', { class: 'row-time' }, [formatDuration(test.duration)]),
|
|
615
|
+
el('span', { class: 'row-meta' }, meta),
|
|
616
|
+
]);
|
|
617
|
+
if (test.id === state.selectedId) button.setAttribute('aria-current', 'true');
|
|
618
|
+
return button;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function paintDetail() {
|
|
622
|
+
var host = document.getElementById('detail');
|
|
623
|
+
if (!host) return;
|
|
624
|
+
var test = testById(state.selectedId);
|
|
625
|
+
if (!test) {
|
|
626
|
+
fill(host, [el('p', { class: 'empty' }, ['Select a test to read its steps and logs.'])]);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (state.attempt >= test.attempts.length) state.attempt = Math.max(0, test.attempts.length - 1);
|
|
630
|
+
var attempt = test.attempts[state.attempt] || null;
|
|
631
|
+
var panes = [
|
|
632
|
+
['steps', 'Steps'],
|
|
633
|
+
['error', 'Error'],
|
|
634
|
+
['logs', 'Logs'],
|
|
635
|
+
['shots', 'Screenshots'],
|
|
636
|
+
];
|
|
637
|
+
var tabs = el('div', { class: 'subtabs', role: 'tablist' }, panes.map(function (pane) {
|
|
638
|
+
return el('button', {
|
|
639
|
+
class: 'subtab',
|
|
640
|
+
type: 'button',
|
|
641
|
+
'data-pane': pane[0],
|
|
642
|
+
'aria-pressed': state.testPane === pane[0] ? 'true' : 'false',
|
|
643
|
+
}, [pane[1]]);
|
|
644
|
+
}));
|
|
645
|
+
var body = [headerBlock(test), tabs];
|
|
646
|
+
if (test.attempts.length > 1) body.push(attemptTabs(test));
|
|
647
|
+
if (attempt) body.push(paneBody(test, attempt));
|
|
648
|
+
fill(host, [el('div', { class: 'detail-body' }, body)]);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function paneBody(test, attempt) {
|
|
652
|
+
if (state.testPane === 'error') return errorBlock(attempt) || el('p', { class: 'empty' }, ['No error on this attempt.']);
|
|
653
|
+
if (state.testPane === 'logs') return logBlock(attempt) || el('p', { class: 'empty' }, ['No logs for this attempt.']);
|
|
654
|
+
if (state.testPane === 'shots') return shotBlock(attempt) || el('p', { class: 'empty' }, ['No screenshots for this attempt.']);
|
|
655
|
+
return stepsBlock(attempt) || el('p', { class: 'empty' }, ['No steps recorded.']);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function headerBlock(test) {
|
|
659
|
+
var bits = [];
|
|
660
|
+
if (test.group && test.group.length) bits.push(el('p', { class: 'kicker' }, [test.group.join(' / ')]));
|
|
661
|
+
bits.push(el('h2', null, [test.title]));
|
|
662
|
+
var where = [el('span', null, [test.file + ':' + test.line])];
|
|
663
|
+
if (test.project) where.push(el('span', null, [test.project]));
|
|
664
|
+
(test.tags || []).forEach(function (tag) { where.push(el('span', null, [tag])); });
|
|
665
|
+
bits.push(el('p', { class: 'where' }, where));
|
|
666
|
+
bits.push(el('p', { class: 'status-line' }, [
|
|
667
|
+
el('span', { class: 'sw', 'data-status': test.status }, [statusLabel(test)]),
|
|
668
|
+
' in ' + formatDuration(test.duration),
|
|
669
|
+
]));
|
|
670
|
+
if (test.expectedFailure) bits.push(el('p', { class: 'note' }, ['This failure was expected.']));
|
|
671
|
+
(test.annotations || []).forEach(function (annotation) {
|
|
672
|
+
if (!annotation.description) return;
|
|
673
|
+
bits.push(el('p', { class: 'note' }, [annotation.type + ': ' + annotation.description]));
|
|
674
|
+
});
|
|
675
|
+
return el('div', null, bits);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function attemptTabs(test) {
|
|
679
|
+
return el('div', { class: 'attempts' }, test.attempts.map(function (attempt, index) {
|
|
680
|
+
return el('button', {
|
|
681
|
+
class: 'attempt',
|
|
682
|
+
type: 'button',
|
|
683
|
+
'data-index': String(index),
|
|
684
|
+
'aria-pressed': index === state.attempt ? 'true' : 'false',
|
|
685
|
+
}, ['Attempt ' + (index + 1) + ' ' + attemptLabel(attempt.status)]);
|
|
686
|
+
}));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function errorBlock(attempt) {
|
|
690
|
+
if (!attempt.errors || !attempt.errors.length) return null;
|
|
691
|
+
var image = (attempt.attachments || []).find(function (item) { return item.kind === 'image' && safeSrc(item.path); });
|
|
692
|
+
var messages = attempt.errors.map(function (error) {
|
|
693
|
+
var nodes = [el('pre', null, [error.message || 'The test failed.'])];
|
|
694
|
+
if (error.location) nodes.push(el('p', { class: 'where-error' }, [error.location]));
|
|
695
|
+
if (error.snippet) nodes.push(el('details', { class: 'stack' }, [el('summary', null, ['Source']), el('pre', null, [error.snippet])]));
|
|
696
|
+
if (error.stack) nodes.push(el('details', { class: 'stack' }, [el('summary', null, ['Stack']), el('pre', null, [error.stack])]));
|
|
697
|
+
return el('div', { class: 'error' }, nodes);
|
|
698
|
+
});
|
|
699
|
+
var column = el('div', { class: 'evidence-copy' }, messages);
|
|
700
|
+
if (!image) return el('div', { class: 'block' }, [el('h3', null, ['Error'])].concat(messages));
|
|
701
|
+
return el('div', { class: 'block' }, [
|
|
702
|
+
el('h3', null, ['Error']),
|
|
703
|
+
el('div', { class: 'evidence' }, [column, shotButton(image)]),
|
|
704
|
+
]);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function stepsBlock(attempt) {
|
|
708
|
+
var steps = attempt.steps || [];
|
|
709
|
+
if (!steps.length) return null;
|
|
710
|
+
var visible = data.steps === 'all' ? visibleSteps(steps, state.showApi) : steps;
|
|
711
|
+
var head = [el('h3', null, ['Steps'])];
|
|
712
|
+
if (data.steps === 'all' && hasApi(steps)) {
|
|
713
|
+
head.push(el('button', {
|
|
714
|
+
id: 'api-toggle',
|
|
715
|
+
class: 'ghost',
|
|
716
|
+
type: 'button',
|
|
717
|
+
'aria-pressed': state.showApi ? 'true' : 'false',
|
|
718
|
+
}, [state.showApi ? 'Hide browser calls' : 'Browser calls']));
|
|
719
|
+
}
|
|
720
|
+
var nodes = [el('div', { class: 'block-head' }, head)];
|
|
721
|
+
if (!visible.length) nodes.push(el('p', { class: 'empty' }, ['No steps recorded.']));
|
|
722
|
+
else nodes.push(renderSteps(visible, 0, Boolean(attempt.errors && attempt.errors.length)));
|
|
723
|
+
return el('div', { class: 'block' }, nodes);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function renderSteps(steps, depth, suppressError) {
|
|
727
|
+
return el('div', { class: depth ? 'children' : '' }, steps.map(function (step) {
|
|
728
|
+
var bad = Boolean(step.error) || hasBad(step.steps || []);
|
|
729
|
+
var title = [step.title || 'Step'];
|
|
730
|
+
if (step.subtitle) title.push(el('span', { class: 'sub' }, [' ' + step.subtitle]));
|
|
731
|
+
var row = el('div', { class: bad ? 'step bad' : 'step' }, [
|
|
732
|
+
el('span', { class: 'mark' }),
|
|
733
|
+
el('span', null, title),
|
|
734
|
+
el('span', { class: 'loc' }, [formatDuration(step.duration || 0)]),
|
|
735
|
+
]);
|
|
736
|
+
var kids = (step.steps && step.steps.length) ? [row, renderSteps(step.steps, depth + 1, suppressError)] : [row];
|
|
737
|
+
if (step.error && !suppressError) kids.push(el('p', { class: 'where-error' }, [step.error]));
|
|
738
|
+
return el('div', null, kids);
|
|
739
|
+
}));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function logBlock(attempt) {
|
|
743
|
+
if (!attempt.stdout && !attempt.stderr) return null;
|
|
744
|
+
var nodes = [el('h3', null, ['Logs'])];
|
|
745
|
+
if (attempt.stdout) nodes.push(el('div', { class: 'well' }, [el('p', null, ['Output']), el('pre', null, [attempt.stdout])]));
|
|
746
|
+
if (attempt.stderr) nodes.push(el('div', { class: 'well' }, [el('p', null, ['Error output']), el('pre', null, [attempt.stderr])]));
|
|
747
|
+
return el('div', { class: 'block' }, nodes);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function shotBlock(attempt) {
|
|
751
|
+
var images = (attempt.attachments || []).filter(function (item) { return item.kind === 'image' && safeSrc(item.path); });
|
|
752
|
+
if (!images.length) return null;
|
|
753
|
+
return el('div', { class: 'block' }, [el('h3', null, ['Screenshots'])].concat(images.map(shotButton)));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function shotButton(item) {
|
|
757
|
+
var src = safeSrc(item.path);
|
|
758
|
+
return el('button', { class: 'shot', type: 'button', 'data-src': src, 'data-alt': item.name || 'Screenshot' }, [
|
|
759
|
+
el('img', { src: src, alt: item.name || 'Screenshot', loading: 'lazy', decoding: 'async' }),
|
|
760
|
+
]);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function ensureShots() {
|
|
764
|
+
if (built.screenshots) return;
|
|
765
|
+
built.screenshots = true;
|
|
766
|
+
var host = document.getElementById('panel-screenshots');
|
|
767
|
+
var filters = el('div', { class: 'legend', id: 'shot-filters' }, [
|
|
768
|
+
shotFilter('all', 'All'),
|
|
769
|
+
shotFilter('step', 'Every step'),
|
|
770
|
+
shotFilter('failure', 'Last failing step'),
|
|
771
|
+
shotFilter('last', 'Last step'),
|
|
772
|
+
]);
|
|
773
|
+
var tiles = [];
|
|
774
|
+
data.tests.forEach(function (test) {
|
|
775
|
+
(test.attempts || []).forEach(function (attempt, index) {
|
|
776
|
+
(attempt.attachments || []).forEach(function (file) {
|
|
777
|
+
if (file.kind !== 'image' || !safeSrc(file.path)) return;
|
|
778
|
+
var src = safeSrc(file.path);
|
|
779
|
+
var copy = el('div', { class: 'tile-copy' }, [
|
|
780
|
+
el('p', { class: 'tile-title' }, [test.title]),
|
|
781
|
+
el('p', { class: 'tile-meta' }, [file.stepTitle || file.name || 'Screenshot']),
|
|
782
|
+
el('button', { class: 'open-test', type: 'button', 'data-id': test.id, 'data-attempt': String(index) }, ['Open test']),
|
|
783
|
+
]);
|
|
784
|
+
var tile = el('div', { class: 'tile', 'data-role': file.role || '', 'data-status': test.status }, [
|
|
785
|
+
el('button', { class: 'shot', type: 'button', 'data-src': src, 'data-alt': file.name || test.title }, [
|
|
786
|
+
el('img', { alt: file.name || test.title, loading: 'lazy', decoding: 'async' }),
|
|
787
|
+
]),
|
|
788
|
+
copy,
|
|
789
|
+
]);
|
|
790
|
+
tile.style.setProperty('--edge', canvasColor(test.status));
|
|
791
|
+
tiles.push(tile);
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
var empty = el('p', { class: 'empty', id: 'shot-empty' }, ['']);
|
|
796
|
+
fill(host, [el('h2', null, ['Screenshots']), filters, empty, el('div', { class: 'gallery', id: 'gallery' }, tiles)]);
|
|
797
|
+
document.querySelectorAll('#gallery img').forEach(function (img) {
|
|
798
|
+
img.src = img.parentElement.dataset.src;
|
|
799
|
+
});
|
|
800
|
+
applyShotFilter();
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function shotFilter(id, label) {
|
|
804
|
+
return el('button', { class: 'count shot-filter', type: 'button', 'data-shot': id, 'aria-pressed': 'false' }, [label]);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function applyShotFilter() {
|
|
808
|
+
var visible = 0;
|
|
809
|
+
document.querySelectorAll('#gallery .tile').forEach(function (tile) {
|
|
810
|
+
var role = tile.dataset.role || '';
|
|
811
|
+
var show = state.shotFilter === 'all' || role === state.shotFilter;
|
|
812
|
+
tile.hidden = !show;
|
|
813
|
+
if (show) visible += 1;
|
|
814
|
+
});
|
|
815
|
+
document.querySelectorAll('.shot-filter').forEach(function (button) {
|
|
816
|
+
button.setAttribute('aria-pressed', button.dataset.shot === state.shotFilter ? 'true' : 'false');
|
|
817
|
+
});
|
|
818
|
+
var empty = document.getElementById('shot-empty');
|
|
819
|
+
if (!empty) return;
|
|
820
|
+
empty.hidden = visible > 0;
|
|
821
|
+
if (!visible) empty.textContent = emptyShotMessage();
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function emptyShotMessage() {
|
|
825
|
+
if (state.shotFilter === 'step') return 'No step screenshots. Set screenshots to steps and use step().';
|
|
826
|
+
if (state.shotFilter === 'failure') return 'No failing screenshot in this run.';
|
|
827
|
+
if (state.shotFilter === 'last') return 'No final screenshot of a passing attempt.';
|
|
828
|
+
return 'No screenshots in this run.';
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function ensureLogs() {
|
|
832
|
+
if (built.logs) return;
|
|
833
|
+
built.logs = true;
|
|
834
|
+
var host = document.getElementById('panel-logs');
|
|
835
|
+
var cards = [];
|
|
836
|
+
data.tests.forEach(function (test) {
|
|
837
|
+
(test.attempts || []).forEach(function (attempt, index) {
|
|
838
|
+
if (!attempt.stdout && !attempt.stderr) return;
|
|
839
|
+
var blob = (test.title + ' ' + (attempt.stdout || '') + ' ' + (attempt.stderr || '')).toLowerCase();
|
|
840
|
+
var nodes = [
|
|
841
|
+
el('h3', null, [test.title]),
|
|
842
|
+
el('p', { class: 'tile-meta' }, ['Attempt ' + (index + 1) + ' · ' + attemptLabel(attempt.status)]),
|
|
843
|
+
];
|
|
844
|
+
if (attempt.stdout) nodes.push(el('div', { class: 'well' }, [el('p', null, ['Output']), el('pre', null, [attempt.stdout])]));
|
|
845
|
+
if (attempt.stderr) nodes.push(el('div', { class: 'well' }, [el('p', null, ['Error output']), el('pre', null, [attempt.stderr])]));
|
|
846
|
+
nodes.push(el('button', { class: 'open-test', type: 'button', 'data-id': test.id, 'data-attempt': String(index) }, ['Open test']));
|
|
847
|
+
var card = el('article', { class: 'log-card', 'data-blob': blob }, nodes);
|
|
848
|
+
cards.push(card);
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
fill(host, [
|
|
852
|
+
el('h2', null, ['Logs']),
|
|
853
|
+
el('input', { id: 'log-find', class: 'find', type: 'search', placeholder: 'Find in logs', 'aria-label': 'Find in logs' }),
|
|
854
|
+
el('div', { id: 'log-list' }, cards.length ? cards : [el('p', { class: 'empty' }, ['No logs in this run.'])]),
|
|
855
|
+
]);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function applyLogFilter() {
|
|
859
|
+
var query = state.logQuery.trim().toLowerCase();
|
|
860
|
+
document.querySelectorAll('.log-card').forEach(function (card) {
|
|
861
|
+
card.hidden = Boolean(query) && card.dataset.blob.indexOf(query) === -1;
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function ensureFiles() {
|
|
866
|
+
if (built.files) return;
|
|
867
|
+
built.files = true;
|
|
868
|
+
var host = document.getElementById('panel-files');
|
|
869
|
+
var groups = [];
|
|
870
|
+
data.tests.forEach(function (test) {
|
|
871
|
+
var rows = [];
|
|
872
|
+
(test.attempts || []).forEach(function (attempt, index) {
|
|
873
|
+
(attempt.attachments || []).forEach(function (file) {
|
|
874
|
+
if (file.kind === 'image') return;
|
|
875
|
+
var label = file.kind === 'trace' ? 'Trace' : file.kind === 'video' ? 'Video' : (file.name || 'File');
|
|
876
|
+
var line = [el('span', { class: 'file-kind' }, [label])];
|
|
877
|
+
if ((test.attempts || []).length > 1) line.push(el('span', { class: 'now-file' }, ['Attempt ' + (index + 1)]));
|
|
878
|
+
if (file.omitted || !safeSrc(file.path)) {
|
|
879
|
+
line.push(el('span', { class: 'now-file' }, ['Not copied']));
|
|
880
|
+
} else if (file.kind === 'video') {
|
|
881
|
+
line.push(el('video', { class: 'video', controls: 'true', preload: 'metadata', src: safeSrc(file.path) }));
|
|
882
|
+
} else {
|
|
883
|
+
var href = safeSrc(file.path);
|
|
884
|
+
line.push(el('a', { class: 'file-link', href: href, download: '' }, [file.name || 'Download']));
|
|
885
|
+
if (file.kind === 'trace') {
|
|
886
|
+
line.push(el('span', { class: 'now-file' }, ['npx playwright show-trace ' + href]));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
rows.push(el('div', { class: 'file-row' }, line));
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
if (rows.length) groups.push(el('section', { class: 'file-group' }, [el('h3', null, [test.title])].concat(rows)));
|
|
893
|
+
});
|
|
894
|
+
fill(host, [el('h2', null, ['Files'])].concat(groups.length ? groups : [el('p', { class: 'empty' }, ['No other files were attached.'])]));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function bind() {
|
|
898
|
+
window.addEventListener('resize', placeTabGlow);
|
|
899
|
+
app.addEventListener('click', function (event) {
|
|
900
|
+
var target = event.target instanceof Element ? event.target : null;
|
|
901
|
+
if (!target) return;
|
|
902
|
+
var tab = target.closest('.tab');
|
|
903
|
+
if (tab) {
|
|
904
|
+
showTab(tab.dataset.tab);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
var shotMode = target.closest('.shot-filter');
|
|
908
|
+
if (shotMode) {
|
|
909
|
+
state.shotFilter = shotMode.dataset.shot || 'all';
|
|
910
|
+
applyShotFilter();
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
var opener = target.closest('.open-test');
|
|
914
|
+
if (opener) {
|
|
915
|
+
openTest(opener.dataset.id, Number(opener.dataset.attempt || 0));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
var view = target.closest('.view');
|
|
919
|
+
if (view) {
|
|
920
|
+
state.overview = view.dataset.overview === 'timeline' ? 'timeline' : 'chart';
|
|
921
|
+
applyOverview();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
var band = target.closest('.band');
|
|
925
|
+
if (band) {
|
|
926
|
+
state.band = state.band === band.dataset.band ? 'all' : band.dataset.band;
|
|
927
|
+
state.order = state.band === 'all' ? state.order : 'slowest';
|
|
928
|
+
state.listLimit = 200;
|
|
929
|
+
keepSelectionInFilter();
|
|
930
|
+
if (built.tests) {
|
|
931
|
+
paintOrder();
|
|
932
|
+
paintList();
|
|
933
|
+
paintDetail();
|
|
934
|
+
}
|
|
935
|
+
paintDuration();
|
|
936
|
+
showTab('tests');
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
var clearBand = target.closest('.band-clear');
|
|
940
|
+
if (clearBand) {
|
|
941
|
+
state.band = 'all';
|
|
942
|
+
keepSelectionInFilter();
|
|
943
|
+
paintDuration();
|
|
944
|
+
paintOrder();
|
|
945
|
+
paintList();
|
|
946
|
+
paintDetail();
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
var order = target.closest('.order-btn');
|
|
950
|
+
if (order) {
|
|
951
|
+
state.order = order.dataset.order || 'grouped';
|
|
952
|
+
keepSelectionInFilter();
|
|
953
|
+
paintOrder();
|
|
954
|
+
paintList();
|
|
955
|
+
paintDetail();
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
var count = target.closest('.count');
|
|
959
|
+
if (count && !count.classList.contains('shot-filter') && !count.classList.contains('count-static')) {
|
|
960
|
+
state.filter = count.dataset.status || 'all';
|
|
961
|
+
state.listLimit = 200;
|
|
962
|
+
syncPressed();
|
|
963
|
+
keepSelectionInFilter();
|
|
964
|
+
if (count.closest('#legend')) showTab('tests');
|
|
965
|
+
if (built.tests) {
|
|
966
|
+
paintList();
|
|
967
|
+
paintDetail();
|
|
968
|
+
markStrip();
|
|
969
|
+
showNow(state.selectedId);
|
|
970
|
+
}
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
var segment = target.closest('.run-row');
|
|
974
|
+
if (segment) {
|
|
975
|
+
choose(segment.dataset.id, true);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
var row = target.closest('.row, .bar-row');
|
|
979
|
+
if (row) {
|
|
980
|
+
choose(row.dataset.id, false);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
var pane = target.closest('.subtab');
|
|
984
|
+
if (pane) {
|
|
985
|
+
state.testPane = pane.dataset.pane || 'steps';
|
|
986
|
+
paintDetail();
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
var attempt = target.closest('.attempt');
|
|
990
|
+
if (attempt) {
|
|
991
|
+
state.attempt = Number(attempt.dataset.index || 0);
|
|
992
|
+
paintDetail();
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (target.closest('#api-toggle')) {
|
|
996
|
+
state.showApi = !state.showApi;
|
|
997
|
+
paintDetail();
|
|
998
|
+
var toggle = document.getElementById('api-toggle');
|
|
999
|
+
if (toggle) toggle.focus();
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (target.closest('#show-rest')) {
|
|
1003
|
+
state.listLimit = 100000;
|
|
1004
|
+
paintList();
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
var shot = target.closest('.shot');
|
|
1008
|
+
if (shot) openShot(shot.dataset.src, shot.dataset.alt || 'Screenshot');
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
app.addEventListener('input', function (event) {
|
|
1012
|
+
if (!event.target) return;
|
|
1013
|
+
if (event.target.id === 'find') {
|
|
1014
|
+
window.clearTimeout(findTimer);
|
|
1015
|
+
var value = event.target.value;
|
|
1016
|
+
findTimer = window.setTimeout(function () {
|
|
1017
|
+
state.query = value;
|
|
1018
|
+
state.listLimit = 200;
|
|
1019
|
+
keepSelectionInFilter();
|
|
1020
|
+
paintList();
|
|
1021
|
+
paintDetail();
|
|
1022
|
+
markStrip();
|
|
1023
|
+
showNow(state.selectedId);
|
|
1024
|
+
}, 80);
|
|
1025
|
+
}
|
|
1026
|
+
if (event.target.id === 'log-find') {
|
|
1027
|
+
state.logQuery = event.target.value;
|
|
1028
|
+
applyLogFilter();
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
app.addEventListener('mouseover', function (event) {
|
|
1033
|
+
var segment = event.target && event.target.closest ? event.target.closest('.run-row') : null;
|
|
1034
|
+
if (segment) showNow(segment.dataset.id);
|
|
1035
|
+
});
|
|
1036
|
+
app.addEventListener('mouseout', function (event) {
|
|
1037
|
+
var segment = event.target && event.target.closest ? event.target.closest('.run-row') : null;
|
|
1038
|
+
if (!segment) return;
|
|
1039
|
+
var next = event.relatedTarget && event.relatedTarget.closest ? event.relatedTarget.closest('.run-row') : null;
|
|
1040
|
+
if (!next) showNow(state.selectedId);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
document.addEventListener('keydown', function (event) {
|
|
1044
|
+
var typing = event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement;
|
|
1045
|
+
if (typing) {
|
|
1046
|
+
if (event.key === 'Escape') {
|
|
1047
|
+
event.target.value = '';
|
|
1048
|
+
if (event.target.id === 'find') {
|
|
1049
|
+
state.query = '';
|
|
1050
|
+
keepSelectionInFilter();
|
|
1051
|
+
paintList();
|
|
1052
|
+
paintDetail();
|
|
1053
|
+
markStrip();
|
|
1054
|
+
showNow(state.selectedId);
|
|
1055
|
+
}
|
|
1056
|
+
if (event.target.id === 'log-find') {
|
|
1057
|
+
state.logQuery = '';
|
|
1058
|
+
applyLogFilter();
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (event.key >= '1' && event.key <= '5') {
|
|
1064
|
+
showTab(TABS[Number(event.key) - 1]);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (event.key === '/') {
|
|
1068
|
+
event.preventDefault();
|
|
1069
|
+
showTab('tests');
|
|
1070
|
+
var find = document.getElementById('find');
|
|
1071
|
+
if (find) find.focus();
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if ((event.key === 'ArrowRight' || event.key === 'ArrowLeft' || event.key === 'Home' || event.key === 'End') && event.target.closest && event.target.closest('.tabbar')) {
|
|
1075
|
+
event.preventDefault();
|
|
1076
|
+
var index = TABS.indexOf(state.tab);
|
|
1077
|
+
if (event.key === 'Home') index = 0;
|
|
1078
|
+
else if (event.key === 'End') index = TABS.length - 1;
|
|
1079
|
+
else if (event.key === 'ArrowRight') index = (index + 1) % TABS.length;
|
|
1080
|
+
else index = (index - 1 + TABS.length) % TABS.length;
|
|
1081
|
+
showTab(TABS[index]);
|
|
1082
|
+
var next = document.querySelector('.tab[data-tab="' + TABS[index] + '"]');
|
|
1083
|
+
if (next) next.focus();
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (state.tab !== 'tests' || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) return;
|
|
1087
|
+
var rows = Array.prototype.slice.call(document.querySelectorAll('#tests .row'));
|
|
1088
|
+
if (!rows.length) return;
|
|
1089
|
+
event.preventDefault();
|
|
1090
|
+
var current = -1;
|
|
1091
|
+
rows.forEach(function (row, i) { if (row.dataset.id === state.selectedId) current = i; });
|
|
1092
|
+
var nextIndex = event.key === 'ArrowDown' ? current + 1 : current - 1;
|
|
1093
|
+
if (nextIndex < 0) nextIndex = 0;
|
|
1094
|
+
if (nextIndex >= rows.length) nextIndex = rows.length - 1;
|
|
1095
|
+
choose(rows[nextIndex].dataset.id, false);
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function choose(id, resetFilter) {
|
|
1100
|
+
if (!id || !testById(id)) return;
|
|
1101
|
+
if (resetFilter) {
|
|
1102
|
+
state.filter = 'all';
|
|
1103
|
+
state.query = '';
|
|
1104
|
+
state.band = 'all';
|
|
1105
|
+
var find = document.getElementById('find');
|
|
1106
|
+
if (find) find.value = '';
|
|
1107
|
+
}
|
|
1108
|
+
if (state.selectedId !== id) {
|
|
1109
|
+
var test = testById(id);
|
|
1110
|
+
state.selectedId = id;
|
|
1111
|
+
state.attempt = failedAttempt(test);
|
|
1112
|
+
state.testPane = defaultPane(test);
|
|
1113
|
+
}
|
|
1114
|
+
showTab('tests');
|
|
1115
|
+
if (built.tests) {
|
|
1116
|
+
syncPressed();
|
|
1117
|
+
paintOrder();
|
|
1118
|
+
paintList();
|
|
1119
|
+
paintDetail();
|
|
1120
|
+
markStrip();
|
|
1121
|
+
}
|
|
1122
|
+
paintDuration();
|
|
1123
|
+
showNow(state.selectedId);
|
|
1124
|
+
var row = document.querySelector('#tests .row[data-id="' + cssEscape(id) + '"]');
|
|
1125
|
+
if (row && row.scrollIntoView) row.scrollIntoView({ block: 'nearest', behavior: reduce ? 'auto' : 'smooth' });
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function keepSelectionInFilter() {
|
|
1129
|
+
var tests = filteredTests();
|
|
1130
|
+
if (tests.some(function (test) { return test.id === state.selectedId; })) return;
|
|
1131
|
+
var next = tests[0];
|
|
1132
|
+
if (!next) {
|
|
1133
|
+
state.selectedId = null;
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
state.selectedId = next.id;
|
|
1137
|
+
state.attempt = failedAttempt(next);
|
|
1138
|
+
state.testPane = defaultPane(next);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function openTest(id, attempt) {
|
|
1142
|
+
var test = testById(id);
|
|
1143
|
+
if (!test) return;
|
|
1144
|
+
state.filter = 'all';
|
|
1145
|
+
state.query = '';
|
|
1146
|
+
state.band = 'all';
|
|
1147
|
+
var find = document.getElementById('find');
|
|
1148
|
+
if (find) find.value = '';
|
|
1149
|
+
state.selectedId = id;
|
|
1150
|
+
state.attempt = attempt;
|
|
1151
|
+
state.testPane = 'shots';
|
|
1152
|
+
showTab('tests');
|
|
1153
|
+
if (built.tests) {
|
|
1154
|
+
paintOrder();
|
|
1155
|
+
paintList();
|
|
1156
|
+
paintDetail();
|
|
1157
|
+
markStrip();
|
|
1158
|
+
}
|
|
1159
|
+
paintDuration();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function openShot(src, alt) {
|
|
1163
|
+
var safe = safeSrc(src);
|
|
1164
|
+
if (!safe) return;
|
|
1165
|
+
var close = el('button', { class: 'close-shot', type: 'button' }, ['Close']);
|
|
1166
|
+
var dialog = el('dialog', null, [close, el('img', { src: safe, alt: alt })]);
|
|
1167
|
+
close.addEventListener('click', function () { dialog.close(); });
|
|
1168
|
+
dialog.addEventListener('click', function (event) { if (event.target === dialog) dialog.close(); });
|
|
1169
|
+
dialog.addEventListener('close', function () { dialog.remove(); });
|
|
1170
|
+
document.body.appendChild(dialog);
|
|
1171
|
+
dialog.showModal();
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function showNow(id) {
|
|
1175
|
+
var host = document.getElementById('now');
|
|
1176
|
+
if (!host) return;
|
|
1177
|
+
var test = id ? testById(id) : null;
|
|
1178
|
+
if (!test) {
|
|
1179
|
+
fill(host, [data.tests.length ? 'Hover a test, or click one.' : 'No tests ran.']);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
fill(host, [
|
|
1183
|
+
el('span', { class: 'sw', 'data-status': test.status }, [statusLabel(test)]),
|
|
1184
|
+
el('span', { class: 'name' }, [test.title]),
|
|
1185
|
+
el('span', null, [formatDuration(test.duration)]),
|
|
1186
|
+
el('span', { class: 'now-file' }, [test.file]),
|
|
1187
|
+
]);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function filteredTests() {
|
|
1191
|
+
var query = state.query.trim().toLowerCase();
|
|
1192
|
+
return data.tests.filter(function (test) {
|
|
1193
|
+
if (state.filter !== 'all' && test.status !== state.filter) return false;
|
|
1194
|
+
if (state.band !== 'all' && (test.status === 'skipped' || durationBand(test.duration) !== state.band)) return false;
|
|
1195
|
+
if (!query) return true;
|
|
1196
|
+
var blob = [test.title, test.file, test.project].concat(test.group || [], test.tags || []).join(' ').toLowerCase();
|
|
1197
|
+
if (blob.indexOf(query) !== -1) return true;
|
|
1198
|
+
return (test.attempts || []).some(function (attempt) {
|
|
1199
|
+
return (attempt.errors || []).some(function (error) {
|
|
1200
|
+
return (error.message || '').toLowerCase().indexOf(query) !== -1;
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function testById(id) {
|
|
1207
|
+
return data.tests.find(function (test) { return test.id === id; }) || null;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function failedAttempt(test) {
|
|
1211
|
+
if (!test || !test.attempts || !test.attempts.length) return 0;
|
|
1212
|
+
var index = -1;
|
|
1213
|
+
test.attempts.forEach(function (attempt, i) {
|
|
1214
|
+
if (index === -1 && attempt.status !== 'passed' && attempt.status !== 'skipped') index = i;
|
|
1215
|
+
});
|
|
1216
|
+
return index === -1 ? test.attempts.length - 1 : index;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function defaultPane(test) {
|
|
1220
|
+
if (!test) return 'steps';
|
|
1221
|
+
if (test.status === 'failed' || test.status === 'timedOut' || test.status === 'flaky') return 'error';
|
|
1222
|
+
return 'steps';
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function isProblem(test) {
|
|
1226
|
+
return test.status === 'failed' || test.status === 'timedOut' || test.status === 'interrupted' || test.status === 'flaky';
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function statusLabel(test) {
|
|
1230
|
+
if (test.status === 'passed' && test.expectedFailure) return 'Expected fail';
|
|
1231
|
+
if (test.status === 'timedOut') return 'Timed out';
|
|
1232
|
+
var status = test.status || 'passed';
|
|
1233
|
+
return status.charAt(0).toUpperCase() + status.slice(1);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function attemptLabel(status) {
|
|
1237
|
+
if (status === 'timedOut') return 'Timed out';
|
|
1238
|
+
return status.charAt(0).toUpperCase() + status.slice(1);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function visibleSteps(steps, showApi) {
|
|
1242
|
+
var out = [];
|
|
1243
|
+
steps.forEach(function (step) {
|
|
1244
|
+
var kids = visibleSteps(step.steps || [], showApi);
|
|
1245
|
+
var api = step.category === 'pw:api' || step.category === 'fixture';
|
|
1246
|
+
var quietHook = step.category === 'hook' && !step.error && kids.length === 0;
|
|
1247
|
+
if (!showApi && api) {
|
|
1248
|
+
out = out.concat(kids);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (!showApi && quietHook) return;
|
|
1252
|
+
out.push(Object.assign({}, step, { steps: kids }));
|
|
1253
|
+
});
|
|
1254
|
+
return out;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function hasApi(steps) {
|
|
1258
|
+
return steps.some(function (step) {
|
|
1259
|
+
return step.category === 'pw:api' || step.category === 'fixture' || hasApi(step.steps || []);
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function hasBad(steps) {
|
|
1264
|
+
return steps.some(function (step) { return step.error || hasBad(step.steps || []); });
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function safeSrc(value) {
|
|
1268
|
+
if (typeof value !== 'string' || !value) return '';
|
|
1269
|
+
if (!value.startsWith('assets/attachments/')) return '';
|
|
1270
|
+
if (value.indexOf('..') !== -1 || value.indexOf('\\') !== -1 || value.indexOf('://') !== -1) return '';
|
|
1271
|
+
return value;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function metaBits() {
|
|
1275
|
+
var bits = [];
|
|
1276
|
+
if (data.projectName) bits.push(data.projectName);
|
|
1277
|
+
if (Array.isArray(data.projects) && data.projects.length) bits.push(data.projects.join(', '));
|
|
1278
|
+
if (data.startTime) bits.push(formatWhen(data.startTime));
|
|
1279
|
+
if (data.playwrightVersion) bits.push('Playwright ' + data.playwrightVersion);
|
|
1280
|
+
if (data.shard) bits.push('Shard ' + data.shard);
|
|
1281
|
+
return bits;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function formatDuration(ms) {
|
|
1285
|
+
if (!isFinite(ms) || ms < 0) ms = 0;
|
|
1286
|
+
if (ms < 1000) return Math.round(ms) + 'ms';
|
|
1287
|
+
if (ms < 60000) {
|
|
1288
|
+
var seconds = ms / 1000;
|
|
1289
|
+
var text = seconds >= 10 ? String(Math.round(seconds)) : seconds.toFixed(1).replace(/\.0$/, '');
|
|
1290
|
+
return text + 's';
|
|
1291
|
+
}
|
|
1292
|
+
var total = Math.round(ms / 1000);
|
|
1293
|
+
return Math.floor(total / 60) + 'm ' + String(total % 60).padStart(2, '0') + 's';
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function formatWhen(value) {
|
|
1297
|
+
var date = new Date(value);
|
|
1298
|
+
if (isNaN(date.getTime())) return '';
|
|
1299
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
1300
|
+
month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit',
|
|
1301
|
+
}).format(date);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function countUp(node, target, format) {
|
|
1305
|
+
if (!node) return;
|
|
1306
|
+
if (reduce || target <= 0) {
|
|
1307
|
+
node.textContent = format(target);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
var start = performance.now();
|
|
1311
|
+
function frame(now) {
|
|
1312
|
+
var t = Math.min(1, (now - start) / 800);
|
|
1313
|
+
var eased = 1 - Math.pow(1 - t, 3);
|
|
1314
|
+
node.textContent = format(target * eased);
|
|
1315
|
+
if (t < 1) requestAnimationFrame(frame);
|
|
1316
|
+
}
|
|
1317
|
+
requestAnimationFrame(frame);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function cssEscape(value) {
|
|
1321
|
+
if (window.CSS && CSS.escape) return CSS.escape(value);
|
|
1322
|
+
return String(value).replace(/["\\]/g, '\\$&');
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function el(tag, attrs, children) {
|
|
1326
|
+
var node = document.createElement(tag);
|
|
1327
|
+
var props = attrs || {};
|
|
1328
|
+
Object.keys(props).forEach(function (key) {
|
|
1329
|
+
var value = props[key];
|
|
1330
|
+
if (value == null || value === false) return;
|
|
1331
|
+
if (key === 'class') node.className = value;
|
|
1332
|
+
else node.setAttribute(key, String(value));
|
|
1333
|
+
});
|
|
1334
|
+
(children || []).forEach(function (child) {
|
|
1335
|
+
if (child == null || child === false) return;
|
|
1336
|
+
node.append(child instanceof Node ? child : document.createTextNode(String(child)));
|
|
1337
|
+
});
|
|
1338
|
+
return node;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function fill(node, children) {
|
|
1342
|
+
var nodes = [];
|
|
1343
|
+
children.forEach(function (child) {
|
|
1344
|
+
if (child == null || child === false) return;
|
|
1345
|
+
nodes.push(child instanceof Node ? child : document.createTextNode(String(child)));
|
|
1346
|
+
});
|
|
1347
|
+
node.replaceChildren.apply(node, nodes);
|
|
1348
|
+
}
|
|
1349
|
+
})();
|