codeceptjs 3.1.3 → 3.2.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +49 -1
  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 +1 -1
  6. package/docs/build/FileSystem.js +1 -0
  7. package/docs/build/Playwright.js +19 -26
  8. package/docs/build/Protractor.js +9 -24
  9. package/docs/build/Puppeteer.js +9 -27
  10. package/docs/build/REST.js +1 -0
  11. package/docs/build/WebDriver.js +1 -23
  12. package/docs/changelog.md +49 -1
  13. package/docs/custom-helpers.md +1 -36
  14. package/docs/helpers/Appium.md +1 -1
  15. package/docs/helpers/FileSystem.md +1 -1
  16. package/docs/helpers/Playwright.md +16 -18
  17. package/docs/helpers/Puppeteer.md +1 -17
  18. package/docs/helpers/REST.md +3 -1
  19. package/docs/helpers/WebDriver.md +1 -17
  20. package/docs/mobile-react-native-locators.md +3 -0
  21. package/docs/plugins.md +125 -0
  22. package/docs/reports.md +2 -2
  23. package/lib/actor.js +19 -1
  24. package/lib/codecept.js +2 -0
  25. package/lib/command/info.js +1 -1
  26. package/lib/config.js +12 -0
  27. package/lib/container.js +3 -1
  28. package/lib/helper/FileSystem.js +1 -0
  29. package/lib/helper/Playwright.js +19 -16
  30. package/lib/helper/Protractor.js +2 -14
  31. package/lib/helper/Puppeteer.js +2 -17
  32. package/lib/helper/REST.js +1 -0
  33. package/lib/helper/WebDriver.js +1 -13
  34. package/lib/interfaces/featureConfig.js +3 -0
  35. package/lib/interfaces/scenarioConfig.js +4 -0
  36. package/lib/listener/steps.js +21 -3
  37. package/lib/listener/timeout.js +71 -0
  38. package/lib/locator.js +3 -0
  39. package/lib/plugin/allure.js +6 -1
  40. package/lib/plugin/retryTo.js +130 -0
  41. package/lib/plugin/screenshotOnFail.js +1 -0
  42. package/lib/plugin/stepByStepReport.js +7 -0
  43. package/lib/plugin/stepTimeout.js +90 -0
  44. package/lib/recorder.js +16 -5
  45. package/lib/step.js +3 -0
  46. package/lib/store.js +2 -0
  47. package/lib/ui.js +2 -2
  48. package/package.json +4 -6
  49. package/typings/index.d.ts +6 -1
  50. package/typings/types.d.ts +40 -64
  51. package/docs/angular.md +0 -325
  52. package/docs/helpers/Protractor.md +0 -1658
  53. package/docs/webapi/waitUntil.mustache +0 -11
  54. package/typings/Protractor.d.ts +0 -16
