codeceptjs 3.1.1 → 3.2.1

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 (70) hide show
  1. package/CHANGELOG.md +120 -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 +1 -1
  6. package/docs/bdd.md +55 -1
  7. package/docs/build/Appium.js +22 -4
  8. package/docs/build/FileSystem.js +1 -0
  9. package/docs/build/Playwright.js +40 -42
  10. package/docs/build/Protractor.js +9 -24
  11. package/docs/build/Puppeteer.js +28 -30
  12. package/docs/build/REST.js +1 -0
  13. package/docs/build/WebDriver.js +2 -24
  14. package/docs/changelog.md +120 -0
  15. package/docs/commands.md +21 -7
  16. package/docs/configuration.md +15 -2
  17. package/docs/custom-helpers.md +1 -36
  18. package/docs/helpers/Appium.md +49 -50
  19. package/docs/helpers/FileSystem.md +1 -1
  20. package/docs/helpers/Playwright.md +16 -18
  21. package/docs/helpers/Puppeteer.md +18 -18
  22. package/docs/helpers/REST.md +3 -1
  23. package/docs/helpers/WebDriver.md +1 -17
  24. package/docs/mobile-react-native-locators.md +3 -0
  25. package/docs/playwright.md +40 -0
  26. package/docs/plugins.md +187 -70
  27. package/docs/reports.md +23 -5
  28. package/lib/actor.js +20 -2
  29. package/lib/codecept.js +15 -2
  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 +22 -4
  35. package/lib/helper/FileSystem.js +1 -0
  36. package/lib/helper/Playwright.js +40 -32
  37. package/lib/helper/Protractor.js +2 -14
  38. package/lib/helper/Puppeteer.js +21 -20
  39. package/lib/helper/REST.js +1 -0
  40. package/lib/helper/WebDriver.js +2 -14
  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 +71 -0
  48. package/lib/locator.js +3 -0
  49. package/lib/mochaFactory.js +13 -9
  50. package/lib/plugin/allure.js +6 -1
  51. package/lib/plugin/{puppeteerCoverage.js → coverage.js} +10 -22
  52. package/lib/plugin/customLocator.js +2 -2
  53. package/lib/plugin/retryFailedStep.js +4 -3
  54. package/lib/plugin/retryTo.js +130 -0
  55. package/lib/plugin/screenshotOnFail.js +1 -0
  56. package/lib/plugin/stepByStepReport.js +7 -0
  57. package/lib/plugin/stepTimeout.js +90 -0
  58. package/lib/plugin/subtitles.js +88 -0
  59. package/lib/plugin/tryTo.js +1 -1
  60. package/lib/recorder.js +21 -8
  61. package/lib/step.js +7 -2
  62. package/lib/store.js +2 -0
  63. package/lib/ui.js +2 -2
  64. package/package.json +6 -7
  65. package/typings/index.d.ts +8 -1
  66. package/typings/types.d.ts +104 -71
  67. package/docs/angular.md +0 -325
  68. package/docs/helpers/Protractor.md +0 -1658
  69. package/docs/webapi/waitUntil.mustache +0 -11
  70. package/typings/Protractor.d.ts +0 -16
