From b233f31a2df8c2402292c43da3fb2db386a91a77 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Tue, 9 Dec 2025 11:30:05 +0200 Subject: [PATCH 1/8] adapted screenshot tests for chrome 132 --- config/wdio.conf.js | 75 ++++++++++----- screenshot/utils/confHelpers.js | 162 +++++++++++++++++++++++++++++++- utils/Page.js | 66 +++++++++++-- 3 files changed, 270 insertions(+), 33 deletions(-) diff --git a/config/wdio.conf.js b/config/wdio.conf.js index 0ecaa5ac..c7ef7b13 100644 --- a/config/wdio.conf.js +++ b/config/wdio.conf.js @@ -15,13 +15,13 @@ export const configure = (options) => { if (!process.env.CHROME_DRIVER) { // TODO: Update this version when chromedriver version in CI/CD is updated - process.env.CHROME_DRIVER = base === 'screenshot' ? '120.0.6099.109' : '132.0.6834.159'; + process.env.CHROME_DRIVER = '132.0.6834.159'; console.log('Chrome Driver Version : ' + process.env.CHROME_DRIVER); } return Object.assign( - opts, + {}, { path: '/', // @@ -87,25 +87,31 @@ export const configure = (options) => { We need to specify a browser version that matches chromedriver version running in CI/CD environment to ensure testing accuracy. */ browserVersion: process.env.CHROME_DRIVER, - 'goog:chromeOptions': visibleBrowser ? + 'goog:chromeOptions': { args: [ + '--disable-infobars', + '--disable-search-engine-choice-screen', + '--disable-notifications', + '--disable-popup-blocking', '--disable-lcd-text', '--force-device-scale-factor=1', '--start-maximized', - '--start-fullscreen', '--disable-gpu', - '--window-size=1920,1080' - ] - } : { - args: [ - '--disable-lcd-text', - '--force-device-scale-factor=1', - '--start-maximized', - '--start-fullscreen', - '--headless', - '--disable-gpu', - '--window-size=1920,1080' + '--disable-dev-shm-usage', + '--no-sandbox', + '--disable-features=AudioServiceOutOfProcess', + '--disable-background-networking', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-breakpad', + '--disable-component-extensions-with-background-pages', + '--disable-extensions', + '--disable-renderer-backgrounding', + '--metrics-recording-only', + '--mute-audio', + '--js-flags=--max-old-space-size=384', // Changed from 512 + ...(visibleBrowser ? [] : ['--headless=new']) ] }, webSocketUrl: false, // disables BiDi, forces classic mode @@ -135,21 +141,23 @@ export const configure = (options) => { baseUrl: 'http://localhost:4567', // // Default timeout for all waitFor* commands. - waitforTimeout: 10000, + waitforTimeout: 45000, // // Default timeout in milliseconds for request // if Selenium Grid doesn't send response - connectionRetryTimeout: 120000, + connectionRetryTimeout: 240000, // // Default request retries count - connectionRetryCount: 3, + connectionRetryCount: 5, // Ignore deprecation warnings deprecationWarnings: false, // // Default timeouts // timeouts: { - script: 60000 // extend script timeout to 60s just in case + script: 600000, + pageLoad: 600000, + implicit: 20000 }, // // Initialize the browser instance with a WebdriverIO plugin. The object should have the @@ -202,6 +210,9 @@ export const configure = (options) => { ui: 'bdd', timeout: 60 * 60 * 1000 }, + // Retry failed spec files + specFileRetries: 1, + specFileRetriesDelay: 10, /** * Gets executed before test execution begins. At this point you can access to all global * variables like `browser`. It is the perfect place to define custom commands. @@ -209,14 +220,32 @@ export const configure = (options) => { before: async function () { global.wdioExpect = global.expect; // in Chrome 132, the browser window size takes into account also the address bar and tab area - await browser.maximizeWindow(); - let browserHeight = base === 'screenshot' ? 1080 : 1272; - await browser.setWindowSize(1920, browserHeight); + await browser.setWindowSize(1920, 1167); + + // Small pause to let window resize complete + await browser.pause(200); + + // Verify the window is ready + await browser.waitUntil( + async () => { + try { + const state = await browser.execute(() => document.readyState); + return state === 'complete'; + } catch (e) { + return false; + } + }, + { + timeout: 15000, + timeoutMsg: 'Page did not reach ready state' + } + ); if (options.before) { await options.before(); } } - } + }, + opts ); }; diff --git a/screenshot/utils/confHelpers.js b/screenshot/utils/confHelpers.js index b4930a88..c968d4fc 100644 --- a/screenshot/utils/confHelpers.js +++ b/screenshot/utils/confHelpers.js @@ -52,6 +52,9 @@ function initFile (name, content) { } function onPrepare () { + global.workerFailures = new Map(); + global.failedWorkers = new Set(); + if (!fs.existsSync('tests/screenshot/dist/screenshots/reference')) { console.log('No reference screenshots found, creating new references!'); } @@ -62,7 +65,61 @@ function onPrepare () { return buildApps('screenshot'); } -function beforeTest (testData) { +async function beforeTest (testData) { + const workerId = browser.sessionId; + + // Check if this worker has been marked as dead + if (global.failedWorkers && global.failedWorkers.has(workerId)) { + throw new Error('Worker is marked as failed - skipping remaining tests'); + } + + // Skip health check if worker was just recovered + if (global.recentlyRecovered && global.recentlyRecovered.has(workerId)) { + global.recentlyRecovered.delete(workerId); + } else { + // Quick health check with short timeout + try { + await Promise.race([ + browser.execute(() => true), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Health check timeout')), 3000) + ) + ]); + + // Success - reset failure counter + if (global.workerFailures) { + global.workerFailures.set(workerId, 0); + } + } catch (e) { + // Track consecutive failures + const failures = (global.workerFailures?.get(workerId) || 0) + 1; + if (global.workerFailures) { + global.workerFailures.set(workerId, failures); + } + + console.log(`Worker ${workerId} health check failed (failure ${failures}/3)`); + + // Only mark as dead after 3 consecutive failures + if (failures >= 3) { + console.log(`Worker ${workerId} has failed 3 times - marking as dead`); + if (global.failedWorkers) { + global.failedWorkers.add(workerId); + } + throw new Error('Worker health check failed - marking as dead'); + } + + // Try quick recovery for first 2 failures + console.log(`Attempting quick recovery for worker ${workerId}...`); + try { + await browser.reloadSession(); + await browser.setWindowSize(1920, 1167); + console.log(`Worker ${workerId} recovered`); + } catch (recoveryError) { + console.log(`Recovery attempt failed, will retry next test`); + } + } + } + // If title doesn't have a '/', it's not a screenshot test, don't save if (testData && testData.title && testData.title.indexOf('/') > 0) { const filename = generateReferenceName({test: testData}); @@ -78,7 +135,7 @@ function beforeTest (testData) { } } -function afterTest (testData, _context, {passed}) { +async function afterTest (testData, _context, {error, passed}) { // If this doesn't include context data, not a screenshot test if (testData && testData.title && testData.context && testData.context.params) { const fileName = testData.context.fileName.replace(/ /g, '_') + '.png'; @@ -115,6 +172,107 @@ function afterTest (testData, _context, {passed}) { }); } } + + if (error) { + const isTimeout = error.message && + (error.message.includes('timeout') || + error.message.includes('aborted') || + error.message.includes('HEADERS_TIMEOUT') || + error.message.includes('ECONNREFUSED')); + + if (isTimeout) { + const workerId = browser.sessionId; + + // Track consecutive failures + if (!global.workerFailures) { + global.workerFailures = new Map(); + } + + const failures = (global.workerFailures.get(workerId) || 0) + 1; + global.workerFailures.set(workerId, failures); + + console.log(`Timeout #${failures} in worker ${workerId} - test: "${testData.title}"`); + + // Circuit breaker: after 3 consecutive timeouts, kill the worker + if (failures >= 3) { + console.log(`Worker ${workerId} has failed 3 times consecutively - marking as dead`); + + if (!global.failedWorkers) { + global.failedWorkers = new Set(); + } + global.failedWorkers.add(workerId); + + // Try to clean up with very short timeout, then give up + try { + await Promise.race([ + (async () => { + try { + await browser.execute(() => window.stop()); + } catch (e) { + // Ignore + } + await browser.deleteSession(); + })(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Cleanup timeout')), 2000) + ) + ]); + console.log('Worker cleanup completed'); + } catch (e) { + console.log('Worker cleanup timed out - worker is dead'); + } + + return; // Don't try to recover + } + + // For first 2 failures, attempt recovery + console.log(`Attempting recovery for worker ${workerId} (attempt ${failures}/3)`); + + try { + // Try light recovery with timeout + await Promise.race([ + (async () => { + try { + await browser.execute(() => window.stop()); + } catch (e) { + // Ignore + } + await browser.deleteSession(); + await browser.reloadSession(); + await browser.setWindowSize(1920, 1167); + await browser.pause(1000); + })(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Recovery timeout')), 10000) + ) + ]); + + console.log('Session recovered successfully'); + + // Reset failure count on successful recovery + global.workerFailures.set(workerId, 0); + + // Mark that we just recovered - skip next health check + if (!global.recentlyRecovered) { + global.recentlyRecovered = new Set(); + } + global.recentlyRecovered.add(workerId); + + } catch (recoveryError) { + console.error(`Session recovery failed: ${recoveryError.message}`); + } + } else { + const workerId = browser.sessionId; + if (global.workerFailures && global.workerFailures.has(workerId)) { + global.workerFailures.set(workerId, 0); + } + } + } else if (passed) { + const workerId = browser.sessionId; + if (global.workerFailures && global.workerFailures.has(workerId)) { + global.workerFailures.set(workerId, 0); + } + } } function onComplete () { diff --git a/utils/Page.js b/utils/Page.js index 6e0e92ba..b76d11e5 100644 --- a/utils/Page.js +++ b/utils/Page.js @@ -11,18 +11,68 @@ export class Page { } async open (appPath, urlExtra = '?locale=en-US') { - await browser.execute(function () { - document.body.innerHTML = ''; - }); - this._url = `/${appPath}/${urlExtra}`; - await browser.url(this.url); + const maxAttempts = 2; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + await browser.url(this.url); + + // Wait for page to be ready + await browser.waitUntil( + async () => { + try { + const state = await browser.execute(() => document.readyState); + return state === 'complete' || state === 'interactive'; + } catch (e) { + return false; + } + }, + { + timeout: 30000, + timeoutMsg: `Page did not become ready: ${this.url}` + } + ); + + await browser.pause(200); + + // Clear body content using async execution + await browser.executeAsync((done) => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(() => { + document.body.innerHTML = ''; + done(); + }); + }); + }); - const body = await $('body'); - await body.waitForDisplayed({timeout: 10000}); + const body = await $('body'); + await body.waitForDisplayed({timeout: 10000}); - await this.delay(200); + // Small final pause + await browser.pause(100); + + return; + + } catch (error) { + console.log(`Navigation attempt ${attempt}/${maxAttempts} failed: ${error.message}`); + + if (attempt === maxAttempts) { + // All retries exhausted - throw error + throw error; + } + + // Quick recovery attempt before next retry + try { + await browser.execute(() => window.stop()); + } catch (e) { + // Ignore + } + + await this.delay(200); + } + } } serializeParams (params) { From 40ae51c39c1ec0483574562ce0db360f6e1e7cd8 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Wed, 10 Dec 2025 17:25:55 +0200 Subject: [PATCH 2/8] reverted page.js --- utils/Page.js | 66 +++++++-------------------------------------------- 1 file changed, 8 insertions(+), 58 deletions(-) diff --git a/utils/Page.js b/utils/Page.js index b76d11e5..6e0e92ba 100644 --- a/utils/Page.js +++ b/utils/Page.js @@ -11,68 +11,18 @@ export class Page { } async open (appPath, urlExtra = '?locale=en-US') { - this._url = `/${appPath}/${urlExtra}`; - - const maxAttempts = 2; - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - await browser.url(this.url); - - // Wait for page to be ready - await browser.waitUntil( - async () => { - try { - const state = await browser.execute(() => document.readyState); - return state === 'complete' || state === 'interactive'; - } catch (e) { - return false; - } - }, - { - timeout: 30000, - timeoutMsg: `Page did not become ready: ${this.url}` - } - ); - - await browser.pause(200); - - // Clear body content using async execution - await browser.executeAsync((done) => { - window.requestAnimationFrame(() => { - window.requestAnimationFrame(() => { - document.body.innerHTML = ''; - done(); - }); - }); - }); - - const body = await $('body'); - await body.waitForDisplayed({timeout: 10000}); - - // Small final pause - await browser.pause(100); - - return; + await browser.execute(function () { + document.body.innerHTML = ''; + }); - } catch (error) { - console.log(`Navigation attempt ${attempt}/${maxAttempts} failed: ${error.message}`); + this._url = `/${appPath}/${urlExtra}`; - if (attempt === maxAttempts) { - // All retries exhausted - throw error - throw error; - } + await browser.url(this.url); - // Quick recovery attempt before next retry - try { - await browser.execute(() => window.stop()); - } catch (e) { - // Ignore - } + const body = await $('body'); + await body.waitForDisplayed({timeout: 10000}); - await this.delay(200); - } - } + await this.delay(200); } serializeParams (params) { From 1e23472bbfacda463807e966f37197edd2f1e67f Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Wed, 10 Dec 2025 18:32:05 +0200 Subject: [PATCH 3/8] removed retries --- config/wdio.conf.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/config/wdio.conf.js b/config/wdio.conf.js index c7ef7b13..7b0179c5 100644 --- a/config/wdio.conf.js +++ b/config/wdio.conf.js @@ -210,9 +210,6 @@ export const configure = (options) => { ui: 'bdd', timeout: 60 * 60 * 1000 }, - // Retry failed spec files - specFileRetries: 1, - specFileRetriesDelay: 10, /** * Gets executed before test execution begins. At this point you can access to all global * variables like `browser`. It is the perfect place to define custom commands. From 20be66ed877916831862cb04941815386b16f237 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Thu, 11 Dec 2025 16:01:52 +0200 Subject: [PATCH 4/8] added changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0163fa19..1023875a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## [unreleased] + +* Updated Chrome driver version to 132 for screenshot tests. + ## [4.0.0] (November 14, 2025) * Updated Chrome driver version to 132. From 4467d632a573308f0a37de29b9cd6eeaafb58a72 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Fri, 12 Dec 2025 13:37:26 +0200 Subject: [PATCH 5/8] added retry options for tests --- config/wdio.conf.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/config/wdio.conf.js b/config/wdio.conf.js index 7b0179c5..2b702784 100644 --- a/config/wdio.conf.js +++ b/config/wdio.conf.js @@ -98,19 +98,15 @@ export const configure = (options) => { '--force-device-scale-factor=1', '--start-maximized', '--disable-gpu', - '--disable-dev-shm-usage', + '--window-size=1920,1080', + // Critical for Chrome 132 in Jenkins/Linux '--no-sandbox', - '--disable-features=AudioServiceOutOfProcess', - '--disable-background-networking', - '--disable-background-timer-throttling', - '--disable-backgrounding-occluded-windows', - '--disable-breakpad', - '--disable-component-extensions-with-background-pages', - '--disable-extensions', - '--disable-renderer-backgrounding', - '--metrics-recording-only', - '--mute-audio', - '--js-flags=--max-old-space-size=384', // Changed from 512 + '--disable-dev-shm-usage', + '--disable-setuid-sandbox', + // Performance optimizations for Chrome 132 + '--disable-features=VizDisplayCompositor', + '--disable-features=IsolateOrigins,site-per-process', + '--js-flags=--max-old-space-size=512', ...(visibleBrowser ? [] : ['--headless=new']) ] }, @@ -208,7 +204,8 @@ export const configure = (options) => { // See the full list at http://mochajs.org/ mochaOpts: { ui: 'bdd', - timeout: 60 * 60 * 1000 + timeout: 60 * 60 * 1000, + retries: 2 // Retry failed tests up to 2 times (important for timeout recovery) }, /** * Gets executed before test execution begins. At this point you can access to all global From 8a4234181f046a5c17d5b58edbb3b0edcd21102d Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Wed, 17 Dec 2025 16:35:46 +0200 Subject: [PATCH 6/8] added retry options for tests --- screenshot/utils/confHelpers.js | 48 ++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/screenshot/utils/confHelpers.js b/screenshot/utils/confHelpers.js index c968d4fc..d31f5e24 100644 --- a/screenshot/utils/confHelpers.js +++ b/screenshot/utils/confHelpers.js @@ -156,20 +156,38 @@ async function afterTest (testData, _context, {error, passed}) { } if (!passed) { - const screenPath = path.join(screenshotRelativePath, 'actual', fileName); - const diffPath = path.join(screenshotRelativePath, 'diff', fileName); - fs.open(failedScreenshotFilename, 'a', (err, fd) => { - if (err) { - console.error('Unable to create failed test log file!'); - } else { - const title = testData.title.replace(/~\//g, '/'); - const {params, url} = testData.context; - const output = {title, diffPath, referencePath, screenPath, params, url}; - fs.appendFile(fd, `${JSON.stringify(output)},`, 'utf8', () => { - fs.close(fd); - }); - } - }); + // Track failed tests to avoid duplicate logging during retries + if (!global.loggedFailures) { + global.loggedFailures = new Set(); + } + + const testIdentifier = testData.title + '::' + fileName; + + // Only log if we haven't already logged this test failure + if (!global.loggedFailures.has(testIdentifier)) { + global.loggedFailures.add(testIdentifier); + + const screenPath = path.join(screenshotRelativePath, 'actual', fileName); + const diffPath = path.join(screenshotRelativePath, 'diff', fileName); + fs.open(failedScreenshotFilename, 'a', (err, fd) => { + if (err) { + console.error('Unable to create failed test log file!'); + } else { + const title = testData.title.replace(/~\//g, '/'); + const {params, url} = testData.context; + const output = {title, diffPath, referencePath, screenPath, params, url}; + fs.appendFile(fd, `${JSON.stringify(output)},`, 'utf8', () => { + fs.close(fd); + }); + } + }); + } + } else if (passed) { + // Test passed on retry - remove from logged failures + if (global.loggedFailures) { + const testIdentifier = testData.title + '::' + fileName; + global.loggedFailures.delete(testIdentifier); + } } } @@ -304,4 +322,4 @@ export { onComplete, onPrepare, screenshotFolder -}; +}; \ No newline at end of file From 24af6eed0174e1d0d68635112ac5ed542f0f60e9 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Wed, 17 Dec 2025 17:55:11 +0200 Subject: [PATCH 7/8] eslint fix --- screenshot/utils/confHelpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/screenshot/utils/confHelpers.js b/screenshot/utils/confHelpers.js index d31f5e24..052404d5 100644 --- a/screenshot/utils/confHelpers.js +++ b/screenshot/utils/confHelpers.js @@ -322,4 +322,4 @@ export { onComplete, onPrepare, screenshotFolder -}; \ No newline at end of file +}; From 12950e77cb5310dc2d2a1ff254e68c6ab53dfead Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Thu, 18 Dec 2025 12:32:38 +0200 Subject: [PATCH 8/8] moved session failure check in own functions --- screenshot/utils/confHelpers.js | 241 +++++++++++++++++--------------- 1 file changed, 125 insertions(+), 116 deletions(-) diff --git a/screenshot/utils/confHelpers.js b/screenshot/utils/confHelpers.js index 052404d5..b85c787b 100644 --- a/screenshot/utils/confHelpers.js +++ b/screenshot/utils/confHelpers.js @@ -52,8 +52,8 @@ function initFile (name, content) { } function onPrepare () { - global.workerFailures = new Map(); - global.failedWorkers = new Set(); + global.sessionFailures = new Map(); + global.failedSessions = new Set(); if (!fs.existsSync('tests/screenshot/dist/screenshots/reference')) { console.log('No reference screenshots found, creating new references!'); @@ -65,19 +65,20 @@ function onPrepare () { return buildApps('screenshot'); } -async function beforeTest (testData) { - const workerId = browser.sessionId; +/* Checks if a browser session is healthy. If not, it will attempt to recover. */ +async function checkSessionHealth () { + const sessionId = browser.sessionId; - // Check if this worker has been marked as dead - if (global.failedWorkers && global.failedWorkers.has(workerId)) { - throw new Error('Worker is marked as failed - skipping remaining tests'); + // Check if this session has been marked as dead + if (global.failedSessions && global.failedSessions.has(sessionId)) { + throw new Error('Session is marked as failed - skipping remaining tests'); } - // Skip health check if worker was just recovered - if (global.recentlyRecovered && global.recentlyRecovered.has(workerId)) { - global.recentlyRecovered.delete(workerId); + // Skip health check if the session was just recovered + if (global.recentlyRecovered && global.recentlyRecovered.has(sessionId)) { + global.recentlyRecovered.delete(sessionId); } else { - // Quick health check with short timeout + // Quick health check with a short timeout try { await Promise.race([ browser.execute(() => true), @@ -87,110 +88,41 @@ async function beforeTest (testData) { ]); // Success - reset failure counter - if (global.workerFailures) { - global.workerFailures.set(workerId, 0); + if (global.sessionFailures) { + global.sessionFailures.set(sessionId, 0); } } catch (e) { // Track consecutive failures - const failures = (global.workerFailures?.get(workerId) || 0) + 1; - if (global.workerFailures) { - global.workerFailures.set(workerId, failures); + const failures = (global.sessionFailures?.get(sessionId) || 0) + 1; + if (global.sessionFailures) { + global.sessionFailures.set(sessionId, failures); } - console.log(`Worker ${workerId} health check failed (failure ${failures}/3)`); + console.log(`Session ${sessionId} health check failed (failure ${failures}/3)`); // Only mark as dead after 3 consecutive failures if (failures >= 3) { - console.log(`Worker ${workerId} has failed 3 times - marking as dead`); - if (global.failedWorkers) { - global.failedWorkers.add(workerId); + console.log(`Session ${sessionId} has failed 3 times - marking as dead`); + if (global.failedSessions) { + global.failedSessions.add(sessionId); } - throw new Error('Worker health check failed - marking as dead'); + throw new Error('Session health check failed - marking as dead'); } - // Try quick recovery for first 2 failures - console.log(`Attempting quick recovery for worker ${workerId}...`); + // Try quick recovery for the first 2 failures + console.log(`Attempting quick recovery for session ${sessionId}...`); try { await browser.reloadSession(); await browser.setWindowSize(1920, 1167); - console.log(`Worker ${workerId} recovered`); + console.log(`Session ${sessionId} recovered`); } catch (recoveryError) { console.log(`Recovery attempt failed, will retry next test`); } } } - - // If title doesn't have a '/', it's not a screenshot test, don't save - if (testData && testData.title && testData.title.indexOf('/') > 0) { - const filename = generateReferenceName({test: testData}); - testData.ctx.isNewScreenshot = !fs.existsSync(filename); - - // if there are no reference screenshots, we must create the folder before running the tests. - const specsPath = testData.title.split('~/'); - specsPath.pop(); - const referenceSpecsPath = path.join('tests/screenshot/dist/screenshots/reference', ...specsPath).replace(/ /g, '_'); - if (testData.ctx.isNewScreenshot) { - fs.mkdirSync(referenceSpecsPath, {recursive: true}); - } - } } -async function afterTest (testData, _context, {error, passed}) { - // If this doesn't include context data, not a screenshot test - if (testData && testData.title && testData.context && testData.context.params) { - const fileName = testData.context.fileName.replace(/ /g, '_') + '.png'; - const referencePath = path.join(baselineRelativePath, fileName); - - if (_context.isNewScreenshot) { - fs.open(newScreenshotFilename, 'a', (err, fd) => { - if (err) { - console.error('Unable to create log file!'); - } else { - const {params, url} = testData.context; - const output = {title: testData.title.replace(/~\//g, '/'), path: referencePath, params, url}; - fs.appendFile(fd, `${JSON.stringify(output)},`, 'utf8', () => { - fs.close(fd); - }); - } - }); - } - - if (!passed) { - // Track failed tests to avoid duplicate logging during retries - if (!global.loggedFailures) { - global.loggedFailures = new Set(); - } - - const testIdentifier = testData.title + '::' + fileName; - - // Only log if we haven't already logged this test failure - if (!global.loggedFailures.has(testIdentifier)) { - global.loggedFailures.add(testIdentifier); - - const screenPath = path.join(screenshotRelativePath, 'actual', fileName); - const diffPath = path.join(screenshotRelativePath, 'diff', fileName); - fs.open(failedScreenshotFilename, 'a', (err, fd) => { - if (err) { - console.error('Unable to create failed test log file!'); - } else { - const title = testData.title.replace(/~\//g, '/'); - const {params, url} = testData.context; - const output = {title, diffPath, referencePath, screenPath, params, url}; - fs.appendFile(fd, `${JSON.stringify(output)},`, 'utf8', () => { - fs.close(fd); - }); - } - }); - } - } else if (passed) { - // Test passed on retry - remove from logged failures - if (global.loggedFailures) { - const testIdentifier = testData.title + '::' + fileName; - global.loggedFailures.delete(testIdentifier); - } - } - } - +async function cleanUpSessionHealthCheck (testData, error, passed) { if (error) { const isTimeout = error.message && (error.message.includes('timeout') || @@ -199,26 +131,26 @@ async function afterTest (testData, _context, {error, passed}) { error.message.includes('ECONNREFUSED')); if (isTimeout) { - const workerId = browser.sessionId; + const sessionId = browser.sessionId; // Track consecutive failures - if (!global.workerFailures) { - global.workerFailures = new Map(); + if (!global.sessionFailures) { + global.sessionFailures = new Map(); } - const failures = (global.workerFailures.get(workerId) || 0) + 1; - global.workerFailures.set(workerId, failures); + const failures = (global.sessionFailures.get(sessionId) || 0) + 1; + global.sessionFailures.set(sessionId, failures); - console.log(`Timeout #${failures} in worker ${workerId} - test: "${testData.title}"`); + console.log(`Timeout #${failures} in session ${sessionId} - test: "${testData.title}"`); - // Circuit breaker: after 3 consecutive timeouts, kill the worker + // Circuit breaker: after 3 consecutive timeouts, kill the session if (failures >= 3) { - console.log(`Worker ${workerId} has failed 3 times consecutively - marking as dead`); + console.log(`Session ${sessionId} has failed 3 times consecutively - marking as dead`); - if (!global.failedWorkers) { - global.failedWorkers = new Set(); + if (!global.failedSessions) { + global.failedSessions = new Set(); } - global.failedWorkers.add(workerId); + global.failedSessions.add(sessionId); // Try to clean up with very short timeout, then give up try { @@ -235,16 +167,16 @@ async function afterTest (testData, _context, {error, passed}) { setTimeout(() => reject(new Error('Cleanup timeout')), 2000) ) ]); - console.log('Worker cleanup completed'); + console.log('Session cleanup completed'); } catch (e) { - console.log('Worker cleanup timed out - worker is dead'); + console.log('Session cleanup timed out - session is dead'); } return; // Don't try to recover } // For first 2 failures, attempt recovery - console.log(`Attempting recovery for worker ${workerId} (attempt ${failures}/3)`); + console.log(`Attempting recovery for session ${sessionId} (attempt ${failures}/3)`); try { // Try light recovery with timeout @@ -268,31 +200,108 @@ async function afterTest (testData, _context, {error, passed}) { console.log('Session recovered successfully'); // Reset failure count on successful recovery - global.workerFailures.set(workerId, 0); + global.sessionFailures.set(sessionId, 0); // Mark that we just recovered - skip next health check if (!global.recentlyRecovered) { global.recentlyRecovered = new Set(); } - global.recentlyRecovered.add(workerId); + global.recentlyRecovered.add(sessionId); } catch (recoveryError) { console.error(`Session recovery failed: ${recoveryError.message}`); } } else { - const workerId = browser.sessionId; - if (global.workerFailures && global.workerFailures.has(workerId)) { - global.workerFailures.set(workerId, 0); + const sessionId = browser.sessionId; + if (global.sessionFailures && global.sessionFailures.has(sessionId)) { + global.sessionFailures.set(sessionId, 0); } } } else if (passed) { - const workerId = browser.sessionId; - if (global.workerFailures && global.workerFailures.has(workerId)) { - global.workerFailures.set(workerId, 0); + const sessionId = browser.sessionId; + if (global.sessionFailures && global.sessionFailures.has(sessionId)) { + global.sessionFailures.set(sessionId, 0); + } + } +} + +async function beforeTest (testData) { + await checkSessionHealth(); + + // If title doesn't have a '/', it's not a screenshot test, don't save + if (testData && testData.title && testData.title.indexOf('/') > 0) { + const filename = generateReferenceName({test: testData}); + testData.ctx.isNewScreenshot = !fs.existsSync(filename); + + // if there are no reference screenshots, we must create the folder before running the tests. + const specsPath = testData.title.split('~/'); + specsPath.pop(); + const referenceSpecsPath = path.join('tests/screenshot/dist/screenshots/reference', ...specsPath).replace(/ /g, '_'); + if (testData.ctx.isNewScreenshot) { + fs.mkdirSync(referenceSpecsPath, {recursive: true}); } } } +async function afterTest (testData, _context, {error, passed}) { + // If this doesn't include context data, not a screenshot test + if (testData && testData.title && testData.context && testData.context.params) { + const fileName = testData.context.fileName.replace(/ /g, '_') + '.png'; + const referencePath = path.join(baselineRelativePath, fileName); + + if (_context.isNewScreenshot) { + fs.open(newScreenshotFilename, 'a', (err, fd) => { + if (err) { + console.error('Unable to create log file!'); + } else { + const {params, url} = testData.context; + const output = {title: testData.title.replace(/~\//g, '/'), path: referencePath, params, url}; + fs.appendFile(fd, `${JSON.stringify(output)},`, 'utf8', () => { + fs.close(fd); + }); + } + }); + } + + if (!passed) { + // Track failed tests to avoid duplicate logging during retries + if (!global.loggedFailures) { + global.loggedFailures = new Set(); + } + + const testIdentifier = testData.title + '::' + fileName; + + // Only log if we haven't already logged this test failure + if (!global.loggedFailures.has(testIdentifier)) { + global.loggedFailures.add(testIdentifier); + + const screenPath = path.join(screenshotRelativePath, 'actual', fileName); + const diffPath = path.join(screenshotRelativePath, 'diff', fileName); + fs.open(failedScreenshotFilename, 'a', (err, fd) => { + if (err) { + console.error('Unable to create failed test log file!'); + } else { + const title = testData.title.replace(/~\//g, '/'); + const {params, url} = testData.context; + const output = {title, diffPath, referencePath, screenPath, params, url}; + fs.appendFile(fd, `${JSON.stringify(output)},`, 'utf8', () => { + fs.close(fd); + }); + } + }); + } + } else if (passed) { + // Test passed on retry - remove from logged failures + if (global.loggedFailures) { + const testIdentifier = testData.title + '::' + fileName; + global.loggedFailures.delete(testIdentifier); + } + } + } + + await cleanUpSessionHealthCheck(testData, error, passed); +} + function onComplete () { const {size: newSize} = fs.statSync(newScreenshotFilename), {size: failedSize} = fs.statSync(failedScreenshotFilename);