@testim/testim-cli 3.235.0 → 3.236.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testim/testim-cli",
3
- "version": "3.235.0",
3
+ "version": "3.236.0",
4
4
  "description": "Command line interface for running Testing on your CI",
5
5
  "author": "Oren Rubin",
6
6
  "contributors": [{
@@ -4,7 +4,7 @@ const StepAction = require('./stepAction');
4
4
  const _ = require('lodash');
5
5
  const logger = require('../../commons/logger').getLogger('input-file-step-action');
6
6
  const { codeSnippets, utils } = require('../../commons/getSessionPlayerRequire');
7
- const { extractElementId } = require('../../utils');
7
+ const { extractElementId, download } = require('../../utils');
8
8
  const inputFileUtils = require('../../inputFileUtils');
9
9
  const featureFlagService = require('../../commons/featureFlags');
10
10
 
@@ -41,18 +41,15 @@ class InputFileStepAction extends StepAction {
41
41
  }
42
42
 
43
43
  async uploadFilesAndForceVisibility(gridLocalFiles, target) {
44
- const preUploadStep = this.driver.isSafari() ?
45
- // in safari we force the visibility ahead of time because making it visible the second time around doesn't work
46
- this.safariPreUploadActions(target) :
47
- Promise.resolve();
48
-
49
44
  try {
50
- await preUploadStep;
45
+ if (this.driver.isSafari()) {
46
+ await this.safariPreUploadActions(target);
47
+ }
51
48
  await this.uploadFiles(gridLocalFiles, target);
52
49
  } catch (err) {
53
50
  const edgeErrorEditableMessage = 'The element is not editable';
54
51
  const edgeErrorFocusableMessage = 'The element is not focusable';
55
- const safariErrorVisibleMessege = 'An element command could not be completed because the element is not visible on the page.';
52
+ const safariErrorVisibleMessage = 'An element command could not be completed because the element is not visible on the page.';
56
53
  const elementNotInteractable = 'element not interactable';
57
54
  const elementNotPointerOrKeyboardInteractable = 'element is not pointer- or keyboard interactable';
58
55
  const invalidStateMsg = 'invalid element state: Element is not currently interactable and may not be manipulated';
@@ -64,7 +61,7 @@ class InputFileStepAction extends StepAction {
64
61
  _.startsWith(errorMsg, mustBeVisibleMsg) ||
65
62
  _.startsWith(errorMsg, edgeErrorEditableMessage) ||
66
63
  _.startsWith(errorMsg, edgeErrorFocusableMessage) ||
67
- _.startsWith(errorMsg, safariErrorVisibleMessege) ||
64
+ _.startsWith(errorMsg, safariErrorVisibleMessage) ||
68
65
  _.includes(errorMsg, notReachableByKeyboard) ||
69
66
  _.includes(errorMsg, elementNotInteractable) ||
70
67
  _.includes(errorMsg, elementNotPointerOrKeyboardInteractable)
@@ -88,9 +85,18 @@ class InputFileStepAction extends StepAction {
88
85
  async performAction() {
89
86
  const target = this.context.data[this.step.targetId || 'targetId'];
90
87
  const overrideAzureStorageUrl = featureFlagService.flags.overrideAzureStorageUrl.isEnabled();
91
-
92
- const fileUrls = await utils.addTokenToFileUrl(this.context.project.id, this.step.fileUrls, this.stepActionUtils.testimServicesApi, overrideAzureStorageUrl, logger);
93
88
  const useJsInputCodeInSafari = featureFlagService.flags.useJsInputCodeInSafari.isEnabled();
89
+ const downloadToBase64 = featureFlagService.flags.downloadToBase64.isEnabled();
90
+
91
+ let fileUrls = await utils.addTokenToFileUrl(this.context.project.id, this.step.fileUrls, this.stepActionUtils.testimServicesApi, overrideAzureStorageUrl, logger);
92
+
93
+ if (downloadToBase64) {
94
+ fileUrls = await Promise.all(fileUrls.map(async ({ name, url }) => {
95
+ const res = await download(url);
96
+ return { name, url: `data:${res.type};base64,${Buffer.from(res.body).toString('base64')}` };
97
+ }));
98
+ }
99
+
94
100
  if (this.driver.isSafari() && (useJsInputCodeInSafari || fileUrls.length > 1)) {
95
101
  await this.driver.executeJSWithArray(`
96
102
  const getLocatedElement = ${codeSnippets.getLocatedElementCode};
@@ -165,15 +171,18 @@ function downloadAndUpload() {
165
171
  return new File([blob], name, { type: blob.type });
166
172
  }));
167
173
 
168
- fileList.item = function(ind) { return this[ind]; };
174
+ const dt = new DataTransfer();
175
+ for (const file of fileList) {
176
+ dt.items.add(file);
177
+ }
178
+ element.files = dt.files;
179
+
169
180
  let changeWasFired = false;
170
181
  const changeFiredHandler = (e) => {
171
182
  changeWasFired = true;
172
183
  };
173
184
 
174
185
  element.addEventListener("change", changeFiredHandler, true);
175
- Reflect.deleteProperty(element, 'files');
176
- Reflect.defineProperty(element, 'files', { get() { return fileList; }, configurable: true });
177
186
  await Promise.resolve(); // wait microtick
178
187
  element.dispatchEvent(new Event("input", { bubbles: true }));
179
188
  if (!changeWasFired) {
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
 
3
+
3
4
  const sessionPlayer = require('../../commons/getSessionPlayerRequire');
4
5
 
5
6
  const StepAction = require('./stepAction');
@@ -7,6 +8,7 @@ const constants = sessionPlayer.commonConstants.stepResult;
7
8
  const paramEvaluator = sessionPlayer.stepParamExpressionEvaluator;
8
9
  const utils = sessionPlayer.utils;
9
10
 
11
+
10
12
  const Promise = require('bluebird');
11
13
 
12
14
  class TextValidationStepAction extends StepAction {
@@ -15,6 +17,7 @@ class TextValidationStepAction extends StepAction {
15
17
  var context = this.context;
16
18
  var target = this.getTarget();
17
19
  var frameHandler = this.frameHandler;
20
+
18
21
 
19
22
  return new Promise(resolve => {
20
23
  var onFail = resultInfo => {
@@ -59,6 +59,7 @@ class WebDriver extends WebDriverApi {
59
59
  this.cdpUrl = undefined;
60
60
  this.browserClosedCallbacks = [];
61
61
  this.browserClosedFailedKeepAlives = 0;
62
+ this.ignoreHiddenTagsText = false;
62
63
  }
63
64
 
64
65
  registerToClosedBrowser(callback) {
@@ -71,6 +72,7 @@ class WebDriver extends WebDriverApi {
71
72
 
72
73
  async init(browserOptions, testName, testRunConfig, gridInfo, customExtensionLocalLocation, executionId, testResultId, seleniumPerfStats = new SeleniumPerfStats(), fastInit = false, lambdatestService) {
73
74
  this.browserClosedFailedKeepAlives = 0;
75
+ this.ignoreHiddenTagsText = _(browserOptions).get('company.activePlan.premiumFeatures.ignoreHiddenTagsText');
74
76
  this.browserClosedCallbacks = [];
75
77
  const capabilities = desiredCapabilitiesBuilder.buildSeleniumOptions(browserOptions, testName, testRunConfig, gridInfo, customExtensionLocalLocation, executionId, testResultId, lambdatestService);
76
78
  if (capabilities.desiredCapabilities) {
@@ -360,7 +362,14 @@ class WebDriver extends WebDriverApi {
360
362
  return element.shadowRoot.textContent.replace(/(\r\n|\n|\r)/gm, "");
361
363
  }
362
364
  } catch (err) { }
363
- return element.textContent.replace(/(\r\n|\n|\r)/gm, "");
365
+ if (this.ignoreHiddenTagsText && Array.prototype.some.call(element.children, elem => elem.hidden)) {
366
+ const dupElement = element.cloneNode(true);
367
+ const hiddenChildren = Array.prototype.filter.call(dupElement.children, elem => elem.hidden);
368
+ hiddenChildren.forEach(child => {
369
+ dupElement.removeChild(child);
370
+ });
371
+ return dupElement.textContent.replace(/(\r\n|\n|\r)/gm, "");
372
+ } return element.textContent.replace(/(\r\n|\n|\r)/gm, "");
364
373
  }
365
374
 
366
375
  function getElementTextContent(element) {
package/runOptions.js CHANGED
@@ -322,6 +322,7 @@ program
322
322
  // Agent mode
323
323
  .option('connect, --agent [enable-agent-mode]', 'enable Testim CLI agent mode', false)
324
324
  .option('start [enable-start]', 'Connect to testim and open the editor in a standalone browser', false)
325
+ .option('--download-browser', 'when used with the start option, downloads a fixed version to run Testim editor in', false)
325
326
  .option('--agent-port [agent-port]', 'set agent port', Number, 42543)
326
327
  .option('--agent-bind [agent-host-bind]', 'set agent host bind', '127.0.0.1')
327
328
 
@@ -5,12 +5,16 @@
5
5
  const { ArgError } = require('./errors');
6
6
  const _ = require('lodash');
7
7
  const runOptionsUtils = require('./runOptionsUtils');
8
+ const analytics = require('./commons/testimAnalytics');
8
9
 
9
10
  /**
10
11
  *
11
12
  * @param {import("commander").CommanderStatic} program
12
13
  */
13
14
  function isAgentFlow(program) {
15
+ if (program.start) {
16
+ analytics.track(null, 'cli-start-command', { downloadBrowser: Boolean(program.downloadBrowser) });
17
+ }
14
18
  if (program.startV2 || program.start || program.agent) {
15
19
  return true;
16
20
  }
@@ -73,6 +77,7 @@ async function runAgentFlow(program) {
73
77
  canary: program.canary,
74
78
  playerPath: program.playerPath,
75
79
  playerRequirePath: program.playerRequirePath,
80
+ downloadBrowser: Boolean(program.downloadBrowser),
76
81
  };
77
82
  }
78
83
 
package/runner.js CHANGED
@@ -25,7 +25,6 @@ const FREE_PLAN_MINIMUM_BROWSER_TIMEOUT = 30 * 60 * 1000;
25
25
  const TestPlanRunner = require('./runners/TestPlanRunner');
26
26
  const labFeaturesService = require('./services/labFeaturesService');
27
27
  const featureAvailabilityService = require('./commons/featureAvailabilityService');
28
- const featureFlagService = require('./commons/featureFlags');
29
28
 
30
29
  const logger = require('./commons/logger').getLogger('runner');
31
30
 
@@ -269,7 +268,7 @@ async function init(options) {
269
268
  await labFeaturesService.loadLabFeatures(projectById.id, companyByProjectId.activePlan);
270
269
  }
271
270
 
272
- if (options.lightweightMode && options.lightweightMode.type === 'turboMode' && (featureFlagService.flags.highSpeedMode.getValue() === 'disabled' || options.company.planType === 'free')) {
271
+ if (options.lightweightMode && options.lightweightMode.type === 'turboMode' && (featureFlags.flags.highSpeedMode.getValue() === 'disabled' || options.company.planType === 'free')) {
273
272
  delete options.lightweightMode;
274
273
  }
275
274
 
@@ -8,7 +8,6 @@ const config = require('../commons/config');
8
8
  const ExecutionQueue = require('../executionQueue');
9
9
 
10
10
  const testimCustomToken = require('../commons/testimCustomToken');
11
- const testimServicesApi = require('../commons/testimServicesApi');
12
11
  const labFeaturesService = require('../services/labFeaturesService');
13
12
  const perf = require('../commons/performance-logger');
14
13
  const { StopRunOnError } = require('../errors');
@@ -20,7 +20,6 @@ const { getSuite, calcTestResultStatus, validateConfig } = require('./runnerUtil
20
20
  const { StopRunOnError, ArgError } = require('../errors');
21
21
  const Logger = require('../commons/logger');
22
22
  const perf = require('../commons/performance-logger');
23
- const featureFlags = require('../commons/featureFlags');
24
23
 
25
24
  const guid = utils.guid;
26
25
  const logger = Logger.getLogger('test-plan-runner');
@@ -35,14 +34,14 @@ class TestPlanRunner {
35
34
  const executionResults = {};
36
35
  const authData = testimCustomToken.getTokenV3UserData();
37
36
 
38
- const runBeforeTests = (beforeTests, testStatus, executionId, executionName, tpOptions, branchToUse, authData) => {
37
+ const runBeforeTests = () => {
39
38
  const workerCount = 1;
40
39
  const stopOnError = true;
41
40
  return this.workerManager.runTests(beforeTests, testStatus, executionId, executionName, tpOptions, branchToUse, authData, workerCount, stopOnError)
42
41
  .then(beforeTestsResults => Object.assign(executionResults, beforeTestsResults));
43
42
  };
44
43
 
45
- const runTestPlanTests = (tests, testStatus, executionId, executionName, tpOptions, branchToUse, authData) => {
44
+ const runTestPlanTests = () => {
46
45
  const workerCount = config.TESTIM_CONCURRENT_WORKER_COUNT || tpOptions.parallel;
47
46
  const stopOnError = false;
48
47
  perf.log('right before this.workerManager.runTests');
@@ -51,30 +50,30 @@ class TestPlanRunner {
51
50
  .then(testsResults => Object.assign(executionResults, testsResults));
52
51
  };
53
52
 
54
- const runAfterTests = (afterTests, testStatus, executionId, executionName, tpOptions, branchToUse, authData) => {
53
+ const runAfterTests = () => {
55
54
  const workerCount = 1;
56
55
  const stopOnError = false;
57
56
  return this.workerManager.runTests(afterTests, testStatus, executionId, executionName, tpOptions, branchToUse, authData, workerCount, stopOnError)
58
57
  .then(afterTestsResults => Object.assign(executionResults, afterTestsResults));
59
58
  };
60
59
 
61
- function catchBeforeTestsFailed(executionId) {
60
+ function catchBeforeTestsFailed() {
62
61
  return testStatus.markAllQueuedTests(executionId, constants.runnerTestStatus.ABORTED, 'aborted', false);
63
62
  }
64
63
 
65
64
  const sessionType = utils.getSessionType(tpOptions);
66
65
  analyticsService.analyticsExecsStart({ authData, executionId, projectId: tpOptions.project, sessionType });
67
66
  perf.log('right before runBeforeTests');
68
- return runBeforeTests(beforeTests, testStatus, executionId, executionName, tpOptions, branchToUse, authData)
67
+ return runBeforeTests()
69
68
  .log('right before runTestPlanTests')
70
- .then(() => runTestPlanTests(tests, testStatus, executionId, executionName, tpOptions, branchToUse, authData))
69
+ .then(() => runTestPlanTests())
71
70
  .log('right after runTestPlanTests')
72
- .then(() => runAfterTests(afterTests, testStatus, executionId, executionName, tpOptions, branchToUse, authData))
71
+ .then(() => runAfterTests())
73
72
  .then(() => executionResults)
74
73
  .catch(err => {
75
74
  logger.error('error running test plan', { err });
76
75
  if (err instanceof StopRunOnError) {
77
- return catchBeforeTestsFailed(executionId);
76
+ return catchBeforeTestsFailed();
78
77
  }
79
78
  throw err;
80
79
  })
@@ -1,13 +1,12 @@
1
1
  const os = require('os');
2
- const { spawn } = require('child_process');
2
+ const childProcess = require('child_process');
3
3
  const Promise = require('bluebird');
4
4
  const fse = require('fs-extra');
5
5
  const portfinder = require('portfinder');
6
6
  const ms = require('ms');
7
7
 
8
- const {
9
- downloadAndSave, unzipFile, guid, runWithRetries, isURL,
10
- } = require('../utils');
8
+ const { guid, isURL } = require('../utils');
9
+ const utils = require('../utils');
11
10
  const { gridTypes, CLI_MODE } = require('../commons/constants');
12
11
  const httpRequest = require('../commons/httpRequest');
13
12
  const { ArgError } = require('../errors');
@@ -86,8 +85,8 @@ class LambdatestService {
86
85
  throw new Error(`tunnel on ${os.platform() + os.arch()} platform is not supported.`);
87
86
  }
88
87
  const zipLocation = `${LT_TUNNEL_BINARY_DIRECTORY}.zip`;
89
- await downloadAndSave(`${LT_TUNNEL_BINARY_ORIGIN}/${downloadUrl}`, zipLocation);
90
- await unzipFile(zipLocation, LT_TUNNEL_BINARY_DIRECTORY);
88
+ await utils.downloadAndSave(`${LT_TUNNEL_BINARY_ORIGIN}/${downloadUrl}`, zipLocation);
89
+ await utils.unzipFile(zipLocation, LT_TUNNEL_BINARY_DIRECTORY);
91
90
  }
92
91
 
93
92
  static async connectTunnel(runnerOptions) {
@@ -140,7 +139,7 @@ class LambdatestService {
140
139
  tunnelArgs = [...tunnelArgs, '--mitm'];
141
140
  }
142
141
 
143
- LambdatestService.tunnel = spawn('./LT', tunnelArgs, { cwd: LT_TUNNEL_BINARY_DIRECTORY });
142
+ LambdatestService.tunnel = childProcess.spawn('./LT', tunnelArgs, { cwd: LT_TUNNEL_BINARY_DIRECTORY });
144
143
 
145
144
  let stdoutResult = '';
146
145
  let stderrResult = '';
@@ -155,7 +154,7 @@ class LambdatestService {
155
154
 
156
155
  // verify that LT tunnel strated successfully
157
156
  try {
158
- const ltInfo = await runWithRetries(() => httpRequest.get(`http://127.0.0.1:${infoAPIPort}/api/v1.0/info`, {}, {}, undefined, { skipProxy: true }), 30, 2000);
157
+ const ltInfo = await utils.runWithRetries(() => httpRequest.get(`http://127.0.0.1:${infoAPIPort}/api/v1.0/info`, {}, {}, undefined, { skipProxy: true }), 30, 2000);
159
158
  logger.info('LT tunnel info', ltInfo);
160
159
  } catch (err) {
161
160
  logger.error('Failed to start LT tunnel', { err, stdoutResult, stderrResult });
@@ -170,7 +169,7 @@ class LambdatestService {
170
169
  return new Promise((resolve, reject) => {
171
170
  LambdatestService.tunnel.on('close', (code) => {
172
171
  if (code) {
173
- reject();
172
+ reject(new Error(`tunnel process exited with code ${code}`));
174
173
  }
175
174
  resolve();
176
175
  });
@@ -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
  });