@@ -481,10 +481,11 @@ class Appium extends Webdriver {
481
481
  * ```js
482
482
  * I.removeApp('appName', 'com.example.android.apis');
483
483
  * ```
484
- * @param {string} appId
485
- * @param {string} bundleId String ID of bundle
486
484
  *
487
485
  * Appium: support only Android
486
+ *
487
+ * @param {string} appId
488
+ * @param {string} [bundleId] ID of bundle
488
489
  */
489
490
  async removeApp(appId, bundleId) {
490
491
  onlyForApps.call(this, 'Android');
@@ -820,9 +821,10 @@ class Appium extends Webdriver {
820
821
  * I.hideDeviceKeyboard('pressKey', 'Done');
821
822
  * ```
822
823
  *
823
- * @param {'tapOutside' | 'pressKey'} strategy desired strategy to close keyboard (‘tapOutside’ or ‘pressKey’)
824
- *
825
824
  * Appium: support Android and iOS
825
+ *
826
+ * @param {'tapOutside' | 'pressKey'} [strategy] Desired strategy to close keyboard (‘tapOutside’ or ‘pressKey’)
827
+ * @param {string} [key] Optional key
826
828
  */
827
829
  async hideDeviceKeyboard(strategy, key) {
828
830
  onlyForApps.call(this);
@@ -1162,6 +1164,8 @@ class Appium extends Webdriver {
1162
1164
  * ```
1163
1165
  *
1164
1166
  * Appium: support Android and iOS
1167
+ *
1168
+ * @param {Array} actions Array of touch actions
1165
1169
  */
1166
1170
  async touchPerform(actions) {
1167
1171
  onlyForApps.call(this);
@@ -1397,6 +1401,20 @@ class Appium extends Webdriver {
1397
1401
  return super.grabValueFrom(parseLocator.call(this, locator));
1398
1402
  }
1399
1403
 
1404
+ /**
1405
+ * Saves a screenshot to ouput folder (set in codecept.json or codecept.conf.js).
1406
+ * Filename is relative to output folder.
1407
+ *
1408
+ * ```js
1409
+ * I.saveScreenshot('debug.png');
1410
+ * ```
1411
+ *
1412
+ * @param {string} fileName file name to save.
1413
+ */
1414
+ async saveScreenshot(fileName) {
1415
+ return super.saveScreenshot(fileName, false);
1416
+ }
1417
+
1400
1418
  /**
1401
1419
  * {{> scrollIntoView }}
1402
1420
  *
@@ -91,6 +91,7 @@ class FileSystem extends Helper {
91
91
  * I.amInPath('output/downloads');
92
92
  * I.seeFileNameMatching('.pdf');
93
93
  * ```
94
+ * @param {string} text
94
95
  */
95
96
  seeFileNameMatching(text) {
96
97
  assert.ok(
@@ -80,6 +80,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
80
80
  * * `basicAuth`: (optional) the basic authentication to pass to base url. Example: {username: 'username', password: 'password'}
81
81
  * * `windowSize`: (optional) default window size. Set a dimension like `640x480`.
82
82
  * * `userAgent`: (optional) user-agent string.
83
+ * * `locale`: (optional) locale string. Example: 'en-GB', 'de-DE', 'fr-FR', ...
83
84
  * * `manualStart`: (optional, default: false) - do not start browser before a test, start it manually inside a helper with `this.helpers["Playwright"]._startBrowser()`.
84
85
  * * `chromium`: (optional) pass additional chromium options
85
86
  * * `electron`: (optional) pass additional electron options
@@ -197,6 +198,19 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
197
198
  * }
198
199
  * ```
199
200
  *
201
+ * #### Example #7: Launch test with a specifc user locale
202
+ *
203
+ * ```js
204
+ * {
205
+ * helpers: {
206
+ * Playwright : {
207
+ * url: "http://localhost",
208
+ * locale: "fr-FR",
209
+ * }
210
+ * }
211
+ * }
212
+ * ```
213
+ *
200
214
  * Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
201
215
  *
202
216
  * ## Access From Helpers
@@ -341,19 +355,10 @@ class Playwright extends Helper {
341
355
  }
342
356
 
343
357
  async _before() {
344
- recorder.retry({
345
- retries: 5,
346
- when: err => {
347
- if (!err || typeof (err.message) !== 'string') {
348
- return false;
349
- }
350
- // ignore context errors
351
- return err.message.includes('context');
352
- },
353
- });
354
358
  if (this.options.restart && !this.options.manualStart) await this._startBrowser();
355
359
  if (!this.isRunning && !this.options.manualStart) await this._startBrowser();
356
360
 
361
+ this.isAuthenticated = false;
357
362
  if (this.isElectron) {
358
363
  this.browserContext = this.browser.context();
359
364
  } else if (this.userDataDir) {
@@ -364,8 +369,14 @@ class Playwright extends Helper {
364
369
  acceptDownloads: true,
365
370
  ...this.options.emulate,
366
371
  };
372
+ if (this.options.basicAuth) {
373
+ contextOptions.httpCredentials = this.options.basicAuth;
374
+ this.isAuthenticated = true;
375
+ }
367
376
  if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo;
368
377
  if (this.storageState) contextOptions.storageState = this.storageState;
378
+ if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent;
379
+ if (this.options.locale) contextOptions.locale = this.options.locale;
369
380
  this.browserContext = await this.browser.newContext(contextOptions); // Adding the HTTPSError ignore in the context so that we can ignore those errors
370
381
  }
371
382
 
@@ -557,9 +568,14 @@ class Playwright extends Helper {
557
568
  this.page = page;
558
569
  if (!page) return;
559
570
  page.setDefaultNavigationTimeout(this.options.getPageTimeout);
571
+
572
+ page.on('crash', async () => {
573
+ console.log('ERROR: Page has crashed, closing page!');
574
+ await page.close();
575
+ });
560
576
  this.context = await this.page;
561
577
  this.contextLocator = null;
562
- if (this.config.browser === 'chrome') {
578
+ if (this.options.browser === 'chrome') {
563
579
  await page.bringToFront();
564
580
  }
565
581
  }
@@ -714,9 +730,9 @@ class Playwright extends Helper {
714
730
  url = this.options.url + url;
715
731
  }
716
732
 
717
- if (this.config.basicAuth && (this.isAuthenticated !== true)) {
733
+ if (this.options.basicAuth && (this.isAuthenticated !== true)) {
718
734
  if (url.includes(this.options.url)) {
719
- await this.browserContext.setHTTPCredentials(this.config.basicAuth);
735
+ await this.browserContext.setHTTPCredentials(this.options.basicAuth);
720
736
  this.isAuthenticated = true;
721
737
  }
722
738
  }
@@ -775,7 +791,7 @@ class Playwright extends Helper {
775
791
  if (!customHeaders) {
776
792
  throw new Error('Cannot send empty headers.');
777
793
  }
778
- return this.page.setExtraHTTPHeaders(customHeaders);
794
+ return this.browserContext.setExtraHTTPHeaders(customHeaders);
779
795
  }
780
796
 
781
797
  /**
@@ -1851,12 +1867,17 @@ class Playwright extends Helper {
1851
1867
 
1852
1868
  async _failed(test) {
1853
1869
  await this._withinEnd();
1870
+
1871
+ if (!test.artifacts) {
1872
+ test.artifacts = {};
1873
+ }
1874
+
1854
1875
  if (this.options.recordVideo && this.page.video()) {
1855
1876
  test.artifacts.video = await this.page.video().path();
1856
1877
  }
1857
1878
 
1858
1879
  if (this.options.trace) {
1859
- const path = `${global.output_dir}/trace/${clearString(test.title)}.zip`;
1880
+ const path = `${global.output_dir}/trace/${clearString(test.title).slice(0, 255)}.zip`;
1860
1881
  await this.browserContext.tracing.stop({ path });
1861
1882
  test.artifacts.trace = path;
1862
1883
  }
@@ -1867,7 +1888,7 @@ class Playwright extends Helper {
1867
1888
  if (this.options.keepVideoForPassedTests) {
1868
1889
  test.artifacts.video = await this.page.video().path();
1869
1890
  } else {
1870
- this.page.video().delete();
1891
+ this.page.video().delete().catch(e => {});
1871
1892
  }
1872
1893
  }
1873
1894
 
@@ -2136,11 +2157,11 @@ class Playwright extends Helper {
2136
2157
  }
2137
2158
 
2138
2159
  /**
2139
- * Waits for a network request.
2160
+ * Waits for a network response.
2140
2161
  *
2141
2162
  * ```js
2142
2163
  * I.waitForResponse('http://example.com/resource');
2143
- * I.waitForResponse(request => request.url() === 'http://example.com' && request.method() === 'GET');
2164
+ * I.waitForResponse(response => response.url() === 'https://example.com' && response.status() === 200);
2144
2165
  * ```
2145
2166
  *
2146
2167
  * @param {string|function} urlOrPredicate
@@ -2226,16 +2247,6 @@ class Playwright extends Helper {
2226
2247
  return this.page.waitForNavigation(opts);
2227
2248
  }
2228
2249
 
2229
- /**
2230
- * {{> waitUntil }}
2231
- */
2232
- async waitUntil(fn, sec = null) {
2233
- console.log('This method will remove in CodeceptJS 1.4; use `waitForFunction` instead!');
2234
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2235
- const context = await this._getContext();
2236
- return context.waitForFunction(fn, { timeout: waitTimeout });
2237
- }
2238
-
2239
2250
  async waitUntilExists(locator, sec) {
2240
2251
  console.log(`waitUntilExists deprecated:
2241
2252
  * use 'waitForElement' to wait for element to be attached
@@ -2652,13 +2663,10 @@ async function targetCreatedHandler(page) {
2652
2663
  });
2653
2664
  });
2654
2665
  page.on('console', (msg) => {
2655
- this.debugSection(`Browser:${ucfirst(msg.type())}`, (msg._text || '') + msg.args().join(' '));
2666
+ this.debugSection(`Browser:${ucfirst(msg.type())}`, (msg.text && msg.text() || msg._text || '') + msg.args().join(' '));
2656
2667
  consoleLogStore.add(msg);
2657
2668
  });
2658
2669
 
2659
- if (this.options.userAgent) {
2660
- await page.setUserAgent(this.options.userAgent);
2661
- }
2662
2670
  if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0 && this._getType() === 'Browser') {
2663
2671
  await page.setViewportSize(parseWindowSize(this.options.windowSize));
2664
2672
  }
@@ -838,11 +838,7 @@ class Protractor extends Helper {
838
838
  }
839
839
 
840
840
  /**
841
- * Checks that title is equal to provided one.
842
- *
843
- * ```js
844
- * I.seeTitleEquals('Test title.');
845
- * ```
841
+ * {{> seeTitleEquals }}
846
842
  */
847
843
  async seeTitleEquals(text) {
848
844
  const title = await this.browser.getTitle();
@@ -1018,7 +1014,7 @@ class Protractor extends Helper {
1018
1014
  }
1019
1015
 
1020
1016
  /**
1021
- * {{> seeInCurrentUrl }}
1017
+ * {{> seeInCurrentUrl }}
1022
1018
  */
1023
1019
  async seeInCurrentUrl(url) {
1024
1020
  return this.browser.getCurrentUrl().then(currentUrl => stringIncludes('url').assert(url, currentUrl));
@@ -1498,14 +1494,6 @@ class Protractor extends Helper {
1498
1494
  return this.browser.wait(() => this.browser.executeScript.call(this.browser, fn, ...args), aSec * 1000);
1499
1495
  }
1500
1496
 
1501
- /**
1502
- * {{> waitUntil }}
1503
- */
1504
- async waitUntil(fn, sec = null, timeoutMsg = null) {
1505
- const aSec = sec || this.options.waitForTimeout;
1506
- return this.browser.wait(fn, aSec * 1000, timeoutMsg);
1507
- }
1508
-
1509
1497
  /**
1510
1498
  * {{> waitInUrl }}
1511
1499
  */
@@ -129,8 +129,9 @@ const consoleLogStore = new Console();
129
129
  * }
130
130
  * }
131
131
  * ```
132
+ * > Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
132
133
  *
133
- * #### Example #5: Target URL with provided basic authentication
134
+ * #### Example #5: Target URL with provided basic authentication
134
135
  *
135
136
  * ```js
136
137
  * {
@@ -143,10 +144,25 @@ const consoleLogStore = new Console();
143
144
  * }
144
145
  * }
