playwright-reborn 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/report.js CHANGED
@@ -17,20 +17,22 @@
17
17
  var TABS = ['overview', 'tests', 'screenshots'];
18
18
  if (data.showLogs !== false) TABS.push('logs');
19
19
  if (data.showFiles !== false) TABS.push('files');
20
+ var testIndex = new Map(data.tests.map(function (test) { return [test.id, test]; }));
20
21
  var route = readRoute();
21
22
  var state = {
22
23
  tab: route.tab,
23
24
  filter: 'all',
24
25
  query: '',
25
- selectedId: route.id || initialFailure(),
26
+ selectedId: testIndex.has(route.id) ? route.id : initialFailure(),
26
27
  attempt: 0,
27
28
  showApi: false,
28
29
  testPane: 'steps',
29
30
  listLimit: 200,
31
+ timelineLimit: 200,
30
32
  shotFilter: 'all',
31
33
  logQuery: '',
32
- tag: 'all',
33
- projectFilter: 'all',
34
+ tag: null,
35
+ projectFilter: null,
34
36
  overview: data.overview === 'timeline' ? 'timeline' : 'chart',
35
37
  order: 'grouped',
36
38
  band: 'all',
@@ -49,7 +51,7 @@
49
51
  '<div class="shell">',
50
52
  '<aside class="rail">',
51
53
  '<div class="tabbar" role="tablist" aria-label="Report">',
52
- '<span class="tab-glow" id="tab-glow"></span>',
54
+
53
55
  TABS.map(function (id) {
54
56
  return tabButton(id, id.charAt(0).toUpperCase() + id.slice(1));
55
57
  }).join(''),
@@ -70,17 +72,18 @@
70
72
  document.body.dataset.accent = accentName();
71
73
  document.title = companyName() + (data.summary ? ' · ' + data.summary : '');
72
74
  fill(document.getElementById('meta'), metaBits().map(function (bit) { return el('span', null, [bit]); }));
73
- fill(document.getElementById('foot'), [
74
- el('span', null, ['Open source by World of Z']),
75
- el('span', null, [formatWhen(data.generatedAt)]),
76
- el('span', null, ['Keys 1 to 5 switch tabs. Press / to find a test.']),
77
- ]);
75
+ var footBits = [];
76
+ if (data.showCredit === true) footBits.push(el('span', null, ['Open source by World of Z']));
77
+ footBits.push(el('span', null, [formatWhen(data.generatedAt)]));
78
+ footBits.push(el('span', null, ['Keys 1 onward switch pages. Press / to find a test.']));
79
+ fill(document.getElementById('foot'), footBits);
78
80
  var BANDS = [
79
81
  ['under1', 'Under 1 minute'],
80
82
  ['1to3', '1 to 3 minutes'],
81
83
  ['3to5', '3 to 5 minutes'],
82
84
  ['over5', 'Over 5 minutes'],
83
85
  ];
86
+ var CHART_COLORS = ['var(--accent)', 'rgba(255,255,255,0.72)', 'rgba(255,255,255,0.42)', 'rgba(255,255,255,0.18)'];
84
87
 
85
88
  paintOverview();
86
89
  showTab(state.tab, true);
@@ -92,6 +95,7 @@
92
95
  var canvas = document.querySelector('.atmosphere .bands');
93
96
  if (!canvas || !canvas.getContext) return;
94
97
  var ctx = canvas.getContext('2d');
98
+ if (!ctx) return;
95
99
  var ratio = 1;
96
100
  var columns = [];
97
101
  var count = 22;
@@ -118,9 +122,9 @@
118
122
  var t = reduce ? 8 : now / 1000;
119
123
  ctx.clearRect(0, 0, w, h);
120
124
  var wash = ctx.createLinearGradient(0, 0, 0, h);
121
- wash.addColorStop(0, '#123d24');
122
- wash.addColorStop(0.42, '#0a2416');
123
- wash.addColorStop(1, '#050806');
125
+ wash.addColorStop(0, 'rgba(' + rgb + ', 0.10)');
126
+ wash.addColorStop(0.42, '#080b0a');
127
+ wash.addColorStop(1, '#080808');
124
128
  ctx.fillStyle = wash;
125
129
  ctx.fillRect(0, 0, w, h);
126
130
  columns.forEach(function (column) {
@@ -138,11 +142,16 @@
138
142
  ctx.fillStyle = gradient;
139
143
  ctx.fillRect(x - spread, 0, spread * 2, h * 0.92);
140
144
  });
141
- if (!reduce) requestAnimationFrame(draw);
145
+ if (!reduce && !document.hidden) curtainFrame = requestAnimationFrame(draw);
142
146
  }
147
+ var curtainFrame = 0;
143
148
  resize();
