@enact/ui-test-utils 4.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/CHANGELOG.md +4 -0
- package/config/wdio.conf.js +47 -24
- package/npm-shrinkwrap.json +1027 -1229
- package/package.json +11 -11
- package/screenshot/utils/confHelpers.js +201 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enact/ui-test-utils",
|
|
3
|
-
"version": "4.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,34 +23,34 @@
|
|
|
23
23
|
"author": "Roy Sutton <roy.sutton@lge.com>",
|
|
24
24
|
"license": "Apache-2.0",
|
|
25
25
|
"engines": {
|
|
26
|
-
"node": "^
|
|
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.
|
|
32
|
+
"@wdio/cli": "^9.21.1",
|
|
33
33
|
"@wdio/dot-reporter": "^9.20.0",
|
|
34
|
-
"@wdio/local-runner": "^9.
|
|
35
|
-
"@wdio/mocha-framework": "^9.
|
|
34
|
+
"@wdio/local-runner": "^9.21.0",
|
|
35
|
+
"@wdio/mocha-framework": "^9.21.0",
|
|
36
36
|
"@wdio/spec-reporter": "^9.20.0",
|
|
37
37
|
"@wdio/static-server-service": "^9.20.0",
|
|
38
|
-
"@wdio/visual-service": "^9.0.
|
|
38
|
+
"@wdio/visual-service": "^9.0.2",
|
|
39
39
|
"chalk": "^5.6.2",
|
|
40
40
|
"cross-spawn": "^7.0.6",
|
|
41
|
-
"expect-webdriverio": "^5.
|
|
41
|
+
"expect-webdriverio": "^5.5.0",
|
|
42
42
|
"fs-extra": "^11.3.2",
|
|
43
43
|
"minimist": "^1.2.8",
|
|
44
44
|
"query-string": "^7.1.3",
|
|
45
45
|
"ramda": "^0.31.3",
|
|
46
46
|
"readdirp": "^3.6.0",
|
|
47
47
|
"wdio-docker-service": "^3.2.1",
|
|
48
|
-
"webdriverio": "^9.
|
|
48
|
+
"webdriverio": "^9.21.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"eslint": "^9.
|
|
52
|
-
"eslint-config-enact": "^5.0.
|
|
53
|
-
"globals": "^16.
|
|
51
|
+
"eslint": "^9.39.1",
|
|
52
|
+
"eslint-config-enact": "^5.0.3",
|
|
53
|
+
"globals": "^16.5.0"
|
|
54
54
|
},
|
|
55
55
|
"overrides": {
|
|
56
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 () {
|