@applitools/eyes-playwright 1.30.2 → 1.31.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,79 @@ 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} <a href=${
456
+ result.appUrls.session
457
+ } target='_blank' title='View in Eyes Dashboard'>
458
+ <span>${window.__icons.link}</span>
459
+ </a><div class="eyes-info">${this.createEyesInfo(getSingleSessionStatus(result), false)}</div></span>`;
293
460
 
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);
461
+ chip.querySelector('.chip-header-text a').onclick = event => event.stopPropagation();
298
462
 
299
- const url = this.fixUrl(result.eyesServer.eyesServerUrl);
463
+ return chip
464
+ }
465
+ _createTestIframe = (iframeSrc, result) => {
466
+ const iframe = document.createElement('iframe');
467
+ iframe.classList.add('eyes-session-result');
468
+ iframe.setAttribute('value', result.id);
300
469
 
301
- iframe.onload = () => {
302
- const iframeUrl = `${url}app/html-report/index.html`;
303
- iframe.contentWindow.postMessage({call: 'sendValue', value: result}, iframeUrl);
470
+ const url = this.fixUrl(result.eyesServer.eyesServerUrl);
304
471
 
305
- iframe.classList.add('ready');
472
+ iframe.onload = () => {
473
+ const iframeUrl = `${url}app/html-report/index.html`;
474
+ iframe.contentWindow.postMessage({call: 'sendValue', value: result}, iframeUrl);
306
475
 
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
- };
476
+ iframe.classList.add('ready');
477
+
478
+ window.addEventListener('message', async event => {
479
+ if (event.origin !== url && `${event.origin}/` !== url) return
480
+ if (event.data === 'forceRefreshData') return this.pollForTestsStatus()
481
+ // if (event.data === 'forceRefreshData') return this._pollAndForceRefresh()
482
+ });
483
+ };
484
+
485
+ iframe.src = `${url}${iframeSrc}`;
486
+
487
+ return iframe
488
+ }
489
+ _createEyesResultIframes = (test, iframeSrc, selectedRetryIndex, shouldFilterUnresolved) => {
490
+ this.cleanupExistingChips();
312
491
 
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;
492
+ const {chipTemplate, chipTemplateHeader, firstChip} = this._createTemplateChip();
316
493
 
317
- const sessionText = `${result.hostApp} ${result.hostDisplaySize.width} x ${result.hostDisplaySize.height}`;
494
+ let createdIframes = 0;
495
+ test.eyesResults.forEach(result => {
496
+ if (result.playwrightRetry !== selectedRetryIndex) return
318
497
 
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>`;
498
+ if (shouldFilterUnresolved && getStatus([result]).status !== 'unresolved') return
324
499
 
325
- chip.querySelector('.chip-header-text a').onclick = event => event.stopPropagation();
500
+ const chip = this._createChipForTest(result, chipTemplate, chipTemplateHeader);
501
+ const chipBody = chip.querySelector('.chip-body');
326
502
 
327
- chip.querySelector('.chip-body').appendChild(iframe);
503
+ let bodyElement;
504
+ if (result.steps === 0) {
505
+ bodyElement = document.createElement('div');
506
+ bodyElement.innerHTML = 'No steps were executed for this test';
507
+ chipBody.classList.add('empty');
508
+ } else bodyElement = this._createTestIframe(iframeSrc, result);
509
+
510
+ chipBody.appendChild(bodyElement);
328
511
  firstChip.parentNode.appendChild(chip);
329
- if (index > 0) {
512
+ if (createdIframes > 0) {
330
513
  chip.querySelector('.chip-header').click();
331
514
  }
515
+
516
+ ++createdIframes;
332
517
  });
333
518
  }
334
519
 
@@ -397,6 +582,7 @@ class ReportRenderer {
397
582
  }, {});
398
583
 
399
584
  this.waitForResults = this.mergeResults(testResults, statusBySessionId);
585
+ this.waitForResults.then(this.refreshReport);
400
586
 
401
587
  this.refreshEyesTestsStatus();
402
588
  this.refreshCurrentTestStatus();
@@ -423,10 +609,33 @@ class ReportRenderer {
423
609
  step.expectedImageSize = expectedAppOutput?.image.size;
424
610
  step.actualImageSize = actualAppOutput?.image.size;
425
611
  step.appOutputResolution = appOutputResolution;
612
+ step.expectedAppOutput = expectedAppOutput;
613
+ step.actualAppOutput = actualAppOutput;
426
614
  });
427
615
  });
428
616
  });
429
617
 
