@testim/testim-cli 3.210.0 → 3.214.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.
- package/agent/routers/codim/service.js +24 -9
- package/commons/httpRequest.js +21 -9
- package/commons/httpRequestCounters.js +41 -10
- package/commons/httpRequestCounters.test.js +9 -9
- package/npm-shrinkwrap.json +12 -12
- package/package.json +1 -2
- package/player/seleniumTestPlayer.js +4 -2
- package/player/services/frameLocator.js +106 -102
- package/player/services/tabService.js +5 -3
- package/player/stepActions/inputFileStepAction.js +4 -1
- package/player/stepActions/pixelValidationStepAction.js +8 -1
- package/player/utils/imageCaptureUtils.js +1 -1
- package/player/utils/screenshotUtils.js +6 -6
- package/runners/ParallelWorkerManager.js +3 -0
- package/services/analyticsService.js +4 -2
- package/testRunHandler.js +16 -5
- package/workers/BaseWorker.js +10 -10
- package/workers/WorkerExtension.js +6 -6
- package/workers/WorkerSelenium.js +5 -10
- package/workers/workerUtils.js +0 -36
|
@@ -36,30 +36,45 @@ async function getLocalLocators() {
|
|
|
36
36
|
|
|
37
37
|
async function findTests(folder = process.cwd()) {
|
|
38
38
|
const testFolder = await findTestFolder(folder);
|
|
39
|
-
|
|
40
|
-
const filesWithoutStat = await fs.readdirAsync(testFolder);
|
|
41
|
-
const filesWithStat = await Promise.all(filesWithoutStat.map(name => Promise.props({ stat: fs.statAsync(name).catch(e => {}), name })));
|
|
39
|
+
const filesWithStat = await fs.promises.readdir(testFolder, { withFileTypes: true });
|
|
42
40
|
|
|
43
41
|
// things we know are not tests but end in js
|
|
44
42
|
const excluded = ['webpack.config.js', 'tsconfig.js', '.DS_Store', 'functions.js'];
|
|
45
43
|
const excludedFileTypes = ['.html', '.json'];
|
|
46
|
-
return filesWithStat
|
|
47
|
-
.filter(x =>
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
return filesWithStat
|
|
45
|
+
.filter(x =>
|
|
46
|
+
!excluded.includes(x.name) &&
|
|
47
|
+
!excludedFileTypes.some(type => x.name.endsWith(type)) &&
|
|
48
|
+
x.isFile() &&
|
|
49
|
+
!x.name.startsWith('.'),
|
|
50
|
+
)
|
|
50
51
|
.map(x => x.name);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
/**
|
|
55
|
+
* @param {Record<string, object | Promise<object>>} propsObject
|
|
56
|
+
* @returns {Promise<Record<string, object>>}
|
|
57
|
+
* */
|
|
58
|
+
async function promiseFromProps(propsObject) {
|
|
59
|
+
const entries = Object.entries(propsObject);
|
|
60
|
+
const values = entries.map(([, value]) => value);
|
|
61
|
+
const resolvedValues = await Promise.all(values);
|
|
62
|
+
for (let i = 0; i < resolvedValues.length; i++) {
|
|
63
|
+
entries[i][1] = resolvedValues[i];
|
|
64
|
+
}
|
|
65
|
+
return Object.fromEntries(entries);
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
async function getLocalLocatorContents(locators, full = false, originFolder = process.cwd()) {
|
|
54
69
|
const props = {};
|
|
55
70
|
if (full) {
|
|
56
71
|
const folder = await findTestFolder(originFolder);
|
|
57
72
|
for (const key of Object.values(locators)) {
|
|
58
|
-
props[key] = fs.
|
|
73
|
+
props[key] = fs.promises.readFile(path.join(folder, 'locators', `locator.${key}.json`)).then(JSON.parse);
|
|
59
74
|
}
|
|
60
75
|
}
|
|
61
76
|
try {
|
|
62
|
-
const contents = await
|
|
77
|
+
const contents = await promiseFromProps(props);
|
|
63
78
|
return contents;
|
|
64
79
|
} catch (e) {
|
|
65
80
|
console.error(e);
|
package/commons/httpRequest.js
CHANGED
|
@@ -32,6 +32,11 @@ function getProxy() {
|
|
|
32
32
|
return global.proxyUri;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const logErrorAndRethrow = (logMsg, data) => err => {
|
|
36
|
+
logger.error(logMsg, { ...data, error: err });
|
|
37
|
+
throw err;
|
|
38
|
+
};
|
|
39
|
+
|
|
35
40
|
function deleteMethod(url, headers, timeout) {
|
|
36
41
|
return deleteFullRes(url, headers, timeout)
|
|
37
42
|
.then(res => {
|
|
@@ -40,7 +45,7 @@ function deleteMethod(url, headers, timeout) {
|
|
|
40
45
|
}
|
|
41
46
|
return res.body;
|
|
42
47
|
})
|
|
43
|
-
.
|
|
48
|
+
.catch(logErrorAndRethrow('failed to delete request', { url }));
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
function deleteFullRes(url, headers = {}, timeout = DEFAULT_REQUEST_TIMEOUT) {
|
|
@@ -70,7 +75,7 @@ function post({
|
|
|
70
75
|
}
|
|
71
76
|
return res.body;
|
|
72
77
|
})
|
|
73
|
-
.
|
|
78
|
+
.catch(logErrorAndRethrow('failed to post request', { url }));
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
function postFullRes(url, body, headers = {}, timeout = DEFAULT_REQUEST_TIMEOUT, retry) {
|
|
@@ -128,7 +133,7 @@ function postForm(url, fields, files, headers = {}, timeout = DEFAULT_REQUEST_TI
|
|
|
128
133
|
}
|
|
129
134
|
return res.body;
|
|
130
135
|
})
|
|
131
|
-
.
|
|
136
|
+
.catch(logErrorAndRethrow('failed to post request', { url }));
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
function _get(url, query, headers = {}, timeout = DEFAULT_REQUEST_TIMEOUT, { isBinary = false, skipProxy = false } = {}) {
|
|
@@ -156,13 +161,16 @@ function _get(url, query, headers = {}, timeout = DEFAULT_REQUEST_TIMEOUT, { isB
|
|
|
156
161
|
function getText(url, query, headers) {
|
|
157
162
|
return _get(url, query, headers)
|
|
158
163
|
.then((res) => res.text)
|
|
159
|
-
.
|
|
164
|
+
.catch(logErrorAndRethrow('failed to getText request', { url, query }));
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
function get(url, query, headers, timeout, options) {
|
|
163
168
|
return _get(url, query, headers, timeout, options)
|
|
164
169
|
.then((res) => res.body)
|
|
165
|
-
.
|
|
170
|
+
.catch(err => {
|
|
171
|
+
logger.warn('failed to get request', { url, query, error: err });
|
|
172
|
+
throw err;
|
|
173
|
+
});
|
|
166
174
|
}
|
|
167
175
|
|
|
168
176
|
function getFullRes(url, query, headers, timeout) {
|
|
@@ -183,7 +191,7 @@ function head(url) {
|
|
|
183
191
|
}
|
|
184
192
|
|
|
185
193
|
return Promise.fromCallback((callback) => request.end(callback))
|
|
186
|
-
.
|
|
194
|
+
.catch(logErrorAndRethrow('failed to head request', { url }));
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
function put(url, body, headers = {}, timeout = DEFAULT_REQUEST_TIMEOUT) {
|
|
@@ -203,7 +211,7 @@ function put(url, body, headers = {}, timeout = DEFAULT_REQUEST_TIMEOUT) {
|
|
|
203
211
|
|
|
204
212
|
return Promise.fromCallback((callback) => request.end(callback))
|
|
205
213
|
.then((res) => res.body)
|
|
206
|
-
.
|
|
214
|
+
.catch(logErrorAndRethrow('failed to put request', { url }));
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
function download(url) {
|
|
@@ -224,8 +232,11 @@ function download(url) {
|
|
|
224
232
|
}
|
|
225
233
|
|
|
226
234
|
return Promise.fromCallback((callback) => request.end(callback))
|
|
227
|
-
.
|
|
228
|
-
|
|
235
|
+
.then(response => {
|
|
236
|
+
logger.info('finished to download', { url });
|
|
237
|
+
return response;
|
|
238
|
+
})
|
|
239
|
+
.catch(logErrorAndRethrow('failed to download', { url }));
|
|
229
240
|
}
|
|
230
241
|
|
|
231
242
|
module.exports = {
|
|
@@ -241,4 +252,5 @@ module.exports = {
|
|
|
241
252
|
head: wrapWithMonitoring(head),
|
|
242
253
|
download: wrapWithMonitoring(download),
|
|
243
254
|
isNetworkHealthy: wrapWithMonitoring.isNetworkHealthy,
|
|
255
|
+
didNetworkConnectivityTestFail: wrapWithMonitoring.didNetworkConnectivityTestFail,
|
|
244
256
|
};
|
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
'use strit';
|
|
2
2
|
|
|
3
3
|
const { sum } = require('lodash');
|
|
4
|
-
const
|
|
4
|
+
const Bluebird = require('bluebird');
|
|
5
|
+
const dns = require('dns');
|
|
6
|
+
const _ = require('lodash');
|
|
7
|
+
const config = require('./config');
|
|
8
|
+
|
|
9
|
+
const logger = require('./logger').getLogger('http-request-counters');
|
|
10
|
+
|
|
11
|
+
let networkConnectivityTestFailed = false;
|
|
12
|
+
|
|
13
|
+
/** Tests network connectivity with DNS resolution (a basic test for a 7 later stack parallel but essential to most HTTP requests) */
|
|
14
|
+
const testNetworkConnectivity = async () => {
|
|
15
|
+
if (config.IS_ON_PREM) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const hostnames = ['www.google.com', 'www.facebook.com', 'www.microsoft.com', 'testim.io'];
|
|
19
|
+
try {
|
|
20
|
+
// If any of these domains resolve we consider the connectivity to be ok
|
|
21
|
+
const result = Boolean(await Bluebird.any(hostnames.map(host => dns.promises.lookup(host))));
|
|
22
|
+
if (!result) {
|
|
23
|
+
networkConnectivityTestFailed = true;
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
logger.error('network connectivity test failed');
|
|
28
|
+
networkConnectivityTestFailed = true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const throttledTestNetworkConnectivity = _.throttle(testNetworkConnectivity, 10 * 1000);
|
|
33
|
+
|
|
5
34
|
// we remove entries after 15 minutes, note that this isn't accurate because
|
|
6
35
|
// we remove the "fail"/"success" 10 minutes after we add them (and not the "call")
|
|
7
36
|
// this is fine since these values are an estimation and not an accurate representation
|
|
@@ -24,7 +53,7 @@ module.exports.makeCounters = () => {
|
|
|
24
53
|
}, ttl);
|
|
25
54
|
}
|
|
26
55
|
function wrapWithMonitoring(fn, name = fn.name) {
|
|
27
|
-
return method(async function (...args) {
|
|
56
|
+
return Bluebird.method(async function (...args) {
|
|
28
57
|
update(counters.call, name);
|
|
29
58
|
try {
|
|
30
59
|
const result = await fn.call(this, ...args);
|
|
@@ -32,24 +61,26 @@ module.exports.makeCounters = () => {
|
|
|
32
61
|
return result;
|
|
33
62
|
} catch (e) {
|
|
34
63
|
update(counters.fail, name);
|
|
64
|
+
if (!networkConnectivityTestFailed) {
|
|
65
|
+
throttledTestNetworkConnectivity();
|
|
66
|
+
}
|
|
35
67
|
throw e;
|
|
36
68
|
}
|
|
37
69
|
});
|
|
38
70
|
}
|
|
39
|
-
wrapWithMonitoring.isNetworkHealthy = function isNetworkHealthy() {
|
|
71
|
+
wrapWithMonitoring.isNetworkHealthy = async function isNetworkHealthy() {
|
|
72
|
+
if (networkConnectivityTestFailed || !(await testNetworkConnectivity())) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
40
75
|
const allFailed = sum([...counters.fail.values()]);
|
|
41
|
-
const allSuccess = sum([...counters.success.values()]);
|
|
42
76
|
const allCalls = sum([...counters.call.values()]);
|
|
43
77
|
// we declare a test unhealthy network wise if
|
|
44
|
-
//
|
|
45
|
-
// 2. more than 10% of requests (out of finished requests) failed
|
|
78
|
+
// 10% or more of requests (out of finished requests) failed
|
|
46
79
|
// note that the network can be unhealthy but the test would still pass
|
|
47
|
-
|
|
48
|
-
return allFailed < 10;
|
|
49
|
-
}
|
|
50
|
-
return (allSuccess + allFailed) > allFailed * 10;
|
|
80
|
+
return allFailed < allCalls * 0.1;
|
|
51
81
|
};
|
|
52
82
|
wrapWithMonitoring.counters = counters; // expose the counters used to the outside
|
|
53
83
|
wrapWithMonitoring.isNetworkHealthy.counters = wrapWithMonitoring.counters;
|
|
84
|
+
wrapWithMonitoring.didNetworkConnectivityTestFail = () => networkConnectivityTestFailed;
|
|
54
85
|
return wrapWithMonitoring;
|
|
55
86
|
};
|
|
@@ -9,30 +9,30 @@ describe('the http request counters', () => {
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('Marks an always failing network as unhealthy', async () => {
|
|
12
|
-
const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }),
|
|
12
|
+
const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }), 30, 1);
|
|
13
13
|
await rejects(fn);
|
|
14
|
-
strictEqual(wrapWithMonitoring.isNetworkHealthy(), false);
|
|
14
|
+
strictEqual(await wrapWithMonitoring.isNetworkHealthy(), false);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('Marks an unstable network as unhealthy', async () => {
|
|
18
|
-
const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }),
|
|
18
|
+
const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }), 30, 1);
|
|
19
19
|
const fn2 = runWithRetries(wrapWithMonitoring(() => 'hello'), 20, 1);
|
|
20
20
|
await rejects(fn);
|
|
21
21
|
await doesNotReject(fn2);
|
|
22
|
-
strictEqual(wrapWithMonitoring.isNetworkHealthy(), false);
|
|
22
|
+
strictEqual(await wrapWithMonitoring.isNetworkHealthy(), false);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
it('Marks a
|
|
26
|
-
const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }),
|
|
25
|
+
it('Marks a trivial amount of failed requests as healthy', async () => {
|
|
26
|
+
const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }), 30, 1);
|
|
27
27
|
await rejects(fn);
|
|
28
28
|
const fn2 = wrapWithMonitoring(() => 'hello');
|
|
29
|
-
await Promise.all(Array(
|
|
30
|
-
strictEqual(wrapWithMonitoring.isNetworkHealthy(), true);
|
|
29
|
+
await Promise.all(Array(290).fill().map(fn2));
|
|
30
|
+
strictEqual(await wrapWithMonitoring.isNetworkHealthy(), true);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it('Marks a healthy network as healthy', async () => {
|
|
34
34
|
const fn2 = wrapWithMonitoring(() => 'hello');
|
|
35
35
|
await Promise.all(Array(200).fill().map(fn2));
|
|
36
|
-
strictEqual(wrapWithMonitoring.isNetworkHealthy(), true);
|
|
36
|
+
strictEqual(await wrapWithMonitoring.isNetworkHealthy(), true);
|
|
37
37
|
});
|
|
38
38
|
});
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -2337,9 +2337,9 @@
|
|
|
2337
2337
|
"dev": true
|
|
2338
2338
|
},
|
|
2339
2339
|
"caniuse-lite": {
|
|
2340
|
-
"version": "1.0.
|
|
2341
|
-
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.
|
|
2342
|
-
"integrity": "sha512-
|
|
2340
|
+
"version": "1.0.30001294",
|
|
2341
|
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001294.tgz",
|
|
2342
|
+
"integrity": "sha512-LiMlrs1nSKZ8qkNhpUf5KD0Al1KCBE3zaT7OLOwEkagXMEDij98SiOovn9wxVGQpklk9vVC/pUSqgYmkmKOS8g==",
|
|
2343
2343
|
"dev": true
|
|
2344
2344
|
},
|
|
2345
2345
|
"caseless": {
|
|
@@ -2509,9 +2509,9 @@
|
|
|
2509
2509
|
},
|
|
2510
2510
|
"dependencies": {
|
|
2511
2511
|
"@types/node": {
|
|
2512
|
-
"version": "17.0.
|
|
2513
|
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.
|
|
2514
|
-
"integrity": "sha512-
|
|
2512
|
+
"version": "17.0.5",
|
|
2513
|
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.5.tgz",
|
|
2514
|
+
"integrity": "sha512-w3mrvNXLeDYV1GKTZorGJQivK6XLCoGwpnyJFbJVK/aTBQUxOCaa/GlFAAN3OTDFcb7h5tiFG+YXCO2By+riZw=="
|
|
2515
2515
|
},
|
|
2516
2516
|
"mkdirp": {
|
|
2517
2517
|
"version": "0.5.5",
|
|
@@ -3501,9 +3501,9 @@
|
|
|
3501
3501
|
"integrity": "sha512-GJCAeDBKfREgkBtgrYSf9hQy9kTb3helv0zGdzqhM7iAkW8FA/ZF97VQDbwFiwIT8MQLLOe5VlPZOEvZAqtUAQ=="
|
|
3502
3502
|
},
|
|
3503
3503
|
"electron-to-chromium": {
|
|
3504
|
-
"version": "1.4.
|
|
3505
|
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.
|
|
3506
|
-
"integrity": "sha512-
|
|
3504
|
+
"version": "1.4.29",
|
|
3505
|
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.29.tgz",
|
|
3506
|
+
"integrity": "sha512-N2Jbwxo5Rum8G2YXeUxycs1sv4Qme/ry71HG73bv8BvZl+I/4JtRgK/En+ST/Wh/yF1fqvVCY4jZBgMxnhjtBA==",
|
|
3507
3507
|
"dev": true
|
|
3508
3508
|
},
|
|
3509
3509
|
"emoji-regex": {
|
|
@@ -5860,9 +5860,9 @@
|
|
|
5860
5860
|
},
|
|
5861
5861
|
"dependencies": {
|
|
5862
5862
|
"acorn": {
|
|
5863
|
-
"version": "8.
|
|
5864
|
-
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.
|
|
5865
|
-
"integrity": "sha512-
|
|
5863
|
+
"version": "8.7.0",
|
|
5864
|
+
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
|
5865
|
+
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ=="
|
|
5866
5866
|
},
|
|
5867
5867
|
"acorn-globals": {
|
|
5868
5868
|
"version": "6.0.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testim/testim-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.214.0",
|
|
4
4
|
"description": "Command line interface for running Testing on your CI",
|
|
5
5
|
"author": "Oren Rubin",
|
|
6
6
|
"contributors": [{
|
|
@@ -115,7 +115,6 @@
|
|
|
115
115
|
"testim": "cli.js"
|
|
116
116
|
},
|
|
117
117
|
"scripts": {
|
|
118
|
-
"postpublish": "scripts/publish-docker.js",
|
|
119
118
|
"test": "IS_UNIT_TEST=1 ./node_modules/mocha/bin/_mocha --timeout 2000 --reporter spec --exit --recursive \"./src/**/*.test.js\" --exclude ./src/codim/template.js/tests/examples/**/*.test.js",
|
|
120
119
|
"test:watch": "IS_UNIT_TEST=1 ./node_modules/mocha/bin/_mocha --timeout 2000 --exit --recursive \"./src/**/*.test.js\" --exclude ./src/codim/template.js/tests/examples/**/*.test.js --watch",
|
|
121
120
|
"test:cov": "nyc --reporter=lcov --reporter=text yarn test",
|
|
@@ -4,7 +4,7 @@ const TabService = require('./services/tabService');
|
|
|
4
4
|
const PortSelector = require('./services/portSelector');
|
|
5
5
|
const windowCreationListener = require('./services/windowCreationListener');
|
|
6
6
|
const CookieUtils = require('./utils/cookieUtils');
|
|
7
|
-
const
|
|
7
|
+
const frameLocatorFactory = require('./services/frameLocator');
|
|
8
8
|
const Promise = require('bluebird');
|
|
9
9
|
const { isDebuggerConnected } = require('../commons/detectDebugger');
|
|
10
10
|
|
|
@@ -45,12 +45,14 @@ class SeleniumTestPlayer {
|
|
|
45
45
|
|
|
46
46
|
this.tabService.createSesion(id);
|
|
47
47
|
|
|
48
|
+
const FrameLocator = frameLocatorFactory(this.driver);
|
|
49
|
+
|
|
48
50
|
this.sessionPlayer = new player(
|
|
49
51
|
id,
|
|
50
52
|
this.tabService,
|
|
51
53
|
CookieUtils(this.driver),
|
|
52
54
|
windowCreationListener,
|
|
53
|
-
FrameLocator
|
|
55
|
+
FrameLocator,
|
|
54
56
|
PortSelector,
|
|
55
57
|
null,
|
|
56
58
|
null /* Not in use, placeholder for the order of arguments */,
|
|
@@ -8,123 +8,127 @@ const featureFlags = require('../../commons/featureFlags');
|
|
|
8
8
|
const SELENIUM_ELEMENT_KEY = 'ELEMENT';
|
|
9
9
|
const SELENIUM_GUID_KEY = 'element-6066-11e4-a52e-4f735466cecf';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this._cache = {};
|
|
16
|
-
};
|
|
11
|
+
const _getGuidFromSeleniumElement = (seleniumElement) => {
|
|
12
|
+
if (!seleniumElement) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
16
|
+
return seleniumElement[SELENIUM_ELEMENT_KEY] || seleniumElement[SELENIUM_GUID_KEY];
|
|
17
|
+
};
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
/** @param {import('../webdriver')} driver*/
|
|
20
|
+
module.exports = function frameLocatorFactory(driver) {
|
|
21
|
+
class FrameLocator {
|
|
22
|
+
constructor(frameManager, locateElementPlayer) {
|
|
23
|
+
this.frameManager = frameManager;
|
|
24
|
+
this.locateElementPlayer = locateElementPlayer;
|
|
25
|
+
this._cache = {};
|
|
26
|
+
}
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
cacheResults(seleniumGuid, resultsUrl) {
|
|
29
|
+
this._cache[seleniumGuid] = resultsUrl;
|
|
30
|
+
}
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
getResultsFromCache(seleniumGuid) {
|
|
33
|
+
return this._cache[seleniumGuid];
|
|
34
|
+
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
cacheFrameLocateResults(frameHandler) {
|
|
37
|
+
if (frameHandler && frameHandler.seleniumFrameElement && frameHandler.frameLocateResultUrl) {
|
|
38
|
+
const guid = _getGuidFromSeleniumElement(frameHandler.seleniumFrameElement);
|
|
39
|
+
if (guid) {
|
|
40
|
+
this.cacheResults(guid, frameHandler.frameLocateResultUrl);
|
|
41
|
+
}
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
|
-
};
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
foundFrameCallback(result, frameTree, testimFrameId) {
|
|
46
|
+
const { frameOffset, locatedElement } = result;
|
|
47
|
+
if (locatorBuilderUtils.isEmptyResult(locatedElement)) {
|
|
48
|
+
logger.error('got empty result in frame result, not rejected from locate element player');
|
|
49
|
+
return Promise.reject();
|
|
50
|
+
}
|
|
51
|
+
return driver.switchToLocatedFrame(locatedElement)
|
|
52
|
+
.then(el => {
|
|
53
|
+
const guid = _getGuidFromSeleniumElement(el.value);
|
|
54
|
+
const frameLocateResultUrl = this.getResultsFromCache(guid);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
frameId: -1,
|
|
58
|
+
frameOffset,
|
|
59
|
+
tabInfo: frameTree.tabInfo,
|
|
60
|
+
tabId: frameTree.tabId,
|
|
61
|
+
testimFrameId,
|
|
62
|
+
testimFullFrameId: `${this.currentFrameHandler.testimFullFrameId}-${testimFrameId}`,
|
|
63
|
+
seleniumFrameElement: el.value,
|
|
64
|
+
frameLocateResultUrl,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
48
67
|
}
|
|
49
|
-
return driver.switchToLocatedFrame(locatedElement)
|
|
50
|
-
.then(el => {
|
|
51
|
-
const guid = _getGuidFromSeleniumElement(el.value);
|
|
52
|
-
const frameLocateResultUrl = this.getResultsFromCache(guid);
|
|
53
68
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
locate(frameLocator, frameDepth, currentFrame, context, frameTree, stepData) {
|
|
70
|
+
// eslint-disable-next-line new-cap
|
|
71
|
+
const locateElementPlayer = new this.locateElementPlayer(context);
|
|
72
|
+
frameLocator.targetId = `frameLocator_${frameDepth}`;
|
|
73
|
+
return locateElementPlayer.locate(frameLocator, currentFrame, frameLocator.targetId)
|
|
74
|
+
.then(result => {
|
|
75
|
+
result.isVisible = true; // frame visibility check is done on the target element
|
|
76
|
+
return locateElementPlayer.handleLocateResult(result, stepData, frameLocator)
|
|
77
|
+
.catch(() => { throw new Error(); }); // silence [object object] errors;
|
|
78
|
+
})
|
|
79
|
+
.then(result => {
|
|
80
|
+
const { locatedElement } = context.data[frameLocator.targetId];
|
|
81
|
+
return driver.getElementLocationWithPadding(locatedElement)
|
|
82
|
+
.then(location => {
|
|
83
|
+
const value = location.value || { top: 0, left: 0 };
|
|
84
|
+
result.frameOffset = {
|
|
85
|
+
top: currentFrame.frameOffset.top + value.top,
|
|
86
|
+
left: currentFrame.frameOffset.left + value.left,
|
|
87
|
+
};
|
|
88
|
+
return result;
|
|
89
|
+
});
|
|
90
|
+
})
|
|
91
|
+
.then(result => {
|
|
92
|
+
if (locateElementPlayer.addFrameDataToContext) {
|
|
93
|
+
locateElementPlayer.addFrameDataToContext(result.targetId, result.locateResult);
|
|
94
|
+
}
|
|
95
|
+
return this.foundFrameCallback(result, frameTree, frameLocator.testimFrameId);
|
|
96
|
+
})
|
|
97
|
+
.then(frameHandler => {
|
|
98
|
+
this.currentFrameHandler = frameHandler;
|
|
99
|
+
return frameHandler;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
66
102
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const { locatedElement } = context.data[frameLocator.targetId];
|
|
79
|
-
return driver.getElementLocationWithPadding(locatedElement)
|
|
80
|
-
.then(location => {
|
|
81
|
-
const value = location.value || { top: 0, left: 0 };
|
|
82
|
-
result.frameOffset = {
|
|
83
|
-
top: currentFrame.frameOffset.top + value.top,
|
|
84
|
-
left: currentFrame.frameOffset.left + value.left,
|
|
85
|
-
};
|
|
86
|
-
return result;
|
|
87
|
-
});
|
|
88
|
-
})
|
|
89
|
-
.then(result => {
|
|
90
|
-
if (locateElementPlayer.addFrameDataToContext) {
|
|
91
|
-
locateElementPlayer.addFrameDataToContext(result.targetId, result.locateResult);
|
|
103
|
+
findFrame(stepData, frameLocators, context, frameTree) {
|
|
104
|
+
const allowNoFrameSwitch = featureFlags.flags.enableFrameSwitchOptimization.isEnabled();
|
|
105
|
+
const chronologicalResults = context.playback.resultsHandler.resultsByChronologicOrder;
|
|
106
|
+
const lastResult = chronologicalResults[chronologicalResults.length - 1];
|
|
107
|
+
const allowedRetries = 1;
|
|
108
|
+
const moreThanAllowedRetries = Boolean(lastResult) && lastResult.stepId === stepData.id && lastResult.results.length > allowedRetries;
|
|
109
|
+
if (allowNoFrameSwitch && !moreThanAllowedRetries && this.currentFrameHandler) {
|
|
110
|
+
const currentFramePos = frameLocators.findIndex(x => x.testimFrameId === this.currentFrameHandler.testimFrameId);
|
|
111
|
+
if (currentFramePos > -1) {
|
|
112
|
+
const shorterPath = frameLocators.slice(currentFramePos + 1);
|
|
113
|
+
return Promise.reduce(shorterPath, (currentFrame, frameLocator, index) => this.locate(frameLocator, index, currentFrame, context, frameTree, stepData), this.currentFrameHandler);
|
|
92
114
|
}
|
|
93
|
-
return this.foundFrameCallback(result, frameTree, frameLocator.testimFrameId);
|
|
94
|
-
})
|
|
95
|
-
.tap(frameHandler => {
|
|
96
|
-
this.currentFrameHandler = frameHandler;
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
FrameLocator.prototype.findFrame = function (stepData, frameLocators, context, frameTree) {
|
|
101
|
-
const allowNoFrameSwitch = featureFlags.flags.enableFrameSwitchOptimization.isEnabled();
|
|
102
|
-
const chronologicalResults = context.playback.resultsHandler.resultsByChronologicOrder;
|
|
103
|
-
const lastResult = chronologicalResults[chronologicalResults.length - 1];
|
|
104
|
-
const allowedRetries = 1;
|
|
105
|
-
const moreThanAllowedRetries = Boolean(lastResult) && lastResult.stepId === stepData.id && lastResult.results.length > allowedRetries;
|
|
106
|
-
if (allowNoFrameSwitch && !moreThanAllowedRetries && this.currentFrameHandler) {
|
|
107
|
-
const currentFramePos = frameLocators.findIndex(x => x.testimFrameId === this.currentFrameHandler.testimFrameId);
|
|
108
|
-
if (currentFramePos > -1) {
|
|
109
|
-
const shorterPath = frameLocators.slice(currentFramePos + 1);
|
|
110
|
-
return Promise.reduce(shorterPath, (currentFrame, frameLocator, index) => this.locate(frameLocator, index, currentFrame, context, frameTree, stepData), this.currentFrameHandler);
|
|
111
115
|
}
|
|
112
|
-
}
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
return frameTree.getTopFrameHandler()
|
|
118
|
+
.then(topFrameHandler => {
|
|
119
|
+
topFrameHandler.frameOffset = { top: 0, left: 0 };
|
|
120
|
+
const switchToTop = (allowNoFrameSwitch && this.currentFrameHandler === topFrameHandler) ?
|
|
121
|
+
Promise.resolve(this.currentFrameHandler) :
|
|
122
|
+
driver.switchToTopFrame();
|
|
123
|
+
return switchToTop.then(() => {
|
|
124
|
+
this.cacheFrameLocateResults(this.currentFrameHandler);
|
|
125
|
+
this.currentFrameHandler = topFrameHandler;
|
|
126
|
+
return Promise.reduce(frameLocators, (currentFrame, frameLocator, index) =>
|
|
127
|
+
this.locate(frameLocator, index, currentFrame, context, frameTree, stepData), topFrameHandler);
|
|
128
|
+
});
|
|
125
129
|
});
|
|
126
|
-
|
|
127
|
-
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
128
132
|
|
|
129
133
|
return FrameLocator;
|
|
130
134
|
};
|
|
@@ -350,14 +350,16 @@ class TabService {
|
|
|
350
350
|
mainTabPromise = this.driver.executeJS('return window.__isMainTestimTab').get('value');
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
return Promise.
|
|
354
|
-
|
|
353
|
+
return Promise.all([this.driver.getTitle(), this.driver.getUrl(), mainTabPromise]).then(
|
|
354
|
+
([title, url, isMainTab]) => ({ title, url, isMainTab }),
|
|
355
|
+
err => {
|
|
355
356
|
logger.error('failed to get url or title', { err });
|
|
356
357
|
return {
|
|
357
358
|
title: '',
|
|
358
359
|
url: '',
|
|
359
360
|
};
|
|
360
|
-
}
|
|
361
|
+
},
|
|
362
|
+
);
|
|
361
363
|
})
|
|
362
364
|
.catch(err => {
|
|
363
365
|
logger.error('failed to switch to tab', { tabId, err });
|
|
@@ -34,7 +34,10 @@ class InputFileStepAction extends StepAction {
|
|
|
34
34
|
top: '10px',
|
|
35
35
|
};
|
|
36
36
|
return this.forceInputToBeVisible(target, options)
|
|
37
|
-
.
|
|
37
|
+
.catch(err => {
|
|
38
|
+
logger.error('failed to set input file in Safari recovery', { err });
|
|
39
|
+
throw err;
|
|
40
|
+
});
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
uploadFilesAndForceVisibility(gridLocalFiles, target) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const _ = require('lodash');
|
|
3
4
|
const StepAction = require('./stepAction');
|
|
4
5
|
const { eyeSdkService } = require('../utils/eyeSdkService');
|
|
5
6
|
const logger = require('../../commons/logger').getLogger('pixel-validation-step-action');
|
|
@@ -9,6 +10,7 @@ class PixelValidationStepAction extends StepAction {
|
|
|
9
10
|
async performAction() {
|
|
10
11
|
const { shouldUseVisualGrid, applitoolsSdkConfig: config, testResultId } = this.context;
|
|
11
12
|
this.runContext = this.context.getRunContext(undefined);
|
|
13
|
+
const finalParams = (this.runContext.incomingParams && this.runContext.incomingParams.final) || {};
|
|
12
14
|
const batchId = (config.batch && config.batch.id) || testResultId;
|
|
13
15
|
const eyeManager = await eyeSdkService.getManager(shouldUseVisualGrid, this.context.config.applitoolsConcurrency || 5, batchId, this.runContext.applitoolsIntegrationData);
|
|
14
16
|
const targetElementData = this.getTarget() || {};
|
|
@@ -16,7 +18,12 @@ class PixelValidationStepAction extends StepAction {
|
|
|
16
18
|
try {
|
|
17
19
|
const openedEye = await eyeManager.openEyes({ driver: this.driver.client, config });
|
|
18
20
|
const region = (this.step.action === 'element' && targetElementData.seleniumElement) || undefined;
|
|
19
|
-
|
|
21
|
+
const settings = { region, fully: this.step.action === 'stitched' };
|
|
22
|
+
if (finalParams.applitoolsStepSettings && _.isPlainObject(finalParams.applitoolsStepSettings)) {
|
|
23
|
+
Object.assign(settings, finalParams.applitoolsStepSettings);
|
|
24
|
+
logger.info('Applitools SDK step executed with applitoolsStepSettings parameter', { applitoolsStepSettings: finalParams.applitoolsStepSettings });
|
|
25
|
+
}
|
|
26
|
+
await openedEye.check({ settings });
|
|
20
27
|
const eyesResults = await openedEye.close();
|
|
21
28
|
|
|
22
29
|
result = { isApplitoolsSdkResult: true, success: true, eyesResults };
|
|
@@ -290,7 +290,7 @@ ImageCaptureUtils.prototype = {
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
return Promise.all([windowUtil.getFullPageSize(), windowUtil.getViewportSize()])
|
|
293
|
-
.
|
|
293
|
+
.then(([fullPageSize, viewPortSize]) => createStitchImage(fullPageSize, viewPortSize));
|
|
294
294
|
},
|
|
295
295
|
};
|
|
296
296
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
3
|
const Promise = require('bluebird');
|
|
4
4
|
const utils = require('../../utils');
|
|
5
5
|
|
|
6
6
|
class ScreenshotUtils {
|
|
7
|
-
constructor(tabId, driver, options = { takeScreenshots: true}){
|
|
7
|
+
constructor(tabId, driver, options = { takeScreenshots: true }) {
|
|
8
8
|
this.tabId = tabId;
|
|
9
9
|
this.driver = driver;
|
|
10
10
|
this.options = options;
|
|
@@ -30,12 +30,12 @@ class ScreenshotUtils {
|
|
|
30
30
|
const devicePixelRatioPromise = this.currentDevicePixelRatio ? Promise.resolve(this.currentDevicePixelRatio) : this.getDevicePixelRatio();
|
|
31
31
|
const getScreenshot = () => Promise.all([devicePixelRatioPromise, this.driver.takeScreenshot()]);
|
|
32
32
|
return utils.runWithRetries(getScreenshot, MAX_RETRY_COUNT, SCREENSHOT_RETRY_DELAY)
|
|
33
|
-
.
|
|
33
|
+
.then(([devicePixelRatio, image]) => {
|
|
34
34
|
const base64 = image ? image.value : '';
|
|
35
|
-
const dataUrl =
|
|
35
|
+
const dataUrl = `data:image/png;base64,${this.base64AddPadding(base64.replace(/[\r\n]/g, ''))}`;
|
|
36
36
|
return {
|
|
37
37
|
image: dataUrl,
|
|
38
|
-
devicePixelRatio
|
|
38
|
+
devicePixelRatio,
|
|
39
39
|
};
|
|
40
40
|
});
|
|
41
41
|
}
|
|
@@ -52,7 +52,7 @@ class ScreenshotUtils {
|
|
|
52
52
|
return this.driver.executeJS(devicePixelRatioJS).then(result => Promise.resolve(result.value));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
forcePixelRatio(forceRatio = 1){
|
|
55
|
+
forcePixelRatio(forceRatio = 1) {
|
|
56
56
|
this.currentDevicePixelRatio = forceRatio;
|
|
57
57
|
return Promise.resolve();
|
|
58
58
|
}
|
|
@@ -63,6 +63,7 @@ class ParallelWorkerManager {
|
|
|
63
63
|
const source = options.source || 'cli';
|
|
64
64
|
const user = options.user;
|
|
65
65
|
const companyPlan = options.company && options.company.planType;
|
|
66
|
+
const isStartUp = options.company && options.company.isStartUp;
|
|
66
67
|
const projectName = options.projectData && options.projectData.name;
|
|
67
68
|
const lightweightMode = options.lightweightMode;
|
|
68
69
|
const sessionType = utils.getSessionType(options);
|
|
@@ -82,6 +83,7 @@ class ParallelWorkerManager {
|
|
|
82
83
|
source,
|
|
83
84
|
user,
|
|
84
85
|
lightweightMode,
|
|
86
|
+
isStartUp,
|
|
85
87
|
});
|
|
86
88
|
return testStatus.testStartAndReport(wid, executionId, resultId, isRerun, testRetryKey);
|
|
87
89
|
};
|
|
@@ -146,6 +148,7 @@ class ParallelWorkerManager {
|
|
|
146
148
|
user,
|
|
147
149
|
lightweightMode,
|
|
148
150
|
logger,
|
|
151
|
+
isStartUp,
|
|
149
152
|
});
|
|
150
153
|
if (stopOnError && !testResult.success) {
|
|
151
154
|
reject(new StopRunOnError());
|
|
@@ -31,7 +31,7 @@ function setLightweightAnalytics(properties, lightweightMode) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function analyticsTestStart({
|
|
34
|
-
executionId, projectId, testId, resultId, companyId, companyName, projectName, companyPlan, sessionType, source, user, lightweightMode,
|
|
34
|
+
executionId, projectId, testId, resultId, companyId, companyName, projectName, companyPlan, sessionType, source, user, lightweightMode, isStartUp,
|
|
35
35
|
}) {
|
|
36
36
|
const properties = setLightweightAnalytics({
|
|
37
37
|
executionId,
|
|
@@ -44,13 +44,14 @@ function analyticsTestStart({
|
|
|
44
44
|
companyPlan,
|
|
45
45
|
sessionType,
|
|
46
46
|
source: calcSource(source, user),
|
|
47
|
+
isStartUp,
|
|
47
48
|
}, lightweightMode);
|
|
48
49
|
analytics.trackWithCIUser('test-run-ci', properties);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
function analyticsTestEnd({
|
|
52
53
|
executionId, projectId, testId, resultId, result, companyId, companyName, projectName, companyPlan, sessionType, source, user, lightweightMode,
|
|
53
|
-
logger,
|
|
54
|
+
logger, isStartUp,
|
|
54
55
|
}) {
|
|
55
56
|
try {
|
|
56
57
|
const properties = setLightweightAnalytics({
|
|
@@ -65,6 +66,7 @@ function analyticsTestEnd({
|
|
|
65
66
|
sessionType,
|
|
66
67
|
mockNetworkEnabled: result.wasMockNetworkActivated,
|
|
67
68
|
source: calcSource(source, user),
|
|
69
|
+
isStartUp,
|
|
68
70
|
}, lightweightMode);
|
|
69
71
|
|
|
70
72
|
if (result.success) {
|
package/testRunHandler.js
CHANGED
|
@@ -18,6 +18,8 @@ const { SeleniumPerfStats } = require('./commons/SeleniumPerfStats');
|
|
|
18
18
|
const { preloadTests } = require('./commons/preloadTests');
|
|
19
19
|
|
|
20
20
|
const RETRIES_ON_TIMEOUT = 3;
|
|
21
|
+
const MAX_LIGHTWEIGHT_MODE_RUN_DATA_SIZE = 20 * 1000; // max size, in characters, of stringified run data sent over URL params. Chosen arbitrarily, this value should be changed according to data.
|
|
22
|
+
const canSendRunDataOverUrl = (runData) => JSON.stringify(runData).length < MAX_LIGHTWEIGHT_MODE_RUN_DATA_SIZE;
|
|
21
23
|
|
|
22
24
|
const TestRun = function (executionId, executionName, test, options, branchToUse, testRunStatus) {
|
|
23
25
|
this._executionId = executionId;
|
|
@@ -175,7 +177,14 @@ TestRun.prototype.getRunRequestParams = async function () {
|
|
|
175
177
|
|
|
176
178
|
if (this._options.lightweightMode && this._options.lightweightMode.general) {
|
|
177
179
|
runRequestParams.company = this._options.company;
|
|
178
|
-
|
|
180
|
+
const runData = this.getRunData();
|
|
181
|
+
runRequestParams.lightweightMode.isRunDataSentInUrl = canSendRunDataOverUrl(runData);
|
|
182
|
+
if (runRequestParams.lightweightMode.isRunDataSentInUrl) {
|
|
183
|
+
runRequestParams.runData = runData;
|
|
184
|
+
logger.info(`Run data sent as URL param, test id: ${this.getTestId()} run data length: ${JSON.stringify(runData).length}`);
|
|
185
|
+
} else {
|
|
186
|
+
logger.warn(`Run data is too big to be sent as a URL param. Test id: ${this.getTestId()}, run data size: ${JSON.stringify(runData).length} (limit: ${MAX_LIGHTWEIGHT_MODE_RUN_DATA_SIZE} characters)`);
|
|
187
|
+
}
|
|
179
188
|
runRequestParams.isLocalRun = Boolean(this._options.useLocalChromeDriver || this._options.useChromeLauncher);
|
|
180
189
|
}
|
|
181
190
|
|
|
@@ -195,9 +204,11 @@ TestRun.prototype.getRunRequestParams = async function () {
|
|
|
195
204
|
return runRequestParams;
|
|
196
205
|
};
|
|
197
206
|
|
|
198
|
-
TestRun.prototype.getRunTestUrl = function () {
|
|
199
|
-
|
|
200
|
-
|
|
207
|
+
TestRun.prototype.getRunTestUrl = async function () {
|
|
208
|
+
const runRequestParams = await this.getRunRequestParams();
|
|
209
|
+
const url = `https://run.testim.io/?params=${encodeURIComponent(JSON.stringify(runRequestParams))}`;
|
|
210
|
+
logger.info(`Test (${this.getTestId()}) run URL length: ${url.length}`);
|
|
211
|
+
return url;
|
|
201
212
|
};
|
|
202
213
|
|
|
203
214
|
TestRun.prototype.setSessionId = function (sessionId) {
|
|
@@ -240,7 +251,7 @@ TestRun.prototype.clearTestResult = function () {
|
|
|
240
251
|
const mustClearPreviousStepResults = !this.isAllowReportTestResultRetries() && (this._timeoutRetryCount > 1 || this._retryCount > 1);
|
|
241
252
|
|
|
242
253
|
if (this._options.lightweightMode && this._options.lightweightMode.disableResults &&
|
|
243
|
-
|
|
254
|
+
!mustClearPreviousStepResults && canSendRunDataOverUrl(runData)) {
|
|
244
255
|
return Promise.resolve();
|
|
245
256
|
}
|
|
246
257
|
|
package/workers/BaseWorker.js
CHANGED
|
@@ -8,7 +8,7 @@ const { timeoutMessages, testRunStatus, stepResult, runnerTestStatus } = require
|
|
|
8
8
|
const logger = require('../commons/logger').getLogger('base-worker');
|
|
9
9
|
const testResultService = require('../commons/socket/testResultService');
|
|
10
10
|
const remoteStepService = require('../commons/socket/remoteStepService');
|
|
11
|
-
const { isNetworkHealthy } = require('../commons/httpRequest');
|
|
11
|
+
const { isNetworkHealthy, didNetworkConnectivityTestFail } = require('../commons/httpRequest');
|
|
12
12
|
const testimServicesApi = require('../commons/testimServicesApi');
|
|
13
13
|
const gridService = require('../services/gridService');
|
|
14
14
|
const LambdatestService = require('../services/lambdatestService');
|
|
@@ -264,13 +264,14 @@ class BaseWorker {
|
|
|
264
264
|
return undefined;
|
|
265
265
|
}
|
|
266
266
|
};
|
|
267
|
+
const getNetworkErrorMessage = () => 'Due to network connectivity issues, Testim CLI has been unable to connect to the grid.\n' +
|
|
268
|
+
`Please make sure the CLI has stable access to the internet. ${didNetworkConnectivityTestFail() ? '(Internal: network connectivity test failed)' : ''}`;
|
|
267
269
|
|
|
268
|
-
const buildError = (err) => {
|
|
269
|
-
if (!
|
|
270
|
+
const buildError = (err, wasNetworkHealthy) => {
|
|
271
|
+
if (!wasNetworkHealthy && featureFlags.flags.errorMessageOnBadNetwork.isEnabled()) {
|
|
270
272
|
return {
|
|
271
273
|
errorType: NETWORK_ERROR,
|
|
272
|
-
reason:
|
|
273
|
-
'Please make sure the CLI has stable access to the internet.',
|
|
274
|
+
reason: getNetworkErrorMessage(),
|
|
274
275
|
};
|
|
275
276
|
}
|
|
276
277
|
|
|
@@ -324,17 +325,16 @@ class BaseWorker {
|
|
|
324
325
|
};
|
|
325
326
|
|
|
326
327
|
const onRunError = async (err, testRunHandler) => {
|
|
327
|
-
|
|
328
|
+
const wasNetworkHealthy = await isNetworkHealthy();
|
|
329
|
+
if (!wasNetworkHealthy && featureFlags.flags.warnOnBadNetwork.isEnabled()) {
|
|
328
330
|
// intentional, we want to log to stderr:
|
|
329
331
|
// eslint-disable-next-line no-console
|
|
330
|
-
console.warn(
|
|
331
|
-
// eslint-disable-next-line no-console
|
|
332
|
-
console.warn('Please make sure the CLI has stable access to the internet.');
|
|
332
|
+
console.warn(getNetworkErrorMessage());
|
|
333
333
|
}
|
|
334
334
|
logger.warn('error on run', { err });
|
|
335
335
|
|
|
336
336
|
const projectId = this.userData && this.userData.projectId;
|
|
337
|
-
const { errorType, reason } = buildError(err);
|
|
337
|
+
const { errorType, reason } = buildError(err, wasNetworkHealthy);
|
|
338
338
|
testimServicesApi.updateTestResult(projectId, this.testResultId, this.testId, {
|
|
339
339
|
status: testRunStatus.COMPLETED,
|
|
340
340
|
success: false,
|
|
@@ -87,7 +87,10 @@ class WorkerExtension extends BaseWorker {
|
|
|
87
87
|
.then(url => {
|
|
88
88
|
reporter.onWaitToTestStart(this.id);
|
|
89
89
|
return Promise.all([
|
|
90
|
-
driver.url(url).tap(() => { startStausDetails.driverUrlFinished = true; }).
|
|
90
|
+
driver.url(url).tap(() => { startStausDetails.driverUrlFinished = true; }).catch(err => {
|
|
91
|
+
logger.error('error from driver.url', { err, testResultId, executionId, testId, url, urlLength: url.length });
|
|
92
|
+
throw err;
|
|
93
|
+
}),
|
|
91
94
|
testRunHandler.onStarted(TEST_START_TIMEOUT_MS).tap(() => { startStausDetails.testRunHandlerStartedFinished = true; }),
|
|
92
95
|
])
|
|
93
96
|
.timeout(TEST_START_TIMEOUT_MS, timeoutMessages.TEST_START_TIMEOUT_MSG)
|
|
@@ -107,14 +110,11 @@ class WorkerExtension extends BaseWorker {
|
|
|
107
110
|
};
|
|
108
111
|
driver.registerToClosedBrowser(onBrowserClosed);
|
|
109
112
|
return testRunHandler.onCompleted().timeout(this.testRunTimeout, timeoutMessages.TEST_COMPLETE_TIMEOUT_MSG)
|
|
110
|
-
.
|
|
113
|
+
.then(async testResult => {
|
|
111
114
|
driver.unregisterToClosedBrowser(onBrowserClosed);
|
|
112
115
|
if (this.lambdatestService.isLambdatestRun()) {
|
|
113
|
-
|
|
116
|
+
await driver.executeJS(`lambda-status=${!testResult.success ? 'failed' : 'passed'}`).catch(() => { });
|
|
114
117
|
}
|
|
115
|
-
return undefined;
|
|
116
|
-
})
|
|
117
|
-
.then(testResult => {
|
|
118
118
|
if (!driver.isAlive()) {
|
|
119
119
|
logger.warn(`possible grid unresponsive for test ${this.testId}, result ${this.testResultId} (execution: ${this.executionId})`);
|
|
120
120
|
testResult.gridIssues = 'could not validate grid is alive';
|
|
@@ -168,13 +168,10 @@ class WorkerSelenium extends BaseWorker {
|
|
|
168
168
|
}
|
|
169
169
|
throw err;
|
|
170
170
|
})
|
|
171
|
-
.
|
|
171
|
+
.then(async testResult => {
|
|
172
172
|
if (sessionPlayerInit.localAssetService) {
|
|
173
|
-
|
|
173
|
+
await sessionPlayerInit.localAssetService.drain();
|
|
174
174
|
}
|
|
175
|
-
return undefined;
|
|
176
|
-
})
|
|
177
|
-
.then(testResult => {
|
|
178
175
|
testResult.stepsResults = null;
|
|
179
176
|
testResult.resultId = this.testResultId;
|
|
180
177
|
if (!driver.isAlive()) {
|
|
@@ -187,13 +184,11 @@ class WorkerSelenium extends BaseWorker {
|
|
|
187
184
|
logger.warn(`possible browser keep alive issue ${this.testId}, result ${this.testResultId} (execution: ${this.executionId})`);
|
|
188
185
|
testResult.keepAliveIssue = maxKeepAliveGap;
|
|
189
186
|
}
|
|
190
|
-
|
|
191
|
-
})
|
|
192
|
-
.tap(testResult => {
|
|
187
|
+
const resultWithStats = { ...testResult, ...testRunHandler.seleniumPerfStats.getStats() };
|
|
193
188
|
if (this.lambdatestService.isLambdatestRun()) {
|
|
194
|
-
|
|
189
|
+
await driver.executeJS(`lambda-status=${!resultWithStats.success ? 'failed' : 'passed'}`).catch(() => { });
|
|
195
190
|
}
|
|
196
|
-
return
|
|
191
|
+
return resultWithStats;
|
|
197
192
|
});
|
|
198
193
|
}
|
|
199
194
|
|
package/workers/workerUtils.js
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
const Promise = require('bluebird');
|
|
2
|
-
const utils = require('../utils');
|
|
3
2
|
const gridService = require('../services/gridService');
|
|
4
3
|
const logger = require('../commons/logger').getLogger('worker-utils');
|
|
5
|
-
const { timeoutMessages } = require('../commons/constants');
|
|
6
|
-
const { GetBrowserError, PageNotAvailableError } = require('../errors');
|
|
7
|
-
|
|
8
|
-
const waitUntilBrowserTimeout = (err, startTime, interval, projectId, workerId, player, releaseSlotOnTestFinished) => {
|
|
9
|
-
logger.warn('failed getting browser from grid', { err });
|
|
10
|
-
return releasePlayer(workerId, releaseSlotOnTestFinished, projectId, player)
|
|
11
|
-
.then(() => {
|
|
12
|
-
const requestTime = Date.now() - startTime;
|
|
13
|
-
const timeDiff = interval - requestTime;
|
|
14
|
-
return Promise.delay(timeDiff).then(() => Promise.reject(err));
|
|
15
|
-
});
|
|
16
|
-
};
|
|
17
4
|
|
|
18
5
|
const releaseGridSlot = (workerId, releaseSlotOnTestFinished, projectId) => {
|
|
19
6
|
if (!releaseSlotOnTestFinished) {
|
|
@@ -29,26 +16,3 @@ const releasePlayer = (workerId, releaseSlotOnTestFinished, projectId, player) =
|
|
|
29
16
|
};
|
|
30
17
|
|
|
31
18
|
module.exports.releasePlayer = releasePlayer;
|
|
32
|
-
|
|
33
|
-
module.exports.getBrowserWithRetries = ({ getBrowserOnce, testPlayerFactory, releaseSlotOnTestFinished }, { totalTimeoutDuration, singleGetBrowserDuration, projectId, workerId, reporter }) => {
|
|
34
|
-
const maxGetBrowserAttempts = totalTimeoutDuration / singleGetBrowserDuration;
|
|
35
|
-
let failedAttempts = 0;
|
|
36
|
-
|
|
37
|
-
return utils.runWithRetries(() => {
|
|
38
|
-
const startTime = Date.now();
|
|
39
|
-
const player = testPlayerFactory();
|
|
40
|
-
return getBrowserOnce(player)
|
|
41
|
-
.then((getBrowserRes) => player || getBrowserRes)
|
|
42
|
-
.timeout(singleGetBrowserDuration, timeoutMessages.GET_BROWSER_TIMEOUT_MSG)
|
|
43
|
-
.tapCatch(() => reporter.onGetBrowserFailure(workerId, projectId, ++failedAttempts))
|
|
44
|
-
.tap(() => reporter.onGetBrowserSuccess(workerId, projectId))
|
|
45
|
-
.catch(err => waitUntilBrowserTimeout(err, startTime, singleGetBrowserDuration, projectId, workerId, player, releaseSlotOnTestFinished));
|
|
46
|
-
}, maxGetBrowserAttempts).catch(err => {
|
|
47
|
-
if (err instanceof PageNotAvailableError) {
|
|
48
|
-
throw err;
|
|
49
|
-
}
|
|
50
|
-
throw new GetBrowserError(err);
|
|
51
|
-
});
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
module.exports.isGetBrowserError = err => err && err.constructor && err.constructor.name === 'GetBrowserError';
|