codeceptjs 3.5.9-beta.1 → 3.5.9-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -96,7 +96,117 @@ module.exports = function() {
96
96
  });
97
97
  }
98
98
  ```
99
+ You could get test stats when running with workers
99
100
 
101
+ ```js
102
+ const { event } = require('codeceptjs');
103
+
104
+ module.exports = function() {
105
+
106
+ event.dispatcher.on(event.workers.result, function (result) {
107
+
108
+ console.log(result);
109
+
110
+ });
111
+ }
112
+
113
+ // in console log
114
+ FAIL | 7 passed, 1 failed, 1 skipped // 2s
115
+ {
116
+ "tests": {
117
+ "passed": [
118
+ {
119
+ "type": "test",
120
+ "title": "Assert @C3",
121
+ "body": "() => { }",
122
+ "async": 0,
123
+ "sync": true,
124
+ "_timeout": 2000,
125
+ "_slow": 75,
126
+ "_retries": -1,
127
+ "timedOut": false,
128
+ "_currentRetry": 0,
129
+ "pending": false,
130
+ "opts": {},
131
+ "tags": [
132
+ "@C3"
133
+ ],
134
+ "uid": "xe4q1HdqpRrZG5dPe0JG+A",
135
+ "workerIndex": 3,
136
+ "retries": -1,
137
+ "duration": 493,
138
+ "err": null,
139
+ "parent": {
140
+ "title": "My",
141
+ "ctx": {},
142
+ "suites": [],
143
+ "tests": [],
144
+ "root": false,
145
+ "pending": false,
146
+ "_retries": -1,
147
+ "_beforeEach": [],
148
+ "_beforeAll": [],
149
+ "_afterEach": [],
150
+ "_afterAll": [],
151
+ "_timeout": 2000,
152
+ "_slow": 75,
153
+ "_bail": false,
154
+ "_onlyTests": [],
155
+ "_onlySuites": [],
156
+ "delayed": false
157
+ },
158
+ "steps": [
159
+ {
160
+ "actor": "I",
161
+ "name": "amOnPage",
162
+ "status": "success",
163
+ "agrs": [
164
+ "https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST"
165
+ ],
166
+ "startedAt": 1698760652610,
167
+ "startTime": 1698760652611,
168
+ "endTime": 1698760653098,
169
+ "finishedAt": 1698760653098,
170
+ "duration": 488
171
+ },
172
+ {
173
+ "actor": "I",
174
+ "name": "grabCurrentUrl",
175
+ "status": "success",
176
+ "agrs": [],
177
+ "startedAt": 1698760653098,
178
+ "startTime": 1698760653098,
179
+ "endTime": 1698760653099,
180
+ "finishedAt": 1698760653099,
181
+ "duration": 1
182
+ }
183
+ ]
184
+ }
185
+ ],
186
+ "failed": [],
187
+ "skipped": []
188
+ }
189
+ }
190
+ ```
191
+
192
+ CodeceptJS also exposes the env var `process.env.RUNS_WITH_WORKERS` when running tests with `run-workers` command so that you could handle the events better in your plugins/helpers
193
+
194
+ ```js
195
+ const { event } = require('codeceptjs');
196
+
197
+ module.exports = function() {
198
+ // this event would trigger the `_publishResultsToTestrail` when running `run-workers` command
199
+ event.dispatcher.on(event.workers.result, async () => {
200
+ await _publishResultsToTestrail();
201
+ });
202
+
203
+ // this event would not trigger the `_publishResultsToTestrail` multiple times when running `run-workers` command
204
+ event.dispatcher.on(event.all.result, async () => {
205
+ // when running `run` command, this env var is undefined
206
+ if (!process.env.RUNS_WITH_WORKERS) await _publishResultsToTestrail();
207
+ });
208
+ }
209
+ ```
100
210
  Available events:
101
211
 
102
212
  * `event.test.before(test)` - *async* when `Before` hooks from helpers and from test is executed
@@ -119,6 +229,7 @@ Available events:
119
229
  * `event.all.result` - when results are printed
120
230
  * `event.workers.before` - before spawning workers in parallel run
121
231
  * `event.workers.after` - after workers finished in parallel run
232
+ * `event.workers.result` - test results after workers finished in parallel run
122
233
 
123
234
 
124
235
  > *sync* - means that event is fired in the moment of the action happening.
package/docs/plugins.md CHANGED
@@ -65,12 +65,14 @@ If a session expires automatically logs in again.
65
65
  ```js