@@ -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,71 @@
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
+
7
+ module.exports = function () {
8
+ let timeout;
9
+ let timeoutStack = [];
10
+ let currentTest;
11
+ let currentTimeout;
12
+
13
+ if (!timeouts) {
14
+ console.log('Timeouts were disabled');
15
+ return;
16
+ }
17
+
18
+ event.dispatcher.on(event.suite.before, (suite) => {
19
+ timeoutStack = [];
20
+ const globalTimeout = Config.get('timeout');
21
+ if (globalTimeout) {
22
+ if (globalTimeout >= 1000) {
23
+ console.log(`Warning: Timeout was set to ${globalTimeout}secs.\nGlobal timeout should be specified in seconds.`);
24
+ }
25
+ timeoutStack.push(globalTimeout);
26
+ }
27
+ if (suite.totalTimeout) timeoutStack.push(suite.totalTimeout);
28
+ output.log(`Timeouts: ${timeoutStack}`);
29
+ });
30
+
31
+ event.dispatcher.on(event.test.before, (test) => {
32
+ currentTest = test;
33
+ timeout = test.totalTimeout || timeoutStack[timeoutStack.length - 1];
34
+ if (!timeout) return;
35
+ currentTimeout = timeout;
36
+ output.debug(`Test Timeout: ${timeout}s`);
37
+ timeout *= 1000;
38
+ });
39
+
40
+ event.dispatcher.on(event.test.passed, (test) => {
41
+ currentTest = null;
42
+ });
43
+
44
+ event.dispatcher.on(event.test.failed, (test) => {
45
+ currentTest = null;
46
+ });
47
+
48
+ event.dispatcher.on(event.step.before, (step) => {
49
+ if (typeof timeout !== 'number') return;
50
+
51
+ if (timeout < 0) {
52
+ step.totalTimeout = 0.01;
53
+ } else {
54
+ step.totalTimeout = timeout;
55
+ }
56
+ });
57
+
58
+ event.dispatcher.on(event.step.finished, (step) => {
59
+ timeout -= step.duration;
60
+
61
+ if (typeof timeout === 'number' && timeout <= 0 && recorder.isRunning()) {
62
+ if (currentTest && currentTest.callback) {
63
+ recorder.reset();
64
+ // replace mocha timeout with custom timeout
65
+ currentTest.timeout(0);
66
+ currentTest.callback(new Error(`Timeout ${currentTimeout}s exceeded (with Before hook)`));
67
+ currentTest.timedOut = true;
68
+ }
69
+ }
70
+ });
71
+ };
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) {
@@ -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,90 @@
1
+ const event = require('../event');
2
+
3
+ const defaultConfig = {
4
+ timeout: 150,
5
+ force: false,
6
+ noTimeoutSteps: [
7
+ 'amOnPage',
8
+ 'wait*',
9
+ ],
10
+ customTimeoutSteps: [],
11
+ };
12
+
13
+ /**
14
+ * Set timeout for test steps globally.
15
+ *
16
+ * Add this plugin to config file:
17
+ *
18
+ * ```js
19
+ * plugins: {
20
+ * stepTimeout: {
21
+ * enabled: true
22
+ * }
23
+ * }
24
+ * ```
25
+ *
26
+ *
27
+ * Run tests with plugin enabled:
28
+ *
29
+ * ```
30
+ * npx codeceptjs run --plugins stepTimeout
31
+ * ```
32
+ *
33
+ * #### Configuration:
34
+ *
35
+ * * `timeout` - global step timeout, default 150 seconds
36
+ * * `force` - whether to use timeouts set in plugin config to override step timeouts set in code with I.limitTime(x).action(...), default false
37
+ * * `noTimeoutSteps` - an array of steps with no timeout. Default:
38
+ * * `amOnPage`
39
+ * * `wait*`
40
+ *
41
+ * you could set your own noTimeoutSteps which would replace the default one.
42
+ *
43
+ * * `customTimeoutSteps` - an array of step actions with custom timeout. Use it to override or extend noTimeoutSteps.
44
+ * You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`.
45
+ *
46
+ * #### Example
47
+ *
48
+ * ```js
49
+ * plugins: {
50
+ * stepTimeout: {
51
+ * enabled: true,
52
+ * force: true,
53
+ * noTimeoutSteps: [
54
+ * 'scroll*', // ignore all scroll steps
55
+ * /Cookie/, // ignore all steps with a Cookie in it (by regexp)
56
+ * ],
57
+ * customTimeoutSteps: [
58
+ * ['myFlakyStep*', 1],
59
+ * ['scrollWhichRequiresTimeout', 5],
60
+ * ]
61
+ * }
62
+ * }
63
+ * ```
64
+ *
65
+ */
66
+ module.exports = (config) => {
67
+ config = Object.assign(defaultConfig, config);
68
+ // below override rule makes sure customTimeoutSteps go first but then they override noTimeoutSteps in case of exact pattern match
69
+ config.customTimeoutSteps = config.customTimeoutSteps.concat(config.noTimeoutSteps).concat(config.customTimeoutSteps);
70
+
71
+ event.dispatcher.on(event.step.before, (step) => {
72
+ let stepTimeout;
73
+ for (let stepRule of config.customTimeoutSteps) {
74
+ let customTimeout = 0;
75
+ if (Array.isArray(stepRule)) {
76
+ if (stepRule.length > 1) customTimeout = stepRule[1];
77
+ stepRule = stepRule[0];
78
+ }
79
+ if (stepRule instanceof RegExp
80
+ ? step.name.match(stepRule)
81
+ : (step.name === stepRule || stepRule.indexOf('*') && step.name.startsWith(stepRule.slice(0, -1)))
82
+ ) {
83
+ stepTimeout = customTimeout;
84
+ break;
85
+ }
86
+ }
87
+ stepTimeout = stepTimeout === undefined ? config.timeout : stepTimeout;
88
+ step.totalTimeout = stepTimeout * 1000;
89
+ });
90
+ };
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();
@@ -180,13 +181,15 @@ module.exports = {
180
181
  const retryOpts = this.retries.slice(-1).pop();
181
182
  // no retries or unnamed tasks
182
183
  if (!retryOpts || !taskName || !retry) {
183
- return Promise.resolve(res).then(fn);
184
+ const [promise, timer] = getTimeoutPromise(timeout, taskName);
185
+ return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer));
184
186
  }
185
187
 
186
188
  const retryRules = this.retries.slice().reverse();
