playwright-reborn 0.2.2 → 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(''),
@@ -93,6 +95,7 @@
93
95
  var canvas = document.querySelector('.atmosphere .bands');
94
96
  if (!canvas || !canvas.getContext) return;
95
97
  var ctx = canvas.getContext('2d');
98
+ if (!ctx) return;
96
99
  var ratio = 1;
97
100
  var columns = [];
98
101
  var count = 22;
@@ -119,9 +122,9 @@
119
122
  var t = reduce ? 8 : now / 1000;
120
123
  ctx.clearRect(0, 0, w, h);
121
124
  var wash = ctx.createLinearGradient(0, 0, 0, h);
122
- wash.addColorStop(0, '#123d24');
123
- wash.addColorStop(0.42, '#0a2416');
124
- wash.addColorStop(1, '#050806');
125
+ wash.addColorStop(0, 'rgba(' + rgb + ', 0.10)');
126
+ wash.addColorStop(0.42, '#080b0a');
127
+ wash.addColorStop(1, '#080808');
125
128
  ctx.fillStyle = wash;
126
129
  ctx.fillRect(0, 0, w, h);
127
130
  columns.forEach(function (column) {
@@ -139,11 +142,16 @@
139
142
  ctx.fillStyle = gradient;
140
143
  ctx.fillRect(x - spread, 0, spread * 2, h * 0.92);
141
144
  });
142
- if (!reduce) requestAnimationFrame(draw);
145
+ if (!reduce && !document.hidden) curtainFrame = requestAnimationFrame(draw);
143
146
  }
147
+ var curtainFrame = 0;
144
148
  resize();
145
- window.addEventListener('resize', resize);
146
- 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) {
147
155
  if (!canvas.width) resize();
148
156
  draw(now);
149
157
  });
@@ -152,13 +160,14 @@
152
160
  function watchHeader() {
153
161
  var rail = document.querySelector('.rail');
154
162
  if (!rail) return;
163
+ rail.addEventListener('focusin', function () { rail.classList.remove('is-hidden'); });
155
164
  var lastY = window.scrollY;
156
165
  window.addEventListener('scroll', function () {
157
166
  var y = window.scrollY;
158
167
  var down = y > lastY + 6;
159
168
  var up = y < lastY - 6;
160
169
  if (y < 24) rail.classList.remove('is-hidden');
161
- else if (down) rail.classList.add('is-hidden');
170
+ else if (down && !rail.contains(document.activeElement)) rail.classList.add('is-hidden');
162
171
  else if (up) rail.classList.remove('is-hidden');
163
172
  lastY = y;
164
173
  }, { passive: true });
@@ -185,16 +194,17 @@
185
194
  }
186
195
 
187
196
  function tabButton(id, label) {
188
- 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>';
189
198
  }
190
199
 
191
200
  function readRoute() {
192
- var hash = '';
193
- try { hash = decodeURIComponent(location.hash.slice(1)); } catch (error) { hash = ''; }
194
- 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' }; }
195
206
  if (TABS.indexOf(hash) !== -1) return { tab: hash };
196
- if (hash.indexOf('tests/') === 0) return { tab: 'tests', id: decodeURIComponent(hash.slice(6)) };
197
- 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 };
198
208
  return { tab: 'overview' };
199
209
  }
200
210
 
@@ -208,11 +218,13 @@
208
218
  state.tab = tab;
209
219
  document.querySelectorAll('[role="tab"]').forEach(function (button) {
210
220
  button.setAttribute('aria-selected', button.dataset.tab === tab ? 'true' : 'false');
221
+ button.tabIndex = button.dataset.tab === tab ? 0 : -1;
211
222
  });