66
66
  // inside a test file
67
67
  // use login to inject auto-login function
68
+ Feature('Login');
69
+
68
70
  Before(({ login }) => {
69
71
  login('user'); // login using user session
70
72
  });
71
73
 
72
- // Alternatively log in for one scenario
73
- Scenario('log me in', ( {I, login} ) => {
74
+ // Alternatively log in for one scenario.
75
+ Scenario('log me in', ( { I, login } ) => {
74
76
  login('admin');
75
77
  I.see('I am logged in');
76
78
  });
@@ -124,7 +124,7 @@ function executeRun(runName, runConfig) {
124
124
  if (browserConfig.outputName) {
125
125
  outputDir += typeof browserConfig.outputName === 'function' ? browserConfig.outputName() : browserConfig.outputName;
126
126
  } else {
127
- const hash = crypto.createHash('md5');
127
+ const hash = crypto.createHash('sha256');
128
128
  hash.update(JSON.stringify(browserConfig));
129
129
  outputDir += hash.digest('hex');
130
130
  }
@@ -7,6 +7,11 @@ const Workers = require('../workers');
7
7
  module.exports = async function (workerCount, selectedRuns, options) {
8
8
  process.env.profile = options.profile;
9
9
 
10
+ const suiteArr = [];
11
+ const passedTestArr = [];
12
+ const failedTestArr = [];
13
+ const skippedTestArr = [];
14
+
10
15
  const { config: testConfig, override = '' } = options;
11
16
  const overrideConfigs = tryOrDefault(() => JSON.parse(override), {});
12
17
  const by = options.suites ? 'suite' : 'test';
@@ -26,15 +31,36 @@ module.exports = async function (workerCount, selectedRuns, options) {
26
31
 
27
32
  const workers = new Workers(numberOfWorkers, config);
28
33
  workers.overrideConfig(overrideConfigs);
29
- workers.on(event.test.failed, (failedTest) => {
30
- output.test.failed(failedTest);
34
+
35
+ workers.on(event.suite.before, (suite) => {
36
+ suiteArr.push(suite);
37
+ });
38
+
39
+ workers.on(event.test.failed, (test) => {
40
+ failedTestArr.push(test);
41
+ output.test.failed(test);
42
+ });
43
+
44
+ workers.on(event.test.passed, (test) => {
45
+ passedTestArr.push(test);
46
+ output.test.passed(test);
31
47
  });
32
48
 
33
- workers.on(event.test.passed, (successTest) => {
34
- output.test.passed(successTest);
49
+ workers.on(event.test.skipped, (test) => {
50
+ skippedTestArr.push(test);
51
+ output.test.passed(test);
35
52
  });
36
53
 
37
54
  workers.on(event.all.result, () => {
55
+ // expose test stats after all workers finished their execution
56
+ event.dispatcher.emit(event.workers.result, {
57
+ suites: suiteArr,
58
+ tests: {
59
+ passed: passedTestArr,
60
+ failed: failedTestArr,
61
+ skipped: skippedTestArr,
62
+ },
63
+ });
38
64
  workers.printResults();
39
65
  });
40
66
 
@@ -132,9 +132,32 @@ function initializeListeners() {
132
132
  duration: test.duration || 0,
133
133
  err,
134
134
  parent,
135
+ steps: test.steps ? simplifyStepsInTestObject(test.steps, err) : [],
135
136
  };
136
137
  }
137
138
 
139
+ function simplifyStepsInTestObject(steps, err) {
140
+ steps = [...steps];
141
+ const _steps = [];
142
+
143
+ for (step of steps) {
144
+ _steps.push({
145
+ actor: step.actor,
146
+ name: step.name,
147
+ status: step.status,
148
+ agrs: step.args,
149
+ startedAt: step.startedAt,
150
+ startTime: step.startTime,
151
+ endTime: step.endTime,
152
+ finishedAt: step.finishedAt,
153
+ duration: step.duration,
154
+ err,
155
+ });
156
+ }
157
+
158
+ return _steps;
159
+ }
160
+
138
161
  function simplifyStep(step, err = null) {
139
162
  step = { ...step };
140
163
 
package/lib/event.js CHANGED
@@ -127,10 +127,12 @@ module.exports = {
127
127
  * @inner
128
128
  * @property {'workers.before'} before
129
129
  * @property {'workers.after'} after
130
+ * @property {'workers.result'} result
130
131
  */
131
132
  workers: {
132
133
  before: 'workers.before',
133
134
  after: 'workers.after',
135
+ result: 'workers.result',
134
136
  },
135
137
 
136
138
  /**
@@ -368,7 +368,7 @@ class Appium extends Webdriver {
368
368
  if (context.web) return this.switchToWeb(context.web);
369
369
  if (context.webview) return this.switchToWeb(context.webview);
370
370
  }
371
- return this._switchToContext(context);
371
+ return this.switchToContext(context);
372
372
  }
373
373
 
374
374
  _withinEnd() {
@@ -423,8 +423,8 @@ class Appium extends Webdriver {
423
423
  async runOnIOS(caps, fn) {
424
424
  if (this.platform !== 'ios') return;
425
425
  recorder.session.start('iOS-only actions');
426
- this._runWithCaps(caps, fn);
427
- recorder.add('restore from iOS session', () => recorder.session.restore());
426
+ await this._runWithCaps(caps, fn);
427
+ await recorder.add('restore from iOS session', () => recorder.session.restore());
428
428
  return recorder.promise();
429
429
  }
430
430
 
@@ -465,8 +465,8 @@ class Appium extends Webdriver {
465
465
  async runOnAndroid(caps, fn) {
466
466
  if (this.platform !== 'android') return;
467
467
  recorder.session.start('Android-only actions');
468
- this._runWithCaps(caps, fn);
469
- recorder.add('restore from Android session', () => recorder.session.restore());
468
+ await this._runWithCaps(caps, fn);
469
+ await recorder.add('restore from Android session', () => recorder.session.restore());
470
470
  return recorder.promise();
471
471
  }
472
472
 
@@ -834,7 +834,7 @@ class Appium extends Webdriver {
834
834
  *
835
835
  * @param {*} context the context to switch to
836
836
  */
837
- async _switchToContext(context) {
837
+ async switchToContext(context) {
838
838
  return this.browser.switchContext(context);
839
839
  }
840
840
 
@@ -858,11 +858,11 @@ class Appium extends Webdriver {
858
858
  this.isWeb = true;
859
859
  this.defaultContext = 'body';
860
860
 
861
- if (context) return this._switchToContext(context);
861
+ if (context) return this.switchToContext(context);
862
862
  const contexts = await this.grabAllContexts();
863
863
  this.debugSection('Contexts', contexts.toString());
864
864
  for (const idx in contexts) {
865
- if (contexts[idx].match(/^WEBVIEW/)) return this._switchToContext(contexts[idx]);
865
+ if (contexts[idx].match(/^WEBVIEW/)) return this.switchToContext(contexts[idx]);
866
866
  }
867
867
 
868
868
  throw new Error('No WEBVIEW could be guessed, please specify one in params');
@@ -885,8 +885,8 @@ class Appium extends Webdriver {
885
885
  this.isWeb = false;
886
886
  this.defaultContext = '//*';
887
887
 
888
- if (context) return this._switchToContext(context);
889
- return this._switchToContext('NATIVE_APP');
888
+ if (context) return this.switchToContext(context);
889
+ return this.switchToContext('NATIVE_APP');
890
890
  }
891
891
 
892
892
  /**
@@ -94,6 +94,7 @@ const pathSeparator = path.sep;
94
94
  * @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
95
95
  * @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
96
96
  * @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
97
+ * @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
97
98
  */
98
99
  const config = {};
99
100
 
@@ -141,6 +142,21 @@ const config = {};
141
142
  * * `trace`: enables trace recording for failed tests; trace are saved into `output/trace` folder
142
143
  * * `keepTraceForPassedTests`: - save trace for passed tests
143
144
  *
145
+ * #### HAR Recording Customization
146
+ *
147
+ * A HAR file is an HTTP Archive file that contains a record of all the network requests that are made when a page is loaded.
148
+ * It contains information about the request and response headers, cookies, content, timings, and more. You can use HAR files to mock network requests in your tests.
149
+ * HAR will be saved to `output/har`. More info could be found here https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har.
150
+ *
151
+ * ```
152
+ * ...
153
+ * recordHar: {
154
+ * mode: 'minimal', // possible values: 'minimal'|'full'.
155
+ * content: 'embed' // possible values: "omit"|"embed"|"attach".
156
+ * }
157
+ * ...
158
+ *```
159
+ *
144
160
  * #### Example #1: Wait for 0 network connections.
145
161
  *
146
162
  * ```js
@@ -346,7 +362,7 @@ class Playwright extends Helper {
346
362
  ignoreLog: ['warning', 'log'],
347
363
  uniqueScreenshotNames: false,
348
364
  manualStart: false,
349
- getPageTimeout: 0,
365
+ getPageTimeout: 30000,
350
366
  waitForNavigation: 'load',
351
367
  restart: false,
352
368
  keepCookies: false,
@@ -455,9 +471,10 @@ class Playwright extends Helper {
455
471
  }
456
472
  }
457
473
 
458
- async _before() {
474
+ async _before(test) {
475
+ this.currentRunningTest = test;
459
476
  recorder.retry({
460
- retries: 5,
477
+ retries: process.env.FAILED_STEP_RETRIES || 3,
461
478
  when: err => {
462
479
  if (!err || typeof (err.message) !== 'string') {
463
480
  return false;
@@ -487,6 +504,15 @@ class Playwright extends Helper {
487
504
  }
488
505
  if (this.options.bypassCSP) contextOptions.bypassCSP = this.options.bypassCSP;
489
506
  if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo;
507
+ if (this.options.recordHar) {
508
+ const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har';
509
+ const fileName = `${`${global.output_dir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`;
510
+ const dir = path.dirname(fileName);
511
+ if (!fileExists(dir)) fs.mkdirSync(dir);
512
+ this.options.recordHar.path = fileName;
513
+ this.currentRunningTest.artifacts.har = fileName;
514
+ contextOptions.recordHar = this.options.recordHar;
515
+ }
490
516
  if (this.storageState) contextOptions.storageState = this.storageState;
491
517
  if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent;
492
518
  if (this.options.locale) contextOptions.locale = this.options.locale;
@@ -834,6 +860,7 @@ class Playwright extends Helper {
834
860
  this.context = null;
835
861
  this.frame = null;
836
862
  popupStore.clear();
863
+ if (this.options.recordHar) await this.browserContext.close();
837
864
  await this.browser.close();
838
865
  }
839
866
 
@@ -1089,6 +1116,33 @@ class Playwright extends Helper {
1089
1116
  return this.page.reload({ timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation });
1090
1117
  }
1091
1118
 
1119
+ /**
1120
+ * Replaying from HAR
1121
+ *
1122
+ * ```js
1123
+ * // Replay API requests from HAR.
1124
+ * // Either use a matching response from the HAR,
1125
+ * // or abort the request if nothing matches.
1126
+ * I.replayFromHar('./output/har/something.har', { url: "*\/**\/api/v1/fruits" });
1127
+ * I.amOnPage('https://demo.playwright.dev/api-mocking');
1128
+ * I.see('CodeceptJS');
1129
+ * ```
1130
+ *
1131
+ * @param {string} harFilePath Path to recorded HAR file
1132
+ * @param {object} [opts] [Options for replaying from HAR](https://playwright.dev/docs/api/class-page#page-route-from-har)
1133
+ *
1134
+ * @returns Promise<void>
1135
+ */
1136
+ async replayFromHar(harFilePath, opts) {
1137
+ const file = path.join(global.codecept_dir, harFilePath);
1138
+
1139
+ if (!fileExists(file)) {
1140
+ throw new Error(`File at ${file} cannot be found on local system`);
1141
+ }
1142
+
1143
+ await this.page.routeFromHAR(harFilePath, opts);
1144
+ }
1145
+
1092
1146
  /**
1093
1147
  * {{> scrollPageToTop }}
1094
1148
  */
@@ -1711,8 +1765,15 @@ class Playwright extends Helper {
1711
1765
  const el = els[0];
1712
1766
 
1713
1767
  await highlightActiveElement.call(this, el);
1768
+ let optionToSelect = '';
1769
+
1770
+ try {
1771
+ optionToSelect = await el.locator('option', { hasText: option }).textContent();
1772
+ } catch (e) {
1773
+ optionToSelect = option;
1774
+ }
1714
1775
 
1715
- if (!Array.isArray(option)) option = [option];
1776
+ if (!Array.isArray(option)) option = [optionToSelect];
1716
1777
 
1717
1778
  await el.selectOption(option);
1718
1779
  return this._waitForAction();
@@ -2062,7 +2123,9 @@ class Playwright extends Helper {
2062
2123
  let chunked = chunkArray(props, values.length);
2063
2124
  chunked = chunked.filter((val) => {
2064
2125
  for (let i = 0; i < val.length; ++i) {
2065
- if (val[i] !== values[i]) return false;
2126
+ const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
2127
+ const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
2128
+ if (_acutal !== _expected) return false;
2066
2129
  }
2067
2130
  return true;
2068
2131
  });
@@ -2262,6 +2325,10 @@ class Playwright extends Helper {
2262
2325
  test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`);
2263
2326
  }
2264
2327
  }
2328
+
2329
+ if (this.options.recordHar) {
2330
+ test.artifacts.har = this.currentRunningTest.artifacts.har;
2331
+ }
2265
2332
  }
2266
2333
 
2267
2334
  async _passed(test) {
@@ -2289,6 +2356,10 @@ class Playwright extends Helper {
2289
2356
  await this.browserContext.tracing.stop();
2290
2357
  }
2291
2358
  }
2359
+
2360
+ if (this.options.recordHar) {
2361
+ test.artifacts.har = this.currentRunningTest.artifacts.har;
2362
+ }
2292
2363
  }
2293
2364
 
2294
2365
  /**
@@ -297,7 +297,7 @@ class Puppeteer extends Helper {
297
297
  this.sessionPages = {};
298
298
  this.currentRunningTest = test;
299
299
  recorder.retry({
300
- retries: 3,
300
+ retries: process.env.FAILED_STEP_RETRIES || 3,
301
301
  when: err => {
302
302
  if (!err || typeof (err.message) !== 'string') {
303
303
  return false;
@@ -1784,7 +1784,9 @@ class Puppeteer extends Helper {
1784
1784
  let chunked = chunkArray(props, values.length);
1785
1785
  chunked = chunked.filter((val) => {
1786
1786
  for (let i = 0; i < val.length; ++i) {
1787
- if (val[i] !== values[i]) return false;
1787
+ const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
1788
+ const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
1789
+ if (_acutal !== _expected) return false;
1788
1790
  }
1789
1791
  return true;
1790
1792
  });
@@ -1815,7 +1817,9 @@ class Puppeteer extends Helper {
1815
1817
  let chunked = chunkArray(attrs, values.length);
1816
1818
  chunked = chunked.filter((val) => {
1817
1819
  for (let i = 0; i < val.length; ++i) {
1818
- if (val[i] !== values[i]) return false;
1820
+ const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
1821
+ const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
1822
+ if (_acutal !== _expected) return false;
1819
1823
  }
1820
1824
  return true;
1821
1825
  });
@@ -1508,7 +1508,9 @@ class WebDriver extends Helper {
1508
1508
  let chunked = chunkArray(props, values.length);
1509
1509
  chunked = chunked.filter((val) => {
1510
1510
  for (let i = 0; i < val.length; ++i) {
1511
- if (val[i] !== values[i]) return false;
1511
+ const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
1512
+ const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
1513
+ if (_acutal !== _expected) return false;
1512
1514
  }
1513
1515
  return true;
1514
1516
  });
@@ -1535,7 +1537,9 @@ class WebDriver extends Helper {
1535
1537
  let chunked = chunkArray(attrs, values.length);
1536
1538
  chunked = chunked.filter((val) => {
1537
1539
  for (let i = 0; i < val.length; ++i) {
1538
- if (val[i] !== values[i]) return false;
1540
+ const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
1541
+ const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
1542
+ if (_acutal !== _expected) return false;
1539
1543
  }
1540
1544
  return true;
1541
1545
  });
@@ -119,7 +119,14 @@ module.exports = (text, file) => {
119
119
  });
120
120
  }
121
121
  const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name));
122
- const title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
122
+ let title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
123
+
124
+ for (const [key, value] of Object.entries(current)) {
125
+ if (title.includes(`<${key}>`)) {
126
+ title = title.replace(JSON.stringify(current), '').replace(`<${key}>`, value);
127
+ }
128
+ }
129
+
123
130
  const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)));
124
131
  test.tags = suite.tags.concat(tags);
125
132
  test.file = file;
@@ -35,6 +35,7 @@ class ScenarioConfig {
35
35
  * @returns {this}
36
36
  */
37
37
  retry(retries) {
38
+ if (process.env.SCENARIO_ONLY) retries = -retries;
38
39
  this.test.retries(retries);
39
40
  return this;
40
41
  }
package/lib/locator.js CHANGED
@@ -1,4 +1,4 @@
1
- const cssToXPath = require('css-to-xpath');
1
+ const cssToXPath = require('convert-cssxpath');
2
2
  const { sprintf } = require('sprintf-js');
3
3
 
4
4
  const { xpathLocator } = require('./utils');
@@ -162,7 +162,7 @@ class Locator {
162
162
  */
163
163
  toXPath() {
164
164
  if (this.isXPath()) return this.value;
165
- if (this.isCSS()) return cssToXPath(this.value);
165
+ if (this.isCSS()) return cssToXPath.convert(this.value);
166
166
 
167
167
  throw new Error('Can\'t be converted to XPath');
168
168
  }
@@ -38,12 +38,14 @@ const defaultConfig = {
38
38
  * ```js
39
39
  * // inside a test file
40
40
  * // use login to inject auto-login function
41
+ * Feature('Login');
42
+ *
41
43
  * Before(({ login }) => {
42
44
  * login('user'); // login using user session
43
45
  * });
44
46
  *
45
- * // Alternatively log in for one scenario
46
- * Scenario('log me in', ( {I, login} ) => {
47
+ * // Alternatively log in for one scenario.
48
+ * Scenario('log me in', ( { I, login } ) => {
47
49
  * login('admin');
48
50
  * I.see('I am logged in');
49
51
  * });
@@ -1,5 +1,6 @@
1
1
  const event = require('../event');
2
2
  const recorder = require('../recorder');
3
+ const container = require('../container');
3
4
 
4
5
  const defaultConfig = {
5
6
  retries: 3,
@@ -98,6 +99,8 @@ module.exports = (config) => {
98
99
  config.when = when;
99
100
 
100
101
  event.dispatcher.on(event.step.started, (step) => {
102
+ if (container.plugins('tryTo')) return;
103
+
101
104
  // if a step is ignored - return
102
105
  for (const ignored of config.ignoredSteps) {
103
106
  if (step.name === ignored) return;
@@ -114,6 +117,8 @@ module.exports = (config) => {
114
117
 
115
118
  event.dispatcher.on(event.test.before, (test) => {
116
119
  if (test && test.disableRetryFailedStep) return; // disable retry when a test is not active
120
+ // this env var is used to set the retries inside _before() block of helpers
121
+ process.env.FAILED_STEP_RETRIES = config.retries;
117
122
  recorder.retry(config);
118
123
  });
119
124
  };
@@ -89,8 +89,8 @@ module.exports = function (config) {
89
89
  const reportDir = config.output ? path.resolve(global.codecept_dir, config.output) : defaultConfig.output;
90
90
 
91
91
  event.dispatcher.on(event.test.before, (test) => {
92
- const md5hash = crypto.createHash('md5').update(test.file + test.title).digest('hex');
93
- dir = path.join(reportDir, `record_${md5hash}`);
92
+ const sha256hash = crypto.createHash('sha256').update(test.file + test.title).digest('hex');
93
+ dir = path.join(reportDir, `record_${sha256hash}`);
94
94
  mkdirp.sync(dir);
95
95
  stepNum = 0;
96
96
  error = null;
package/lib/ui.js CHANGED
@@ -168,6 +168,7 @@ module.exports = function (suite) {
168
168
  context.Scenario.only = function (title, opts, fn) {
169
169
  const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}`;
170
170
  mocha.grep(new RegExp(reString));
171
+ process.env.SCENARIO_ONLY = true;
171
172
  return addScenario(title, opts, fn);
172
173
  };
173
174
 
package/lib/workers.js CHANGED
@@ -359,6 +359,7 @@ class Workers extends EventEmitter {
359
359
  this.stats.start = new Date();
360
360
  recorder.startUnlessRunning();
361
361
  event.dispatcher.emit(event.workers.before);
362
+ process.env.RUNS_WITH_WORKERS = 'true';
362
363
  recorder.add('starting workers', () => {
363
364
  for (const worker of this.workers) {
364
365
  const workerThread = createWorker(worker);
@@ -492,6 +493,7 @@ class Workers extends EventEmitter {
492
493
  }
493
494
 
494
495
  output.result(this.stats.passes, this.stats.failures, this.stats.pending, ms(this.stats.duration));
496
+ process.env.RUNS_WITH_WORKERS = 'false';
495
497
  }
496
498
  }
497
499