@testim/testim-cli 3.243.0 → 3.246.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/chromiumInstaller.js +92 -0
- package/cli.js +89 -87
- package/cliAgentMode.js +39 -114
- package/commons/chromedriverWrapper.js +4 -3
- package/commons/featureFlags.js +1 -0
- package/commons/getSessionPlayerRequire.js +2 -1
- package/commons/httpRequestCounters.test.js +9 -9
- package/commons/npmWrapper.js +3 -3
- package/commons/npmWrapper.test.js +1 -1
- package/commons/prepareRunner.js +2 -0
- package/commons/socket/baseSocketServiceSocketIO.js +2 -2
- package/commons/testimAnalytics.js +8 -3
- package/commons/testimServicesApi.js +45 -39
- package/errors.js +2 -1
- package/npm-shrinkwrap.json +595 -494
- package/package.json +2 -2
- package/player/WebDriverHttpRequest.js +45 -37
- package/player/chromeLauncherTestPlayer.js +2 -2
- package/player/services/playbackTimeoutCalculator.js +5 -1
- package/player/stepActions/RefreshStepAction.js +2 -4
- package/player/stepActions/inputFileStepAction.js +6 -2
- package/player/stepActions/scripts/doClick.js +3 -1
- package/player/stepActions/scripts/html5dragAction.js +3 -1
- package/player/stepActions/scripts/html5dragActionV2.js +3 -1
- package/player/stepActions/scripts/runCode.js +23 -6
- package/player/stepActions/scripts/scroll.js +1 -6
- package/player/stepActions/scripts/setText.js +4 -1
- package/player/stepActions/sfdcStepAction.js +27 -0
- package/player/stepActions/stepActionRegistrar.js +12 -0
- package/player/utils/screenshotUtils.js +2 -2
- package/player/utils/windowUtils.js +3 -3
- package/player/webdriver.js +51 -52
- package/runOptions.js +3 -1
- package/runner.js +44 -41
- package/services/lambdatestService.js +2 -2
- package/services/lambdatestService.test.js +2 -1
- package/testRunHandler.js +7 -3
- package/testRunStatus.js +2 -1
- package/utils.js +56 -21
- package/utils.test.js +27 -1
- package/workers/BaseWorker.js +9 -7
- package/workers/BaseWorker.test.js +11 -12
package/utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const moment = require('moment');
|
|
4
|
-
const
|
|
4
|
+
const pRetry = require('p-retry');
|
|
5
5
|
const _ = require('lodash');
|
|
6
6
|
const Promise = require('bluebird');
|
|
7
7
|
const fs = Promise.promisifyAll(require('fs'));
|
|
@@ -10,6 +10,10 @@ const { sessionType, testStatus: testStatusConst } = require('./commons/constant
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const httpRequest = require('./commons/httpRequest');
|
|
12
12
|
const decompress = require('decompress');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
const HOMEDIR = os.homedir();
|
|
16
|
+
const TESTIM_BROWSER_DIR = path.join(HOMEDIR, '.testim-browser-profile');
|
|
13
17
|
|
|
14
18
|
const OSS = [
|
|
15
19
|
{ osName: 'Linux', bs: { os: 'LINUX' }, sl: { platform: 'Linux' } },
|
|
@@ -71,20 +75,16 @@ function getRunConfigByBrowserName(browser, saucelabs, browserstack) {
|
|
|
71
75
|
return _.merge(selectedBrowser, selectedOS);
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
function handleAngularURIComponent(part) {
|
|
75
|
-
return part.replace(/(~|\/)/g, m => ({ '~': '~~', '/': '~2F' }[m]));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
78
|
function getTestUrl(editorUrl, projectId, testId, resultId, branch) {
|
|
79
79
|
let testUrl = '';
|
|
80
|
-
branch = branch ?
|
|
80
|
+
branch = branch ? encodeURIComponent(branch) : 'master';
|
|
81
81
|
if (projectId && testId) {
|
|
82
82
|
testUrl = `${editorUrl}/#/project/${projectId}/branch/${branch}/test/${testId}`;
|
|
83
83
|
if (resultId) {
|
|
84
84
|
testUrl += `?result-id=${resultId}`;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
return
|
|
87
|
+
return testUrl;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
function isPromise(obj) {
|
|
@@ -100,17 +100,6 @@ function getDurationSec(ms) {
|
|
|
100
100
|
return moment.duration(ms).asSeconds();
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function runWithRetries(task, times, interval, predicate) {
|
|
104
|
-
times = times || 3;
|
|
105
|
-
interval = interval || 3000;
|
|
106
|
-
|
|
107
|
-
const opt = { max_tries: times, interval, throw_original: true };
|
|
108
|
-
if (predicate) {
|
|
109
|
-
opt.predicate = predicate;
|
|
110
|
-
}
|
|
111
|
-
return retry(task, opt);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
103
|
function getRunnerVersion() {
|
|
115
104
|
try {
|
|
116
105
|
const pack = require(`${__dirname}/package.json`);
|
|
@@ -198,7 +187,7 @@ function extractElementId(element) {
|
|
|
198
187
|
}
|
|
199
188
|
|
|
200
189
|
function isURL(path) {
|
|
201
|
-
const legacyPattern =
|
|
190
|
+
const legacyPattern = /^(https?:\/\/)/i;
|
|
202
191
|
|
|
203
192
|
// https://gist.github.com/dperini/729294 (validator.js based on).
|
|
204
193
|
const pattern = new RegExp(
|
|
@@ -247,7 +236,7 @@ function isURL(path) {
|
|
|
247
236
|
}
|
|
248
237
|
|
|
249
238
|
const DOWNLOAD_RETRY = 3;
|
|
250
|
-
const download = async (url) =>
|
|
239
|
+
const download = async (url) => pRetry(() => httpRequest.download(url), { retries: DOWNLOAD_RETRY });
|
|
251
240
|
|
|
252
241
|
const downloadAndSave = async (url, saveToLocation) => {
|
|
253
242
|
const res = await download(url);
|
|
@@ -315,6 +304,16 @@ function getPlanType(plan) {
|
|
|
315
304
|
return 'free';
|
|
316
305
|
}
|
|
317
306
|
|
|
307
|
+
/**
|
|
308
|
+
* @param time {number} in ms
|
|
309
|
+
* @returns {Promise}
|
|
310
|
+
*/
|
|
311
|
+
function delay(time) {
|
|
312
|
+
return new Promise(((resolve) => {
|
|
313
|
+
setTimeout(resolve, time);
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
|
|
318
317
|
const calcPercentile = (arr, percentile) => {
|
|
319
318
|
if (arr.length === 0) return 0;
|
|
320
319
|
if (typeof percentile !== 'number') throw new TypeError('p must be a number');
|
|
@@ -361,12 +360,45 @@ function groupTestsByRetries(testResults = []) { // NOTE: This duplicates a func
|
|
|
361
360
|
.value();
|
|
362
361
|
}
|
|
363
362
|
|
|
363
|
+
async function getCdpAddressForHost(browserInstanceHost, timeout) {
|
|
364
|
+
try {
|
|
365
|
+
/**
|
|
366
|
+
Example response:
|
|
367
|
+
{
|
|
368
|
+
"Browser": "Chrome/81.0.4044.138",
|
|
369
|
+
"Protocol-Version": "1.3",
|
|
370
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36",
|
|
371
|
+
"V8-Version": "8.1.307.32",
|
|
372
|
+
"WebKit-Version": "537.36 (@8c6c7ba89cc9453625af54f11fd83179e23450fa)",
|
|
373
|
+
"webSocketDebuggerUrl": "ws://localhost:58938/devtools/browser/d4290379-ec08-4d03-a41a-ab9d9d4c36ac"
|
|
374
|
+
}
|
|
375
|
+
*/
|
|
376
|
+
const debuggerAddress = await httpRequest.get(`http://${browserInstanceHost}/json/version`, undefined, undefined, timeout);
|
|
377
|
+
return debuggerAddress.webSocketDebuggerUrl;
|
|
378
|
+
} catch (e) {
|
|
379
|
+
throw new Error('unable to connect to devtools server');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function getArgsOnRemoteRunFailure() {
|
|
384
|
+
const { argv: args } = process;
|
|
385
|
+
if (!args.includes('--remoteRunId')) {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
remoteRunId: args[args.indexOf('--remoteRunId') + 1],
|
|
390
|
+
projectId: args[args.indexOf('--project') + 1],
|
|
391
|
+
token: args[args.indexOf('--token') + 1],
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
364
396
|
module.exports = {
|
|
397
|
+
TESTIM_BROWSER_DIR,
|
|
365
398
|
removePropertyFromObject,
|
|
366
399
|
getTestUrl,
|
|
367
400
|
getDuration,
|
|
368
401
|
getDurationSec,
|
|
369
|
-
runWithRetries,
|
|
370
402
|
getRunnerVersion,
|
|
371
403
|
getEnginesVersion,
|
|
372
404
|
getEnginesVersionAsync,
|
|
@@ -395,4 +427,7 @@ module.exports = {
|
|
|
395
427
|
isQuarantineAndNotRemoteRun,
|
|
396
428
|
groupTestsByRetries,
|
|
397
429
|
getPlanType,
|
|
430
|
+
delay,
|
|
431
|
+
getCdpAddressForHost,
|
|
432
|
+
getArgsOnRemoteRunFailure,
|
|
398
433
|
};
|
package/utils.test.js
CHANGED
|
@@ -32,11 +32,37 @@ describe('utils', () => {
|
|
|
32
32
|
expect(utils.getTestUrl('http://localhost:8080', 'project', 'test', 'result', 'normal-branch-name'))
|
|
33
33
|
.to.equal('http://localhost:8080/#/project/project/branch/normal-branch-name/test/test?result-id=result');
|
|
34
34
|
expect(utils.getTestUrl('http://localhost:8080', 'project', 'test', 'result', 'branch/with/slashes'))
|
|
35
|
-
.to.equal('http://localhost:8080/#/project/project/branch/branch
|
|
35
|
+
.to.equal('http://localhost:8080/#/project/project/branch/branch%2Fwith%2Fslashes/test/test?result-id=result');
|
|
36
36
|
expect(utils.getTestUrl('http://localhost:8080', 'project', 'test', 'result', 'branch with spaces'))
|
|
37
37
|
.to.equal('http://localhost:8080/#/project/project/branch/branch%20with%20spaces/test/test?result-id=result');
|
|
38
38
|
expect(utils.getTestUrl('http://localhost:8080', 'project', 'test', 'result', 'encoded%20branch'))
|
|
39
39
|
.to.equal('http://localhost:8080/#/project/project/branch/encoded%2520branch/test/test?result-id=result');
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
describe('getArgsOnRemoteRunFailure', () => {
|
|
44
|
+
let originalArgv;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
originalArgv = process.argv;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
process.argv = originalArgv;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return undefined if no remote run is current', () => {
|
|
55
|
+
process.argv = ['node', 'file.js', '--token', 'token', '--project', 'project-id'];
|
|
56
|
+
expect(utils.getArgsOnRemoteRunFailure()).to.be.undefined;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should return details if remote run is current', () => {
|
|
60
|
+
process.argv = ['node', 'file.js', '--token', 'token', '--project', 'project-id', '--remoteRunId', 'remote-run-id'];
|
|
61
|
+
expect(utils.getArgsOnRemoteRunFailure()).to.eql({
|
|
62
|
+
remoteRunId: 'remote-run-id',
|
|
63
|
+
projectId: 'project-id',
|
|
64
|
+
token: 'token',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
42
68
|
});
|
package/workers/BaseWorker.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const Bluebird = require('bluebird');
|
|
4
4
|
const moment = require('moment');
|
|
5
|
+
const pRetry = require('p-retry');
|
|
5
6
|
const ms = require('ms');
|
|
6
7
|
|
|
7
8
|
const { timeoutMessages, testRunStatus, stepResult, runnerTestStatus } = require('../commons/constants');
|
|
@@ -116,7 +117,8 @@ class BaseWorker {
|
|
|
116
117
|
try {
|
|
117
118
|
perf.log('before getSlotOnce retries');
|
|
118
119
|
let failedGetSlotAttempts = 0;
|
|
119
|
-
|
|
120
|
+
|
|
121
|
+
let gridInfo = await pRetry(async () => {
|
|
120
122
|
const startTime = Date.now();
|
|
121
123
|
try {
|
|
122
124
|
return await Bluebird.resolve(this.getSlotOnce(testRunHandler))
|
|
@@ -124,10 +126,10 @@ class BaseWorker {
|
|
|
124
126
|
} catch (error) {
|
|
125
127
|
logger.error('error getting grid slot', { error, testId: this.testId, testResultId: this.testResultId, executionId: this.executionId });
|
|
126
128
|
failedGetSlotAttempts++;
|
|
127
|
-
await
|
|
129
|
+
await utils.delay(this.options.getBrowserTimeout - (Date.now() - startTime));
|
|
128
130
|
throw error;
|
|
129
131
|
}
|
|
130
|
-
}, this.options.getBrowserRetries);
|
|
132
|
+
}, { retries: this.options.getBrowserRetries - 1, minTimeout: 0 });
|
|
131
133
|
perf.log('after getSlotOnce retries');
|
|
132
134
|
|
|
133
135
|
perf.log('before getBrowserOnce retries');
|
|
@@ -136,7 +138,7 @@ class BaseWorker {
|
|
|
136
138
|
throw new Error('No free browser slots in desired grid');
|
|
137
139
|
}
|
|
138
140
|
let failedGetBrowserAttempts = 0;
|
|
139
|
-
testPlayer = await
|
|
141
|
+
testPlayer = await pRetry(async () => {
|
|
140
142
|
const startTime = Date.now();
|
|
141
143
|
const player = this.initPlayer(testRunHandler);
|
|
142
144
|
try {
|
|
@@ -162,11 +164,11 @@ class BaseWorker {
|
|
|
162
164
|
player.onDone();
|
|
163
165
|
|
|
164
166
|
if (!(error instanceof PageNotAvailableError)) {
|
|
165
|
-
await
|
|
167
|
+
await utils.delay(this.options.getBrowserTimeout - (Date.now() - startTime));
|
|
166
168
|
}
|
|
167
169
|
throw error;
|
|
168
170
|
}
|
|
169
|
-
}, getBrowserRetriesNumber
|
|
171
|
+
}, { retries: getBrowserRetriesNumber - 1, minTimeout: 0 });
|
|
170
172
|
perf.log('after getBrowserOnce retries');
|
|
171
173
|
} catch (err) {
|
|
172
174
|
await releasePlayer(this.id, this.releaseSlotOnTestFinished, projectId, testPlayer);
|
|
@@ -235,7 +237,7 @@ class BaseWorker {
|
|
|
235
237
|
testResult.testRetryKey = testRetryKey;
|
|
236
238
|
await this.onTestCompleted(this.id, this.testId, testResult, sessionId, shouldRerun);
|
|
237
239
|
if (this.executionQueue.hasMoreTests() && !(this.options.lightweightMode && this.options.lightweightMode.general)) {
|
|
238
|
-
await
|
|
240
|
+
await utils.delay(DELAY_BETWEEN_TESTS);
|
|
239
241
|
}
|
|
240
242
|
await this.runTestCleanup();
|
|
241
243
|
if (shouldRerun) {
|
|
@@ -3,10 +3,9 @@ const proxyquire = require('proxyquire');
|
|
|
3
3
|
const { expect, sinon } = require('../../test/utils/testUtils');
|
|
4
4
|
const gridService = require('../services/gridService');
|
|
5
5
|
const reporter = require('../reports/reporter');
|
|
6
|
-
const Bluebird = require('bluebird');
|
|
7
6
|
const { PageNotAvailableError, GridError, GetBrowserError } = require('../errors');
|
|
8
7
|
const servicesApi = require('../commons/testimServicesApi');
|
|
9
|
-
const
|
|
8
|
+
const utils = require('../utils');
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
describe('BaseWorker', () => {
|
|
@@ -41,7 +40,7 @@ describe('BaseWorker', () => {
|
|
|
41
40
|
getGridSlotStub = sinon.stub(gridService, 'getGridSlot').resolves({});
|
|
42
41
|
|
|
43
42
|
|
|
44
|
-
sandbox.stub(
|
|
43
|
+
sandbox.stub(utils, 'delay').returns(Promise.resolve());
|
|
45
44
|
sandbox.stub(reporter);
|
|
46
45
|
});
|
|
47
46
|
|
|
@@ -57,7 +56,7 @@ describe('BaseWorker', () => {
|
|
|
57
56
|
it('should get grid slot from server', async () => {
|
|
58
57
|
await worker.getTestPlayer(testRunHandlerMock);
|
|
59
58
|
sinon.assert.calledOnce(getGridSlotStub);
|
|
60
|
-
sinon.assert.notCalled(
|
|
59
|
+
sinon.assert.notCalled(utils.delay);
|
|
61
60
|
});
|
|
62
61
|
it('should retry getting slot until it succeeds', async () => {
|
|
63
62
|
getGridSlotStub.onFirstCall().rejects();
|
|
@@ -65,7 +64,7 @@ describe('BaseWorker', () => {
|
|
|
65
64
|
|
|
66
65
|
await worker.getTestPlayer(testRunHandlerMock);
|
|
67
66
|
sinon.assert.calledTwice(getGridSlotStub);
|
|
68
|
-
sinon.assert.
|
|
67
|
+
sinon.assert.calledOnce(utils.delay);
|
|
69
68
|
});
|
|
70
69
|
it('should not proceed to getting browser if all retries used for getting a slot', async () => {
|
|
71
70
|
getGridSlotStub.resolves({});
|
|
@@ -74,7 +73,7 @@ describe('BaseWorker', () => {
|
|
|
74
73
|
sinon.assert.calledOnce(getGridSlotStub);
|
|
75
74
|
sinon.assert.notCalled(worker.getBrowserOnce);
|
|
76
75
|
sinon.assert.notCalled(worker.initPlayer);
|
|
77
|
-
sinon.assert.notCalled(
|
|
76
|
+
sinon.assert.notCalled(utils.delay);
|
|
78
77
|
});
|
|
79
78
|
});
|
|
80
79
|
|
|
@@ -84,7 +83,7 @@ describe('BaseWorker', () => {
|
|
|
84
83
|
sinon.assert.calledOnce(getGridSlotStub);
|
|
85
84
|
sinon.assert.calledOnce(worker.getBrowserOnce);
|
|
86
85
|
sinon.assert.calledOnce(worker.initPlayer);
|
|
87
|
-
sinon.assert.notCalled(
|
|
86
|
+
sinon.assert.notCalled(utils.delay);
|
|
88
87
|
});
|
|
89
88
|
it('should retry getting browser until it succeeds', async () => {
|
|
90
89
|
worker.getBrowserOnce.onFirstCall().rejects();
|
|
@@ -95,16 +94,16 @@ describe('BaseWorker', () => {
|
|
|
95
94
|
sinon.assert.calledTwice(worker.getBrowserOnce);
|
|
96
95
|
sinon.assert.calledTwice(worker.initPlayer);
|
|
97
96
|
sinon.assert.calledOnce(testPlayerMock.onDone);
|
|
98
|
-
sinon.assert.
|
|
97
|
+
sinon.assert.calledOnce(utils.delay);
|
|
99
98
|
});
|
|
100
99
|
it('should not retry if page is not available', async () => {
|
|
101
100
|
worker.getBrowserOnce.throws(() => new PageNotAvailableError());
|
|
102
101
|
|
|
103
|
-
await expect(worker.getTestPlayer(testRunHandlerMock)).to.eventually.be.rejectedWith(
|
|
102
|
+
await expect(worker.getTestPlayer(testRunHandlerMock)).to.eventually.be.rejectedWith(Error);
|
|
104
103
|
sinon.assert.calledOnce(getGridSlotStub);
|
|
105
104
|
sinon.assert.calledOnce(worker.getBrowserOnce);
|
|
106
105
|
sinon.assert.calledOnce(testPlayerMock.onDone);
|
|
107
|
-
sinon.assert.notCalled(
|
|
106
|
+
sinon.assert.notCalled(utils.delay);
|
|
108
107
|
});
|
|
109
108
|
it('should handle get grid error', async () => {
|
|
110
109
|
worker.getBrowserOnce.throws(() => new GridError());
|
|
@@ -153,8 +152,8 @@ describe('BaseWorker', () => {
|
|
|
153
152
|
getHybridGridProviderStub.onFirstCall().resolves({ provider: 'loacker', connectionDetails: { host: 'localhost', external: { user: 'user', key: 'password' } } });
|
|
154
153
|
getHybridGridProviderStub.onSecondCall().resolves({ provider: 'nitzan', connectionDetails: { host: 'google.com', port: 443 } });
|
|
155
154
|
getHybridGridProviderStub.onThirdCall().resolves({ provider: 'a', connectionDetails: { host: 'google.com', port: 4444 } });
|
|
156
|
-
worker.getBrowserOnce.onFirstCall().rejects();
|
|
157
|
-
worker.getBrowserOnce.onSecondCall().rejects();
|
|
155
|
+
worker.getBrowserOnce.onFirstCall().rejects(new Error());
|
|
156
|
+
worker.getBrowserOnce.onSecondCall().rejects(new Error());
|
|
158
157
|
worker.getBrowserOnce.onThirdCall().resolves({});
|
|
159
158
|
|
|
160
159
|
await worker.getTestPlayer(testRunHandlerMock);
|