@testim/testim-cli 3.207.0 → 3.213.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 +17 -8
- package/commons/httpRequestCounters.js +41 -10
- package/commons/httpRequestCounters.test.js +9 -9
- package/commons/testimNgrok.js +30 -1
- package/commons/testimTunnel.js +1 -1
- package/npm-shrinkwrap.json +115 -328
- package/package.json +3 -5
- package/player/seleniumTestPlayer.js +15 -6
- 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/player/utils/stepActionUtils.js +2 -2
- package/runOptions.js +5 -6
- package/runners/ParallelWorkerManager.js +3 -0
- package/services/analyticsService.js +4 -2
- package/testRunHandler.js +10 -2
- package/workers/BaseWorker.js +10 -10
- package/workers/WorkerExtension.js +5 -2
- package/workers/workerUtils.js +4 -1
- package/player/utils/downloadsApiUtils.js +0 -13
|
@@ -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) {
|
|
@@ -225,7 +233,7 @@ function download(url) {
|
|
|
225
233
|
|
|
226
234
|
return Promise.fromCallback((callback) => request.end(callback))
|
|
227
235
|
.tap(() => logger.info('finished to download', { url }))
|
|
228
|
-
.
|
|
236
|
+
.catch(logErrorAndRethrow('failed to download', { url }));
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
module.exports = {
|
|
@@ -241,4 +249,5 @@ module.exports = {
|
|
|
241
249
|
head: wrapWithMonitoring(head),
|
|
242
250
|
download: wrapWithMonitoring(download),
|
|
243
251
|
isNetworkHealthy: wrapWithMonitoring.isNetworkHealthy,
|
|
252
|
+
didNetworkConnectivityTestFail: wrapWithMonitoring.didNetworkConnectivityTestFail,
|
|
244
253
|
};
|
|
@@ -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/commons/testimNgrok.js
CHANGED
|
@@ -8,6 +8,7 @@ const logger = require('./logger').getLogger('testimNgrok');
|
|
|
8
8
|
const WHITELISTED_TUNNEL_DOMAIN_SUFFIX = '.whitelisted-ngrok.testim.io';
|
|
9
9
|
|
|
10
10
|
let ngrokTunnelUrl = '';
|
|
11
|
+
let statsTimeout;
|
|
11
12
|
|
|
12
13
|
const connectTunnel = async (options, authData) => {
|
|
13
14
|
if (!authData.ngrokToken) {
|
|
@@ -28,6 +29,9 @@ const connectTunnel = async (options, authData) => {
|
|
|
28
29
|
if (options.tunnelHostHeader) {
|
|
29
30
|
connectOpt.host_header = options.tunnelHostHeader;
|
|
30
31
|
}
|
|
32
|
+
if (options.tunnelRegion) {
|
|
33
|
+
connectOpt.region = options.tunnelRegion;
|
|
34
|
+
}
|
|
31
35
|
|
|
32
36
|
const ngrok = await lazyRequire('ngrok');
|
|
33
37
|
const url = await ngrok.connect(connectOpt);
|
|
@@ -41,14 +45,39 @@ const connectTunnel = async (options, authData) => {
|
|
|
41
45
|
ngrokTunnelUrl = url;
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
if (options.tunnelDiagnostics) {
|
|
49
|
+
collectNgrokStats();
|
|
50
|
+
}
|
|
44
51
|
options.baseUrl = ngrokTunnelUrl;
|
|
45
52
|
};
|
|
46
53
|
|
|
47
|
-
const
|
|
54
|
+
const collectNgrokStats = async (rerun = true) => {
|
|
55
|
+
try {
|
|
56
|
+
const ngrok = await lazyRequire('ngrok');
|
|
57
|
+
const api = ngrok.getApi();
|
|
58
|
+
const { tunnels } = await api.get({ url: 'api/tunnels', json: true });
|
|
59
|
+
const tunnel = tunnels.find(t => t.public_url === ngrokTunnelUrl);
|
|
60
|
+
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.log('ngrok stats', tunnel);
|
|
63
|
+
logger.info('ngrok stats', { tunnel });
|
|
64
|
+
} catch (err) {
|
|
65
|
+
logger.error('error collecting ngrok stats', { err });
|
|
66
|
+
}
|
|
67
|
+
if (rerun) {
|
|
68
|
+
statsTimeout = setTimeout(() => collectNgrokStats(), 10000);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const disconnectTunnel = async (options) => {
|
|
48
73
|
if (!ngrokTunnelUrl) {
|
|
49
74
|
return;
|
|
50
75
|
}
|
|
51
76
|
|
|
77
|
+
clearTimeout(statsTimeout);
|
|
78
|
+
if (options.tunnelDiagnostics) {
|
|
79
|
+
await collectNgrokStats(false);
|
|
80
|
+
}
|
|
52
81
|
const ngrok = await lazyRequire('ngrok');
|
|
53
82
|
await ngrok.disconnect(ngrokTunnelUrl);
|
|
54
83
|
};
|
package/commons/testimTunnel.js
CHANGED
|
@@ -41,7 +41,7 @@ const disconnect = async (options) => {
|
|
|
41
41
|
if (shouldUseLambdatestTunnel(options)) {
|
|
42
42
|
await LambdatestService.disconnectTunnel(options);
|
|
43
43
|
} else {
|
|
44
|
-
await testimNgrok.disconnectTunnel();
|
|
44
|
+
await testimNgrok.disconnectTunnel(options);
|
|
45
45
|
}
|
|
46
46
|
} catch (err) {
|
|
47
47
|
const msg = 'catch error - failed to close tunnel';
|