codeceptjs 3.1.3 → 3.2.3

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 (75) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/README.md +2 -3
  3. package/bin/codecept.js +1 -0
  4. package/docs/advanced.md +99 -61
  5. package/docs/basics.md +27 -2
  6. package/docs/bdd.md +2 -2
  7. package/docs/build/Appium.js +62 -0
  8. package/docs/build/FileSystem.js +12 -1
  9. package/docs/build/Playwright.js +37 -33
  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 +2 -24
  14. package/docs/changelog.md +75 -1
  15. package/docs/configuration.md +8 -8
  16. package/docs/custom-helpers.md +2 -37
  17. package/docs/data.md +9 -9
  18. package/docs/helpers/Appium.md +240 -198
  19. package/docs/helpers/FileSystem.md +12 -2
  20. package/docs/helpers/Playwright.md +226 -225
  21. package/docs/helpers/Puppeteer.md +1 -17
  22. package/docs/helpers/REST.md +3 -1
  23. package/docs/helpers/WebDriver.md +1 -17
  24. package/docs/installation.md +1 -1
  25. package/docs/mobile-react-native-locators.md +3 -0
  26. package/docs/mobile.md +11 -11
  27. package/docs/nightmare.md +3 -3
  28. package/docs/pageobjects.md +2 -0
  29. package/docs/playwright.md +4 -4
  30. package/docs/plugins.md +138 -9
  31. package/docs/puppeteer.md +5 -5
  32. package/docs/reports.md +3 -3
  33. package/docs/testcafe.md +1 -1
  34. package/docs/translation.md +1 -1
  35. package/docs/visual.md +2 -2
  36. package/docs/vue.md +1 -1
  37. package/docs/webdriver.md +2 -2
  38. package/lib/actor.js +19 -1
  39. package/lib/cli.js +25 -20
  40. package/lib/codecept.js +2 -0
  41. package/lib/command/info.js +1 -1
  42. package/lib/command/workers/runTests.js +25 -7
  43. package/lib/config.js +12 -0
  44. package/lib/container.js +3 -1
  45. package/lib/helper/Appium.js +62 -0
  46. package/lib/helper/FileSystem.js +12 -1
  47. package/lib/helper/Playwright.js +37 -23
  48. package/lib/helper/Protractor.js +2 -14
  49. package/lib/helper/Puppeteer.js +3 -18
  50. package/lib/helper/REST.js +1 -0
  51. package/lib/helper/WebDriver.js +2 -14
  52. package/lib/interfaces/featureConfig.js +3 -0
  53. package/lib/interfaces/scenarioConfig.js +4 -0
  54. package/lib/listener/steps.js +21 -3
  55. package/lib/listener/timeout.js +72 -0
  56. package/lib/locator.js +3 -0
  57. package/lib/plugin/allure.js +6 -1
  58. package/lib/plugin/autoLogin.js +1 -1
  59. package/lib/plugin/retryFailedStep.js +4 -3
  60. package/lib/plugin/retryTo.js +130 -0
  61. package/lib/plugin/screenshotOnFail.js +1 -0
  62. package/lib/plugin/stepByStepReport.js +7 -0
  63. package/lib/plugin/stepTimeout.js +91 -0
  64. package/lib/plugin/tryTo.js +6 -0
  65. package/lib/recorder.js +18 -6
  66. package/lib/step.js +58 -0
  67. package/lib/store.js +2 -0
  68. package/lib/ui.js +2 -2
  69. package/package.json +4 -6
  70. package/typings/index.d.ts +8 -1
  71. package/typings/types.d.ts +149 -164
  72. package/docs/angular.md +0 -325
  73. package/docs/helpers/Protractor.md +0 -1658
  74. package/docs/webapi/waitUntil.mustache +0 -11
  75. package/typings/Protractor.d.ts +0 -16