145
146
  * ```
147
+ * #### Troubleshooting
146
148
  *
149
+ * Error Message: `No usable sandbox!`
150
+ *
151
+ * When running Puppeteer on CI try to disable sandbox if you see that message
152
+ *
153
+ * ```
154
+ * helpers: {
155
+ * Puppeteer: {
156
+ * url: 'http://localhost',
157
+ * show: false,
158
+ * chrome: {
159
+ * args: ['--no-sandbox', '--disable-setuid-sandbox']
160
+ * }
161
+ * },
162
+ * }
163
+ * ```
147
164
  *
148
165
  *
149
- * Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
150
166
  *
151
167
  * ## Access From Helpers
152
168
  *
@@ -248,7 +264,7 @@ class Puppeteer extends Helper {
248
264
  async _before() {
249
265
  this.sessionPages = {};
250
266
  recorder.retry({
251
- retries: 5,
267
+ retries: 3,
252
268
  when: err => {
253
269
  if (!err || typeof (err.message) !== 'string') {
254
270
  return false;
@@ -538,10 +554,9 @@ class Puppeteer extends Helper {
538
554
  this.context = null;
539
555
  popupStore.clear();
540
556
  this.isAuthenticated = false;
557
+ await this.browser.close();
541
558
  if (this.isRemoteBrowser) {
542
559
  await this.browser.disconnect();
543
- } else {
544
- await this.browser.close();
545
560
  }
546
561
  }
547
562
 
@@ -758,11 +773,7 @@ class Puppeteer extends Helper {
758
773
  }
759
774
 
760
775
  /**
761
- * Checks that title is equal to provided one.
762
- *
763
- * ```js
764
- * I.seeTitleEquals('Test title.');
765
- * ```
776
+ * {{> seeTitleEquals }}
766
777
  */
767
778
  async seeTitleEquals(text) {
768
779
  const title = await this.page.title();
@@ -2204,16 +2215,6 @@ class Puppeteer extends Helper {
2204
2215
  return this.page.waitForNavigation(opts);
2205
2216
  }
2206
2217
 
2207
- /**
2208
- * {{> waitUntil }}
2209
- */
2210
- async waitUntil(fn, sec = null) {
2211
- console.log('This method will remove in CodeceptJS 1.4; use `waitForFunction` instead!');
2212
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2213
- const context = await this._getContext();
2214
- return context.waitForFunction(fn, { timeout: waitTimeout });
2215
- }
2216
-
2217
2218
  async waitUntilExists(locator, sec) {
2218
2219
  console.log(`waitUntilExists deprecated:
2219
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;
@@ -481,7 +481,7 @@ class WebDriver extends Helper {
481
481
  try {
482
482
  require('webdriverio');
483
483
  } catch (e) {
484
- return ['webdriverio@^5.2.2'];
484
+ return ['webdriverio@^6.12.1'];
485
485
  }
486
486
  }
487
487
 
@@ -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
  */
package/lib/index.js CHANGED
@@ -32,6 +32,8 @@ module.exports = {
32
32
  within: require('./within'),
33
33
  /** @type {typeof CodeceptJS.DataTable} */
34
34
  dataTable: require('./data/table'),
35
+ /** @type {typeof CodeceptJS.DataTableArgument} */
36
+ dataTableArgument: require('./data/dataTableArgument'),
35
37
  /** @type {typeof CodeceptJS.store} */
36
38
  store: require('./store'),
37
39
  /** @type {typeof CodeceptJS.Locator} */
@@ -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
  }
@@ -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