212
223
  TABS.forEach(function (name) {
213
224
  var panel = document.getElementById('panel-' + name);
214
225
  var on = name === tab;
215
226
  panel.hidden = !on;
227
+ panel.setAttribute('aria-labelledby', 'tab-' + name);
216
228
  panel.classList.toggle('panel', true);
217
229
  if (on && !quiet && !reduce) {
218
230
  panel.classList.remove('panel-enter');
@@ -224,25 +236,18 @@
224
236
  if (tab === 'screenshots') ensureShots();
225
237
  if (tab === 'logs') ensureLogs();
226
238
  if (tab === 'files') ensureFiles();
227
- writeHash();
228
- placeTabGlow();
239
+ writeHash(!quiet);
240
+
229
241
  }
230
242
 
231
- function writeHash() {
243
+ function writeHash(push) {
232
244
  var next = state.tab === 'overview' ? 'overview' : state.tab;
233
245
  if (state.tab === 'tests' && state.selectedId) next = 'tests/' + encodeURIComponent(state.selectedId);
234
- if (window.history && history.replaceState) history.replaceState(null, '', '#' + next);
235
- }
236
-
237
- function placeTabGlow() {
238
- var bar = document.querySelector('.tabbar');
239
- var glow = document.getElementById('tab-glow');
240
- var active = document.querySelector('.tab[aria-selected="true"]');
241
- if (!bar || !glow || !active) return;
242
- var barBox = bar.getBoundingClientRect();
243
- var box = active.getBoundingClientRect();
244
- glow.style.width = box.width + 'px';
245
- 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
+ }
246
251
  }
247
252
 
248
253
  function paintOverview() {
@@ -250,7 +255,7 @@
250
255
  host.innerHTML = [
251
256
  '<header class="hero">',
252
257
  '<div><h1 class="word" id="word"></h1><p class="product" id="product"></p><p class="summary" id="summary"></p></div>',
253
- '<p class="time" id="time"></p>',
258
+ '<p class="run-state" id="run-state"></p>',
254
259
  '</header>',
255
260
  '<div id="errors"></div>',
256
261
  '<dl class="info" id="info"></dl>',
@@ -278,14 +283,15 @@
278
283
  if (subtitle) product.textContent = subtitle;
279
284
  else product.hidden = true;
280
285
  document.getElementById('summary').textContent = data.summary || '';
281
- 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';
282
289
  paintErrors();
283
290
  paintInfo();
284
291
  paintCounts(document.getElementById('legend'), false);
285
292
  paintDuration();
286
293
  paintStrip();
287
294
  applyOverview();
288
- showNow(state.selectedId);
289
295
  }
290
296
 
291
297
  function paintInfo() {
@@ -479,7 +485,7 @@
479
485
  function paintCounts(host, withFind) {
480
486
  var counts = data.counts || {};
481
487
  var filters = [
482
- ['all', 'ran', counts.total || 0],
488
+ ['all', 'tests', counts.total || 0],
483
489
  ['passed', 'passed', counts.passed || 0],
484
490
  ['failed', 'failed', counts.failed || 0],
485
491
  ['flaky', 'flaky', counts.flaky || 0],
@@ -517,7 +523,7 @@
517
523
  }
518
524
 
519
525
  function syncPressed() {
520
- document.querySelectorAll('.count').forEach(function (button) {
526
+ document.querySelectorAll('.count[data-status]').forEach(function (button) {
521
527
  button.setAttribute('aria-pressed', button.dataset.status === state.filter ? 'true' : 'false');
522
528
  });
523
529
  }
@@ -538,7 +544,7 @@
538
544
  function paintStrip() {
539
545
  var strip = document.getElementById('strip');
540
546
  if (!data.tests.length) {
541
- fill(strip, []);
547
+ fill(strip, [el('p', { class: 'empty' }, ['No tests ran.'])]);
542
548
  return;
543
549
  }
544
550
  var max = data.tests.reduce(function (hi, test) { return Math.max(hi, test.duration || 0); }, 1);
@@ -547,7 +553,7 @@
547
553
  });
548
554
  strip.className = 'run-list';
549
555
  strip.style.height = '';
550
- fill(strip, ordered.map(function (test) {
556
+ var nodes = ordered.slice(0, state.timelineLimit).map(function (test) {
551
557
  var fillBar = el('span', { class: 'run-fill', 'data-status': test.status });
552
558
  fillBar.style.width = Math.max(2, Math.round(((test.duration || 0) / max) * 100)) + '%';
553
559
  return el('button', {
@@ -562,51 +568,10 @@
562
568
  el('span', { class: 'run-track' }, [fillBar]),
563
569
  el('span', { class: 'run-time' }, [formatDuration(test.duration)]),
564
570
  ]);
565
- }));
566
- markStrip();
567
- }
568
-
569
- function paintCanvasStrip(host) {
570
- var canvas = el('canvas', { class: 'strip-canvas', 'aria-label': 'Tests by duration' });
571
- fill(host, [canvas]);
572
- var hits = [];
573
- function draw() {
574
- var width = host.clientWidth || 600;
575
- var height = 52;
576
- var ratio = window.devicePixelRatio || 1;
577
- canvas.width = Math.floor(width * ratio);
578
- canvas.height = Math.floor(height * ratio);
579
- canvas.style.width = width + 'px';
580
- canvas.style.height = height + 'px';
581
- var ctx = canvas.getContext('2d');
582
- ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
583
- ctx.clearRect(0, 0, width, height);
584
- var total = data.tests.reduce(function (sum, test) { return sum + Math.max(test.duration || 0, 1); }, 0);
585
- var x = 0;
586
- hits = [];
587
- data.tests.forEach(function (test) {
588
- var w = Math.max(2, (Math.max(test.duration || 0, 1) / total) * width);
589
- ctx.fillStyle = canvasColor(test.status);
590
- ctx.fillRect(x, 8, Math.max(1, w - 2), 36);
591
- hits.push({ x: x, w: w, id: test.id });
592
- x += w;
593
- });
594
- }
595
- draw();
596
- window.addEventListener('resize', draw);
597
- canvas.addEventListener('mousemove', function (event) {
598
- var rect = canvas.getBoundingClientRect();
599
- var x = event.clientX - rect.left;
600
- var hit = hits.find(function (item) { return x >= item.x && x < item.x + item.w; });
601
- if (hit) showNow(hit.id);
602
- });
603
- canvas.addEventListener('mouseleave', function () { showNow(state.selectedId); });
604
- canvas.addEventListener('click', function (event) {
605
- var rect = canvas.getBoundingClientRect();
606
- var x = event.clientX - rect.left;
607
- var hit = hits.find(function (item) { return x >= item.x && x < item.x + item.w; });
608
- if (hit) choose(hit.id, true);
609
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();
610
575
  }
611
576
 
612
577
  function canvasColor(status) {
@@ -617,7 +582,7 @@
617
582
  }
618
583
 
619
584
  function markStrip() {
620
- var visible = {};
585
+ var visible = Object.create(null);
621
586
  filteredTests().forEach(function (test) { visible[test.id] = true; });
622
587
  document.querySelectorAll('.run-row').forEach(function (segment) {
623
588
  segment.classList.toggle('is-dim', !visible[segment.dataset.id]);
@@ -637,7 +602,7 @@
637
602
  }
638
603
 
639
604
  function uniqueValues(pick) {
640
- var seen = {};
605
+ var seen = Object.create(null);
641
606
  data.tests.forEach(function (test) {
642
607
  pick(test).forEach(function (value) {
643
608
  if (value) seen[value] = true;
@@ -654,12 +619,12 @@
654
619
  var nodes = [];
655
620
  if (tags.length) {
656
621
  nodes.push(el('span', { class: 'order-label' }, ['Tags']));
657
- nodes.push(chip('tag-btn', 'all', 'All', state.tag));
622
+ nodes.push(chip('tag-btn', '', 'All', state.tag || ''));
658
623
  tags.forEach(function (tag) { nodes.push(chip('tag-btn', tag, tag, state.tag)); });
659
624
  }
660
625
  if (data.showProjectFilter === true && projects.length > 1) {
661
626
  nodes.push(el('span', { class: 'order-label' }, ['Project']));
662
- nodes.push(chip('project-btn', 'all', 'All', state.projectFilter));
627
+ nodes.push(chip('project-btn', '', 'All', state.projectFilter || ''));
663
628
  projects.forEach(function (project) { nodes.push(chip('project-btn', project, project, state.projectFilter)); });
664
629
  }
665
630
  host.hidden = nodes.length === 0;
@@ -711,7 +676,7 @@
711
676
  if (built.tests) return;
712
677
  built.tests = true;
713
678
  var host = document.getElementById('panel-tests');
714
- 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>';
715
680
  paintCounts(document.getElementById('test-tools'), true);
716
681
  paintTags();
717
682
  paintOrder();
@@ -724,8 +689,13 @@
724
689
  var host = document.getElementById('tests');
725
690
  if (!host) return;
726
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
+ ]);
727
697
  if (!tests.length) {
728
- 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.'])]);
729
699
  return;
730
700
  }
731
701
  if (state.order !== 'grouped') {
@@ -733,20 +703,20 @@
733
703
  var shown = sorted.slice(0, state.listLimit);
734
704
  var flat = shown.map(function (test) { return rowButton(test, true); });
735
705
  if (shown.length < sorted.length) {
736
- 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']));
737
707
  }
738
708
  fill(host, flat);
739
709
  return;
740
710
  }
741
711
  var groups = [];
742
- var problemIds = {};
712
+ var problemIds = Object.create(null);
743
713
  var problems = tests.filter(isProblem);
744
714
  if (state.filter === 'all' && !state.query && problems.length && problems.length !== tests.length) {
745
715
  problems.forEach(function (test) { problemIds[test.id] = true; });
746
716
  groups.push({ name: 'Problems', tests: problems });
747
717
  }
748
718
  var byFile = [];
749
- var index = {};
719
+ var index = Object.create(null);
750
720
  tests.forEach(function (test) {
751
721
  if (problemIds[test.id]) return;
752
722
  if (index[test.file] == null) {
@@ -771,7 +741,7 @@
771
741
  return el('div', { class: 'group' }, [el('p', { class: 'file-name' }, [group.name])].concat(rows));
772
742
  }).filter(Boolean);
773
743
  if (truncated) {
774
- 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']));
775
745
  }
776
746
  fill(host, nodes);
777
747
  }
@@ -805,7 +775,7 @@
805
775
  ['logs', 'Logs'],
806
776
  ['shots', 'Screenshots'],
807
777
  ];
808
- 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) {
809
779
  return el('button', {
810
780
  class: 'subtab',
811
781
  type: 'button',
@@ -816,6 +786,7 @@
816
786
  var body = [headerBlock(test), tabs];
817
787
  if (test.attempts.length > 1) body.push(attemptTabs(test));
818
788
  if (attempt) body.push(paneBody(test, attempt));
789
+ else body.push(el('p', { class: 'empty block' }, ['This test did not run.']));
819
790
  fill(host, [el('div', { class: 'detail-body' }, body)]);
820
791
  }
821
792
 
@@ -831,7 +802,7 @@
831
802
  if (test.group && test.group.length) bits.push(el('p', { class: 'kicker' }, [test.group.join(' / ')]));
832
803
  bits.push(el('h2', null, [test.title]));
833
804
  var where = [el('span', null, [test.file + ':' + test.line])];
834
- if (test.project) where.push(el('span', null, [test.project]));
805
+ if (data.showProjects === true && test.project) where.push(el('span', null, [test.project]));
835
806
  (test.tags || []).forEach(function (tag) { where.push(el('span', null, [tag])); });
836
807
  bits.push(el('p', { class: 'where' }, where));
837
808
  bits.push(el('p', { class: 'status-line' }, [
@@ -859,7 +830,8 @@
859
830
 
860
831
  function errorBlock(attempt) {
861
832
  if (!attempt.errors || !attempt.errors.length) return null;
862
- 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];
863
835
  var messages = attempt.errors.map(function (error) {
864
836
  var nodes = [el('pre', null, [error.message || 'The test failed.'])];
865
837
  if (error.location) nodes.push(el('p', { class: 'where-error' }, [error.location]));
@@ -949,8 +921,9 @@
949
921
  var src = safeSrc(file.path);
950
922
  var copy = el('div', { class: 'tile-copy' }, [
951
923
  el('p', { class: 'tile-title' }, [test.title]),
924
+ el('p', { class: 'tile-meta' }, ['Attempt ' + (index + 1) + ' · ' + attemptLabel(attempt.status)]),
952
925
  el('p', { class: 'tile-meta' }, [file.stepTitle || file.name || 'Screenshot']),
953
- 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']),
954
927
  ]);
955
928
  var tile = el('div', { class: 'tile', 'data-role': file.role || '', 'data-status': test.status }, [
956
929
  el('button', { class: 'shot', type: 'button', 'data-src': src, 'data-alt': file.name || test.title }, [
@@ -1014,7 +987,7 @@
1014
987
  ];
1015
988
  if (attempt.stdout) nodes.push(el('div', { class: 'well' }, [el('p', null, ['Output']), el('pre', null, [attempt.stdout])]));
1016
989
  if (attempt.stderr) nodes.push(el('div', { class: 'well' }, [el('p', null, ['Error output']), el('pre', null, [attempt.stderr])]));
1017
- 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']));
1018
991
  var card = el('article', { class: 'log-card', 'data-blob': blob }, nodes);
1019
992
  cards.push(card);
1020
993
  });
@@ -1022,15 +995,21 @@
1022
995
  fill(host, [
1023
996
  el('h2', null, ['Logs']),
1024
997
  el('input', { id: 'log-find', class: 'find', type: 'search', placeholder: 'Find in logs', 'aria-label': 'Find in logs' }),
1025
- 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),
1026
1000
  ]);
1027
1001
  }
1028
1002
 
1029
1003
  function applyLogFilter() {
1030
1004
  var query = state.logQuery.trim().toLowerCase();
1005
+ var visible = 0;
1031
1006
  document.querySelectorAll('.log-card').forEach(function (card) {
1032
1007
  card.hidden = Boolean(query) && card.dataset.blob.indexOf(query) === -1;
1008
+ if (!card.hidden) visible += 1;
1033
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.';
1034
1013
  }
1035
1014
 
1036
1015
  function ensureFiles() {
@@ -1067,10 +1046,30 @@
1067
1046
  }
1068
1047
 
1069
1048
  function bind() {
1070
- 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
+ });
1071
1065
  app.addEventListener('click', function (event) {
1072
1066
  var target = event.target instanceof Element ? event.target : null;
1073
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
+ }
1074
1073
  var tab = target.closest('.tab');
1075
1074
  if (tab) {
1076
1075
  showTab(tab.dataset.tab);
@@ -1084,7 +1083,7 @@
1084
1083
  }
1085
1084
  var opener = target.closest('.open-test');
1086
1085
  if (opener) {
1087
- openTest(opener.dataset.id, Number(opener.dataset.attempt || 0));
1086
+ openTest(opener.dataset.id, Number(opener.dataset.attempt || 0), opener.dataset.openPane);
1088
1087
  return;
1089
1088
  }
1090
1089
  var view = target.closest('.view');
@@ -1121,7 +1120,7 @@
1121
1120
  }
1122
1121
  var tag = target.closest('.tag-btn');
1123
1122
  if (tag) {
1124
- state.tag = tag.dataset.value || 'all';
1123
+ state.tag = tag.dataset.value || null;
1125
1124
  state.listLimit = 200;
1126
1125
  keepSelectionInFilter();
1127
1126
  paintTags();
@@ -1131,7 +1130,7 @@
1131
1130
  }
1132
1131
  var projectButton = target.closest('.project-btn');
1133
1132
  if (projectButton) {
1134
- state.projectFilter = projectButton.dataset.value || 'all';
1133
+ state.projectFilter = projectButton.dataset.value || null;
1135
1134
  state.listLimit = 200;
1136
1135
  keepSelectionInFilter();
1137
1136
  paintTags();
@@ -1159,7 +1158,6 @@
1159
1158
  paintList();
1160
1159
  paintDetail();
1161
1160
  markStrip();
1162
- showNow(state.selectedId);
1163
1161
  }
1164
1162
  return;
1165
1163
  }
@@ -1177,12 +1175,14 @@
1177
1175
  if (pane) {
1178
1176
  state.testPane = pane.dataset.pane || 'steps';
1179
1177
  paintDetail();
1178
+ document.querySelector('.subtab[data-pane="' + state.testPane + '"]').focus();
1180
1179
  return;
1181
1180
  }
1182
1181
  var attempt = target.closest('.attempt');
1183
1182
  if (attempt) {
1184
1183
  state.attempt = Number(attempt.dataset.index || 0);
1185
1184
  paintDetail();
1185
+ document.querySelector('.attempt[data-index="' + state.attempt + '"]').focus();
1186
1186
  return;
1187
1187
  }
1188
1188
  if (target.closest('#api-toggle')) {
@@ -1192,9 +1192,17 @@
1192
1192
  if (toggle) toggle.focus();
1193
1193
  return;
1194
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
+ }
1195
1201
  if (target.closest('#show-rest')) {
1196
- state.listLimit = 100000;
1202
+ state.listLimit += 200;
1197
1203
  paintList();
1204
+ var more = document.getElementById('show-rest');
1205
+ if (more) more.focus();
1198
1206
  return;
1199
1207
  }
1200
1208
  var shot = target.closest('.shot');
@@ -1213,7 +1221,6 @@
1213
1221
  paintList();
1214
1222
  paintDetail();
1215
1223
  markStrip();
1216
- showNow(state.selectedId);
1217
1224
  }, 80);
1218
1225
  }
1219
1226
  if (event.target.id === 'log-find') {
@@ -1222,21 +1229,12 @@
1222
1229
  }
1223
1230
  });
1224
1231
 
1225
- app.addEventListener('mouseover', function (event) {
1226
- var segment = event.target && event.target.closest ? event.target.closest('.run-row') : null;
1227
- if (segment) showNow(segment.dataset.id);
1228
- });
1229
- app.addEventListener('mouseout', function (event) {
1230
- var segment = event.target && event.target.closest ? event.target.closest('.run-row') : null;
1231
- if (!segment) return;
1232
- var next = event.relatedTarget && event.relatedTarget.closest ? event.relatedTarget.closest('.run-row') : null;
1233
- if (!next) showNow(state.selectedId);
1234
- });
1235
-
1236
1232
  document.addEventListener('keydown', function (event) {
1237
- 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;
1238
1235
  if (typing) {
1239
1236
  if (event.key === 'Escape') {
1237
+ window.clearTimeout(findTimer);
1240
1238
  event.target.value = '';
1241
1239
  if (event.target.id === 'find') {
1242
1240
  state.query = '';
@@ -1244,7 +1242,6 @@
1244
1242
  paintList();
1245
1243
  paintDetail();
1246
1244
  markStrip();
1247
- showNow(state.selectedId);
1248
1245
  }
1249
1246
  if (event.target.id === 'log-find') {
1250
1247
  state.logQuery = '';
@@ -1254,7 +1251,8 @@
1254
1251
  return;
1255
1252
  }
1256
1253
  if (event.key >= '1' && event.key <= '5') {
1257
- showTab(TABS[Number(event.key) - 1]);
1254
+ var destination = TABS[Number(event.key) - 1];
1255
+ if (destination) showTab(destination);
1258
1256
  return;
1259
1257
  }
1260
1258
  if (event.key === '/') {
@@ -1276,7 +1274,7 @@
1276
1274
  if (next) next.focus();
1277
1275
  return;
1278
1276
  }
1279
- 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;
1280
1278
  var rows = Array.prototype.slice.call(document.querySelectorAll('#tests .row'));
1281
1279
  if (!rows.length) return;
1282
1280
  event.preventDefault();
@@ -1291,21 +1289,15 @@
1291
1289
 
1292
1290
  function choose(id, resetFilter) {
1293
1291
  if (!id || !testById(id)) return;
1294
- if (resetFilter) {
1295
- state.filter = 'all';
1296
- state.query = '';
1297
- state.band = 'all';
1298
- state.tag = 'all';
1299
- state.projectFilter = 'all';
1300
- var find = document.getElementById('find');
1301
- if (find) find.value = '';
1302
- }
1292
+ if (resetFilter) resetFilters();
1303
1293
  if (state.selectedId !== id) {
1304
1294
  var test = testById(id);
1305
1295
  state.selectedId = id;
1306
1296
  state.attempt = failedAttempt(test);
1307
1297
  state.testPane = defaultPane(test);
1308
1298
  }
1299
+ var position = filteredTests().findIndex(function (test) { return test.id === id; });
1300
+ state.listLimit = Math.max(state.listLimit, position + 1);
1309
1301
  showTab('tests');
1310
1302
  if (built.tests) {
1311
1303
  syncPressed();
@@ -1316,37 +1308,46 @@
1316
1308
  markStrip();
1317
1309
  }
1318
1310
  paintDuration();
1319
- showNow(state.selectedId);
1320
1311
  var row = document.querySelector('#tests .row[data-id="' + cssEscape(id) + '"]');
1312
+ if (row) row.focus({ preventScroll: true });
1321
1313
  if (row && row.scrollIntoView) row.scrollIntoView({ block: 'nearest', behavior: reduce ? 'auto' : 'smooth' });
1322
1314
  }
1323
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
+
1324
1328
  function keepSelectionInFilter() {
1325
1329
  var tests = filteredTests();
1330
+ markStrip();
1326
1331
  if (tests.some(function (test) { return test.id === state.selectedId; })) return;
1327
1332
  var next = tests[0];
1328
1333
  if (!next) {
1329
1334
  state.selectedId = null;
1335
+ writeHash();
1330
1336
  return;
1331
1337
  }
1332
1338
  state.selectedId = next.id;
1333
1339
  state.attempt = failedAttempt(next);
1334
1340
  state.testPane = defaultPane(next);
1341
+ writeHash();
1335
1342
  }
1336
1343
 
1337
- function openTest(id, attempt) {
1344
+ function openTest(id, attempt, pane) {
1338
1345
  var test = testById(id);
1339
1346
  if (!test) return;
1340
- state.filter = 'all';
1341
- state.query = '';
1342
- state.band = 'all';
1343
- state.tag = 'all';
1344
- state.projectFilter = 'all';
1345
- var find = document.getElementById('find');
1346
- if (find) find.value = '';
1347
+ resetFilters();
1347
1348
  state.selectedId = id;
1348
1349
  state.attempt = attempt;
1349
- state.testPane = 'shots';
1350
+ state.testPane = pane === 'logs' ? 'logs' : 'shots';
1350
1351
  showTab('tests');
1351
1352
  if (built.tests) {
1352
1353
  paintTags();
@@ -1355,43 +1356,30 @@
1355
1356
  paintDetail();
1356
1357
  markStrip();
1357
1358
  }
1359
+ syncPressed();
1358
1360
  paintDuration();
1361
+ document.getElementById('detail').scrollIntoView({ block: 'nearest' });
1359
1362
  }
1360
1363
 
1361
1364
  function openShot(src, alt) {
1362
1365
  var safe = safeSrc(src);
1363
1366
  if (!safe) return;
1364
1367
  var close = el('button', { class: 'close-shot', type: 'button' }, ['Close']);
1365
- 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 })]);
1366
1370
  close.addEventListener('click', function () { dialog.close(); });
1367
1371
  dialog.addEventListener('click', function (event) { if (event.target === dialog) dialog.close(); });
1368
- dialog.addEventListener('close', function () { dialog.remove(); });
1372
+ dialog.addEventListener('close', function () { dialog.remove(); if (returnFocus && returnFocus.isConnected) returnFocus.focus(); });
1369
1373
  document.body.appendChild(dialog);
1370
1374
  dialog.showModal();
1371
1375
  }
1372
1376
 
1373
- function showNow(id) {
1374
- var host = document.getElementById('now');
1375
- if (!host) return;
1376
- var test = id ? testById(id) : null;
1377
- if (!test) {
1378
- fill(host, [data.tests.length ? 'Hover a test, or click one.' : 'No tests ran.']);
1379
- return;
1380
- }
1381
- fill(host, [
1382
- el('span', { class: 'sw', 'data-status': test.status }, [statusLabel(test)]),
1383
- el('span', { class: 'name' }, [test.title]),
1384
- el('span', null, [formatDuration(test.duration)]),
1385
- el('span', { class: 'now-file' }, [test.file]),
1386
- ]);
1387
- }
1388
-
1389
1377
  function filteredTests() {
1390
1378
  var query = state.query.trim().toLowerCase();
1391
1379
  return data.tests.filter(function (test) {
1392
1380
  if (state.filter !== 'all' && test.status !== state.filter) return false;
1393
- if (state.tag !== 'all' && (test.tags || []).indexOf(state.tag) === -1) return false;
1394
- 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;
1395
1383
  if (state.band !== 'all' && (test.status === 'skipped' || durationBand(test.duration) !== state.band)) return false;
1396
1384
  if (!query) return true;
1397
1385
  var blob = [test.title, test.file, test.project].concat(test.group || [], test.tags || []).join(' ').toLowerCase();
@@ -1405,7 +1393,7 @@
1405
1393
  }
1406
1394
 
1407
1395
  function testById(id) {
1408
- return data.tests.find(function (test) { return test.id === id; }) || null;
1396
+ return testIndex.get(id) || null;
1409
1397
  }
1410
1398
 
1411
1399
  function failedAttempt(test) {
@@ -1419,7 +1407,7 @@
1419
1407
 
1420
1408
  function defaultPane(test) {
1421
1409
  if (!test) return 'steps';
1422
- if (test.status === 'failed' || test.status === 'timedOut' || test.status === 'flaky') return 'error';
1410
+ if (isProblem(test)) return 'error';
1423
1411
  return 'steps';
1424
1412
  }
1425
1413