@applitools/eyes-playwright 1.30.2 → 1.32.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.
@@ -63,6 +63,7 @@ const getStatus = testStatuses => {
63
63
  let isAborted = false;
64
64
  let isNew = false;
65
65
  let isDifferent = false;
66
+ let isEmpty = true;
66
67
  testStatuses.forEach(testStatus => {
67
68
  const statusPriority = STATUS_PRIORITY.indexOf(testStatus.status.toLowerCase());
68
69
  maxPriority = Math.max(statusPriority, maxPriority);
@@ -70,6 +71,7 @@ const getStatus = testStatuses => {
70
71
  isAborted = isAborted || testStatus.isAborted;
71
72
  isNew = isNew || testStatus.isNew;
72
73
  isDifferent = isDifferent || testStatus.isDifferent;
74
+ if (testStatus.steps > 0) isEmpty = false;
73
75
  });
74
76
  let status = STATUS_PRIORITY[maxPriority];
75
77
 
@@ -77,6 +79,8 @@ const getStatus = testStatuses => {
77
79
  maxPriority = Math.max(2, maxPriority);
78
80
  status = STATUS_PRIORITY[maxPriority];
79
81
  statusText = Status.Aborted;
82
+ } else if (isEmpty) {
83
+ statusText = Status.Empty;
80
84
  } else if (status !== Status.Running && isNew) {
81
85
  statusText = Status.New;
82
86
  } else statusText = status;
@@ -86,22 +90,71 @@ const getStatus = testStatuses => {
86
90
  return {status, statusText, icon}
87
91
  };
88
92
 
93
+ const getSingleSessionStatus = test => {
94
+ let maxPriority = Math.max(STATUS_PRIORITY.indexOf(test.status.toLowerCase()), 0);
95
+ let statusText;
96
+ let status = STATUS_PRIORITY[maxPriority];
97
+ const isEmpty = test.steps === 0;
98
+
99
+ if (test.isAborted) {
100
+ maxPriority = Math.max(2, maxPriority);
101
+ status = STATUS_PRIORITY[maxPriority];
102
+ statusText = Status.Aborted;
103
+ } else if (isEmpty) {
104
+ statusText = Status.Empty;
105
+ } else if (status !== Status.Running && test.isNew) {
106
+ statusText = Status.New;
107
+ } else statusText = status;
108
+
109
+ return {status, statusText}
110
+ };
111
+
89
112
  class ReportRenderer {
90
113
  _statusPollingTimeout
114
+ _currentUrl = ''
91
115
 
92
116
  constructor(waitForResults) {
93
117
  this.waitForResults = waitForResults;
118
+ this.waitForResults.then(this.updatePlaywrightWithEyesResults);
94
119
  }
95
120
 
96
121
  render = () => {
97
- window.onhashchange = this.handleUrlParams;
98
- window.navigation.onnavigate = this.handleUrlParams;
99
- window.navigation.oncurrententrychange = this.handleUrlParams;
122
+ window.addEventListener('hashchange', this.handleUrlParams);
100
123
  document.addEventListener('DOMContentLoaded', this.handleUrlParams);
124
+ window.addEventListener('popstate', this.handleUrlParams);
101
125
 
102
126
  this.observeDomChanges();
103
127
 
104
- this.pollForTestsStatus();
128
+ this._pollAndForceRefresh();
129
+ }
130
+
131
+ _pollAndForceRefresh = async () => {
132
+ return this.pollForTestsStatus().then(() => this.waitForResults.then(this.updatePlaywrightWithEyesResults))
133
+ }
134
+
135
+ updatePlaywrightWithEyesResults = async results => {
136
+ await this.refreshReport(results);
137
+
138
+ window.onload();
139
+
140
+ const hash = this.getHashFromCurrentUrl();
141
+ const testId = hash.get('testId');
142
+ if (!testId) return
143
+
144
+ const test = results.eyesTestResult[testId];
145
+ if (!test) return
146
+
147
+ setTimeout(() => this.createEyesTestResults(test));
148
+ }
149
+
150
+ refreshReport = async ({testsFiles, report}) => {
151
+ const newZip = new JSZip();
152
+ const newFiles = {...testsFiles, 'report.json': report};
153
+ Object.keys(newFiles).forEach(fileName => {
154
+ newZip.file(fileName, JSON.stringify(newFiles[fileName]));
155
+ });
156
+ const generatedZip = await newZip.generateAsync({type: 'base64'});
157
+ window.playwrightReportBase64 = `data:application/zip;base64,${generatedZip}`;
105
158
  }
106
159
 
107
160
  getHashFromCurrentUrl = () => {
@@ -117,24 +170,60 @@ class ReportRenderer {
117
170
  }
118
171
 
119
172
  handleUrlParams = () => {
173
+ try {
174
+ const hash = this.getHashFromCurrentUrl();
175
+ const testId = hash.get('testId');
176
+
177
+ const fromTestToMain = !testId && this._currentUrl && !!this.getTestIdFromUrl(this._currentUrl);
178
+ if (fromTestToMain) {
179
+ this.waitForResults.then(testResults => {
180
+ this.refreshReport(testResults).then(() => {
181
+ window.onload();
182
+ this._handleMainPageChanges(hash);
183
+ });
184
+ });
185
+ } else this._handleMainPageChanges(hash);
186
+
187
+ if (!testId) return
188
+
189
+ const fromMainToTest = testId && this._currentUrl && !this.getTestIdFromUrl(this._currentUrl);
190
+
191
+ const shouldFilterUnresolved = this._currentUrl && this.getUnresolvedFilterFromUrl(this._currentUrl);
192
+ if (shouldFilterUnresolved && !unresolvedFilter) {
193
+ const newUrl = new URL(window.location.href);
194
+ newUrl.searchParams.set('unresolved', 'true');
195
+ window.location.replace(newUrl);
196
+ }
197
+
198
+ this.waitForResults.then(testResults => {
199
+ const test = testResults.eyesTestResult[testId];
200
+ if (!test) return
201
+
202
+ if (fromMainToTest) {
203
+ this.refreshReport(testResults).then(() => {
204
+ window.onload();
205
+ setTimeout(() => this.createEyesTestResults(test));
206
+ });
207
+ } else {
208
+ this.createEyesTestResults(test);
209
+ }
210
+ });
211
+ } finally {
212
+ this._currentUrl = window.location.href;
213
+ }
214
+ }
215
+ _handleMainPageChanges = hash => {
120
216
  this.cleanupExistingChips();
121
217
 
122
- const hash = this.getHashFromCurrentUrl();
123
218
  const eyesFilter = hash.get('eyes');
124
219
  if (eyesFilter) document.getElementsByClassName('htmlreport')[0]?.classList.add('eyes-filter');
125
220
  else document.getElementsByClassName('htmlreport')[0]?.classList.remove('eyes-filter');
126
221
 
127
- this.createLinkToBatch();
128
-
129
- const testId = hash.get('testId');
130
- if (!testId) return
131
-
132
- this.waitForResults.then(testResults => {
133
- const test = testResults.eyesTestResult[testId];
134
- if (!test) return
222
+ const unresolvedFilter = hash.get('unresolved');
223
+ if (unresolvedFilter) document.getElementsByClassName('htmlreport')[0]?.classList.add('unresolved-filter');
224
+ else document.getElementsByClassName('htmlreport')[0]?.classList.remove('unresolved-filter');
135
225
 
136
- this.createEyesTestResults(test);
137
- });
226
+ this.createLinkToBatch();
138
227
  }
139
228
  getHashValueFromUrl = (url, key) => {
140
229
  const hash = this.getHashFromUrlString(url);
@@ -146,6 +235,9 @@ class ReportRenderer {
146
235
  getEyesFilterFromUrl = url => {
147
236
  return this.getHashValueFromUrl(url, 'eyes')
148
237
  }
238
+ getUnresolvedFilterFromUrl = url => {
239
+ return this.getHashValueFromUrl(url, 'unresolved')
240
+ }
149
241
 
150
242
  observeDomChanges = () => {
151
243
  const mutationObserver = new MutationObserver(this.handleDomChanges);
@@ -167,6 +259,9 @@ class ReportRenderer {
167
259
  if (this.getEyesFilterFromUrl(window.location.href)) {
168
260
  node.classList.add('eyes-filter');
169
261
  }
262
+ if (this.getUnresolvedFilterFromUrl(window.location.href)) {
263
+ node.classList.add('unresolved-filter');
264
+ }
170
265
  }
171
266
 
172
267
  if (node.getElementsByClassName('header-view-status-container').length > 0) {
@@ -194,14 +289,16 @@ class ReportRenderer {
194
289
  const test = testResults.eyesTestResult[testId];
195
290
  if (!test) return
196
291
 
292
+ const status = getStatus(test.eyesResults);
197
293
  testElement.parentNode.classList.add('eyes-test');
294
+ testElement.parentNode.setAttribute('status', status.status.toLowerCase());
198
295
 
199
296
  const eyesInfo = document.createElement('div');
200
297
  eyesInfo.classList.add('eyes-info');
201
298
  const eyesInfoLink = document.createElement('a');
202
299
  eyesInfoLink.classList.add('eyes-info-link');
203
300
  eyesInfoLink.href = href;
204
- eyesInfo.innerHTML = this.createEyesInfo(test.eyesResults);
301
+ eyesInfo.innerHTML = this.createEyesInfo(status);
205
302
  eyesInfoLink.appendChild(eyesInfo);
206
303
  testElement.appendChild(eyesInfoLink);
207
304
  });
@@ -209,9 +306,10 @@ class ReportRenderer {
209
306
  createFilters = () => {
210
307
  const allFilters = document.getElementsByClassName('subnav-item');
211
308
  const lastFilter = allFilters[allFilters.length - 1];
309
+
212
310
  const eyesFilter = lastFilter.cloneNode(true);
213
311
  eyesFilter.id = 'eyes-filter';
214
- eyesFilter.firstChild.textContent = 'Eyes VisualAI ';
312
+ eyesFilter.firstChild.textContent = 'Eyes ';
215
313
  this.waitForResults.then(result => {
216
314
  eyesFilter.lastChild.textContent = Object.keys(result.eyesTestResult).length;
217
315
  });
@@ -219,6 +317,19 @@ class ReportRenderer {
219
317
  lastFilter.parentNode.appendChild(eyesFilter);
220
318
 
221
319
  eyesFilter.href = '#eyes=true';
320
+
321
+ const unresolvedFilter = lastFilter.cloneNode(true);
322
+ unresolvedFilter.id = 'unresolved-filter';
323
+ unresolvedFilter.firstChild.textContent = 'Unresolved ';
324
+ this.waitForResults.then(
325
+ result =>
326
+ (unresolvedFilter.lastChild.textContent = Object.values(result.eyesTestResult).filter(
327
+ test => getStatus(test.eyesResults).status === 'unresolved',
328
+ ).length),
329
+ );
330
+ lastFilter.parentNode.appendChild(unresolvedFilter);
331
+
332
+ unresolvedFilter.href = '#unresolved=true';
222
333
  }
223
334
  createLinkToBatch = async () => {
224
335
  const link = document.getElementsByClassName('eyes-batch-link')[0];
@@ -231,16 +342,18 @@ class ReportRenderer {
231
342
  }
232
343
 
233
344
  if (link) return
345
+
346
+ const firstResultsElement = document.getElementsByClassName('chip')[0];
347
+ const urlContainer = document.createElement('div');
348
+ urlContainer.classList.add('eyes-batch-link');
349
+ firstResultsElement?.parentNode?.insertBefore(urlContainer, firstResultsElement);
350
+
234
351
  const testResults = await this.waitForResults;
235
352
  const firstResult = Object.values(testResults.eyesTestResult)[0]?.eyesResults[0];
236
353
 
237
354
  if (!firstResult) return
238
355
 
239
- const firstResultsElement = document.getElementsByClassName('chip')[0];
240
- const urlContainer = document.createElement('div');
241
- urlContainer.classList.add('eyes-batch-link');
242
356
  urlContainer.innerHTML = `<a target='_blank' href=${firstResult.appUrls.batch}>Results in Eyes Dashboard</a>`;
243
- firstResultsElement?.parentNode?.insertBefore(urlContainer, firstResultsElement);
244
357
  }
245
358
 
246
359
  fixUrl = url => {
@@ -266,6 +379,51 @@ class ReportRenderer {
266
379
  });
267
380
  }
268
381
  onTestResultPageReady = test => {
382
+ const shouldFilterUnresolved = new URLSearchParams(window.location.search).get('unresolved');
383
+
384
+ const frontendPath = new URLSearchParams(window.location.search).get('frontendPath');
385
+ const iframeSrc = `app/html-report/index.html?${frontendPath != null ? `frontendPath=${frontendPath}&` : ''}${
386
+ shouldFilterUnresolved ? 'unresolved=true&' : ''
387
+ }cache=${Date.now()}&`; // TODO remove cache and use fixed version
388
+
389
+ this._observeRetryTabSlectionChanged(test, iframeSrc, shouldFilterUnresolved);
390
+
391
+ let currentSelectedRetryIndex = Array.from(document.getElementsByClassName('tabbed-pane-tab-element')).findIndex(
392
+ tab => tab.classList.contains('selected'),
393
+ );
394
+ this._createEyesResultIframes(test, iframeSrc, currentSelectedRetryIndex, shouldFilterUnresolved);
395
+ }
396
+ _observeRetryTabSlectionChanged = (test, iframeSrc) => {
397
+ const retryTabs = document.getElementsByClassName('tabbed-pane-tab-element');
398
+ let selectedRetryIndex = Array.from(retryTabs).findIndex(tab => tab.classList.contains('selected'));
399
+
400
+ const mutationObserver = new MutationObserver(() => {
401
+ mutationObserver.disconnect();
402
+
403
+ const newSelectedRetryIndex = Array.from(retryTabs).findIndex(tab => tab.classList.contains('selected'));
404
+ if (newSelectedRetryIndex === selectedRetryIndex) return
405
+ selectedRetryIndex = newSelectedRetryIndex;
406
+
407
+ mutationObserver.observe(retryTabs[selectedRetryIndex], {
408
+ attributes: true,
409
+ characterData: false,
410
+ childList: false,
411
+ subtree: false,
412
+ attributeOldValue: false,
413
+ characterDataOldValue: false,
414
+ });
415
+ this._createEyesResultIframes(test, iframeSrc, selectedRetryIndex, shouldFilterUnresolved);
416
+ });
417
+ mutationObserver.observe(retryTabs[selectedRetryIndex], {
418
+ attributes: true,
419
+ characterData: false,
420
+ childList: false,
421
+ subtree: false,
422
+ attributeOldValue: false,
423
+ characterDataOldValue: false,
424
+ });
425
+ }
426
+ _createTemplateChip = () => {
269
427
  const firstChip = document.getElementsByClassName('chip')[0];
270
428
  const chipTemplate = firstChip.cloneNode(true);
271
429
  chipTemplate.classList.add('eyes-test-results');
@@ -283,52 +441,80 @@ class ReportRenderer {
283
441
  const chipTemplateBody = chipTemplate.querySelector('.chip-body');
284
442
  chipTemplateBody.innerHTML = '';
285
443
 
286
- this.cleanupExistingChips();
444
+ return {chipTemplate, chipTemplateHeader, firstChip}
445
+ }
446
+ _createChipForTest = (result, chipTemplate, chipTemplateHeader) => {
447
+ const chip = chipTemplate.cloneNode(true);
448
+ chip.setAttribute('value', result.id);
449
+ chip.querySelector('.chip-header').onclick = chipTemplateHeader.onclick;
450
+
451
+ const sessionText = `${result.hostApp} ${result.hostDisplaySize.width} x ${result.hostDisplaySize.height}`;
287
452
 
288
- const iframeSrc = `app/html-report/index.html?${
289
- new URLSearchParams(window.location.search).get('frontend') != null
290
- ? 'frontendPath=html-report/dev/playwright&'
291
- : 'frontendPath=html-report/dev/playwright&' // TODO remove frontendPath when there is no frontend param in the url
292
- }`;
453
+ chip.querySelector(
454
+ '.chip-header-text',
455
+ ).innerHTML = `Eyes Test Results <span class="test-header-info"> - ${sessionText} <div class="eyes-info">${this.createEyesInfo(
456
+ getSingleSessionStatus(result),
457
+ false,
458
+ )}</div><a href=${result.appUrls.session} target='_blank' title='View in Eyes Dashboard'>
459
+ <span>${window.__icons.link}</span>
460
+ </a></span>`;
293
461
 
294
- test.eyesResults.forEach((result, index) => {
295
- const iframe = document.createElement('iframe');
296
- iframe.classList.add('eyes-session-result');
297
- iframe.setAttribute('value', result.id);
462
+ chip.querySelector('.chip-header-text a').onclick = event => event.stopPropagation();
298
463
 
299
- const url = this.fixUrl(result.eyesServer.eyesServerUrl);
464
+ return chip
465
+ }
466
+ _createTestIframe = (iframeSrc, result) => {
467
+ const iframe = document.createElement('iframe');
468
+ iframe.classList.add('eyes-session-result');
469
+ iframe.setAttribute('value', result.id);
300
470
 
301
- iframe.onload = () => {
302
- const iframeUrl = `${url}app/html-report/index.html`;
303
- iframe.contentWindow.postMessage({call: 'sendValue', value: result}, iframeUrl);
471
+ const url = this.fixUrl(result.eyesServer.eyesServerUrl);
304
472
 
305
- iframe.classList.add('ready');
473
+ iframe.onload = () => {
474
+ const iframeUrl = `${url}app/html-report/index.html`;
475
+ iframe.contentWindow.postMessage({call: 'sendValue', value: result}, iframeUrl);
306
476
 
307
- window.addEventListener('message', async event => {
308
- if (event.origin !== url && `${event.origin}/` !== url) return
309
- if (event.data === 'forceRefreshData') return this.pollForTestsStatus()
310
- });
311
- };
477
+ iframe.classList.add('ready');
478
+
479
+ window.addEventListener('message', async event => {
480
+ if (event.origin !== url && `${event.origin}/` !== url) return
481
+ if (event.data === 'forceRefreshData') return this.pollForTestsStatus()
482
+ // if (event.data === 'forceRefreshData') return this._pollAndForceRefresh()
483
+ });
484
+ };
485
+
486
+ iframe.src = `${url}${iframeSrc}`;
487
+
488
+ return iframe
489
+ }
490
+ _createEyesResultIframes = (test, iframeSrc, selectedRetryIndex, shouldFilterUnresolved) => {
491
+ this.cleanupExistingChips();
312
492
 
313
- iframe.src = `${url}${iframeSrc}cache=${Date.now()}`; // TODO remove cache and use fixed version
314
- const chip = chipTemplate.cloneNode(true);
315
- chip.querySelector('.chip-header').onclick = chipTemplateHeader.onclick;
493
+ const {chipTemplate, chipTemplateHeader, firstChip} = this._createTemplateChip();
316
494
 
317
- const sessionText = `${result.hostApp} ${result.hostDisplaySize.width} x ${result.hostDisplaySize.height}`;
495
+ let createdIframes = 0;
496
+ test.eyesResults.forEach(result => {
497
+ if (result.playwrightRetry !== selectedRetryIndex) return
318
498
 
319
- chip.querySelector(
320
- '.chip-header-text',
321
- ).innerHTML = `Eyes Test Results <span class="test-header-info"> - ${sessionText} <a href=${result.appUrls.session} target='_blank' title='View in Eyes Dashboard'>
322
- <span>${window.__icons.link}</span>
323
- </a></span>`;
499
+ if (shouldFilterUnresolved && getStatus([result]).status !== 'unresolved') return
324
500
 
325
- chip.querySelector('.chip-header-text a').onclick = event => event.stopPropagation();
501
+ const chip = this._createChipForTest(result, chipTemplate, chipTemplateHeader);
502
+ const chipBody = chip.querySelector('.chip-body');
326
503
 
327
- chip.querySelector('.chip-body').appendChild(iframe);
504
+ let bodyElement;
505
+ if (result.steps === 0) {
506
+ bodyElement = document.createElement('div');
507
+ bodyElement.innerHTML = 'No steps were executed for this test';
508
+ chipBody.classList.add('empty');
509
+ } else bodyElement = this._createTestIframe(iframeSrc, result);
510
+
511
+ chipBody.appendChild(bodyElement);
328
512
  firstChip.parentNode.appendChild(chip);
329
- if (index > 0) {
513
+ if (createdIframes > 0) {
330
514
  chip.querySelector('.chip-header').click();
331
515
  }
516
+
517
+ ++createdIframes;
332
518
  });
333
519
  }
334
520
 
@@ -397,6 +583,7 @@ class ReportRenderer {
397
583
  }, {});
398
584
 
399
585
  this.waitForResults = this.mergeResults(testResults, statusBySessionId);
586
+ this.waitForResults.then(this.refreshReport);
400
587
 
401
588
  this.refreshEyesTestsStatus();
402
589
  this.refreshCurrentTestStatus();
@@ -423,10 +610,33 @@ class ReportRenderer {
423
610
  step.expectedImageSize = expectedAppOutput?.image.size;
424
611
  step.actualImageSize = actualAppOutput?.image.size;
425
612
  step.appOutputResolution = appOutputResolution;
613
+ step.expectedAppOutput = expectedAppOutput;
614
+ step.actualAppOutput = actualAppOutput;
426
615
  });
427
616
  });
428
617
  });
429
618
 
619
+ Object.values(testResults.testsFiles).forEach(testFile => {
620
+ const fileInReport = testResults.report.files.find(file => file.fileId === testFile.fileId);
621
+ const reportStats = testResults.report.stats;
622
+ testFile.tests.forEach(test => {
623
+ const eyesResultsForTest = testResults.eyesTestResult[test.testId]?.eyesResults;
624
+ if (!eyesResultsForTest) return
625
+
626
+ // go over the test retries and get the eyes results
627
+ const eyesStatuses = test.results.reduce((results, _result) => {
628
+ const eyesResultsForRun = eyesResultsForTest.filter(result => result.playwrightRetry === _result.retry);
629
+ if (eyesResultsForRun.length > 0) {
630
+ // get eyes status for the current retry, so we can later get the status of the test
631
+ results.push(getStatus(eyesResultsForTest).status);
632
+ } else results.push(null);
633
+ return results
634
+ }, []);
635
+
636
+ updatePlaywrightTestStatus(test, fileInReport, reportStats, eyesStatuses);
637
+ });
638
+ });
639
+
430
640
  return testResults
431
641
  }
432
642
  refreshEyesTestsStatus = async () => {
@@ -441,12 +651,30 @@ class ReportRenderer {
441
651
  const testEyesResults = eyesTestResult[testId]?.eyesResults;
442
652
  if (!testEyesResults) return
443
653
 
444
- testElement.getElementsByClassName('eyes-info')[0].innerHTML = this.createEyesInfo(testEyesResults);
654
+ const status = getStatus(testEyesResults);
655
+ testElement.setAttribute('status', status.status.toLowerCase());
656
+
657
+ testElement.getElementsByClassName('eyes-info')[0].innerHTML = this.createEyesInfo(status);
658
+ });
659
+
660
+ const hash = this.getHashFromCurrentUrl();
661
+ const testId = hash.get('testId');
662
+ if (!testId) return
663
+ const testEyesResults = eyesTestResult[testId]?.eyesResults;
664
+ if (!testEyesResults) return
665
+
666
+ const sessionChips = document.getElementsByClassName('eyes-test-results');
667
+
668
+ Array.from(sessionChips).forEach(chip => {
669
+ const sessionId = chip.getAttribute('value');
670
+ const sessionResults = eyesTestResult[testId]?.eyesResults.find(result => result.id === sessionId);
671
+
672
+ const status = getSingleSessionStatus(sessionResults);
673
+ chip.getElementsByClassName('eyes-info')[0].innerHTML = this.createEyesInfo(status, false);
445
674
  });
446
675
  }
447
- createEyesInfo = testStatuses => {
448
- const status = getStatus(testStatuses);
449
- return `<span class="visual-test-icon">${window.__icons.visualTest}</span>
676
+ createEyesInfo = (status, addVisualIcon = true) => {
677
+ return `${addVisualIcon ? `<span class="visual-test-icon">${window.__icons.visualTest}</span>` : ''}
450
678
  <div class="status-bar ${status.status.toLowerCase()}"></div>
451
679
  <span class="status-text">${status.statusText}</span>`
452
680
  }
@@ -485,6 +713,46 @@ class ReportRenderer {
485
713
  }
486
714
  }
487
715
 
716
+ function getOutcomeFromEyesStatuses(eyesStatuses, testOutcome) {
717
+ return eyesStatuses.every(status => status === 'unresolved' || status === 'failed')
718
+ ? 'unexpected'
719
+ : eyesStatuses.some(status => status === 'unresolved' || status === 'failed')
720
+ ? 'flaky'
721
+ : testOutcome
722
+ }
723
+ function getRunStatusFromEyesStatuses(eyesStatus, runStatus) {
724
+ return eyesStatus === 'unresolved' || eyesStatus === 'failed' ? 'failed' : runStatus
725
+ }
726
+
727
+ function updatePlaywrightTestStatus(test, fileInReport, reportStats, eyesStatuses, updateOriginalOutcome = false) {
728
+ if (
729
+ eyesStatuses.filter(status => status !== null).length > 0 &&
730
+ // test outcome: 'skipped' | 'expected' | 'unexpected' | 'flaky';
731
+ ['expected', 'flaky'].includes(test.originalOutcome ?? test.outcome)
732
+ ) {
733
+ test.results.forEach((_result, index) => {
734
+ const eyesStatus = eyesStatuses[index];
735
+ // run status: 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted'
736
+ if ((_result.originalStatus ?? _result.status) === 'passed' && eyesStatus !== null) {
737
+ if (updateOriginalOutcome) _result.originalStatus = _result.status;
738
+ _result.status = getRunStatusFromEyesStatuses(eyesStatus, _result.originalStatus);
739
+ }
740
+ });
741
+
742
+ const testInReport = fileInReport.tests.find(testInReport => testInReport.testId === test.testId);
743
+ if (updateOriginalOutcome) testInReport.originalOutcome = test.originalOutcome = test.outcome;
744
+ const newOutcome = getOutcomeFromEyesStatuses(
745
+ eyesStatuses.filter(status => status !== null),
746
+ test.originalOutcome,
747
+ );
748
+ if (testInReport.outcome !== newOutcome) {
749
+ reportStats[testInReport.outcome]--;
750
+ reportStats[newOutcome]++;
751
+ }
752
+ testInReport.outcome = test.outcome = newOutcome;
753
+ }
754
+ }
755
+
488
756
  async function getTestResults() {
489
757
  const base64Data = window.playwrightReportBase64.replace('data:application/zip;base64,', '');
490
758
  const zip = new JSZip();
@@ -501,19 +769,32 @@ async function getTestResults() {
501
769
  );
502
770
 
503
771
  const reportFile = await zip.file('report.json').async('text');
772
+ const reportFileData = JSON.parse(reportFile);
504
773
 
505
774
  const testsFiles = Object.assign({}, ...resultsByTestFile);
506
775
 
507
776
  const sessionsByBatchId = {};
508
777
 
509
778
  const eyesTestsById = Object.values(testsFiles).reduce((eyesTests, testFile) => {
779
+ const fileInReport = reportFileData.files.find(file => file.fileId === testFile.fileId);
780
+ const reportStats = reportFileData.stats;
510
781
  testFile.tests.forEach(test => {
782
+ const eyesStatuses = [];
783
+
784
+ // go over the test retries and get the eyes results
511
785
  const eyesResults = test.results.reduce((results, _result, index) => {
512
786
  const eyesResultsForTest = __testResultsMap[`${test.testId}--${index}`];
513
787
  if (eyesResultsForTest) {
514
788
  results.push(...eyesResultsForTest);
789
+
790
+ // get eyes status for the current retry, so we can later get the status of the test
791
+ eyesStatuses.push(getStatus(eyesResultsForTest).status);
792
+
515
793
  eyesResultsForTest.forEach(eyesResult => {
794
+ //set retry number for session
516
795
  eyesResult.playwrightRetry = _result.retry || 0;
796
+
797
+ // initialize sessionsByBatchId so later we can get the status of the tests (the request is per batch)
517
798
  if (!sessionsByBatchId[eyesResult.batchId])
518
799
  sessionsByBatchId[eyesResult.batchId] = {
519
800
  sessions: [],
@@ -525,11 +806,14 @@ async function getTestResults() {
525
806
  accessToken: eyesResult.secretToken,
526
807
  });
527
808
  });
528
- }
809
+ } else eyesStatuses.push(null);
529
810
  return results
530
811
  }, []);
531
812
 
532
- if (eyesResults.length > 0) eyesTests[test.testId] = {...test, eyesResults};
813
+ if (eyesResults.length > 0) {
814
+ updatePlaywrightTestStatus(test, fileInReport, reportStats, eyesStatuses, true);
815
+ eyesTests[test.testId] = {...test, eyesResults};
816
+ }
533
817
  });
534
818
 
535
819
  return eyesTests
@@ -537,7 +821,7 @@ async function getTestResults() {
537
821
 
538
822
  return {
539
823
  testsFiles: testsFiles,
540
- report: JSON.parse(reportFile),
824
+ report: reportFileData,
541
825
  eyesTestResult: eyesTestsById,
542
826
  sessionsByBatchId,
543
827
  }
@@ -7,13 +7,22 @@
7
7
  height: 850px;
8
8
  }
9
9
 
10
+ .eyes-test-results>.chip-body.empty {
11
+ height: auto;
12
+ padding: 16px;
13
+ }
14
+
10
15
  .eyes-test-results>.chip-body>iframe {
11
16
  width: 100%;
12
17
  height: 100%;
13
18
  border: none;
14
19
  }
15
20
 
16
- .eyes-filter .test-file-test:not(.eyes-test){
21
+ .eyes-filter .test-file-test:not(.eyes-test) {
22
+ display: none;
23
+ }
24
+
25
+ .unresolved-filter .test-file-test:not(.eyes-test[status=unresolved]) {
17
26
  display: none;
18
27
  }
19
28
 
@@ -25,6 +34,8 @@
25
34
 
26
35
  .eyes-info {
27
36
  display: flex;
37
+ align-items: center;
38
+ line-height: 16px
28
39
  }
29
40
  .eyes-info-link {
30
41
  text-decoration: none;
@@ -44,6 +55,9 @@
44
55
  .test-header-info>a>span {
45
56
  display: inline-flex;
46
57
  }
58
+ .test-header-info>.eyes-info {
59
+ margin-left: 5px;
60
+ }
47
61
 
48
62
  .visual-test-icon {
49
63
  text-align: center;
@@ -51,14 +65,13 @@
51
65
  font-size: 24px;
52
66
  height: 26px;
53
67
  width: 26px;
54
- margin-right: 5px;
68
+ display: grid;
55
69
  }
56
70
  .eyes-info>.status-text {
57
71
  font-size: 14px;
58
72
  text-align: center;
59
73
  vertical-align: middle;
60
74
  margin: auto;
61
- color: rgba(0, 0, 0, 0.35);
62
75
  text-transform: capitalize;
63
76
  padding: 0 5px;
64
77
  }
@@ -69,7 +82,7 @@
69
82
  width: 5px;
70
83
  }
71
84
  .eyes-info>.status-bar.aborted {
72
- background: #ff8f00;
85
+ background: #e80600;
73
86
  }
74
87
  .eyes-info>.status-bar.unresolved {
75
88
  background: #ff8f00;