codeceptjs 3.1.2 → 3.2.2

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +2 -3
  3. package/bin/codecept.js +1 -0
  4. package/docs/advanced.md +94 -60
  5. package/docs/basics.md +27 -2
  6. package/docs/bdd.md +57 -3
  7. package/docs/build/Appium.js +8 -4
  8. package/docs/build/FileSystem.js +1 -0
  9. package/docs/build/Playwright.js +39 -30
  10. package/docs/build/Protractor.js +9 -24
  11. package/docs/build/Puppeteer.js +10 -28
  12. package/docs/build/REST.js +1 -0
  13. package/docs/build/WebDriver.js +3 -25
  14. package/docs/changelog.md +103 -0
  15. package/docs/commands.md +21 -7
  16. package/docs/custom-helpers.md +1 -36
  17. package/docs/helpers/Appium.md +34 -30
  18. package/docs/helpers/FileSystem.md +1 -1
  19. package/docs/helpers/Playwright.md +16 -18
  20. package/docs/helpers/Puppeteer.md +1 -17
  21. package/docs/helpers/REST.md +3 -1
  22. package/docs/helpers/WebDriver.md +1 -17
  23. package/docs/mobile-react-native-locators.md +3 -0
  24. package/docs/pageobjects.md +2 -0
  25. package/docs/playwright.md +16 -33
  26. package/docs/plugins.md +128 -3
  27. package/docs/reports.md +23 -5
  28. package/lib/actor.js +20 -2
  29. package/lib/codecept.js +2 -0
  30. package/lib/command/info.js +1 -1
  31. package/lib/config.js +13 -1
  32. package/lib/container.js +3 -1
  33. package/lib/data/dataTableArgument.js +35 -0
  34. package/lib/helper/Appium.js +8 -4
  35. package/lib/helper/FileSystem.js +1 -0
  36. package/lib/helper/Playwright.js +39 -20
  37. package/lib/helper/Protractor.js +2 -14
  38. package/lib/helper/Puppeteer.js +3 -18
  39. package/lib/helper/REST.js +1 -0
  40. package/lib/helper/WebDriver.js +3 -15
  41. package/lib/index.js +2 -0
  42. package/lib/interfaces/featureConfig.js +3 -0
  43. package/lib/interfaces/gherkin.js +7 -1
  44. package/lib/interfaces/scenarioConfig.js +4 -0
  45. package/lib/listener/helpers.js +1 -0
  46. package/lib/listener/steps.js +21 -3
  47. package/lib/listener/timeout.js +72 -0
  48. package/lib/locator.js +3 -0
  49. package/lib/mochaFactory.js +2 -3
  50. package/lib/plugin/allure.js +6 -1
  51. package/lib/plugin/coverage.js +1 -1
  52. package/lib/plugin/retryFailedStep.js +4 -3
  53. package/lib/plugin/retryTo.js +130 -0
  54. package/lib/plugin/screenshotOnFail.js +1 -0
  55. package/lib/plugin/stepByStepReport.js +7 -0
  56. package/lib/plugin/stepTimeout.js +91 -0
  57. package/lib/recorder.js +23 -9
  58. package/lib/step.js +58 -0
  59. package/lib/store.js +2 -0
  60. package/lib/ui.js +2 -2
  61. package/package.json +4 -6
  62. package/typings/index.d.ts +8 -1
  63. package/typings/types.d.ts +103 -70
  64. package/docs/angular.md +0 -325
  65. package/docs/helpers/Protractor.md +0 -1658
  66. package/docs/webapi/waitUntil.mustache +0 -11
  67. package/typings/Protractor.d.ts +0 -16
@@ -1,5 +1,7 @@
1
+ const debug = require('debug')('codeceptjs:steps');
1
2
  const event = require('../event');
2
3
  const store = require('../store');
4
+ const output = require('../output');
3
5
 
4
6
  let currentTest;
5
7
  let currentHook;
@@ -9,6 +11,7 @@ let currentHook;
9
11
  */