618
+ Object.values(testResults.testsFiles).forEach(testFile => {
619
+ const fileInReport = testResults.report.files.find(file => file.fileId === testFile.fileId);
620
+ const reportStats = testResults.report.stats;
621
+ testFile.tests.forEach(test => {
622
+ const eyesResultsForTest = testResults.eyesTestResult[test.testId]?.eyesResults;
623
+ if (!eyesResultsForTest) return
624
+
625
+ // go over the test retries and get the eyes results
626
+ const eyesStatuses = test.results.reduce((results, _result) => {
627
+ const eyesResultsForRun = eyesResultsForTest.filter(result => result.playwrightRetry === _result.retry);
628
+ if (eyesResultsForRun.length > 0) {
629
+ // get eyes status for the current retry, so we can later get the status of the test
630
+ results.push(getStatus(eyesResultsForTest).status);
631
+ } else results.push(null);
632
+ return results
633
+ }, []);
634
+
635
+ updatePlaywrightTestStatus(test, fileInReport, reportStats, eyesStatuses);
636
+ });
637
+ });
638
+
430
639
  return testResults
431
640
  }
432
641
  refreshEyesTestsStatus = async () => {
@@ -441,12 +650,30 @@ class ReportRenderer {
441
650
  const testEyesResults = eyesTestResult[testId]?.eyesResults;
442
651
  if (!testEyesResults) return
443
652
 
444
- testElement.getElementsByClassName('eyes-info')[0].innerHTML = this.createEyesInfo(testEyesResults);
653
+ const status = getStatus(testEyesResults);
654
+ testElement.setAttribute('status', status.status.toLowerCase());
655
+
656
+ testElement.getElementsByClassName('eyes-info')[0].innerHTML = this.createEyesInfo(status);
657
+ });
658
+
659
+ const hash = this.getHashFromCurrentUrl();
660
+ const testId = hash.get('testId');
661
+ if (!testId) return
662
+ const testEyesResults = eyesTestResult[testId]?.eyesResults;
663
+ if (!testEyesResults) return
664
+
665
+ const sessionChips = document.getElementsByClassName('eyes-test-results');
666
+
667
+ Array.from(sessionChips).forEach(chip => {
668
+ const sessionId = chip.getAttribute('value');
669
+ const sessionResults = eyesTestResult[testId]?.eyesResults.find(result => result.id === sessionId);
670
+
671
+ const status = getSingleSessionStatus(sessionResults);
672
+ chip.getElementsByClassName('eyes-info')[0].innerHTML = this.createEyesInfo(status, false);
445
673
  });
446
674
  }