187
189
  return promiseRetry(Object.assign(defaultRetryOptions, retryOpts), (retry, number) => {
188
190
  if (number > 1) log(`${currentQueue()}Retrying... Attempt #${number}`);
189
- return Promise.resolve(res).then(fn).catch((err) => {
191
+ const [promise, timer] = getTimeoutPromise(timeout, taskName);
192
+ return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer)).catch((err) => {
190
193
  for (const retryObj of retryRules) {
191
194
  if (!retryObj.when) return retry(err);
192
195
  if (retryObj.when && retryObj.when(err)) return retry(err);
@@ -343,6 +346,14 @@ module.exports = {
343
346
 
344
347
  };
345
348
 
349
+ function getTimeoutPromise(timeoutMs, taskName) {
350
+ let timer;
351
+ if (timeoutMs) debug(`Timing out in ${timeoutMs}ms`);
352
+ return [new Promise((done, reject) => {
353
+ timer = setTimeout(() => { reject(new Error(`Action ${taskName} was interrupted on step timeout ${timeoutMs}ms`)); }, timeoutMs || 2e9);
354
+ }), timer];
355
+ }
356
+
346
357
  function currentQueue() {
347
358
  let session = '';
348
359
  if (sessionId) session = `<${sessionId}> `;
package/lib/step.js CHANGED
@@ -38,6 +38,9 @@ class Step {
38
38
  this.metaStep = undefined;
39
39
  /** @member {string} */
40
40
  this.stack = '';
41
+ /** @member {number} */
42
+ this.totalTimeout = undefined;
43
+
41
44
  this.setTrace();
42
45
  }
43
46
 
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;
package/lib/ui.js CHANGED
@@ -65,7 +65,7 @@ module.exports = function (suite) {
65
65
 
66
66
  suite.addTest(scenario.test(test));
67
67
  if (opts.retries) test.retries(opts.retries);
68
- if (opts.timeout) test.timeout(opts.timeout);
68
+ if (opts.timeout) test.totalTimeout = opts.timeout;
69
69
  test.opts = opts;
70
70
 
71
71
  return new ScenarioConfig(test);
@@ -103,7 +103,7 @@ module.exports = function (suite) {
103
103
  suite.timeout(0);
104
104
 
105
105
  if (opts.retries) suite.retries(opts.retries);
106
- if (opts.timeout) suite.timeout(opts.timeout);
106
+ if (opts.timeout) suite.totalTimeout = opts.timeout;
107
107
 
108
108
  suite.tags = title.match(/(\@[a-zA-Z0-9-_]+)/g) || []; // match tags from title
109
109
  suite.file = file;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "3.1.3",
3
+ "version": "3.2.0",
4
4
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
5
5
  "keywords": [
6
6
  "acceptance",
@@ -10,7 +10,6 @@
10
10
  "webdriver",
11
11
  "testcafe",
12
12
  "playwright",
13
- "protractor",
14
13
  "bdd",
15
14
  "tdd",
16
15
  "testing"
@@ -103,7 +102,7 @@
103
102
  "chai-subset": "^1.6.0",
104
103
  "contributor-faces": "^1.0.3",
105
104
  "documentation": "^12.3.0",
106
- "dtslint": "^3.6.12",
105
+ "dtslint": "^4.1.6",
107
106
  "electron": "^12.0.0",
108
107
  "eslint": "^6.8.0",
109
108
  "eslint-config-airbnb-base": "^14.2.1",
@@ -122,7 +121,6 @@
122
121
  "nightmare": "^3.0.2",
123
122
  "nodemon": "^1.19.4",
124
123
  "playwright": "^1.9.1",
125
- "protractor": "^5.4.4",
126
124
  "puppeteer": "^10.0.0",
127
125
  "qrcode-terminal": "^0.12.0",
128
126
  "rosie": "^1.6.0",
@@ -133,9 +131,9 @@
133
131
  "testcafe": "^1.9.4",
134
132
  "ts-morph": "^3.1.3",
135
133
  "tsd-jsdoc": "^2.5.0",
136
- "typescript": "^3.7.5",
134
+ "typescript": "^4.4.3",
137
135
  "wdio-docker-service": "^1.5.0",
138
- "webdriverio": "^6.10.7",
136
+ "webdriverio": "^7.14.1",
139
137
  "xml2js": "^0.4.23",
140
138
  "xmldom": "^0.1.31",
141
139
  "xpath": "0.0.27"
@@ -66,7 +66,7 @@ declare namespace CodeceptJS {
66
66
  type StringOrSecret = string | CodeceptJS.Secret;
67
67
 
68
68
  interface HookCallback {
69
- (args: SupportObject): void;
69
+ (args: SupportObject): void | Promise<void>;
70
70
  }
71
71
  interface Scenario extends IScenario {
72
72
  only: IScenario;
@@ -195,6 +195,7 @@ declare namespace Mocha {
195
195
  }
196
196
 
197
197
  interface Test extends Runnable {
198
+ artifacts: [],
198
199
  tags: any[];
199
200
  }
200
201
  }
@@ -202,3 +203,7 @@ declare namespace Mocha {
202
203
  declare module "codeceptjs" {
203
204
  export = codeceptjs;
204
205
  }
206
+
207
+ declare module "@codeceptjs/helper" {
208
+ export = CodeceptJS.Helper;
209
+ }