@enact/ui-test-utils 3.0.0 → 4.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enact/ui-test-utils",
3
- "version": "3.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "UI Testing for the Enact framework",
5
5
  "repository": "https://github.com/enactjs/ui-test-utils",
6
6
  "type": "module",
@@ -23,35 +23,34 @@
23
23
  "author": "Roy Sutton <roy.sutton@lge.com>",
24
24
  "license": "Apache-2.0",
25
25
  "engines": {
26
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
26
+ "node": "^20.12.0 || ^22.0.0 || >=24.0.0"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@wdio/cli": "^9.15.0",
33
- "@wdio/dot-reporter": "^9.15.0",
34
- "@wdio/local-runner": "9.2.1",
35
- "@wdio/mocha-framework": "^9.15.0",
36
- "@wdio/spec-reporter": "^9.15.0",
37
- "@wdio/static-server-service": "^9.15.0",
38
- "@wdio/visual-service": "^8.0.4",
39
- "chalk": "^5.4.1",
32
+ "@wdio/cli": "^9.21.1",
33
+ "@wdio/dot-reporter": "^9.20.0",
34
+ "@wdio/local-runner": "^9.21.0",
35
+ "@wdio/mocha-framework": "^9.21.0",
36
+ "@wdio/spec-reporter": "^9.20.0",
37
+ "@wdio/static-server-service": "^9.20.0",
38
+ "@wdio/visual-service": "^9.0.2",
39
+ "chalk": "^5.6.2",
40
40
  "cross-spawn": "^7.0.6",
41
- "expect-webdriverio": "^5.3.2",
42
- "fs-extra": "^11.3.0",
41
+ "expect-webdriverio": "^5.5.0",
42
+ "fs-extra": "^11.3.2",
43
43
  "minimist": "^1.2.8",
44
44
  "query-string": "^7.1.3",
45
- "ramda": "^0.30.1",
45
+ "ramda": "^0.31.3",
46
46
  "readdirp": "^3.6.0",
47
47
  "wdio-docker-service": "^3.2.1",
48
- "wdio-selenium-standalone-service": "^0.0.12",
49
- "webdriverio": "^9.15.0"
48
+ "webdriverio": "^9.21.0"
50
49
  },
51
50
  "devDependencies": {
52
- "eslint": "^9.29.0",
53
- "eslint-config-enact": "^5.0.1",
54
- "globals": "^16.3.0"
51
+ "eslint": "^9.39.1",
52
+ "eslint-config-enact": "^5.0.3",
53
+ "globals": "^16.5.0"
55
54
  },
