@testim/testim-cli 3.235.0 → 3.238.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/server.js +2 -2
- package/cli.js +0 -2
- package/cliAgentMode.js +129 -15
- package/commons/chromedriverWrapper.js +1 -0
- package/commons/featureFlags.js +1 -0
- package/commons/prepareRunnerAndTestimStartUtils.js +7 -3
- package/commons/testimCloudflare.js +8 -8
- package/commons/testimCloudflare.test.js +162 -0
- package/commons/testimNgrok.js +6 -4
- package/commons/testimNgrok.test.js +140 -0
- package/commons/testimTunnel.js +5 -5
- package/commons/testimTunnel.test.js +69 -0
- package/executionQueue.js +4 -0
- package/npm-shrinkwrap.json +261 -234
- package/package.json +1 -1
- package/player/stepActions/inputFileStepAction.js +23 -14
- package/player/stepActions/textValidationStepAction.js +3 -0
- package/player/webdriver.js +13 -3
- package/runOptions.js +19 -0
- package/runOptionsAgentFlow.js +5 -0
- package/runner.js +1 -2
- package/runners/ParallelWorkerManager.js +14 -6
- package/runners/TestPlanRunner.js +10 -11
- package/services/lambdatestService.js +8 -9
- package/services/lambdatestService.test.js +259 -0
- package/testRunStatus.js +34 -5
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
const { expect, sinon } = require('../../test/utils/testUtils');
|
|
2
2
|
const servicesApi = require('../commons/testimServicesApi');
|
|
3
|
+
const httpRequest = require('../commons/httpRequest');
|
|
3
4
|
const LambdatestService = require('./lambdatestService');
|
|
5
|
+
const utils = require('../utils');
|
|
6
|
+
const fse = require('fs-extra');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const childProcess = require('child_process');
|
|
9
|
+
const EventEmitter = require('events');
|
|
10
|
+
const portfinder = require('portfinder');
|
|
11
|
+
const { getExtensionsUrl } = require('../runOptionsUtils');
|
|
12
|
+
|
|
13
|
+
class Process extends EventEmitter {
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.stdout = new EventEmitter();
|
|
17
|
+
this.stderr = new EventEmitter();
|
|
18
|
+
}
|
|
19
|
+
setCode(code) {
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
kill() {
|
|
23
|
+
this.emit('close', this.code);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
4
26
|
|
|
5
27
|
describe('LambdatestService', () => {
|
|
28
|
+
let sandbox;
|
|
6
29
|
let lambdatestService;
|
|
7
30
|
beforeEach(() => {
|
|
8
31
|
lambdatestService = new LambdatestService();
|
|
32
|
+
sandbox = sinon.createSandbox();
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
sandbox.restore();
|
|
9
36
|
});
|
|
10
37
|
|
|
11
38
|
describe('isLambdatestGrid', () => {
|
|
@@ -86,8 +113,240 @@ describe('LambdatestService', () => {
|
|
|
86
113
|
lambdatestService.isActive = false;
|
|
87
114
|
expect(lambdatestService.getSessionRetries).to.be.equal(null);
|
|
88
115
|
});
|
|
116
|
+
|
|
117
|
+
it('should not return lt special selenium capabilities when inactove', () => {
|
|
118
|
+
lambdatestService.isActive = false;
|
|
119
|
+
expect(lambdatestService.getCapabilities({})).to.eql({});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should return lt selenium capabilities', () => {
|
|
123
|
+
const browser = utils.guid();
|
|
124
|
+
const executionId = utils.guid();
|
|
125
|
+
const testResultId = utils.guid();
|
|
126
|
+
const testName = utils.guid();
|
|
127
|
+
|
|
128
|
+
lambdatestService.isActive = true;
|
|
129
|
+
LambdatestService.lambdatestConfig = {
|
|
130
|
+
CAPABILITIES: { [browser]: { specificBrowserCaps: 123 } },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
expect(lambdatestService.getCapabilities({}, browser, executionId, testResultId, testName)).to.shallowDeepEqual({
|
|
134
|
+
build: executionId,
|
|
135
|
+
name: `${testResultId} - ${testName}`,
|
|
136
|
+
platform: LambdatestService.lambdatestConfig.PLATFORM,
|
|
137
|
+
// eslint-disable-next-line camelcase
|
|
138
|
+
selenium_version: LambdatestService.lambdatestConfig.SELENIUM_VERSION,
|
|
139
|
+
resolution: LambdatestService.lambdatestConfig.RESOLUTION,
|
|
140
|
+
timezone: LambdatestService.lambdatestConfig.TIMEZONE,
|
|
141
|
+
specificBrowserCaps: 123,
|
|
142
|
+
console: true,
|
|
143
|
+
queueTimeout: 300,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should return lt tunnel name as part of selenium capabilities', () => {
|
|
148
|
+
lambdatestService.isActive = true;
|
|
149
|
+
LambdatestService.lambdatestConfig = { CAPABILITIES: {} };
|
|
150
|
+
LambdatestService.tunnelName = utils.guid();
|
|
151
|
+
|
|
152
|
+
expect(lambdatestService.getCapabilities({})).to.shallowDeepEqual({
|
|
153
|
+
tunnel: true,
|
|
154
|
+
tunnelName: LambdatestService.tunnelName,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should load testim extension when it is not set', () => {
|
|
159
|
+
lambdatestService.isActive = true;
|
|
160
|
+
LambdatestService.lambdatestConfig = { CAPABILITIES: {} };
|
|
161
|
+
LambdatestService.tunnelName = utils.guid();
|
|
162
|
+
|
|
163
|
+
expect(lambdatestService.getCapabilities({ mode: 'extension' }, 'chrome')).to.shallowDeepEqual({ loadExtension: [getExtensionsUrl({}, true).chrome] });
|
|
164
|
+
expect(lambdatestService.getCapabilities({ mode: 'extension' }, 'somOtherBrowser')).to.shallowDeepEqual({ loadExtension: [] });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should load testim extension when passing extensionPath flag', () => {
|
|
168
|
+
lambdatestService.isActive = true;
|
|
169
|
+
LambdatestService.lambdatestConfig = { CAPABILITIES: {} };
|
|
170
|
+
LambdatestService.tunnelName = utils.guid();
|
|
171
|
+
|
|
172
|
+
const extensionPath = 'http://localhost:1234/extension.zip';
|
|
173
|
+
expect(lambdatestService.getCapabilities({ mode: 'extension', extensionPath })).to.shallowDeepEqual({ loadExtension: [extensionPath] });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should load testim extension when passing installCustomExtension flag', () => {
|
|
177
|
+
lambdatestService.isActive = true;
|
|
178
|
+
LambdatestService.lambdatestConfig = { CAPABILITIES: {} };
|
|
179
|
+
LambdatestService.tunnelName = utils.guid();
|
|
180
|
+
|
|
181
|
+
const installCustomExtension = 'http://localhost:1234/extension.zip';
|
|
182
|
+
expect(lambdatestService.getCapabilities({ mode: 'extension', installCustomExtension })).to.shallowDeepEqual({ loadExtension: [installCustomExtension] });
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('prepareTunnel', () => {
|
|
187
|
+
it('should do nothing when tunnel binary already exists', async () => {
|
|
188
|
+
sandbox.stub(fse, 'pathExists').resolves(true);
|
|
189
|
+
const chmod = sandbox.stub(fse, 'chmodSync');
|
|
190
|
+
await LambdatestService.prepareTunnel();
|
|
191
|
+
expect(chmod).not.to.have.been.called;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should throw when unsupported os', async () => {
|
|
195
|
+
sandbox.stub(fse, 'pathExists').resolves(false);
|
|
196
|
+
sandbox.stub(os, 'platform').returns('wtf');
|
|
197
|
+
sandbox.stub(os, 'arch').returns('wtf');
|
|
198
|
+
|
|
199
|
+
await expect(LambdatestService.prepareTunnel()).to.be.rejectedWith(Error, 'tunnel on wtfwtf platform is not supported.');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should download and extract tunnel binary', async () => {
|
|
203
|
+
sandbox.stub(fse, 'pathExists').resolves(false);
|
|
204
|
+
sandbox.stub(os, 'platform').returns('win32');
|
|
205
|
+
sandbox.stub(os, 'arch').returns('x64');
|
|
206
|
+
|
|
207
|
+
const download = sandbox.stub(utils, 'downloadAndSave');
|
|
208
|
+
const unzip = sandbox.stub(utils, 'unzipFile').resolves();
|
|
209
|
+
|
|
210
|
+
await LambdatestService.prepareTunnel();
|
|
211
|
+
sinon.assert.calledOnce(unzip);
|
|
212
|
+
sinon.assert.calledOnce(download);
|
|
213
|
+
expect(download.args[0][0]).to.startWith('https://downloads.lambdatest.com/tunnel/');
|
|
214
|
+
});
|
|
89
215
|
});
|
|
90
216
|
|
|
91
217
|
describe('connectTunnel', () => {
|
|
218
|
+
let prepareTunnelStub;
|
|
219
|
+
let spawnStub;
|
|
220
|
+
let credentials;
|
|
221
|
+
let httpGetStub;
|
|
222
|
+
let processMock;
|
|
223
|
+
|
|
224
|
+
beforeEach(() => {
|
|
225
|
+
processMock = new Process();
|
|
226
|
+
credentials = { gridUsername: utils.guid(), gridPassword: utils.guid() };
|
|
227
|
+
sandbox.stub(portfinder, 'getPortPromise').resolves(1234);
|
|
228
|
+
prepareTunnelStub = sandbox.stub(LambdatestService, 'prepareTunnel');
|
|
229
|
+
spawnStub = sandbox.stub(childProcess, 'spawn').returns(processMock);
|
|
230
|
+
httpGetStub = sandbox.stub(httpRequest, 'get').resolves({});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should do nothing when using externalLambdatestTunnelId', async () => {
|
|
234
|
+
await LambdatestService.connectTunnel({ externalLambdatestTunnelId: 123 });
|
|
235
|
+
sinon.assert.neverCalledWith(prepareTunnelStub);
|
|
236
|
+
sinon.assert.neverCalledWith(spawnStub);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should prepare the tunnel', async () => {
|
|
240
|
+
await LambdatestService.connectTunnel({ ...credentials });
|
|
241
|
+
sinon.assert.calledOnce(prepareTunnelStub);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should reject when no credentials', async () => {
|
|
245
|
+
await expect(LambdatestService.connectTunnel({ })).to.be.rejectedWith(Error, 'tunnel requires username and password');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should spawn the tunnel process', async () => {
|
|
249
|
+
await LambdatestService.connectTunnel({ ...credentials });
|
|
250
|
+
sinon.assert.calledOnce(spawnStub);
|
|
251
|
+
expect(spawnStub.args[0][1]).to.eql([
|
|
252
|
+
'--tunnelName', LambdatestService.tunnelName, '--infoAPIPort', 1234,
|
|
253
|
+
'--user', credentials.gridUsername, '--key', credentials.gridPassword,
|
|
254
|
+
]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should accept tunnelUser and tunnelKey on gridData', async () => {
|
|
258
|
+
credentials = { gridData: { tunnelUser: utils.guid(), tunnelKey: utils.guid() } };
|
|
259
|
+
await LambdatestService.connectTunnel({ ...credentials });
|
|
260
|
+
sinon.assert.calledOnce(spawnStub);
|
|
261
|
+
expect(spawnStub.args[0][1]).to.eql([
|
|
262
|
+
'--tunnelName', LambdatestService.tunnelName, '--infoAPIPort', 1234,
|
|
263
|
+
'--user', credentials.gridData.tunnelUser, '--key', credentials.gridData.tunnelKey,
|
|
264
|
+
]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should allow using externalLambdatestUseWss', async () => {
|
|
268
|
+
await LambdatestService.connectTunnel({ ...credentials, externalLambdatestUseWss: true });
|
|
269
|
+
sinon.assert.calledOnce(spawnStub);
|
|
270
|
+
expect(spawnStub.args[0][1].join(' ')).to.contain('--mode ws');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should allow using proxyUri', async () => {
|
|
274
|
+
global.proxyUri = 'http://localhost';
|
|
275
|
+
await LambdatestService.connectTunnel({ ...credentials });
|
|
276
|
+
sinon.assert.calledOnce(spawnStub);
|
|
277
|
+
expect(spawnStub.args[0][1].join(' ')).to.contain('--proxy-host localhost');
|
|
278
|
+
global.proxyUri = undefined;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should allow using proxyUri port and credentials', async () => {
|
|
282
|
+
global.proxyUri = 'http://user:pass@localhost:1234';
|
|
283
|
+
await LambdatestService.connectTunnel({ ...credentials });
|
|
284
|
+
sinon.assert.calledOnce(spawnStub);
|
|
285
|
+
expect(spawnStub.args[0][1].join(' ')).to.contain('--proxy-host localhost');
|
|
286
|
+
expect(spawnStub.args[0][1].join(' ')).to.contain('--proxy-port 1234');
|
|
287
|
+
expect(spawnStub.args[0][1].join(' ')).to.contain('--proxy-user user');
|
|
288
|
+
expect(spawnStub.args[0][1].join(' ')).to.contain('--proxy-pass pass');
|
|
289
|
+
global.proxyUri = undefined;
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should throw when proxyUri is invalid', async () => {
|
|
293
|
+
global.proxyUri = 'i am invalid';
|
|
294
|
+
await expect(LambdatestService.connectTunnel({ ...credentials })).to.be.rejectedWith(Error, 'proxy url is invalid');
|
|
295
|
+
global.proxyUri = undefined;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should allow using externalLambdatestDisableAutomationTunneling', async () => {
|
|
299
|
+
await LambdatestService.connectTunnel({ ...credentials, externalLambdatestDisableAutomationTunneling: true });
|
|
300
|
+
sinon.assert.calledOnce(spawnStub);
|
|
301
|
+
expect(spawnStub.args[0][1].join(' ')).to.contain('--bypassHosts run.testim.io,services.testim.io,api.coralogix.com,conf.rollout.io,statestore.rollout.io,push.rollout.io,analytic.rollout.io,res.cloudinary.com');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should allow using externalLambdatestMitm', async () => {
|
|
305
|
+
await LambdatestService.connectTunnel({ ...credentials, externalLambdatestMitm: true });
|
|
306
|
+
sinon.assert.calledOnce(spawnStub);
|
|
307
|
+
expect(spawnStub.args[0][1].join(' ')).to.contain('--mitm');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should verify tunnel started', async () => {
|
|
311
|
+
await LambdatestService.connectTunnel({ ...credentials });
|
|
312
|
+
sinon.assert.calledOnce(httpGetStub);
|
|
313
|
+
});
|
|
314
|
+
it('should throw when tunnel did not start', async () => {
|
|
315
|
+
sandbox.stub(utils, 'runWithRetries').rejects(new Error('tunnel did not start'));
|
|
316
|
+
await expect(LambdatestService.connectTunnel({ ...credentials })).to.be.rejectedWith(Error, 'tunnel did not start');
|
|
317
|
+
processMock.stdout.emit('data', '');
|
|
318
|
+
processMock.stderr.emit('data', '');
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('disconnectTunnel', () => {
|
|
323
|
+
let processMock;
|
|
324
|
+
let killStub;
|
|
325
|
+
|
|
326
|
+
beforeEach(() => {
|
|
327
|
+
processMock = new Process();
|
|
328
|
+
killStub = sandbox.stub(processMock, 'kill').callThrough();
|
|
329
|
+
sandbox.stub(childProcess, 'spawn').returns(processMock);
|
|
330
|
+
sandbox.stub(LambdatestService, 'prepareTunnel');
|
|
331
|
+
sandbox.stub(httpRequest, 'get').resolves({});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should kill the tunnel', async () => {
|
|
335
|
+
await LambdatestService.connectTunnel({ tunnel: true, company: {}, gridUsername: utils.guid(), gridPassword: utils.guid() });
|
|
336
|
+
await LambdatestService.disconnectTunnel({ company: {} });
|
|
337
|
+
sinon.assert.calledOnce(killStub);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should reject when killing the tunnel fails', async () => {
|
|
341
|
+
processMock.setCode(1);
|
|
342
|
+
await LambdatestService.connectTunnel({ tunnel: true, company: {}, gridUsername: utils.guid(), gridPassword: utils.guid() });
|
|
343
|
+
await expect(LambdatestService.disconnectTunnel({ company: {} })).to.be.rejectedWith(Error);
|
|
344
|
+
sinon.assert.calledOnce(killStub);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should do nothing when using externalLambdatestTunnelId', async () => {
|
|
348
|
+
await LambdatestService.disconnectTunnel({ externalLambdatestTunnelId: 123 });
|
|
349
|
+
sinon.assert.neverCalledWith(killStub);
|
|
350
|
+
});
|
|
92
351
|
});
|
|
93
352
|
});
|
package/testRunStatus.js
CHANGED
|
@@ -174,7 +174,21 @@ RunStatus.prototype.testStartReport = function (test, executionId, testRetryKey)
|
|
|
174
174
|
}
|
|
175
175
|
return runHook(this.options.beforeTest, Object.assign({}, test, { exportsGlobal: this.exportsGlobal }), this.options.userData.loginData.token)
|
|
176
176
|
.then(async params => {
|
|
177
|
-
|
|
177
|
+
// Temporary Sapiens log (SUP-3192)
|
|
178
|
+
if (this.options.projectData && this.options.projectData.projectId === 'fZ63D61PRQQVvvtGY6Ue' && this.options.suites && this.options.suites.includes('Sanity')) {
|
|
179
|
+
logger.info('testRunStatus - testStartReport', {
|
|
180
|
+
'test.config.testData': test.config.testData,
|
|
181
|
+
'this.exportsGlobal': this.exportsGlobal,
|
|
182
|
+
'this.fileUserParamsData': this.fileUserParamsData,
|
|
183
|
+
'this.beforeSuiteParams': this.beforeSuiteParams,
|
|
184
|
+
params,
|
|
185
|
+
executionId,
|
|
186
|
+
'test.testId': test.testId,
|
|
187
|
+
'test.resultId': test.resultId,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
test.config.testData = Object.assign({}, test.config.testData, this.exportsGlobal, this.fileUserParamsData, this.beforeSuiteParams, params);
|
|
191
|
+
this.options.runParams[test.resultId] = test.config.testData;
|
|
178
192
|
test.startTime = Date.now();
|
|
179
193
|
await this.updateTestStatusRunning(test, executionId, testRetryKey);
|
|
180
194
|
|
|
@@ -234,8 +248,10 @@ RunStatus.prototype.testEnd = function (wid, result, executionId, sessionId, isR
|
|
|
234
248
|
const duration = (result.endTime - result.startTime) || 0;
|
|
235
249
|
test.sessionId = sessionId;
|
|
236
250
|
test.startTime = result.startTime || test.startTime || Date.now();
|
|
237
|
-
test.duration =
|
|
238
|
-
result.
|
|
251
|
+
test.duration = duration;
|
|
252
|
+
result.duration = duration;
|
|
253
|
+
test.failureReason = result.failureReason || result.reason;
|
|
254
|
+
result.failureReason = test.failureReason;
|
|
239
255
|
test.failurePath = result.failurePath;
|
|
240
256
|
test.resultId = result.resultId;
|
|
241
257
|
test.success = result.success;
|
|
@@ -245,8 +261,9 @@ RunStatus.prototype.testEnd = function (wid, result, executionId, sessionId, isR
|
|
|
245
261
|
}
|
|
246
262
|
|
|
247
263
|
test.resultUrl = utils.getTestUrl(this.options.editorUrl, this.options.project, test.testId, test.resultId, this.branchToUse);
|
|
264
|
+
test.status = this.calcResultText(result);
|
|
248
265
|
|
|
249
|
-
|
|
266
|
+
result.status = test.status;
|
|
250
267
|
result.name = test.name;
|
|
251
268
|
result.testStatus = test.testStatus;
|
|
252
269
|
result.testId = result.testId || test.testId;
|
|
@@ -263,7 +280,19 @@ RunStatus.prototype.testEnd = function (wid, result, executionId, sessionId, isR
|
|
|
263
280
|
const isCodeMode = this.options.files.length > 0;
|
|
264
281
|
reporter.onTestFinished(test, wid, isRerun, isCodeMode);
|
|
265
282
|
|
|
266
|
-
|
|
283
|
+
const afterMerge = Object.assign({}, this.exportsGlobal, result.exportsGlobal);
|
|
284
|
+
// Temporary Sapiens log (SUP-3192)
|
|
285
|
+
if (this.options.projectData && this.options.projectData.projectId === 'fZ63D61PRQQVvvtGY6Ue' && this.options.suites && this.options.suites.includes('Sanity')) {
|
|
286
|
+
logger.info('testRunStatus - testEnd', {
|
|
287
|
+
'this.exportsGlobal': this.exportsGlobal,
|
|
288
|
+
'result.exportsGlobal': result.exportsGlobal,
|
|
289
|
+
afterMerge,
|
|
290
|
+
executionId,
|
|
291
|
+
'test.testId': test.testId,
|
|
292
|
+
'test.resultId': test.resultId,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
this.exportsGlobal = afterMerge;
|
|
267
296
|
return test;
|
|
268
297
|
};
|
|
269
298
|
|