10
12
  module.exports = function () {
11
13
  event.dispatcher.on(event.test.before, (test) => {
14
+ test.startedAt = +new Date();
12
15
  test.artifacts = {};
13
16
  });
14
17
 
@@ -19,17 +22,23 @@ module.exports = function () {
19
22
  else currentTest.retryNum += 1;
20
23
  });
21
24
 
22
- event.dispatcher.on(event.test.after, () => {
25
+ event.dispatcher.on(event.test.after, (test) => {
23
26
  currentTest = null;
24
27
  });
25
28
 
26
- event.dispatcher.on(event.hook.passed, () => {
27
- currentHook = null;
29
+ event.dispatcher.on(event.test.finished, (test) => {
28
30
  });
29
31
 
30
32
  event.dispatcher.on(event.hook.started, (suite) => {
31
33
  currentHook = suite.ctx.test;
32
34
  currentHook.steps = [];
35
+
36
+ if (suite.ctx && suite.ctx.test) output.log(`--- STARTED ${suite.ctx.test.title} ---`);
37
+ });
38
+
39
+ event.dispatcher.on(event.hook.passed, (suite) => {
40
+ currentHook = null;
41
+ if (suite.ctx && suite.ctx.test) output.log(`--- ENDED ${suite.ctx.test.title} ---`);
33
42
  });
34
43
 
35
44
  event.dispatcher.on(event.test.failed, () => {
@@ -51,6 +60,7 @@ module.exports = function () {
51
60
  });
52
61
 
53
62
  event.dispatcher.on(event.test.passed, () => {
63
+ if (!currentTest) return;
54
64
  // To be sure that passed test will be passed in report
55
65
  delete currentTest.err;
56
66
  currentTest.state = 'passed';
@@ -58,10 +68,18 @@ module.exports = function () {
58
68
 
59
69
  event.dispatcher.on(event.step.started, (step) => {
60
70
  if (store.debugMode) return;
71
+ step.startedAt = +new Date();
61
72
  if (currentHook && Array.isArray(currentHook.steps)) {
62
73
  return currentHook.steps.push(step);
63
74
  }
64
75
  if (!currentTest || !currentTest.steps) return;
65
76
  currentTest.steps.push(step);
66
77
  });
78
+
79
+ event.dispatcher.on(event.step.finished, (step) => {
80
+ if (store.debugMode) return;
81
+ step.finishedAt = +new Date();
82
+ if (step.startedAt) step.duration = step.finishedAt - step.startedAt;
83
+ debug(`Step '${step}' finished; Duration: ${step.duration}ms`);
84
+ });
67
85
  };
@@ -0,0 +1,72 @@
1
+ const event = require('../event');
2
+ const output = require('../output');
3
+ const recorder = require('../recorder');
4
+ const Config = require('../config');
5
+ const { timeouts } = require('../store');
6
+ const TIMEOUT_ORDER = require('../step').TIMEOUT_ORDER;
7
+
8
+ module.exports = function () {
9
+ let timeout;
10
+ let timeoutStack = [];
11
+ let currentTest;
12
+ let currentTimeout;
13
+
14
+ if (!timeouts) {
15
+ console.log('Timeouts were disabled');
16
+ return;
17
+ }
18
+
19
+ event.dispatcher.on(event.suite.before, (suite) => {
20
+ timeoutStack = [];
21
+ const globalTimeout = Config.get('timeout');
22
+ if (globalTimeout) {
23
+ if (globalTimeout >= 1000) {
24
+ console.log(`Warning: Timeout was set to ${globalTimeout}secs.\nGlobal timeout should be specified in seconds.`);
25
+ }
26
+ timeoutStack.push(globalTimeout);
27
+ }
28
+ if (suite.totalTimeout) timeoutStack.push(suite.totalTimeout);
29
+ output.log(`Timeouts: ${timeoutStack}`);
30
+ });
31
+
32
+ event.dispatcher.on(event.test.before, (test) => {
33
+ currentTest = test;
34
+ timeout = test.totalTimeout || timeoutStack[timeoutStack.length - 1];
35
+ if (!timeout) return;
36
+ currentTimeout = timeout;
37
+ output.debug(`Test Timeout: ${timeout}s`);
38
+ timeout *= 1000;
39
+ });
40
+
41
+ event.dispatcher.on(event.test.passed, (test) => {
42
+ currentTest = null;
43
+ });
44
+
45
+ event.dispatcher.on(event.test.failed, (test) => {
46
+ currentTest = null;
47
+ });
48
+
49
+ event.dispatcher.on(event.step.before, (step) => {
50
+ if (typeof timeout !== 'number') return;
51
+
52
+ if (timeout < 0) {
53
+ step.setTimeout(0.01, TIMEOUT_ORDER.testOrSuite);
54
+ } else {
55
+ step.setTimeout(timeout, TIMEOUT_ORDER.testOrSuite);
56
+ }
57
+ });
58
+
59
+ event.dispatcher.on(event.step.finished, (step) => {
60
+ if (typeof timeout === 'number' && !Number.isNaN(timeout)) timeout -= step.duration;
61
+
62
+ if (typeof timeout === 'number' && timeout <= 0 && recorder.isRunning()) {
63
+ if (currentTest && currentTest.callback) {
64
+ recorder.reset();
65
+ // replace mocha timeout with custom timeout
66
+ currentTest.timeout(0);
67
+ currentTest.callback(new Error(`Timeout ${currentTimeout}s exceeded (with Before hook)`));
68
+ currentTest.timedOut = true;
69
+ }
70
+ }
71
+ });
72
+ };
package/lib/locator.js CHANGED
@@ -313,11 +313,14 @@ Locator.build = (locator) => {
313
313
 
314
314
  /**
315
315
  * Filters to modify locators
316
+ * @type {Array<function(CodeceptJS.LocatorOrString, Locator): void>}
316
317
  */
317
318
  Locator.filters = [];
318
319
 
319
320
  /**
320
321
  * Appends new `Locator` filter to an `Locator.filters` array, and returns the new length of the array.
322
+ * @param {function(CodeceptJS.LocatorOrString, Locator): void} fn
323
+ * @returns {number}
321
324
  */
322
325
  Locator.addFilter = fn => Locator.filters.push(fn);
323
326
 
@@ -2,7 +2,7 @@ const Mocha = require('mocha');
2
2
  const fsPath = require('path');
3
3
  const fs = require('fs');
4
4
  const reporter = require('./cli');
5
- const gherkinParser = require('./interfaces/gherkin.js');
5
+ const gherkinParser = require('./interfaces/gherkin');
6
6
  const output = require('./output');
7
7
  const { genTestId } = require('./utils');
8
8
  const ConnectionRefused = require('./helper/errors/ConnectionRefused');
@@ -35,8 +35,7 @@ class MochaFactory {
35
35
  if (mocha.suite.suites.length === 0) {
36
36
  mocha.files
37
37
  .filter(file => file.match(/\.feature$/))
38
- .map(file => fs.readFileSync(file, 'utf8'))
39
- .forEach(content => mocha.suite.addSuite(gherkinParser(content)));
38
+ .forEach(file => mocha.suite.addSuite(gherkinParser(fs.readFileSync(file, 'utf8'), file)));
40
39
 
41
40
  // remove feature files
42
41
  mocha.files = mocha.files.filter(file => !file.match(/\.feature$/));
@@ -283,7 +283,12 @@ module.exports = (config) => {
283
283
  let maxLevel;
284
284
  function finishMetastep(level) {
285
285
  const metaStepsToFinish = currentMetaStep.splice(maxLevel - level);
286
- metaStepsToFinish.forEach(() => reporter.endStep('passed'));
286
+ metaStepsToFinish.forEach(() => {
287
+ // only if the current step is of type Step, end it.
288
+ if (reporter.suites && reporter.suites.length && reporter.suites[0].currentStep && reporter.suites[0].currentStep.constructor.name === 'Step') {
289
+ reporter.endStep('passed');
290
+ }
291
+ });
287
292
  }
288
293
 
289
294
  function startMetaStep(metaStep, level = 0) {
@@ -48,7 +48,7 @@ function buildFileName(test, uniqueFileName) {
48
48
  *
49
49
  * ```js
50
50
  * plugins: {
51
- * playwrightCoverage: {
51
+ * coverage: {
52
52
  * enabled: true
53
53
  * }
54
54
  * }
@@ -2,7 +2,7 @@ const event = require('../event');
2
2
  const recorder = require('../recorder');
3
3
 
4
4
  const defaultConfig = {
5
- retries: 5,
5
+ retries: 3,
6
6
  defaultIgnoredSteps: [
7
7
  'amOnPage',
8
8
  'wait*',
@@ -11,6 +11,7 @@ const defaultConfig = {
11
11
  'run*',
12
12
  'have*',
13
13
  ],
14
+ factor: 1.5,
14
15
  ignoredSteps: [],
15
16
  };
16
17
 
@@ -36,9 +37,9 @@ const defaultConfig = {
36
37
  *
37
38
  * #### Configuration:
38
39
  *
39
- * * `retries` - number of retries (by default 5),
40
+ * * `retries` - number of retries (by default 3),
40
41
  * * `when` - function, when to perform a retry (accepts error as parameter)
41
- * * `factor` - The exponential factor to use. Default is 2.
42
+ * * `factor` - The exponential factor to use. Default is 1.5.
42
43
  * * `minTimeout` - The number of milliseconds before starting the first retry. Default is 1000.
43
44
  * * `maxTimeout` - The maximum number of milliseconds between two retries. Default is Infinity.
44
45
  * * `randomize` - Randomizes the timeouts by multiplying with a factor between 1 to 2. Default is false.
@@ -0,0 +1,130 @@
1
+ const recorder = require('../recorder');
2
+ const store = require('../store');
3
+ const { debug } = require('../output');
4
+
5
+ const defaultConfig = {
6
+ registerGlobal: true,
7
+ pollInterval: 200,
8
+ };
9
+
10
+ /**
11
+ *
12
+ *
13
+ * Adds global `retryTo` which retries steps a few times before failing.
14
+ *
15
+ * Enable this plugin in `codecept.conf.js` (enabled by default for new setups):
16
+ *
17
+ * ```js
18
+ * plugins: {
19
+ * retryTo: {
20
+ * enabled: true
21
+ * }
22
+ * }
23
+ * ```
24
+ *
25
+ * Use it in your tests:
26
+ *
27
+ * ```js
28
+ * // retry these steps 5 times before failing
29
+ * await retryTo((tryNum) => {
30
+ * I.switchTo('#editor frame');
31
+ * I.click('Open');
32
+ * I.see('Opened')
33
+ * }, 5);
34
+ * ```
35
+ * Set polling interval as 3rd argument (200ms by default):
36
+ *
37
+ * ```js
38
+ * // retry these steps 5 times before failing
39
+ * await retryTo((tryNum) => {
40
+ * I.switchTo('#editor frame');
41
+ * I.click('Open');
42
+ * I.see('Opened')
43
+ * }, 5, 100);
44
+ * ```
45
+ *
46
+ * Default polling interval can be changed in a config:
47
+ *
48
+ * ```js
49
+ * plugins: {
50
+ * retryTo: {
51
+ * enabled: true,
52
+ * pollInterval: 500,
53
+ * }
54
+ * }
55
+ * ```
56
+ *
57
+ * Disables retryFailedStep plugin for steps inside a block;
58
+ *
59
+ * Use this plugin if:
60
+ *
61
+ * * you need repeat a set of actions in flaky tests
62
+ * * iframe was not rendered and you need to retry switching to it
63
+ *
64
+ *
65
+ * #### Configuration
66
+ *
67
+ * * `pollInterval` - default interval between retries in ms. 200 by default.
68
+ * * `registerGlobal` - to register `retryTo` function globally, true by default
69
+ *
70
+ * If `registerGlobal` is false you can use retryTo from the plugin:
71
+ *
72
+ * ```js
73
+ * const retryTo = codeceptjs.container.plugins('retryTo');
74
+ * ```
75
+ *
76
+ */
77
+ module.exports = function (config) {
78
+ config = Object.assign(defaultConfig, config);
79
+
80
+ if (config.registerGlobal) {
81
+ global.retryTo = retryTo;
82
+ }
83
+ return retryTo;
84
+
85
+ function retryTo(callback, maxTries, pollInterval = undefined) {
86
+ const mode = store.debugMode;
87
+ let tries = 1;
88
+ if (!pollInterval) pollInterval = config.pollInterval;
89
+
90
+ let err = null;
91
+
92
+ return new Promise((done) => {
93
+ const tryBlock = () => {
94
+ recorder.session.start(`retryTo ${tries}`);
95
+ callback(tries);
96
+ recorder.add(() => {
97
+ recorder.session.restore(`retryTo ${tries}`);
98
+ done(null);
99
+ });
100
+ recorder.session.catch((e) => {
101
+ err = e;
102
+ recorder.session.restore(`retryTo ${tries}`);
103
+ tries++;
104
+ // recorder.session.restore(`retryTo`);
105
+ if (tries <= maxTries) {
106
+ debug(`Error ${err}... Retrying`);
107
+ err = null;
108
+
109
+ recorder.add(`retryTo ${tries}`, () => {
110
+ tryBlock();
111
+ // recorder.add(() => new Promise(done => setTimeout(done, pollInterval)));
112
+ });
113
+ } else {
114
+ // recorder.throw(err);
115
+ done(null);
116
+ }
117
+ });
118
+ // return recorder.promise();
119
+ };
120
+
121
+ recorder.add('retryTo', async () => {
122
+ store.debugMode = true;
123
+ tryBlock();
124
+ // recorder.add(() => recorder.session.restore(`retryTo ${tries-1}`));
125
+ });
126
+ }).then(() => {
127
+ if (err) recorder.throw(err);
128
+ });
129
+ }
130
+ };
@@ -98,6 +98,7 @@ module.exports = function (config) {
98
98
  }
99
99
  await helper.saveScreenshot(fileName, options.fullPageScreenshots);
100
100
 
101
+ if (!test.artifacts) test.artifacts = {};
101
102
  test.artifacts.screenshot = path.join(global.output_dir, fileName);
102
103
  if (Container.mocha().options.reporterOptions['mocha-junit-reporter'] && Container.mocha().options.reporterOptions['mocha-junit-reporter'].options.attachments) {
103
104
  test.attachments = [path.join(global.output_dir, fileName)];
@@ -20,6 +20,7 @@ const defaultConfig = {
20
20
  fullPageScreenshots: false,
21
21
  output: global.output_dir,
22
22
  screenshotsForAllureReport: false,
23
+ disableScreenshotOnFail: true,
23
24
  };
24
25
 
25
26
  const templates = {};
@@ -56,6 +57,7 @@ const templates = {};
56
57
  * * `fullPageScreenshots`: should full page screenshots be used. Default: false.
57
58
  * * `output`: a directory where reports should be stored. Default: `output`.
58
59
  * * `screenshotsForAllureReport`: If Allure plugin is enabled this plugin attaches each saved screenshot to allure report. Default: false.
60
+ * * `disableScreenshotOnFail : Disables the capturing of screeshots after the failed step. Default: true.
59
61
  *
60
62
  * @param {*} config
61
63
  */
@@ -80,6 +82,7 @@ module.exports = function (config) {
80
82
  let error;
81
83
  let savedStep = null;
82
84
  let currentTest = null;
85
+ let scenarioFailed = false;
83
86
 
84
87
  const recordedTests = {};
85
88
  const pad = '0000';
@@ -135,10 +138,14 @@ module.exports = function (config) {
135
138
  if (isStepIgnored(step)) return;
136
139
  if (savedStep === step) return; // already saved
137
140
  // Ignore steps from BeforeSuite function
141
+ if (scenarioFailed && config.disableScreenshotOnFail) return;
138
142
  if (step.metaStep && step.metaStep.name === 'BeforeSuite') return;
139
143
 
140
144
  const fileName = `${pad.substring(0, pad.length - stepNum.toString().length) + stepNum.toString()}.png`;
141
145
  try {
146
+ if (step.status === 'failed') {
147
+ scenarioFailed = true;
148
+ }
142
149
  stepNum++;
143
150
  slides[fileName] = step;
144
151
  await helper.saveScreenshot(path.relative(reportDir, path.join(dir, fileName)), config.fullPageScreenshots);
@@ -0,0 +1,91 @@
1
+ const event = require('../event');
2
+ const TIMEOUT_ORDER = require('../step').TIMEOUT_ORDER;
3
+
4
+ const defaultConfig = {
5
+ timeout: 150,
6
+ overrideStepLimits: false,
7
+ noTimeoutSteps: [
8
+ 'amOnPage',
9
+ 'wait*',
10
+ ],
11
+ customTimeoutSteps: [],
12
+ };
13
+
14
+ /**
15
+ * Set timeout for test steps globally.
16
+ *
17
+ * Add this plugin to config file:
18
+ *
19
+ * ```js
20
+ * plugins: {
21
+ * stepTimeout: {
22
+ * enabled: true
23
+ * }
24
+ * }
25
+ * ```
26
+ *
27
+ *
28
+ * Run tests with plugin enabled:
29
+ *
30
+ * ```
31
+ * npx codeceptjs run --plugins stepTimeout
32
+ * ```
33
+ *
34
+ * #### Configuration:
35
+ *
36
+ * * `timeout` - global step timeout, default 150 seconds
37
+ * * `overrideStepLimits` - whether to use timeouts set in plugin config to override step timeouts set in code with I.limitTime(x).action(...), default false
38
+ * * `noTimeoutSteps` - an array of steps with no timeout. Default:
39
+ * * `amOnPage`
40
+ * * `wait*`
41
+ *
42
+ * you could set your own noTimeoutSteps which would replace the default one.
43
+ *
44
+ * * `customTimeoutSteps` - an array of step actions with custom timeout. Use it to override or extend noTimeoutSteps.
45
+ * You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`.
46
+ *
47
+ * #### Example
48
+ *
49
+ * ```js
50
+ * plugins: {
51
+ * stepTimeout: {
52
+ * enabled: true,
53
+ * overrideStepLimits: true,
54
+ * noTimeoutSteps: [
55
+ * 'scroll*', // ignore all scroll steps
56
+ * /Cookie/, // ignore all steps with a Cookie in it (by regexp)
57
+ * ],
58
+ * customTimeoutSteps: [
59
+ * ['myFlakyStep*', 1],
60
+ * ['scrollWhichRequiresTimeout', 5],
61
+ * ]
62
+ * }
63
+ * }
64
+ * ```
65
+ *
66
+ */
67
+ module.exports = (config) => {
68
+ config = Object.assign(defaultConfig, config);
69
+ // below override rule makes sure customTimeoutSteps go first but then they override noTimeoutSteps in case of exact pattern match
70
+ config.customTimeoutSteps = config.customTimeoutSteps.concat(config.noTimeoutSteps).concat(config.customTimeoutSteps);
71
+
72
+ event.dispatcher.on(event.step.before, (step) => {
73
+ let stepTimeout;
74
+ for (let stepRule of config.customTimeoutSteps) {
75
+ let customTimeout = 0;
76
+ if (Array.isArray(stepRule)) {
77
+ if (stepRule.length > 1) customTimeout = stepRule[1];
78
+ stepRule = stepRule[0];
79
+ }
80
+ if (stepRule instanceof RegExp
81
+ ? step.name.match(stepRule)
82
+ : (step.name === stepRule || stepRule.indexOf('*') && step.name.startsWith(stepRule.slice(0, -1)))
83
+ ) {
84
+ stepTimeout = customTimeout;
85
+ break;
86
+ }
87
+ }
88
+ stepTimeout = stepTimeout === undefined ? config.timeout : stepTimeout;
89
+ step.setTimeout(stepTimeout * 1000, config.overrideStepLimits ? TIMEOUT_ORDER.stepTimeoutHard : TIMEOUT_ORDER.stepTimeoutSoft);
90
+ });
91
+ };
package/lib/recorder.js CHANGED
@@ -3,6 +3,8 @@ const promiseRetry = require('promise-retry');
3
3
 
4
4
  const { log } = require('./output');
5
5
 
6
+ const MAX_TASKS = 100;
7
+
6
8
  let promise;
7
9
  let running = false;
8
10
  let errFn;
@@ -116,7 +118,7 @@ module.exports = {
116
118
  * @inner
117
119
  */
118
120
  start(name) {
119
- log(`${currentQueue()}Starting <${name}> session`);
121
+ debug(`${currentQueue()}Starting <${name}> session`);
120
122
  tasks.push('--->');
121
123
  oldPromises.push(promise);
122
124
  this.running = true;
@@ -130,7 +132,7 @@ module.exports = {
130
132
  */
131
133
  restore(name) {
132
134
  tasks.push('<---');
133
- log(`${currentQueue()}Finalize <${name}> session`);
135
+ debug(`${currentQueue()}Finalize <${name}> session`);
134
136
  this.running = false;
135
137
  sessionId = null;
136
138
  this.catch(errFn);
@@ -158,10 +160,11 @@ module.exports = {
158
160
  * undefined: `add(fn)` -> `false` and `add('step',fn)` -> `true`
159
161
  * true: it will retries if `retryOpts` set.
160
162
  * false: ignore `retryOpts` and won't retry.
163
+ * @param {number} [timeout]
161
164
  * @return {Promise<*> | undefined}
162
165
  * @inner
163
166
  */
164
- add(taskName, fn = undefined, force = false, retry = undefined) {
167
+ add(taskName, fn = undefined, force = false, retry = undefined, timeout = undefined) {
165
168
  if (typeof taskName === 'function') {
166
169
  fn = taskName;
167
170
  taskName = fn.toString();
@@ -172,19 +175,22 @@ module.exports = {
172
175
  return;
173
176
  }
174
177
  tasks.push(taskName);
175
- debug(`${currentQueue()}Queued | ${taskName}`);
178
+ if (process.env.DEBUG) debug(`${currentQueue()}Queued | ${taskName}`);
176
179
 
177
180
  return promise = Promise.resolve(promise).then((res) => {
178
- const retryOpts = this.retries.slice(-1).pop();
181
+ // prefer options for non-conditional retries
182
+ const retryOpts = this.retries.sort((r1, r2) => r1.when && !r2.when).slice(-1).pop();
179
183
  // no retries or unnamed tasks
180
184
  if (!retryOpts || !taskName || !retry) {
181
- return Promise.resolve(res).then(fn);
185
+ const [promise, timer] = getTimeoutPromise(timeout, taskName);
186
+ return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer));
182
187
  }
183
188
 
184
189
  const retryRules = this.retries.slice().reverse();
185
190
  return promiseRetry(Object.assign(defaultRetryOptions, retryOpts), (retry, number) => {
186
191
  if (number > 1) log(`${currentQueue()}Retrying... Attempt #${number}`);
187
- return Promise.resolve(res).then(fn).catch((err) => {
192
+ const [promise, timer] = getTimeoutPromise(timeout, taskName);
193
+ return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer)).catch((err) => {
188
194
  for (const retryObj of retryRules) {
189
195
  if (!retryObj.when) return retry(err);
190
196
  if (retryObj.when && retryObj.when(err)) return retry(err);
@@ -296,7 +302,7 @@ module.exports = {
296
302
  * @inner
297
303
  */
298
304
  stop() {
299
- debug(this.toString());
305
+ if (process.env.DEBUG) debug(this.toString());
300
306
  log(`${currentQueue()}Stopping recording promises`);
301
307
  running = false;
302
308
  },
@@ -318,7 +324,7 @@ module.exports = {
318
324
  * @inner
319
325
  */
320
326
  scheduled() {
321
- return tasks.join('\n');
327
+ return tasks.slice(-MAX_TASKS).join('\n');
322
328
  },
323
329
 
324
330
  /**
@@ -341,6 +347,14 @@ module.exports = {
341
347
 
342
348
  };
343
349
 
350
+ function getTimeoutPromise(timeoutMs, taskName) {
351
+ let timer;
352
+ if (timeoutMs) debug(`Timing out in ${timeoutMs}ms`);
353
+ return [new Promise((done, reject) => {
354
+ timer = setTimeout(() => { reject(new Error(`Action ${taskName} was interrupted on step timeout ${timeoutMs}ms`)); }, timeoutMs || 2e9);
355
+ }), timer];
356
+ }
357
+
344
358
  function currentQueue() {
345
359
  let session = '';
346
360
  if (sessionId) session = `<${sessionId}> `;
package/lib/step.js CHANGED
@@ -13,6 +13,27 @@ const STACK_LINE = 4;
13
13
  * @param {string} name
14
14
  */
15
15
  class Step {
16
+ static get TIMEOUT_ORDER() {
17
+ return {
18
+ /**
19
+ * timeouts set with order below zero only override timeouts of higher order if their value is smaller
20
+ */
21
+ testOrSuite: -5,
22
+ /**
23
+ * 0-9 - designated for override of timeouts set from code, 5 is used by stepTimeout plugin when stepTimeout.config.overrideStepLimits=true
24
+ */
25
+ stepTimeoutHard: 5,
26
+ /**
27
+ * 10-19 - designated for timeouts set from code, 15 is order of I.setTimeout(t) operation
28
+ */
29
+ codeLimitTime: 15,
30
+ /**
31
+ * 20-29 - designated for timeout settings which could be overriden in tests code, 25 is used by stepTimeout plugin when stepTimeout.config.overrideStepLimits=false
32
+ */
33
+ stepTimeoutSoft: 25,
34
+ };
35
+ }
36
+
16
37
  constructor(helper, name) {
17
38
  /** @member {string} */
18
39
  this.actor = 'I'; // I = actor
@@ -38,6 +59,41 @@ class Step {
38
59
  this.metaStep = undefined;
39
60
  /** @member {string} */
40
61
  this.stack = '';
62
+
63
+ const timeouts = new Map();
64
+ /**
65
+ * @method
66
+ * @returns {number|undefined}
67
+ */
68
+ this.getTimeout = function () {
69
+ let totalTimeout;
70
+ // iterate over all timeouts starting from highest values of order
71
+ new Map([...timeouts.entries()].sort().reverse()).forEach((timeout, order) => {
72
+ if (timeout !== undefined && (
73
+ // when orders >= 0 - timeout value overrides those set with higher order elements
74
+ order >= 0
75
+
76
+ // when `order < 0 && totalTimeout === undefined` - timeout is used when nothing is set by elements with higher order
77
+ || totalTimeout === undefined
78
+
79
+ // when `order < 0` - timeout overrides higher values of timeout or 'no timeout' (totalTimeout === 0) set by elements with higher order
80
+ || timeout > 0 && (timeout < totalTimeout || totalTimeout === 0)
81
+ )) {
82
+ totalTimeout = timeout;
83
+ }
84
+ });
85
+ return totalTimeout;
86
+ };
87
+ /**
88
+ * @method
89
+ * @param {number} timeout - timeout in milliseconds or 0 if no timeout
90
+ * @param {number} order - order defines the priority of timeout, timeouts set with lower order override those set with higher order.
91
+ * When order below 0 value of timeout only override if new value is lower
92
+ */
93
+ this.setTimeout = function (timeout, order) {
94
+ timeouts.set(order, timeout);
95
+ };
96
+
41
97
  this.setTrace();
42
98
  }
43
99
 
@@ -228,6 +284,8 @@ class MetaStep extends Step {
228
284
  }
229
285
  }
230
286
 
287
+ Step.TIMEOUTS = {};
288
+
231
289
  /** @type {Class<MetaStep>} */
232
290
  Step.MetaStep = MetaStep;
233
291
 
package/lib/store.js CHANGED
@@ -5,6 +5,8 @@
5
5
  const store = {
6
6
  /** @type {boolean} */
7
7
  debugMode: false,
8
+ /** @type {boolean} */
9
+ timeouts: true,
8
10
  };
9
11
 
10
12
  module.exports = store;