56
55
  "overrides": {
57
56
  "cross-spawn": "$cross-spawn",
@@ -52,6 +52,9 @@ function initFile (name, content) {
52
52
  }
53
53
 
54
54
  function onPrepare () {
55
+ global.sessionFailures = new Map();
56
+ global.failedSessions = new Set();
57
+
55
58
  if (!fs.existsSync('tests/screenshot/dist/screenshots/reference')) {
56
59
  console.log('No reference screenshots found, creating new references!');
57
60
  }
@@ -62,7 +65,169 @@ function onPrepare () {
62
65
  return buildApps('screenshot');
63
66
  }
64
67
 
65
- function beforeTest (testData) {
68
+ /* Checks if a browser session is healthy. If not, it will attempt to recover. */
69
+ async function checkSessionHealth () {
70
+ const sessionId = browser.sessionId;
71
+
72
+ // Check if this session has been marked as dead
73
+ if (global.failedSessions && global.failedSessions.has(sessionId)) {
74
+ throw new Error('Session is marked as failed - skipping remaining tests');
75
+ }
76
+
77
+ // Skip health check if the session was just recovered
78
+ if (global.recentlyRecovered && global.recentlyRecovered.has(sessionId)) {
79
+ global.recentlyRecovered.delete(sessionId);
80
+ } else {
81
+ // Quick health check with a short timeout
82
+ try {
83
+ await Promise.race([
84
+ browser.execute(() => true),
85
+ new Promise((_, reject) =>
86
+ setTimeout(() => reject(new Error('Health check timeout')), 3000)
87
+ )
88
+ ]);
89
+
90
+ // Success - reset failure counter
91
+ if (global.sessionFailures) {
92
+ global.sessionFailures.set(sessionId, 0);
93
+ }
94
+ } catch (e) {
95
+ // Track consecutive failures
96
+ const failures = (global.sessionFailures?.get(sessionId) || 0) + 1;
97
+ if (global.sessionFailures) {
98
+ global.sessionFailures.set(sessionId, failures);
99
+ }
100
+
101
+ console.log(`Session ${sessionId} health check failed (failure ${failures}/3)`);
102
+
103
+ // Only mark as dead after 3 consecutive failures
104
+ if (failures >= 3) {
105
+ console.log(`Session ${sessionId} has failed 3 times - marking as dead`);
106
+ if (global.failedSessions) {
107
+ global.failedSessions.add(sessionId);
108
+ }
109
+ throw new Error('Session health check failed - marking as dead');
110
+ }
111
+
112
+ // Try quick recovery for the first 2 failures
113
+ console.log(`Attempting quick recovery for session ${sessionId}...`);
114
+ try {
115
+ await browser.reloadSession();
116
+ await browser.setWindowSize(1920, 1167);
117
+ console.log(`Session ${sessionId} recovered`);
118
+ } catch (recoveryError) {
119
+ console.log(`Recovery attempt failed, will retry next test`);
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ async function cleanUpSessionHealthCheck (testData, error) {
126
+ if (error) {
127
+ const isTimeout = error.message &&
128
+ (error.message.includes('timeout') ||
129
+ error.message.includes('aborted') ||
130
+ error.message.includes('HEADERS_TIMEOUT') ||
131
+ error.message.includes('ECONNREFUSED'));
132
+
133
+ if (isTimeout) {
134
+ const sessionId = browser.sessionId;
135
+
136
+ // Track consecutive failures
137
+ if (!global.sessionFailures) {
138
+ global.sessionFailures = new Map();
139
+ }
140
+
141
+ const failures = (global.sessionFailures.get(sessionId) || 0) + 1;
142
+ global.sessionFailures.set(sessionId, failures);
143
+
144
+ console.log(`Timeout #${failures} in session ${sessionId} - test: "${testData.title}"`);
145
+
146
+ // Circuit breaker: after 3 consecutive timeouts, kill the session
147
+ if (failures >= 3) {
148
+ console.log(`Session ${sessionId} has failed 3 times consecutively - marking as dead`);
149
+
150
+ if (!global.failedSessions) {
151
+ global.failedSessions = new Set();
152
+ }
153
+ global.failedSessions.add(sessionId);
154
+
155
+ // Try to clean up with very short timeout, then give up
156
+ try {
157
+ await Promise.race([
158
+ (async () => {
159
+ try {
160
+ await browser.execute(() => window.stop());
161
+ } catch (e) {
162
+ // Ignore
163
+ }
164
+ await browser.deleteSession();
165
+ })(),
166
+ new Promise((_, reject) =>
167
+ setTimeout(() => reject(new Error('Cleanup timeout')), 2000)
168
+ )
169
+ ]);
170
+ console.log('Session cleanup completed');
171
+ } catch (e) {
172
+ console.log('Session cleanup timed out - session is dead');
173
+ }
174
+
175
+ return; // Don't try to recover
176
+ }
177
+
178
+ // For first 2 failures, attempt recovery
179
+ console.log(`Attempting recovery for session ${sessionId} (attempt ${failures}/3)`);
180
+
181
+ try {
182
+ // Try light recovery with timeout
183
+ await Promise.race([
184
+ (async () => {
185
+ try {
186
+ await browser.execute(() => window.stop());
187
+ } catch (e) {
188
+ // Ignore
189
+ }
190
+ await browser.deleteSession();
191
+ await browser.reloadSession();
192
+ await browser.setWindowSize(1920, 1167);
193
+ await browser.pause(1000);
194
+ })(),
195
+ new Promise((_, reject) =>
196
+ setTimeout(() => reject(new Error('Recovery timeout')), 10000)
197
+ )
198
+ ]);
199
+
200
+ console.log('Session recovered successfully');
201
+
202
+ // Reset failure count on successful recovery
203
+ global.sessionFailures.set(sessionId, 0);
204
+
205
+ // Mark that we just recovered - skip next health check
206
+ if (!global.recentlyRecovered) {
207
+ global.recentlyRecovered = new Set();
208
+ }
209
+ global.recentlyRecovered.add(sessionId);
210
+
211
+ } catch (recoveryError) {
212
+ console.error(`Session recovery failed: ${recoveryError.message}`);
213
+ }
214
+ } else {
215
+ const sessionId = browser.sessionId;
216
+ if (global.sessionFailures && global.sessionFailures.has(sessionId)) {
217
+ global.sessionFailures.set(sessionId, 0);
218
+ }
219
+ }
220
+ } else {
221
+ const sessionId = browser.sessionId;
222
+ if (global.sessionFailures && global.sessionFailures.has(sessionId)) {
223
+ global.sessionFailures.set(sessionId, 0);
224
+ }
225
+ }
226
+ }
227
+
228
+ async function beforeTest (testData) {
229
+ await checkSessionHealth();
230
+
66
231
  // If title doesn't have a '/', it's not a screenshot test, don't save
67
232
  if (testData && testData.title && testData.title.indexOf('/') > 0) {
68
233
  const filename = generateReferenceName({test: testData});
@@ -78,7 +243,7 @@ function beforeTest (testData) {
78
243
  }
79
244
  }
80
245
 
81
- function afterTest (testData, _context, {passed}) {
246
+ async function afterTest (testData, _context, {error, passed}) {
82
247
  // If this doesn't include context data, not a screenshot test
83
248
  if (testData && testData.title && testData.context && testData.context.params) {
84
249
  const fileName = testData.context.fileName.replace(/ /g, '_') + '.png';
@@ -99,22 +264,42 @@ function afterTest (testData, _context, {passed}) {
99
264
  }
100
265
 
101
266
  if (!passed) {
102
- const screenPath = path.join(screenshotRelativePath, 'actual', fileName);
103
- const diffPath = path.join(screenshotRelativePath, 'diff', fileName);
104
- fs.open(failedScreenshotFilename, 'a', (err, fd) => {
105
- if (err) {
106
- console.error('Unable to create failed test log file!');
107
- } else {
108
- const title = testData.title.replace(/~\//g, '/');
109
- const {params, url} = testData.context;
110
- const output = {title, diffPath, referencePath, screenPath, params, url};
111
- fs.appendFile(fd, `${JSON.stringify(output)},`, 'utf8', () => {
112
- fs.close(fd);
113
- });
114
- }
115
- });
267
+ // Track failed tests to avoid duplicate logging during retries
268
+ if (!global.loggedFailures) {
269
+ global.loggedFailures = new Set();
270
+ }
271
+
272
+ const testIdentifier = testData.title + '::' + fileName;
273
+
274
+ // Only log if we haven't already logged this test failure
275
+ if (!global.loggedFailures.has(testIdentifier)) {
276
+ global.loggedFailures.add(testIdentifier);
277
+
278
+ const screenPath = path.join(screenshotRelativePath, 'actual', fileName);
279
+ const diffPath = path.join(screenshotRelativePath, 'diff', fileName);
280
+ fs.open(failedScreenshotFilename, 'a', (err, fd) => {
281
+ if (err) {
282
+ console.error('Unable to create failed test log file!');
283
+ } else {
284
+ const title = testData.title.replace(/~\//g, '/');
285
+ const {params, url} = testData.context;
286
+ const output = {title, diffPath, referencePath, screenPath, params, url};
287
+ fs.appendFile(fd, `${JSON.stringify(output)},`, 'utf8', () => {
288
+ fs.close(fd);
289
+ });
290
+ }
291
+ });
292
+ }
293
+ } else {
294
+ // Test passed on retry - remove from logged failures
295
+ if (global.loggedFailures) { // eslint-disable-line no-lonely-if
296
+ const testIdentifier = testData.title + '::' + fileName;
297
+ global.loggedFailures.delete(testIdentifier);
298
+ }
116
299
  }
117
300
  }
301
+
302
+ await cleanUpSessionHealthCheck(testData, error);
118
303
  }
119
304
 
120
305
  function onComplete () {
@@ -33,15 +33,9 @@ const config = Object.assign(
33
33
  'wdio:maxInstances': 1,
34
34
  //
35
35
  browserName: 'chrome',
36
- /* WebdriverIO v8.14 and above downloads and uses the latest Chrome version when running tests.
37
- We need to specify a browser version that match chromedriver version running in CI/CD environment to
38
- ensure testing accuracy.
39
- TODO: Update this version when chromedriver version in CI/CD is updated */
40
- browserVersion: '120.0.6099.109',
41
36
  'goog:chromeOptions': {
42
37
  debuggerAddress: `${process.env.TV_IP}:9998`
43
- },
44
- 'wdio:enforceWebDriverClassic': true
38
+ }
45
39
  }],
46
40
 
47
41
  //
@@ -12,15 +12,9 @@ const config = Object.assign(
12
12
  'wdio:maxInstances': 1,
13
13
  //
14
14
  browserName: 'chrome',
15
- /* WebdriverIO v8.14 and above downloads and uses the latest Chrome version when running tests.
16
- We need to specify a browser version that match chromedriver version running in CI/CD environment to
17
- ensure testing accuracy.
18
- TODO: Update this version when chromedriver version in CI/CD is updated */
19
- browserVersion: '120.0.6099.109',
20
15
  'goog:chromeOptions': {
21
16
  debuggerAddress: `${process.env.TV_IP}:9998`
22
- },
23
- 'wdio:enforceWebDriverClassic': true
17
+ }
24
18
  }],
25
19
 
26
20
  baseUrl: `http://${ipAddress()}:4567`,
package/utils/Page.js CHANGED
@@ -16,15 +16,11 @@ export class Page {
16
16
  });
17
17
 
18
18
  this._url = `/${appPath}/${urlExtra}`;
19
- // Can't resize browser window when connected to remote debugger!
20
- if (!browser._options || !browser._options.remote) {
21
- await browser.setWindowSize(1920, 1080);
22
- }
23
19
 
24
20
  await browser.url(this.url);
25
21
 
26
22
  const body = await $('body');
27
- await body.waitForDisplayed({timeout: 5000});
23
+ await body.waitForDisplayed({timeout: 10000});
28
24
 
29
25
  await this.delay(200);
30
26
  }
package/utils/runTest.js CHANGED
@@ -64,6 +64,7 @@ export const runTest = ({concurrency, filter, Page, testName, ...rest}) => {
64
64
 
65
65
  expect(await browser.checkScreen(screenshotFileName, {
66
66
  disableCSSAnimation: true,
67
+ ignoreAntialiasing: true,
67
68
  ignoreNothing: true,
68
69
  rawMisMatchPercentage: true,
69
70
  waitForFontsLoaded: true