144
- window.addEventListener('resize', resize);
145
- requestAnimationFrame(function (now) {
149
+ window.addEventListener('resize', function () { resize(); if (reduce) draw(0); });
150
+ document.addEventListener('visibilitychange', function () {
151
+ cancelAnimationFrame(curtainFrame);
152
+ if (!document.hidden) curtainFrame = requestAnimationFrame(draw);
153
+ });
154
+ curtainFrame = requestAnimationFrame(function (now) {
146
155
  if (!canvas.width) resize();
147
156
  draw(now);
148
157
  });
@@ -151,13 +160,14 @@
151
160
  function watchHeader() {
152
161
  var rail = document.querySelector('.rail');
153
162
  if (!rail) return;
163
+ rail.addEventListener('focusin', function () { rail.classList.remove('is-hidden'); });
154
164
  var lastY = window.scrollY;
155
165
  window.addEventListener('scroll', function () {
156
166
  var y = window.scrollY;
157
167
  var down = y > lastY + 6;
158
168
  var up = y < lastY - 6;
159
169
  if (y < 24) rail.classList.remove('is-hidden');
160
- else if (down) rail.classList.add('is-hidden');
170
+ else if (down && !rail.contains(document.activeElement)) rail.classList.add('is-hidden');
161
171
  else if (up) rail.classList.remove('is-hidden');
162
172
  lastY = y;
163
173
  }, { passive: true });
@@ -184,16 +194,17 @@
184
194
  }
185
195
 
186
196
  function tabButton(id, label) {
187
- return '<button class="tab" role="tab" type="button" data-tab="' + id + '" aria-selected="false" aria-controls="panel-' + id + '">' + label + '</button>';
197
+ return '<button id="tab-' + id + '" class="tab" role="tab" type="button" data-tab="' + id + '" aria-selected="false" aria-controls="panel-' + id + '">' + label + '</button>';
188
198
  }
189
199
 
190
200
  function readRoute() {
191
- var hash = '';
192
- try { hash = decodeURIComponent(location.hash.slice(1)); } catch (error) { hash = ''; }
193
- if (!hash || hash === 'overview') return { tab: 'overview' };
201
+ var hash = location.hash.slice(1);
202
+ try {
203
+ if (hash.indexOf('tests/') === 0) return { tab: 'tests', id: decodeURIComponent(hash.slice(6)) };
204
+ hash = decodeURIComponent(hash);
205
+ } catch (error) { return { tab: 'overview' }; }
194
206
  if (TABS.indexOf(hash) !== -1) return { tab: hash };
195
- if (hash.indexOf('tests/') === 0) return { tab: 'tests', id: decodeURIComponent(hash.slice(6)) };
196
- if (data.tests.some(function (test) { return test.id === hash; })) return { tab: 'tests', id: hash };
207
+ if (testIndex.has(hash)) return { tab: 'tests', id: hash };
197
208
  return { tab: 'overview' };
198
209
  }
199
210
 
@@ -207,11 +218,13 @@
207
218
  state.tab = tab;
208
219
  document.querySelectorAll('[role="tab"]').forEach(function (button) {
209
220
  button.setAttribute('aria-selected', button.dataset.tab === tab ? 'true' : 'false');
221
+ button.tabIndex = button.dataset.tab === tab ? 0 : -1;
210
222
  });
211
223
  TABS.forEach(function (name) {
212
224
  var panel = document.getElementById('panel-' + name);
213
225
  var on = name === tab;
214
226
  panel.hidden = !on;
227
+ panel.setAttribute('aria-labelledby', 'tab-' + name);
215
228
  panel.classList.toggle('panel', true);
216
229
  if (on && !quiet && !reduce) {
217
230
  panel.classList.remove('panel-enter');
@@ -223,33 +236,26 @@
223
236
  if (tab === 'screenshots') ensureShots();
224
237
  if (tab === 'logs') ensureLogs();
225
238
  if (tab === 'files') ensureFiles();
226
- writeHash();
227
- placeTabGlow();
239
+ writeHash(!quiet);
240
+
228
241
  }
229
242
 
230
- function writeHash() {
243
+ function writeHash(push) {
231
244
  var next = state.tab === 'overview' ? 'overview' : state.tab;
232
245
  if (state.tab === 'tests' && state.selectedId) next = 'tests/' + encodeURIComponent(state.selectedId);
233
- if (window.history && history.replaceState) history.replaceState(null, '', '#' + next);
234
- }
235
-
236
- function placeTabGlow() {
237
- var bar = document.querySelector('.tabbar');
238
- var glow = document.getElementById('tab-glow');
239
- var active = document.querySelector('.tab[aria-selected="true"]');
240
- if (!bar || !glow || !active) return;
241
- var barBox = bar.getBoundingClientRect();
242
- var box = active.getBoundingClientRect();
243
- glow.style.width = box.width + 'px';
244
- glow.style.transform = 'translateX(' + (box.left - barBox.left - 4) + 'px)';
246
+ if (location.hash === '#' + next) return;
247
+ if (window.history && history.replaceState) {
248
+ var url = location.protocol === 'about:' ? location.href.split('#')[0] + '#' + next : '#' + next;
249
+ try { history[push ? 'pushState' : 'replaceState'](null, '', url); } catch (error) { /* Some artifact viewers block history updates. */ }
250
+ }
245
251
  }
246
252
 
247
253
  function paintOverview() {
248
254
  var host = document.getElementById('panel-overview');
249
255
  host.innerHTML = [
250
256
  '<header class="hero">',
251
- '<div><h1 class="word" id="word"></h1><p class="credit">Open source by World of Z</p><p class="summary" id="summary"></p></div>',
252
- '<p class="time" id="time"></p>',
257
+ '<div><h1 class="word" id="word"></h1><p class="product" id="product"></p><p class="summary" id="summary"></p></div>',
258
+ '<p class="run-state" id="run-state"></p>',
253
259
  '</header>',
254
260
  '<div id="errors"></div>',
255
261
  '<dl class="info" id="info"></dl>',
@@ -265,22 +271,27 @@
265
271
  '</div>',
266
272
  '</div>',
267
273
  '<p class="section-note" id="range-note"></p>',
268
- '<div class="legend" id="duration"></div>',
274
+ '<div class="charts" id="duration"></div>',
269
275
  '<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>',
270
276
  '</div>',
271
277
  '</section>',
272
278
  ].join('');
273
279
  var word = document.getElementById('word');
274
280
  word.textContent = companyName();
281
+ var product = document.getElementById('product');
282
+ var subtitle = typeof data.productSubtitle === 'string' ? data.productSubtitle.trim() : 'Test Automation';
283
+ if (subtitle) product.textContent = subtitle;
284
+ else product.hidden = true;
275
285
  document.getElementById('summary').textContent = data.summary || '';
276
- countUp(document.getElementById('time'), data.duration || 0, formatDuration);
286
+ var runState = document.getElementById('run-state');
287
+ runState.dataset.status = data.status;
288
+ runState.textContent = { passed: 'Run passed', failed: 'Run failed', timedout: 'Run timed out', interrupted: 'Run interrupted' }[data.status] || 'Run finished';
277
289
  paintErrors();
278
290
  paintInfo();
279
291
  paintCounts(document.getElementById('legend'), false);
280
292
  paintDuration();
281
293
  paintStrip();
282
294
  applyOverview();
283
- showNow(state.selectedId);
284
295
  }
285
296
 
286
297
  function paintInfo() {
@@ -317,29 +328,125 @@
317
328
  return data.tests.filter(function (test) { return test.status !== 'skipped'; });
318
329
  }
319
330
 
320
- function paintDuration() {
321
- var host = document.getElementById('duration');
322
- if (!host) return;
331
+ function bandCounts() {
323
332
  var counts = { under1: 0, '1to3': 0, '3to5': 0, over5: 0 };
324
333
  countedTests().forEach(function (test) {
325
334
  counts[durationBand(test.duration)] += 1;
326
335
  });
336
+ return counts;
337
+ }
338
+
339
+ function chartStyle() {
340
+ return data.chartStyle === 'pie' || data.chartStyle === 'bar' ? data.chartStyle : 'both';
341
+ }
342
+
343
+ function polar(cx, cy, radius, degrees) {
344
+ var rad = (degrees - 90) * Math.PI / 180;
345
+ return { x: cx + radius * Math.cos(rad), y: cy + radius * Math.sin(rad) };
346
+ }
347
+
348
+ function piePath(start, end) {
349
+ var span = end - start;
350
+ if (span >= 359.99) return '';
351
+ var from = polar(110, 110, 96, start);
352
+ var to = polar(110, 110, 96, end);
353
+ var large = span > 180 ? 1 : 0;
354
+ return 'M 110 110 L ' + from.x.toFixed(2) + ' ' + from.y.toFixed(2) + ' A 96 96 0 ' + large + ' 1 ' + to.x.toFixed(2) + ' ' + to.y.toFixed(2) + ' Z';
355
+ }
356
+
357
+ function paintPie(counts) {
358
+ var total = 0;
359
+ BANDS.forEach(function (band) { total += counts[band[0]]; });
360
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
361
+ svg.setAttribute('viewBox', '0 0 220 220');
362
+ svg.setAttribute('class', 'pie');
363
+ svg.setAttribute('role', 'img');
364
+ svg.setAttribute('aria-label', 'Tests by duration');
365
+ if (!total) {
366
+ var empty = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
367
+ empty.setAttribute('cx', '110');
368
+ empty.setAttribute('cy', '110');
369
+ empty.setAttribute('r', '96');
370
+ empty.setAttribute('fill', 'rgba(255,255,255,0.08)');
371
+ svg.appendChild(empty);
372
+ return svg;
373
+ }
374
+ var angle = 0;
375
+ var full = null;
376
+ BANDS.forEach(function (band, index) {
377
+ var count = counts[band[0]];
378
+ if (!count) return;
379
+ var sweep = (count / total) * 360;
380
+ if (sweep >= 359.99) {
381
+ full = index;
382
+ angle += sweep;
383
+ return;
384
+ }
385
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
386
+ path.setAttribute('d', piePath(angle, angle + sweep));
387
+ path.setAttribute('fill', CHART_COLORS[index]);
388
+ path.setAttribute('data-slice', band[0]);
389
+ path.setAttribute('class', 'slice');
390
+ svg.appendChild(path);
391
+ angle += sweep;
392
+ });
393
+ if (full !== null) {
394
+ var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
395
+ circle.setAttribute('cx', '110');
396
+ circle.setAttribute('cy', '110');
397
+ circle.setAttribute('r', '96');
398
+ circle.setAttribute('fill', CHART_COLORS[full]);
399
+ circle.setAttribute('data-slice', BANDS[full][0]);
400
+ circle.setAttribute('class', 'slice');
401
+ svg.appendChild(circle);
402
+ }
403
+ return svg;
404
+ }
405
+
406
+ function paintDuration() {
407
+ var host = document.getElementById('duration');
408
+ if (!host) return;
409
+ var counts = bandCounts();
327
410
  var max = 1;
328
411
  BANDS.forEach(function (band) { max = Math.max(max, counts[band[0]]); });
329
- fill(host, BANDS.map(function (band) {
330
- var fillBar = el('span', { class: 'band-fill' });
331
- fillBar.style.width = Math.round((counts[band[0]] / max) * 100) + '%';
332
- return el('button', {
333
- class: 'band',
334
- type: 'button',
335
- 'data-band': band[0],
336
- 'aria-pressed': state.band === band[0] ? 'true' : 'false',
337
- }, [
338
- el('span', { class: 'n' }, [String(counts[band[0]])]),
339
- el('span', { class: 'band-label' }, [band[1]]),
340
- el('span', { class: 'band-track' }, [fillBar]),
341
- ]);
342
- }));
412
+ var style = chartStyle();
413
+ var nodes = [];
414
+ if (style !== 'bar') nodes.push(paintPie(counts));
415
+ if (style !== 'pie') {
416
+ nodes.push(el('div', { class: 'bars' }, BANDS.map(function (band, index) {
417
+ var fillBar = el('span', { class: 'band-fill' });
418
+ fillBar.style.width = Math.round((counts[band[0]] / max) * 100) + '%';
419
+ fillBar.style.background = CHART_COLORS[index];
420
+ return el('button', {
421
+ class: 'band',
422
+ type: 'button',
423
+ 'data-band': band[0],
424
+ 'aria-pressed': state.band === band[0] ? 'true' : 'false',
425
+ }, [
426
+ el('span', { class: 'swatch' }),
427
+ el('span', { class: 'band-label' }, [band[1]]),
428
+ el('span', { class: 'n' }, [String(counts[band[0]])]),
429
+ el('span', { class: 'band-track' }, [fillBar]),
430
+ ]);
431
+ })));
432
+ } else {
433
+ nodes.push(el('div', { class: 'bars' }, BANDS.map(function (band, index) {
434
+ return el('button', {
435
+ class: 'band band-key',
436
+ type: 'button',
437
+ 'data-band': band[0],
438
+ 'aria-pressed': state.band === band[0] ? 'true' : 'false',
439
+ }, [
440
+ el('span', { class: 'swatch' }),
441
+ el('span', { class: 'band-label' }, [band[1]]),
442
+ el('span', { class: 'n' }, [String(counts[band[0]])]),
443
+ ]);
444
+ })));
445
+ }
446
+ fill(host, nodes);
447
+ host.querySelectorAll('.swatch').forEach(function (swatch, index) {
448
+ swatch.style.background = CHART_COLORS[index % CHART_COLORS.length];
449
+ });
343
450
  }
344
451
 
345
452
  function applyOverview() {
@@ -378,7 +485,7 @@
378
485
  function paintCounts(host, withFind) {
379
486
  var counts = data.counts || {};
380
487
  var filters = [
381
- ['all', 'ran', counts.total || 0],
488
+ ['all', 'tests', counts.total || 0],
382
489
  ['passed', 'passed', counts.passed || 0],
383
490
  ['failed', 'failed', counts.failed || 0],
384
491
  ['flaky', 'flaky', counts.flaky || 0],
@@ -416,7 +523,7 @@
416
523
  }
417
524
 
418
525
  function syncPressed() {
419
- document.querySelectorAll('.count').forEach(function (button) {
526
+ document.querySelectorAll('.count[data-status]').forEach(function (button) {
420
527
  button.setAttribute('aria-pressed', button.dataset.status === state.filter ? 'true' : 'false');
421
528
  });
422
529
  }
@@ -437,7 +544,7 @@
437
544
  function paintStrip() {
438
545
  var strip = document.getElementById('strip');
439
546
  if (!data.tests.length) {
440
- fill(strip, []);
547
+ fill(strip, [el('p', { class: 'empty' }, ['No tests ran.'])]);
441
548
  return;
442
549
  }
443
550
  var max = data.tests.reduce(function (hi, test) { return Math.max(hi, test.duration || 0); }, 1);
@@ -446,7 +553,7 @@
446
553
  });
447
554
  strip.className = 'run-list';
448
555
  strip.style.height = '';
449
- fill(strip, ordered.map(function (test) {
556
+ var nodes = ordered.slice(0, state.timelineLimit).map(function (test) {
450
557
  var fillBar = el('span', { class: 'run-fill', 'data-status': test.status });
451
558
  fillBar.style.width = Math.max(2, Math.round(((test.duration || 0) / max) * 100)) + '%';
452
559
  return el('button', {
@@ -461,51 +568,10 @@
461
568
  el('span', { class: 'run-track' }, [fillBar]),
462
569
  el('span', { class: 'run-time' }, [formatDuration(test.duration)]),
463
570
  ]);
464
- }));
465
- markStrip();
466
- }
467
-
468
- function paintCanvasStrip(host) {
469
- var canvas = el('canvas', { class: 'strip-canvas', 'aria-label': 'Tests by duration' });
470
- fill(host, [canvas]);
471
- var hits = [];
472
- function draw() {
473
- var width = host.clientWidth || 600;
474
- var height = 52;
475
- var ratio = window.devicePixelRatio || 1;
476
- canvas.width = Math.floor(width * ratio);
477
- canvas.height = Math.floor(height * ratio);
478
- canvas.style.width = width + 'px';
479
- canvas.style.height = height + 'px';
480
- var ctx = canvas.getContext('2d');
481
- ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
482
- ctx.clearRect(0, 0, width, height);
483
- var total = data.tests.reduce(function (sum, test) { return sum + Math.max(test.duration || 0, 1); }, 0);
484
- var x = 0;
485
- hits = [];
486
- data.tests.forEach(function (test) {
487
- var w = Math.max(2, (Math.max(test.duration || 0, 1) / total) * width);
488
- ctx.fillStyle = canvasColor(test.status);
489
- ctx.fillRect(x, 8, Math.max(1, w - 2), 36);
490
- hits.push({ x: x, w: w, id: test.id });
491
- x += w;
492
- });
493
- }
494
- draw();
495
- window.addEventListener('resize', draw);
496
- canvas.addEventListener('mousemove', function (event) {
497
- var rect = canvas.getBoundingClientRect();
498
- var x = event.clientX - rect.left;
499
- var hit = hits.find(function (item) { return x >= item.x && x < item.x + item.w; });
500
- if (hit) showNow(hit.id);
501
- });
502
- canvas.addEventListener('mouseleave', function () { showNow(state.selectedId); });
503
- canvas.addEventListener('click', function (event) {
504
- var rect = canvas.getBoundingClientRect();
505
- var x = event.clientX - rect.left;
506
- var hit = hits.find(function (item) { return x >= item.x && x < item.x + item.w; });
507
- if (hit) choose(hit.id, true);
508
571
  });
572
+ if (ordered.length > state.timelineLimit) nodes.push(el('button', { class: 'more', type: 'button', id: 'show-more-timeline' }, ['Show more tests']));
573
+ fill(strip, nodes);
574
+ markStrip();
509
575
  }
510
576
 
511
577
  function canvasColor(status) {
@@ -516,7 +582,7 @@
516
582
  }
517
583
 
518
584
  function markStrip() {
519
- var visible = {};
585
+ var visible = Object.create(null);
520
586
  filteredTests().forEach(function (test) { visible[test.id] = true; });
521
587
  document.querySelectorAll('.run-row').forEach(function (segment) {
522
588
  segment.classList.toggle('is-dim', !visible[segment.dataset.id]);
@@ -525,8 +591,18 @@
525
591
  });
526
592
  }
527
593
 
594
+ function keptTag(tag) {
595
+ var pattern = typeof data.ignoreTags === 'string' ? data.ignoreTags : '^@?HC2T-';
596
+ if (!pattern) return true;
597
+ try {
598
+ return !new RegExp(pattern, 'i').test(tag);
599
+ } catch (error) {
600
+ return true;
601
+ }
602
+ }
603
+
528
604
  function uniqueValues(pick) {
529
- var seen = {};
605
+ var seen = Object.create(null);
530
606
  data.tests.forEach(function (test) {
531
607
  pick(test).forEach(function (value) {
532
608
  if (value) seen[value] = true;
@@ -538,17 +614,17 @@
538
614
  function paintTags() {
539
615
  var host = document.getElementById('tags');
540
616
  if (!host) return;
541
- var tags = uniqueValues(function (test) { return test.tags || []; });
617
+ var tags = uniqueValues(function (test) { return (test.tags || []).filter(keptTag); });
542
618
  var projects = uniqueValues(function (test) { return test.project ? [test.project] : []; });
543
619
  var nodes = [];
544
620
  if (tags.length) {
545
621
  nodes.push(el('span', { class: 'order-label' }, ['Tags']));
546
- nodes.push(chip('tag-btn', 'all', 'All', state.tag));
622
+ nodes.push(chip('tag-btn', '', 'All', state.tag || ''));
547
623
  tags.forEach(function (tag) { nodes.push(chip('tag-btn', tag, tag, state.tag)); });
548
624
  }
549
- if (projects.length > 1) {
625
+ if (data.showProjectFilter === true && projects.length > 1) {
550
626
  nodes.push(el('span', { class: 'order-label' }, ['Project']));
551
- nodes.push(chip('project-btn', 'all', 'All', state.projectFilter));
627
+ nodes.push(chip('project-btn', '', 'All', state.projectFilter || ''));
552
628
  projects.forEach(function (project) { nodes.push(chip('project-btn', project, project, state.projectFilter)); });
553
629
  }
554
630
  host.hidden = nodes.length === 0;
@@ -600,7 +676,7 @@
600
676
  if (built.tests) return;
601
677
  built.tests = true;
602
678
  var host = document.getElementById('panel-tests');
603
- host.innerHTML = '<div class="legend" id="test-tools"></div><div class="tags" id="tags"></div><div class="order" id="order"></div><div class="split"><div id="tests"></div><section class="detail" id="detail"></section></div>';
679
+ host.innerHTML = '<h2 class="visually-hidden">Tests</h2><div class="legend" id="test-tools"></div><div class="tags" id="tags"></div><div class="order" id="order"></div><div class="list-summary" id="list-summary"></div><div class="split"><div id="tests"></div><section class="detail" id="detail"></section></div>';
604
680
  paintCounts(document.getElementById('test-tools'), true);
605
681
  paintTags();
606
682
  paintOrder();
@@ -613,8 +689,13 @@
613
689
  var host = document.getElementById('tests');
614
690
  if (!host) return;
615
691
  var tests = filteredTests();
692
+ var summary = document.getElementById('list-summary');
693
+ if (summary) fill(summary, [
694
+ el('span', { role: 'status', 'aria-live': 'polite' }, [tests.length + ' of ' + data.tests.length + ' tests']),
695
+ hasFilters() ? el('button', { type: 'button', class: 'reset-filters' }, ['Clear filters']) : null,
696
+ ]);
616
697
  if (!tests.length) {
617
- fill(host, [el('p', { class: 'empty' }, [data.tests.length ? 'No tests match.' : 'No tests ran.'])]);
698
+ fill(host, [el('p', { class: 'empty' }, [data.tests.length ? 'No tests match. Clear filters or try another search.' : 'No tests ran.'])]);
618
699
  return;
619
700
  }
620
701
  if (state.order !== 'grouped') {
@@ -622,20 +703,20 @@
622
703
  var shown = sorted.slice(0, state.listLimit);
623
704
  var flat = shown.map(function (test) { return rowButton(test, true); });
624
705
  if (shown.length < sorted.length) {
625
- flat.push(el('button', { class: 'more', type: 'button', id: 'show-rest' }, ['Show the rest']));
706
+ flat.push(el('button', { class: 'more', type: 'button', id: 'show-rest' }, ['Show more tests']));
626
707
  }
627
708
  fill(host, flat);
628
709
  return;
629
710
  }
630
711
  var groups = [];
631
- var problemIds = {};
712
+ var problemIds = Object.create(null);
632
713
  var problems = tests.filter(isProblem);
633
714
  if (state.filter === 'all' && !state.query && problems.length && problems.length !== tests.length) {
634
715
  problems.forEach(function (test) { problemIds[test.id] = true; });
635
716
  groups.push({ name: 'Problems', tests: problems });
636
717
  }
637
718
  var byFile = [];
638
- var index = {};
719
+ var index = Object.create(null);
639
720
  tests.forEach(function (test) {
640
721
  if (problemIds[test.id]) return;
641
722
  if (index[test.file] == null) {
@@ -660,7 +741,7 @@
660
741
  return el('div', { class: 'group' }, [el('p', { class: 'file-name' }, [group.name])].concat(rows));
661
742
  }).filter(Boolean);
662
743
  if (truncated) {
663
- nodes.push(el('button', { class: 'more', type: 'button', id: 'show-rest' }, ['Show the rest']));
744
+ nodes.push(el('button', { class: 'more', type: 'button', id: 'show-rest' }, ['Show more tests']));
664
745
  }
665
746
  fill(host, nodes);
666
747
  }
@@ -694,7 +775,7 @@
694
775
  ['logs', 'Logs'],
695
776
  ['shots', 'Screenshots'],
696
777
  ];
697
- var tabs = el('div', { class: 'subtabs', role: 'tablist' }, panes.map(function (pane) {
778
+ var tabs = el('div', { class: 'subtabs', role: 'group', 'aria-label': 'Test details' }, panes.map(function (pane) {
698
779
  return el('button', {
699
780
  class: 'subtab',
700
781
  type: 'button',
@@ -705,6 +786,7 @@
705
786
  var body = [headerBlock(test), tabs];
706
787
  if (test.attempts.length > 1) body.push(attemptTabs(test));
707
788
  if (attempt) body.push(paneBody(test, attempt));
789
+ else body.push(el('p', { class: 'empty block' }, ['This test did not run.']));
708
790
  fill(host, [el('div', { class: 'detail-body' }, body)]);
709
791
  }
710
792
 
@@ -720,7 +802,7 @@
720
802
  if (test.group && test.group.length) bits.push(el('p', { class: 'kicker' }, [test.group.join(' / ')]));
721
803
  bits.push(el('h2', null, [test.title]));
722
804
  var where = [el('span', null, [test.file + ':' + test.line])];
723
- if (test.project) where.push(el('span', null, [test.project]));
805
+ if (data.showProjects === true && test.project) where.push(el('span', null, [test.project]));
724
806
  (test.tags || []).forEach(function (tag) { where.push(el('span', null, [tag])); });
725
807
  bits.push(el('p', { class: 'where' }, where));
726
808
  bits.push(el('p', { class: 'status-line' }, [
@@ -748,7 +830,8 @@
748
830
 
749
831
  function errorBlock(attempt) {
750
832
  if (!attempt.errors || !attempt.errors.length) return null;
751
- var image = (attempt.attachments || []).find(function (item) { return item.kind === 'image' && safeSrc(item.path); });
833
+ var images = (attempt.attachments || []).filter(function (item) { return item.kind === 'image' && safeSrc(item.path); });
834
+ var image = images.find(function (item) { return item.role === 'failure'; }) || images[images.length - 1];
752
835
  var messages = attempt.errors.map(function (error) {
753
836
  var nodes = [el('pre', null, [error.message || 'The test failed.'])];
754
837
  if (error.location) nodes.push(el('p', { class: 'where-error' }, [error.location]));
@@ -838,8 +921,9 @@
838
921
  var src = safeSrc(file.path);
839
922
  var copy = el('div', { class: 'tile-copy' }, [
840
923
  el('p', { class: 'tile-title' }, [test.title]),
924
+ el('p', { class: 'tile-meta' }, ['Attempt ' + (index + 1) + ' · ' + attemptLabel(attempt.status)]),
841
925
  el('p', { class: 'tile-meta' }, [file.stepTitle || file.name || 'Screenshot']),
842
- el('button', { class: 'open-test', type: 'button', 'data-id': test.id, 'data-attempt': String(index) }, ['Open test']),
926
+ el('button', { class: 'open-test', type: 'button', 'data-id': test.id, 'data-attempt': String(index), 'data-open-pane': 'shots' }, ['Open test']),
843
927
  ]);
844
928
  var tile = el('div', { class: 'tile', 'data-role': file.role || '', 'data-status': test.status }, [
845
929
  el('button', { class: 'shot', type: 'button', 'data-src': src, 'data-alt': file.name || test.title }, [
@@ -903,7 +987,7 @@
903
987
  ];
904
988
  if (attempt.stdout) nodes.push(el('div', { class: 'well' }, [el('p', null, ['Output']), el('pre', null, [attempt.stdout])]));
905
989
  if (attempt.stderr) nodes.push(el('div', { class: 'well' }, [el('p', null, ['Error output']), el('pre', null, [attempt.stderr])]));
906
- nodes.push(el('button', { class: 'open-test', type: 'button', 'data-id': test.id, 'data-attempt': String(index) }, ['Open test']));
990
+ nodes.push(el('button', { class: 'open-test', type: 'button', 'data-id': test.id, 'data-attempt': String(index), 'data-open-pane': 'logs' }, ['Open test']));
907
991
  var card = el('article', { class: 'log-card', 'data-blob': blob }, nodes);
908
992
  cards.push(card);
909
993
  });
@@ -911,15 +995,21 @@
911
995
  fill(host, [
912
996
  el('h2', null, ['Logs']),
913
997
  el('input', { id: 'log-find', class: 'find', type: 'search', placeholder: 'Find in logs', 'aria-label': 'Find in logs' }),
914
- el('div', { id: 'log-list' }, cards.length ? cards : [el('p', { class: 'empty' }, ['No logs in this run.'])]),
998
+ el('p', { class: 'empty', id: 'log-empty', hidden: cards.length > 0 }, ['No logs in this run.']),
999
+ el('div', { id: 'log-list' }, cards),
915
1000
  ]);
916
1001
  }
917
1002
 
918
1003
  function applyLogFilter() {
919
1004
  var query = state.logQuery.trim().toLowerCase();
1005
+ var visible = 0;
920
1006
  document.querySelectorAll('.log-card').forEach(function (card) {
921
1007
  card.hidden = Boolean(query) && card.dataset.blob.indexOf(query) === -1;
1008
+ if (!card.hidden) visible += 1;
922
1009
  });
1010
+ var empty = document.getElementById('log-empty');
1011
+ empty.hidden = visible > 0;
1012
+ empty.textContent = query ? 'No logs match. Try another search.' : 'No logs in this run.';
923
1013
  }
924
1014
 
925
1015
  function ensureFiles() {
@@ -956,10 +1046,30 @@
956
1046
  }
957
1047
 
958
1048
  function bind() {
959
- window.addEventListener('resize', placeTabGlow);
1049
+ window.addEventListener('hashchange', function () {
1050
+ var next = readRoute();
1051
+ if (next.tab === 'tests' && next.id && testById(next.id)) {
1052
+ resetFilters();
1053
+ state.selectedId = next.id;
1054
+ state.attempt = failedAttempt(testById(next.id));
1055
+ state.testPane = defaultPane(testById(next.id));
1056
+ if (built.tests) { paintTags(); paintOrder(); paintList(); paintDetail(); syncPressed(); }
1057
+ }
1058
+ showTab(next.tab, true);
1059
+ });
1060
+ document.querySelector('.skip').addEventListener('click', function (event) {
1061
+ event.preventDefault();
1062
+ showTab('tests');
1063
+ document.getElementById('find').focus();
1064
+ });
960
1065
  app.addEventListener('click', function (event) {
961
1066
  var target = event.target instanceof Element ? event.target : null;
962
1067
  if (!target) return;
1068
+ if (target.closest('.reset-filters')) {
1069
+ resetFilters(); keepSelectionInFilter(); paintTags(); paintOrder(); paintDuration(); paintList(); paintDetail(); syncPressed();
1070
+ document.getElementById('find').focus();
1071
+ return;
1072
+ }
963
1073
  var tab = target.closest('.tab');
964
1074
  if (tab) {
965
1075
  showTab(tab.dataset.tab);
@@ -973,7 +1083,7 @@
973
1083
  }
974
1084
  var opener = target.closest('.open-test');
975
1085
  if (opener) {
976
- openTest(opener.dataset.id, Number(opener.dataset.attempt || 0));
1086
+ openTest(opener.dataset.id, Number(opener.dataset.attempt || 0), opener.dataset.openPane);
977
1087
  return;
978
1088
  }
979
1089
  var view = target.closest('.view');
@@ -982,9 +1092,10 @@
982
1092
  applyOverview();
983
1093
  return;
984
1094
  }
985
- var band = target.closest('.band');
1095
+ var band = target.closest('.band') || target.closest('.slice');
986
1096
  if (band) {
987
- state.band = state.band === band.dataset.band ? 'all' : band.dataset.band;
1097
+ var bandId = band.dataset.band || band.getAttribute('data-slice');
1098
+ state.band = state.band === bandId ? 'all' : bandId;
988
1099
  state.order = state.band === 'all' ? state.order : 'slowest';
989
1100
  state.listLimit = 200;
990
1101
  keepSelectionInFilter();
@@ -1009,7 +1120,7 @@
1009
1120
  }
1010
1121
  var tag = target.closest('.tag-btn');
1011
1122
  if (tag) {
1012
- state.tag = tag.dataset.value || 'all';
1123
+ state.tag = tag.dataset.value || null;
1013
1124
  state.listLimit = 200;
1014
1125
  keepSelectionInFilter();
1015
1126
  paintTags();
@@ -1019,7 +1130,7 @@
1019
1130
  }
1020
1131
  var projectButton = target.closest('.project-btn');
1021
1132
  if (projectButton) {
1022
- state.projectFilter = projectButton.dataset.value || 'all';
1133
+ state.projectFilter = projectButton.dataset.value || null;
1023
1134
  state.listLimit = 200;
1024
1135
  keepSelectionInFilter();
1025
1136
  paintTags();
@@ -1047,7 +1158,6 @@
1047
1158
  paintList();
1048
1159
  paintDetail();
1049
1160
  markStrip();
1050
- showNow(state.selectedId);
1051
1161
  }
1052
1162
  return;
1053
1163
  }
@@ -1065,12 +1175,14 @@
1065
1175
  if (pane) {
1066
1176
  state.testPane = pane.dataset.pane || 'steps';
1067
1177
  paintDetail();
1178
+ document.querySelector('.subtab[data-pane="' + state.testPane + '"]').focus();
1068
1179
  return;
1069
1180
  }
1070
1181
  var attempt = target.closest('.attempt');
1071
1182
  if (attempt) {
1072
1183
  state.attempt = Number(attempt.dataset.index || 0);
1073
1184
  paintDetail();
1185
+ document.querySelector('.attempt[data-index="' + state.attempt + '"]').focus();
1074
1186
  return;
1075
1187
  }
1076
1188
  if (target.closest('#api-toggle')) {
@@ -1080,9 +1192,17 @@
1080
1192
  if (toggle) toggle.focus();
1081
1193
  return;
1082
1194
  }
1195
+ if (target.closest('#show-more-timeline')) {
1196
+ state.timelineLimit += 200; paintStrip();
1197
+ var moreTimeline = document.getElementById('show-more-timeline');
1198
+ if (moreTimeline) moreTimeline.focus();
1199
+ return;
1200
+ }
1083
1201
  if (target.closest('#show-rest')) {
1084
- state.listLimit = 100000;
1202
+ state.listLimit += 200;
1085
1203
  paintList();
1204
+ var more = document.getElementById('show-rest');
1205
+ if (more) more.focus();
1086
1206
  return;
1087
1207
  }
1088
1208
  var shot = target.closest('.shot');
@@ -1101,7 +1221,6 @@
1101
1221
  paintList();
1102
1222
  paintDetail();
1103
1223
  markStrip();
1104
- showNow(state.selectedId);
1105
1224
  }, 80);
1106
1225
  }
1107
1226
  if (event.target.id === 'log-find') {
@@ -1110,21 +1229,12 @@
1110
1229
  }
1111
1230
  });
1112
1231
 
1113
- app.addEventListener('mouseover', function (event) {
1114
- var segment = event.target && event.target.closest ? event.target.closest('.run-row') : null;
1115
- if (segment) showNow(segment.dataset.id);
1116
- });
1117
- app.addEventListener('mouseout', function (event) {
1118
- var segment = event.target && event.target.closest ? event.target.closest('.run-row') : null;
1119
- if (!segment) return;
1120
- var next = event.relatedTarget && event.relatedTarget.closest ? event.relatedTarget.closest('.run-row') : null;
1121
- if (!next) showNow(state.selectedId);
1122
- });
1123
-
1124
1232
  document.addEventListener('keydown', function (event) {
1125
- var typing = event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement;
1233
+ if (event.defaultPrevented || event.isComposing || event.metaKey || event.ctrlKey || event.altKey || document.querySelector('dialog[open]')) return;
1234
+ var typing = event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLSelectElement || event.target.isContentEditable;
1126
1235
  if (typing) {
1127
1236
  if (event.key === 'Escape') {
1237
+ window.clearTimeout(findTimer);
1128
1238
  event.target.value = '';
1129
1239
  if (event.target.id === 'find') {
1130
1240
  state.query = '';
@@ -1132,7 +1242,6 @@
1132
1242
  paintList();
1133
1243
  paintDetail();
1134
1244
  markStrip();
1135
- showNow(state.selectedId);
1136
1245
  }
1137
1246
  if (event.target.id === 'log-find') {
1138
1247
  state.logQuery = '';
@@ -1142,7 +1251,8 @@
1142
1251
  return;
1143
1252
  }
1144
1253
  if (event.key >= '1' && event.key <= '5') {
1145
- showTab(TABS[Number(event.key) - 1]);
1254
+ var destination = TABS[Number(event.key) - 1];
1255
+ if (destination) showTab(destination);
1146
1256
  return;
1147
1257
  }
1148
1258
  if (event.key === '/') {
@@ -1164,7 +1274,7 @@
1164
1274
  if (next) next.focus();
1165
1275
  return;
1166
1276
  }
1167
- if (state.tab !== 'tests' || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) return;
1277
+ if (state.tab !== 'tests' || !event.target.closest || !event.target.closest('#tests') || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) return;
1168
1278
  var rows = Array.prototype.slice.call(document.querySelectorAll('#tests .row'));
1169
1279
  if (!rows.length) return;
1170
1280
  event.preventDefault();
@@ -1179,21 +1289,15 @@
1179
1289
 
1180
1290
  function choose(id, resetFilter) {
1181
1291
  if (!id || !testById(id)) return;
1182
- if (resetFilter) {
1183
- state.filter = 'all';
1184
- state.query = '';
1185
- state.band = 'all';
1186
- state.tag = 'all';
1187
- state.projectFilter = 'all';
1188
- var find = document.getElementById('find');
1189
- if (find) find.value = '';
1190
- }
1292
+ if (resetFilter) resetFilters();
1191
1293
  if (state.selectedId !== id) {
1192
1294
  var test = testById(id);
1193
1295
  state.selectedId = id;
1194
1296
  state.attempt = failedAttempt(test);
1195
1297
  state.testPane = defaultPane(test);
1196
1298
  }
1299
+ var position = filteredTests().findIndex(function (test) { return test.id === id; });
1300
+ state.listLimit = Math.max(state.listLimit, position + 1);
1197
1301
  showTab('tests');
1198
1302
  if (built.tests) {
1199
1303
  syncPressed();
@@ -1204,37 +1308,46 @@
1204
1308
  markStrip();
1205
1309
  }
1206
1310
  paintDuration();
1207
- showNow(state.selectedId);
1208
1311
  var row = document.querySelector('#tests .row[data-id="' + cssEscape(id) + '"]');
1312
+ if (row) row.focus({ preventScroll: true });
1209
1313
  if (row && row.scrollIntoView) row.scrollIntoView({ block: 'nearest', behavior: reduce ? 'auto' : 'smooth' });
1210
1314
  }
1211
1315
 
1316
+ function hasFilters() {
1317
+ return state.filter !== 'all' || state.query.trim() || state.band !== 'all' || state.tag !== null || state.projectFilter !== null;
1318
+ }
1319
+
1320
+ function resetFilters() {
1321
+ window.clearTimeout(findTimer);
1322
+ state.filter = 'all'; state.query = ''; state.band = 'all'; state.tag = null; state.projectFilter = null;
1323
+ state.listLimit = 200;
1324
+ var find = document.getElementById('find');
1325
+ if (find) find.value = '';
1326
+ }
1327
+
1212
1328
  function keepSelectionInFilter() {
1213
1329
  var tests = filteredTests();
1330
+ markStrip();
1214
1331
  if (tests.some(function (test) { return test.id === state.selectedId; })) return;
1215
1332
  var next = tests[0];
1216
1333
  if (!next) {
1217
1334
  state.selectedId = null;
1335
+ writeHash();
1218
1336
  return;
1219
1337
  }
1220
1338
  state.selectedId = next.id;
1221
1339
  state.attempt = failedAttempt(next);
1222
1340
  state.testPane = defaultPane(next);
1341
+ writeHash();
1223
1342
  }
1224
1343
 
1225
- function openTest(id, attempt) {
1344
+ function openTest(id, attempt, pane) {
1226
1345
  var test = testById(id);
1227
1346
  if (!test) return;
1228
- state.filter = 'all';
1229
- state.query = '';
1230
- state.band = 'all';
1231
- state.tag = 'all';
1232
- state.projectFilter = 'all';
1233
- var find = document.getElementById('find');
1234
- if (find) find.value = '';
1347
+ resetFilters();
1235
1348
  state.selectedId = id;
1236
1349
  state.attempt = attempt;
1237
- state.testPane = 'shots';
1350
+ state.testPane = pane === 'logs' ? 'logs' : 'shots';
1238
1351
  showTab('tests');
1239
1352
  if (built.tests) {
1240
1353
  paintTags();
@@ -1243,43 +1356,30 @@
1243
1356
  paintDetail();
1244
1357
  markStrip();
1245
1358
  }
1359
+ syncPressed();
1246
1360
  paintDuration();
1361
+ document.getElementById('detail').scrollIntoView({ block: 'nearest' });
1247
1362
  }
1248
1363
 
1249
1364
  function openShot(src, alt) {
1250
1365
  var safe = safeSrc(src);
1251
1366
  if (!safe) return;
1252
1367
  var close = el('button', { class: 'close-shot', type: 'button' }, ['Close']);
1253
- var dialog = el('dialog', null, [close, el('img', { src: safe, alt: alt })]);
1368
+ var returnFocus = document.activeElement;
1369
+ var dialog = el('dialog', { 'aria-label': alt || 'Screenshot' }, [close, el('img', { src: safe, alt: alt })]);
1254
1370
  close.addEventListener('click', function () { dialog.close(); });
1255
1371
  dialog.addEventListener('click', function (event) { if (event.target === dialog) dialog.close(); });
1256
- dialog.addEventListener('close', function () { dialog.remove(); });
1372
+ dialog.addEventListener('close', function () { dialog.remove(); if (returnFocus && returnFocus.isConnected) returnFocus.focus(); });
1257
1373
  document.body.appendChild(dialog);
1258
1374
  dialog.showModal();
1259
1375
  }
1260
1376
 
1261
- function showNow(id) {
1262
- var host = document.getElementById('now');
1263
- if (!host) return;
1264
- var test = id ? testById(id) : null;
1265
- if (!test) {
1266
- fill(host, [data.tests.length ? 'Hover a test, or click one.' : 'No tests ran.']);
1267
- return;
1268
- }
1269
- fill(host, [
1270
- el('span', { class: 'sw', 'data-status': test.status }, [statusLabel(test)]),
1271
- el('span', { class: 'name' }, [test.title]),
1272
- el('span', null, [formatDuration(test.duration)]),
1273
- el('span', { class: 'now-file' }, [test.file]),
1274
- ]);
1275
- }
1276
-
1277
1377
  function filteredTests() {
1278
1378
  var query = state.query.trim().toLowerCase();
1279
1379
  return data.tests.filter(function (test) {
1280
1380
  if (state.filter !== 'all' && test.status !== state.filter) return false;
1281
- if (state.tag !== 'all' && (test.tags || []).indexOf(state.tag) === -1) return false;
1282
- if (state.projectFilter !== 'all' && test.project !== state.projectFilter) return false;
1381
+ if (state.tag !== null && (test.tags || []).indexOf(state.tag) === -1) return false;
1382
+ if (state.projectFilter !== null && test.project !== state.projectFilter) return false;
1283
1383
  if (state.band !== 'all' && (test.status === 'skipped' || durationBand(test.duration) !== state.band)) return false;
1284
1384
  if (!query) return true;
1285
1385
  var blob = [test.title, test.file, test.project].concat(test.group || [], test.tags || []).join(' ').toLowerCase();
@@ -1293,7 +1393,7 @@
1293
1393
  }
1294
1394
 
1295
1395
  function testById(id) {
1296
- return data.tests.find(function (test) { return test.id === id; }) || null;
1396
+ return testIndex.get(id) || null;
1297
1397
  }
1298
1398
 
1299
1399
  function failedAttempt(test) {
@@ -1307,7 +1407,7 @@
1307
1407
 
1308
1408
  function defaultPane(test) {
1309
1409
  if (!test) return 'steps';
1310
- if (test.status === 'failed' || test.status === 'timedOut' || test.status === 'flaky') return 'error';
1410
+ if (isProblem(test)) return 'error';
1311
1411
  return 'steps';
1312
1412
  }
1313
1413
 
@@ -1361,12 +1461,19 @@
1361
1461
  return value;
1362
1462
  }
1363
1463
 
1464
+ function commitText() {
1465
+ var row = (data.info || []).find(function (item) { return item.label === 'Commit'; });
1466
+ return row && row.value ? row.value : '';
1467
+ }
1468
+
1364
1469
  function metaBits() {
1365
1470
  var bits = [];
1366
- if (data.projectName) bits.push(data.projectName);
1367
- if (Array.isArray(data.projects) && data.projects.length) bits.push(data.projects.join(', '));
1471
+ if (data.showProjects === true && data.projectName) bits.push(data.projectName);
1472
+ if (data.showProjects === true && Array.isArray(data.projects) && data.projects.length) bits.push(data.projects.join(', '));
1368
1473
  if (data.startTime) bits.push(formatWhen(data.startTime));
1369
- if (data.playwrightVersion) bits.push('Playwright ' + data.playwrightVersion);
1474
+ var sha = commitText();
1475
+ if (sha) bits.push(sha);
1476
+ if (data.showPlaywrightVersion === true && data.playwrightVersion) bits.push('Playwright ' + data.playwrightVersion);
1370
1477
  if (data.shard) bits.push('Shard ' + data.shard);
1371
1478
  return bits;
1372
1479
  }