@@ -264,7 +264,7 @@ class Puppeteer extends Helper {
264
264
  async _before() {
265
265
  this.sessionPages = {};
266
266
  recorder.retry({
267
- retries: 5,
267
+ retries: 3,
268
268
  when: err => {
269
269
  if (!err || typeof (err.message) !== 'string') {
270
270
  return false;
@@ -554,10 +554,9 @@ class Puppeteer extends Helper {
554
554
  this.context = null;
555
555
  popupStore.clear();
556
556
  this.isAuthenticated = false;
557
+ await this.browser.close();
557
558
  if (this.isRemoteBrowser) {
558
559
  await this.browser.disconnect();
559
- } else {
560
- await this.browser.close();
561
560
  }
562
561
  }
563
562
 
@@ -774,11 +773,7 @@ class Puppeteer extends Helper {
774
773
  }
775
774
 
776
775
  /**
777
- * Checks that title is equal to provided one.
778
- *
779
- * ```js
780
- * I.seeTitleEquals('Test title.');
781
- * ```
776
+ * {{> seeTitleEquals }}
782
777
  */
783
778
  async seeTitleEquals(text) {
784
779
  const title = await this.page.title();
@@ -2220,16 +2215,6 @@ class Puppeteer extends Helper {
2220
2215
  return this.page.waitForNavigation(opts);
2221
2216
  }
2222
2217
 
2223
- /**
2224
- * {{> waitUntil }}
2225
- */
2226
- async waitUntil(fn, sec = null) {
2227
- console.log('This method will remove in CodeceptJS 1.4; use `waitForFunction` instead!');
2228
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2229
- const context = await this._getContext();
2230
- return context.waitForFunction(fn, { timeout: waitTimeout });
2231
- }
2232
-
2233
2218
  async waitUntilExists(locator, sec) {
2234
2219
  console.log(`waitUntilExists deprecated:
2235
2220
  * use 'waitForElement' to wait for element to be attached
@@ -131,6 +131,7 @@ class REST extends Helper {
131
131
  * I.setRequestTimeout(10000); // In milliseconds
132
132
  * ```
133
133
  *
134
+ * @param {number} newTimeout - timeout in milliseconds
134
135
  */
135
136
  setRequestTimeout(newTimeout) {
136
137
  this.options.timeout = newTimeout;
@@ -582,7 +582,7 @@ class WebDriver extends Helper {
582
582
  this.context = this.root;
583
583
  if (this.options.restart && !this.options.manualStart) return this._startBrowser();
584
584
  if (!this.isRunning && !this.options.manualStart) return this._startBrowser();
585
- this.$$ = this.browser.$$.bind(this.browser);
585
+ if (this.browser) this.$$ = this.browser.$$.bind(this.browser);
586
586
  return this.browser;
587
587
  }
588
588
 
@@ -875,7 +875,7 @@ class WebDriver extends Helper {
875
875
  * I.defineTimeout({ implicit: 10000, pageLoad: 10000, script: 5000 });
876
876
  * ```
877
877
  *
878
- * @param {WebdriverIO.Timeouts} timeouts WebDriver timeouts object.
878
+ * @param {*} timeouts WebDriver timeouts object.
879
879
  */
880
880
  defineTimeout(timeouts) {
881
881
  return this._defineBrowserTimeout(this.browser, timeouts);
@@ -2353,18 +2353,6 @@ class WebDriver extends Helper {
2353
2353
  return this.browser.waitUntil(async () => this.browser.execute(fn, ...args), { timeout: aSec * 1000, timeoutMsg: '' });
2354
2354
  }
2355
2355
 
2356
- /**
2357
- * {{> waitUntil }}
2358
- */
2359
- async waitUntil(fn, sec = null, timeoutMsg = null, interval = null) {
2360
- const aSec = sec || this.options.waitForTimeout;
2361
- const _interval = typeof interval === 'number' ? interval * 1000 : null;
2362
- if (isWebDriver5()) {
2363
- return this.browser.waitUntil(fn, aSec * 1000, timeoutMsg, _interval);
2364
- }
2365
- return this.browser.waitUntil(fn, { timeout: aSec * 1000, timeoutMsg, interval: _interval });
2366
- }
2367
-
2368
2356
  /**
2369
2357
  * {{> switchTo }}
2370
2358
  */
@@ -21,6 +21,9 @@ class FeatureConfig {
21
21
  * @returns {this}
22
22
  */
23
23
  timeout(timeout) {
24
+ console.log(`Feature('${this.suite.title}').timeout(${timeout}) is deprecated!`);
25
+ console.log(`Please use Feature('${this.suite.title}', { timeout: ${timeout / 1000} }) instead`);
26
+ console.log('Timeout should be set in seconds');
24
27
  this.suite.timeout(timeout);
25
28
  return this;
26
29
  }
@@ -45,6 +45,10 @@ class ScenarioConfig {
45
45
  * @returns {this}
46
46
  */
47
47
  timeout(timeout) {
48
+ console.log(`Scenario('${this.test.title}', () => {}).timeout(${timeout}) is deprecated!`);
49
+ console.log(`Please use Scenario('${this.test.title}', { timeout: ${timeout / 1000} }, () => {}) instead`);
50
+ console.log('Timeout should be set in seconds');
51
+
48
52
  this.test.timeout(timeout);
49
53
  return this;
50
54
  }
@@ -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
 
@@ -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) {
@@ -37,7 +37,7 @@ const defaultConfig = {
37
37
  * ```js
38
38
  * // inside a test file
39
39
  * // use login to inject auto-login function
40
- * Before(login => {
40
+ * Before(({ login }) => {
41
41
  * login('user'); // login using user session
42
42
  * });
43
43
  *
@@ -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
+ };
@@ -42,6 +42,12 @@ const defaultConfig = {
42
42
  * #### Multiple Conditional Assertions
43
43
  *
44
44
  * ```js
45
+ *
46
+ * Add assert requires first:
47
+ * ```js
48
+ * const assert = require('assert');
49
+ * ```
50
+ * Then use the assert:
45
51
  * const result1 = await tryTo(() => I.see('Hello, user'));
46
52
  * const result2 = await tryTo(() => I.seeElement('.welcome'));
47
53
  * assert.ok(result1 && result2, 'Assertions were not succesful');
package/lib/recorder.js CHANGED
@@ -118,7 +118,7 @@ module.exports = {
118
118
  * @inner
119
119
  */
120
120
  start(name) {
121
- log(`${currentQueue()}Starting <${name}> session`);
121
+ debug(`${currentQueue()}Starting <${name}> session`);
122
122
  tasks.push('--->');
123
123
  oldPromises.push(promise);
124
124
  this.running = true;
@@ -132,7 +132,7 @@ module.exports = {
132
132
  */
133
133
  restore(name) {
134
134
  tasks.push('<---');
135
- log(`${currentQueue()}Finalize <${name}> session`);
135
+ debug(`${currentQueue()}Finalize <${name}> session`);
136
136
  this.running = false;
137
137
  sessionId = null;
138
138
  this.catch(errFn);
@@ -160,10 +160,11 @@ module.exports = {
160
160
  * undefined: `add(fn)` -> `false` and `add('step',fn)` -> `true`
161
161
  * true: it will retries if `retryOpts` set.
162
162
  * false: ignore `retryOpts` and won't retry.
163
+ * @param {number} [timeout]
163
164
  * @return {Promise<*> | undefined}
164
165
  * @inner
165
166
  */
166
- add(taskName, fn = undefined, force = false, retry = undefined) {
167
+ add(taskName, fn = undefined, force = false, retry = undefined, timeout = undefined) {
167
168
  if (typeof taskName === 'function') {
168
169
  fn = taskName;
169
170
  taskName = fn.toString();
@@ -177,16 +178,19 @@ module.exports = {
177
178
  if (process.env.DEBUG) debug(`${currentQueue()}Queued | ${taskName}`);
178
179
 
179
180
  return promise = Promise.resolve(promise).then((res) => {
180
- 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();
181
183
  // no retries or unnamed tasks
182
184
  if (!retryOpts || !taskName || !retry) {
183
- 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));
184
187
  }
185
188
 
186
189
  const retryRules = this.retries.slice().reverse();
187
190
  return promiseRetry(Object.assign(defaultRetryOptions, retryOpts), (retry, number) => {
188
191
  if (number > 1) log(`${currentQueue()}Retrying... Attempt #${number}`);
189
- 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) => {
190
194
  for (const retryObj of retryRules) {
191
195
  if (!retryObj.when) return retry(err);
192
196
  if (retryObj.when && retryObj.when(err)) return retry(err);
@@ -343,6 +347,14 @@ module.exports = {
343
347
 
344
348
  };
345
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
+
346
358
  function currentQueue() {
347
359
  let session = '';
348
360
  if (sessionId) session = `<${sessionId}> `;