codeceptjs 3.1.0 → 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 (71) hide show
  1. package/CHANGELOG.md +129 -3
  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/bdd.md +55 -1
  7. package/docs/build/Appium.js +106 -34
  8. package/docs/build/FileSystem.js +1 -0
  9. package/docs/build/Nightmare.js +48 -48
  10. package/docs/build/Playwright.js +97 -94
  11. package/docs/build/Protractor.js +68 -81
  12. package/docs/build/Puppeteer.js +91 -93
  13. package/docs/build/REST.js +1 -0
  14. package/docs/build/TestCafe.js +44 -44
  15. package/docs/build/WebDriver.js +71 -95
  16. package/docs/changelog.md +144 -2
  17. package/docs/commands.md +21 -7
  18. package/docs/configuration.md +15 -2
  19. package/docs/custom-helpers.md +1 -36
  20. package/docs/helpers/Appium.md +97 -95
  21. package/docs/helpers/FileSystem.md +1 -1
  22. package/docs/helpers/Playwright.md +16 -18
  23. package/docs/helpers/Puppeteer.md +18 -18
  24. package/docs/helpers/REST.md +3 -1
  25. package/docs/helpers/WebDriver.md +3 -19
  26. package/docs/mobile-react-native-locators.md +3 -0
  27. package/docs/playwright.md +40 -0
  28. package/docs/plugins.md +185 -68
  29. package/docs/reports.md +23 -5
  30. package/lib/actor.js +20 -2
  31. package/lib/codecept.js +15 -2
  32. package/lib/command/info.js +1 -1
  33. package/lib/config.js +13 -1
  34. package/lib/container.js +3 -1
  35. package/lib/data/dataTableArgument.js +35 -0
  36. package/lib/helper/Appium.js +49 -4
  37. package/lib/helper/FileSystem.js +1 -0
  38. package/lib/helper/Playwright.js +35 -22
  39. package/lib/helper/Protractor.js +2 -14
  40. package/lib/helper/Puppeteer.js +20 -19
  41. package/lib/helper/REST.js +1 -0
  42. package/lib/helper/WebDriver.js +2 -16
  43. package/lib/index.js +2 -0
  44. package/lib/interfaces/featureConfig.js +3 -0
  45. package/lib/interfaces/gherkin.js +7 -1
  46. package/lib/interfaces/scenarioConfig.js +4 -0
  47. package/lib/listener/helpers.js +1 -0
  48. package/lib/listener/steps.js +21 -3
  49. package/lib/listener/timeout.js +71 -0
  50. package/lib/locator.js +3 -0
  51. package/lib/mochaFactory.js +13 -9
  52. package/lib/plugin/allure.js +6 -1
  53. package/lib/plugin/{puppeteerCoverage.js → coverage.js} +10 -22
  54. package/lib/plugin/customLocator.js +2 -2
  55. package/lib/plugin/retryTo.js +130 -0
  56. package/lib/plugin/screenshotOnFail.js +1 -0
  57. package/lib/plugin/stepByStepReport.js +7 -0
  58. package/lib/plugin/stepTimeout.js +90 -0
  59. package/lib/plugin/subtitles.js +88 -0
  60. package/lib/plugin/tryTo.js +1 -1
  61. package/lib/recorder.js +21 -8
  62. package/lib/step.js +7 -2
  63. package/lib/store.js +2 -0
  64. package/lib/ui.js +2 -2
  65. package/package.json +6 -7
  66. package/typings/index.d.ts +8 -1
  67. package/typings/types.d.ts +198 -82
  68. package/docs/angular.md +0 -325
  69. package/docs/helpers/Protractor.md +0 -1658
  70. package/docs/webapi/waitUntil.mustache +0 -11
  71. package/typings/Protractor.d.ts +0 -16
@@ -11,15 +11,19 @@ const transform = require('../transform');
11
11
  const parser = new Parser();
12
12
  parser.stopAtFirstError = false;
13
13
 
