@testim/testim-cli 3.212.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.
@@ -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
- .tapCatch((err) => logger.error('failed to delete request', { url, error: { message: err.message, stack: err.stack } }));
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
- .tapCatch((err) => logger.error('failed to post request', { url, error: { message: err.message, stack: err.stack } }));
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
- .tapCatch((err) => logger.error('failed to post request', { url, error: { message: err.message, stack: err.stack } }));
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
- .tapCatch((err) => logger.error('failed to getText request', { url, query, error: { message: err.message, stack: err.stack } }));
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
- .tapCatch((err) => logger.warn('failed to get request', { url, query, error: { message: err.message, stack: err.stack } }));
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
- .tapCatch((err) => logger.error('failed to head request', { url, error: { message: err.message, stack: err.stack } }));
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
- .tapCatch((err) => logger.error('failed to put request', { url, error: { message: err.message, stack: err.stack } }));
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
- .tapCatch((err) => logger.error('failed to download', { error: { message: err.message, stack: err.stack } }));
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 { method } = require('bluebird');
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
- // 1. more than 10 requests failed and there are fewer than 100 requests
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
- if (allCalls < 100) {
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'); }), 20, 1);
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'); }), 20, 1);
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 recovering network as healthy', async () => {
26
- const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }), 20, 1);
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(200).fill().map(fn2));
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
  });
@@ -2509,9 +2509,9 @@
2509
2509
  },
2510
2510
  "dependencies": {
2511
2511
  "@types/node": {
2512
- "version": "17.0.2",
2513
- "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.2.tgz",
2514
- "integrity": "sha512-JepeIUPFDARgIs0zD/SKPgFsJEAF0X5/qO80llx59gOxFTboS9Amv3S+QfB7lqBId5sFXJ99BN0J6zFRvL9dDA=="
2512
+ "version": "17.0.4",
2513
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.4.tgz",
2514
+ "integrity": "sha512-6xwbrW4JJiJLgF+zNypN5wr2ykM9/jHcL7rQ8fZe2vuftggjzZeRSM4OwRc6Xk8qWjwJ99qVHo/JgOGmomWRog=="
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.26",
3505
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.26.tgz",
3506
- "integrity": "sha512-cA1YwlRzO6TGp7yd3+KAqh9Tt6Z4CuuKqsAJP6uF/H5MQryjAGDhMhnY5cEXo8MaRCczpzSBhMPdqRIodkbZYw==",
3504
+ "version": "1.4.27",
3505
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.27.tgz",
3506
+ "integrity": "sha512-uZ95szi3zUbzRDx1zx/xnsCG+2xgZyy57pDOeaeO4r8zx5Dqe8Jv1ti8cunvBwJHVI5LzPuw8umKwZb3WKYxSQ==",
3507
3507
  "dev": true
3508
3508
  },
3509
3509
  "emoji-regex": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testim/testim-cli",
3
- "version": "3.212.0",
3
+ "version": "3.213.0",
4
4
  "description": "Command line interface for running Testing on your CI",
5
5
  "author": "Oren Rubin",
6
6
  "contributors": [{
@@ -34,7 +34,10 @@ class InputFileStepAction extends StepAction {
34
34
  top: '10px',
35
35
  };
36
36
  return this.forceInputToBeVisible(target, options)
37
- .tapCatch(err => logger.error('failed to set input file in Safari recovery', { err }));
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
- await openedEye.check({ settings: { region, fully: this.step.action === 'stitched' } });
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 };
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 = 50 * 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,13 @@ 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
- runRequestParams.runData = this.getRunData();
180
+ const runData = this.getRunData();
181
+ runRequestParams.lightweightMode.isRunDataSentInUrl = canSendRunDataOverUrl(runData);
182
+ if (runRequestParams.lightweightMode.isRunDataSentInUrl) {
183
+ runRequestParams.runData = runData;
184
+ } else {
185
+ logger.warn(`Run data is too big to be sent as a URL param (lightweight mode). Run data size: ${JSON.stringify(runData).length} (limit: ${MAX_LIGHTWEIGHT_MODE_RUN_DATA_SIZE} characters)`);
186
+ }
179
187
  runRequestParams.isLocalRun = Boolean(this._options.useLocalChromeDriver || this._options.useChromeLauncher);
180
188
  }
181
189
 
@@ -240,7 +248,7 @@ TestRun.prototype.clearTestResult = function () {
240
248
  const mustClearPreviousStepResults = !this.isAllowReportTestResultRetries() && (this._timeoutRetryCount > 1 || this._retryCount > 1);
241
249
 
242
250
  if (this._options.lightweightMode && this._options.lightweightMode.disableResults &&
243
- !mustClearPreviousStepResults) {
251
+ !mustClearPreviousStepResults && canSendRunDataOverUrl(runData)) {
244
252
  return Promise.resolve();
245
253
  }
246
254
 
@@ -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 (!isNetworkHealthy() && featureFlags.flags.errorMessageOnBadNetwork.isEnabled()) {
270
+ const buildError = (err, wasNetworkHealthy) => {
271
+ if (!wasNetworkHealthy && featureFlags.flags.errorMessageOnBadNetwork.isEnabled()) {
270
272
  return {
271
273
  errorType: NETWORK_ERROR,
272
- reason: 'Due to network connectivity issues, Testim CLI has been unable to connect to the grid.' +
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
- if (!isNetworkHealthy() && featureFlags.flags.warnOnBadNetwork.isEnabled()) {
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('Due to network connectivity issues, Testim CLI has been unable to connect to the grid.');
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; }).tapCatch(err => logger.error('error from driver.url', { err, testResultId, executionId, testId })),
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)
@@ -110,7 +113,7 @@ class WorkerExtension extends BaseWorker {
110
113
  .tap(testResult => {
111
114
  driver.unregisterToClosedBrowser(onBrowserClosed);
112
115
  if (this.lambdatestService.isLambdatestRun()) {
113
- return driver.executeJS(`lambda-status=${!testResult.success ? 'failed' : 'passed'}`).catch(() => {});
116
+ return driver.executeJS(`lambda-status=${!testResult.success ? 'failed' : 'passed'}`).catch(() => { });
114
117
  }
115
118
  return undefined;
116
119
  })
@@ -40,7 +40,10 @@ module.exports.getBrowserWithRetries = ({ getBrowserOnce, testPlayerFactory, rel
40
40
  return getBrowserOnce(player)
41
41
  .then((getBrowserRes) => player || getBrowserRes)
42
42
  .timeout(singleGetBrowserDuration, timeoutMessages.GET_BROWSER_TIMEOUT_MSG)
43
- .tapCatch(() => reporter.onGetBrowserFailure(workerId, projectId, ++failedAttempts))
43
+ .catch(err => {
44
+ reporter.onGetBrowserFailure(workerId, projectId, ++failedAttempts);
45
+ throw err;
46
+ })
44
47
  .tap(() => reporter.onGetBrowserSuccess(workerId, projectId))
45
48
  .catch(err => waitUntilBrowserTimeout(err, startTime, singleGetBrowserDuration, projectId, workerId, player, releaseSlotOnTestFinished));
46
49
  }, maxGetBrowserAttempts).catch(err => {