447
- createEyesInfo = testStatuses => {
448
- const status = getStatus(testStatuses);
449
- return `<span class="visual-test-icon">${window.__icons.visualTest}</span>
675
+ createEyesInfo = (status, addVisualIcon = true) => {
676
+ return `${addVisualIcon ? `<span class="visual-test-icon">${window.__icons.visualTest}</span>` : ''}
450
677
  <div class="status-bar ${status.status.toLowerCase()}"></div>
451
678
  <span class="status-text">${status.statusText}</span>`
452
679
  }
@@ -485,6 +712,46 @@ class ReportRenderer {
485
712
  }
486
713
  }
487
714
 
715
+ function getOutcomeFromEyesStatuses(eyesStatuses, testOutcome) {
716
+ return eyesStatuses.every(status => status === 'unresolved' || status === 'failed')
717
+ ? 'unexpected'
718
+ : eyesStatuses.some(status => status === 'unresolved' || status === 'failed')
719
+ ? 'flaky'
720
+ : testOutcome
721
+ }
722
+ function getRunStatusFromEyesStatuses(eyesStatus, runStatus) {
723
+ return eyesStatus === 'unresolved' || eyesStatus === 'failed' ? 'failed' : runStatus
724
+ }
725
+
726
+ function updatePlaywrightTestStatus(test, fileInReport, reportStats, eyesStatuses, updateOriginalOutcome = false) {
727
+ if (
728
+ eyesStatuses.filter(status => status !== null).length > 0 &&
729
+ // test outcome: 'skipped' | 'expected' | 'unexpected' | 'flaky';
730
+ ['expected', 'flaky'].includes(test.originalOutcome ?? test.outcome)
731
+ ) {
732
+ test.results.forEach((_result, index) => {
733
+ const eyesStatus = eyesStatuses[index];
734
+ // run status: 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted'
735
+ if ((_result.originalStatus ?? _result.status) === 'passed' && eyesStatus !== null) {
736
+ if (updateOriginalOutcome) _result.originalStatus = _result.status;
737
+ _result.status = getRunStatusFromEyesStatuses(eyesStatus, _result.originalStatus);
738
+ }
739
+ });
740
+
741
+ const testInReport = fileInReport.tests.find(testInReport => testInReport.testId === test.testId);
742
+ if (updateOriginalOutcome) testInReport.originalOutcome = test.originalOutcome = test.outcome;
743
+ const newOutcome = getOutcomeFromEyesStatuses(
744
+ eyesStatuses.filter(status => status !== null),
745
+ test.originalOutcome,
746
+ );
747
+ if (testInReport.outcome !== newOutcome) {
748
+ reportStats[testInReport.outcome]--;
749
+ reportStats[newOutcome]++;
750
+ }
751
+ testInReport.outcome = test.outcome = newOutcome;
752
+ }
753
+ }
754
+
488
755
  async function getTestResults() {
489
756
  const base64Data = window.playwrightReportBase64.replace('data:application/zip;base64,', '');
490
757
  const zip = new JSZip();
@@ -501,19 +768,32 @@ async function getTestResults() {
501
768
  );
502
769
 
503
770
  const reportFile = await zip.file('report.json').async('text');
771
+ const reportFileData = JSON.parse(reportFile);
504
772
 
505
773
  const testsFiles = Object.assign({}, ...resultsByTestFile);
506
774
 
507
775
  const sessionsByBatchId = {};
508
776
 
509
777
  const eyesTestsById = Object.values(testsFiles).reduce((eyesTests, testFile) => {
778
+ const fileInReport = reportFileData.files.find(file => file.fileId === testFile.fileId);
779
+ const reportStats = reportFileData.stats;
510
780
  testFile.tests.forEach(test => {
781
+ const eyesStatuses = [];
782
+
783
+ // go over the test retries and get the eyes results
511
784
  const eyesResults = test.results.reduce((results, _result, index) => {
512
785
  const eyesResultsForTest = __testResultsMap[`${test.testId}--${index}`];
513
786
  if (eyesResultsForTest) {
514
787
  results.push(...eyesResultsForTest);
788
+
789
+ // get eyes status for the current retry, so we can later get the status of the test
790
+ eyesStatuses.push(getStatus(eyesResultsForTest).status);
791
+
515
792
  eyesResultsForTest.forEach(eyesResult => {
793
+ //set retry number for session
516
794
  eyesResult.playwrightRetry = _result.retry || 0;
795
+
796
+ // initialize sessionsByBatchId so later we can get the status of the tests (the request is per batch)
517
797
  if (!sessionsByBatchId[eyesResult.batchId])
518
798
  sessionsByBatchId[eyesResult.batchId] = {
519
799
  sessions: [],
@@ -525,11 +805,14 @@ async function getTestResults() {
525
805
  accessToken: eyesResult.secretToken,
526
806
  });
527
807
  });
528
- }
808
+ } else eyesStatuses.push(null);
529
809
  return results
530
810
  }, []);
531
811
 
532
- if (eyesResults.length > 0) eyesTests[test.testId] = {...test, eyesResults};
812
+ if (eyesResults.length > 0) {
813
+ updatePlaywrightTestStatus(test, fileInReport, reportStats, eyesStatuses, true);
814
+ eyesTests[test.testId] = {...test, eyesResults};
815
+ }
533
816
  });
534
817
 
535
818
  return eyesTests
@@ -537,7 +820,7 @@ async function getTestResults() {
537
820
 
538
821
  return {
539
822
  testsFiles: testsFiles,
540
- report: JSON.parse(reportFile),
823
+ report: reportFileData,
541
824
  eyesTestResult: eyesTestsById,
542
825
  sessionsByBatchId,
543
826
  }
@@ -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;
@@ -51,14 +62,13 @@
51
62
  font-size: 24px;
52
63
  height: 26px;
53
64
  width: 26px;
54
- margin-right: 5px;
65
+ display: grid;
55
66
  }
56
67
  .eyes-info>.status-text {
57
68
  font-size: 14px;
58
69
  text-align: center;
59
70
  vertical-align: middle;
60
71
  margin: auto;
61
- color: rgba(0, 0, 0, 0.35);
62
72
  text-transform: capitalize;
63
73
  padding: 0 5px;
64
74
  }
@@ -69,7 +79,7 @@
69
79
  width: 5px;
70
80
  }
71
81
  .eyes-info>.status-bar.aborted {
72
- background: #ff8f00;
82
+ background: #e80600;
73
83
  }
74
84
  .eyes-info>.status-bar.unresolved {
75
85
  background: #ff8f00;