14
- module.exports = (text) => {
14
+ module.exports = (text, file) => {
15
15
  const ast = parser.parse(text);
16
16
 
17
+ if (!ast.feature) {
18
+ throw new Error(`No 'Features' available in Gherkin '${file}' provided!`);
19
+ }
17
20
  const suite = new Suite(ast.feature.name, new Context());
18
21
  const tags = ast.feature.tags.map(t => t.name);
19
22
  suite.title = `${suite.title} ${tags.join(' ')}`.trim();
20
23
  suite.tags = tags || [];
21
24
  suite.comment = ast.feature.description;
22
25
  suite.feature = ast.feature;
26
+ suite.file = file;
23
27
  suite.timeout(0);
24
28
 
25
29
  suite.beforeEach('codeceptjs.before', () => scenario.setup(suite));
@@ -95,6 +99,7 @@ module.exports = (text) => {
95
99
  const title = `${child.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
96
100
  const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)));
97
101
  test.tags = suite.tags.concat(tags);
102
+ test.file = file;
98
103
  suite.addTest(scenario.test(test));
99
104
  }
100
105
  }
@@ -104,6 +109,7 @@ module.exports = (text) => {
104
109
  const title = `${child.name} ${tags.join(' ')}`.trim();
105
110
  const test = new Test(title, async () => runSteps(child.steps));
106
111
  test.tags = suite.tags.concat(tags);
112
+ test.file = file;
107
113
  suite.addTest(scenario.test(test));
108
114
  }
109
115
 
@@ -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,3 +1,4 @@
1
+ const path = require('path');
1
2
  const event = require('../event');
2
3
  const container = require('../container');
3
4
  const recorder = require('../recorder');
@@ -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
 
@@ -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');
@@ -17,12 +17,6 @@ class MochaFactory {
17
17
  output.process(opts.child);
18
18
  mocha.ui(scenarioUi);
19
19
 
20
- // process.on('unhandledRejection', (reason) => {
21
- // output.error('Unhandled rejection');
22
- // console.log(Error.captureStackTrace(reason));
23
- // output.error(reason);
24
- // });
25
-
26
20
  Mocha.Runner.prototype.uncaught = function (err) {
27
21
  if (err) {
28
22
  if (err.toString().indexOf('ECONNREFUSED') >= 0) {
@@ -41,8 +35,7 @@ class MochaFactory {
41
35
  if (mocha.suite.suites.length === 0) {
42
36
  mocha.files
43
37
  .filter(file => file.match(/\.feature$/))
44
- .map(file => fs.readFileSync(file, 'utf8'))
45
- .forEach(content => mocha.suite.addSuite(gherkinParser(content)));
38
+ .forEach(file => mocha.suite.addSuite(gherkinParser(fs.readFileSync(file, 'utf8'), file)));
46
39
 
47
40
  // remove feature files
48
41
  mocha.files = mocha.files.filter(file => !file.match(/\.feature$/));
@@ -51,19 +44,30 @@ class MochaFactory {
51
44
 
52
45
  // add ids for each test and check uniqueness
53
46
  const dupes = [];
47
+ let missingFeatureInFile = [];
54
48
  const seenTests = [];
55
49
  mocha.suite.eachTest(test => {
56
50
  test.id = genTestId(test);
51
+
57
52
  const name = test.fullTitle();
58
53
  if (seenTests.includes(test.id)) {
59
54
  dupes.push(name);
60
55
  }
61
56
  seenTests.push(test.id);
57
+
58
+ if (name.slice(0, name.indexOf(':')) === '') {
59
+ missingFeatureInFile.push(test.file);
60
+ }
62
61
  });
63
62
  if (dupes.length) {
64
63
  // ideally this should be no-op and throw (breaking change)...
65
64
  output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`);
66
65
  }
66
+
67
+ if (missingFeatureInFile.length) {
68
+ missingFeatureInFile = [...new Set(missingFeatureInFile)];
69
+ output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`);
70
+ }
67
71
  }
68
72
  };
69
73
 
@@ -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) {
@@ -1,4 +1,4 @@
1
- const debug = require('debug')('codeceptjs:plugin:puppeteerCoverage');
1
+ const debugModule = require('debug');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
 
@@ -13,7 +13,7 @@ const defaultConfig = {
13
13
  uniqueFileName: true,
14
14
  };
15
15
 
16
- const supportedHelpers = ['Puppeteer'];
16
+ const supportedHelpers = ['Puppeteer', 'Playwright'];
17
17
 
18
18
  function buildFileName(test, uniqueFileName) {
19
19
  let fileName = clearString(test.title);
@@ -41,15 +41,14 @@ function buildFileName(test, uniqueFileName) {
41
41
  }
42
42
 
43
43
  /**
44
- * Dumps puppeteers code coverage after every test.
44
+ * Dumps code coverage from Playwright/Puppeteer after every test.
45
45
  *
46
46
  * #### Configuration
47
47
  *
48
- * Configuration can either be taken from a corresponding helper (deprecated) or a from plugin config (recommended).
49
48
  *
50
49
  * ```js
51
50
  * plugins: {
52
- * puppeteerCoverage: {
51
+ * coverage: {
53
52
  * enabled: true
54
53
  * }
55
54
  * }
@@ -59,33 +58,22 @@ function buildFileName(test, uniqueFileName) {
59
58
  *
60
59
  * * `coverageDir`: directory to dump coverage files
61
60
  * * `uniqueFileName`: generate a unique filename by adding uuid
62
- *
63
- * First of all, your mileage may vary!
64
- *
65
- * To work, you need the client javascript code to be NOT uglified. They need to be built in "development" mode.
66
- * And the end of your tests, you'll get a directory full of coverage per test run. Now what?
67
- * You'll need to convert the coverage code to something istanbul can read. Good news is someone wrote the code
68
- * for you (see puppeteer-to-istanbul link below). Then using istanbul you need to combine the converted
69
- * coverage and create a report. Good luck!
70
- *
71
- * Links:
72
- * * https://github.com/GoogleChrome/puppeteer/blob/v1.12.2/docs/api.md#class-coverage
73
- * * https://github.com/istanbuljs/puppeteer-to-istanbul
74
- * * https://github.com/gotwarlost/istanbul
75
61
  */
76
62
  module.exports = function (config) {
77
63
  const helpers = Container.helpers();
78
64
  let coverageRunning = false;
79
65
  let helper;
80
66
 
67
+ let debug;
81
68
  for (const helperName of supportedHelpers) {
82
69
  if (Object.keys(helpers).indexOf(helperName) > -1) {
83
70
  helper = helpers[helperName];
71
+ debug = debugModule(`codeceptjs:plugin:${helperName.toLowerCase()}Coverage`);
84
72
  }
85
73
  }
86
74
 
87
75
  if (!helper) {
88
- console.error('Coverage is only supported in Puppeteer');
76
+ console.error('Coverage is only supported in Puppeteer, Playwright');
89
77
  return; // no helpers for screenshot
90
78
  }
91
79
 
@@ -102,7 +90,7 @@ module.exports = function (config) {
102
90
  'starting coverage',
103
91
  async () => {
104
92
  try {
105
- if (!coverageRunning) {
93
+ if (!coverageRunning && helper.page && helper.page.coverage) {
106
94
  debug('--> starting coverage <--');
107
95
  coverageRunning = true;
108
96
  await helper.page.coverage.startJSCoverage();
@@ -115,13 +103,13 @@ module.exports = function (config) {
115
103
  );
116
104
  });
117
105
 
118
- // Save puppeteer coverage data after every test run
106
+ // Save coverage data after every test run
119
107
  event.dispatcher.on(event.test.after, async (test) => {
120
108
  recorder.add(
121
109
  'saving coverage',
122
110
  async () => {
123
111
  try {
124
- if (coverageRunning) {
112
+ if (coverageRunning && helper.page && helper.page.coverage) {
125
113
  debug('--> stopping coverage <--');
126
114
  coverageRunning = false;
127
115
  const coverage = await helper.page.coverage.stopJSCoverage();
@@ -38,7 +38,7 @@ const defaultConfig = {
38
38
  * // in codecept.conf.js
39
39
  * plugins: {
40
40
  * customLocator: {
41
- * enabled: true
41
+ * enabled: true,
42
42
  * attribute: 'data-test'
43
43
  * }
44
44
  * }
@@ -57,7 +57,7 @@ const defaultConfig = {
57
57
  * // in codecept.conf.js
58
58
  * plugins: {
59
59
  * customLocator: {
60
- * enabled: true
60
+ * enabled: true,
61
61
  * prefix: '=',
62
62
  * attribute: 'data-qa'
63
63
  * }
@@ -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
+ };