codeceptjs 3.0.7 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +96 -2
  2. package/README.md +9 -1
  3. package/bin/codecept.js +27 -17
  4. package/docs/bdd.md +55 -1
  5. package/docs/build/Appium.js +76 -4
  6. package/docs/build/Playwright.js +186 -69
  7. package/docs/build/Protractor.js +2 -0
  8. package/docs/build/Puppeteer.js +56 -18
  9. package/docs/build/REST.js +12 -0
  10. package/docs/build/WebDriver.js +1 -3
  11. package/docs/changelog.md +96 -2
  12. package/docs/commands.md +21 -7
  13. package/docs/configuration.md +15 -2
  14. package/docs/helpers/Appium.md +96 -94
  15. package/docs/helpers/Playwright.md +259 -202
  16. package/docs/helpers/Puppeteer.md +17 -1
  17. package/docs/helpers/REST.md +23 -9
  18. package/docs/helpers/WebDriver.md +2 -2
  19. package/docs/mobile.md +2 -1
  20. package/docs/playwright.md +156 -6
  21. package/docs/plugins.md +61 -69
  22. package/docs/react.md +1 -1
  23. package/docs/reports.md +21 -3
  24. package/lib/actor.js +2 -3
  25. package/lib/codecept.js +13 -2
  26. package/lib/command/definitions.js +8 -1
  27. package/lib/command/run-multiple/collection.js +4 -0
  28. package/lib/config.js +1 -1
  29. package/lib/container.js +3 -3
  30. package/lib/data/dataTableArgument.js +35 -0
  31. package/lib/helper/Appium.js +49 -4
  32. package/lib/helper/Playwright.js +186 -69
  33. package/lib/helper/Protractor.js +2 -0
  34. package/lib/helper/Puppeteer.js +56 -18
  35. package/lib/helper/REST.js +12 -0
  36. package/lib/helper/WebDriver.js +1 -3
  37. package/lib/helper/errors/ConnectionRefused.js +1 -1
  38. package/lib/helper/extras/Popup.js +1 -1
  39. package/lib/helper/extras/React.js +44 -32
  40. package/lib/index.js +2 -0
  41. package/lib/interfaces/gherkin.js +8 -1
  42. package/lib/listener/exit.js +2 -4
  43. package/lib/listener/helpers.js +4 -4
  44. package/lib/locator.js +7 -0
  45. package/lib/mochaFactory.js +13 -9
  46. package/lib/output.js +2 -2
  47. package/lib/plugin/allure.js +7 -18
  48. package/lib/plugin/commentStep.js +1 -1
  49. package/lib/plugin/{puppeteerCoverage.js → coverage.js} +10 -22
  50. package/lib/plugin/customLocator.js +2 -2
  51. package/lib/plugin/subtitles.js +88 -0
  52. package/lib/plugin/tryTo.js +1 -1
  53. package/lib/recorder.js +5 -3
  54. package/lib/step.js +4 -2
  55. package/package.json +4 -3
  56. package/typings/index.d.ts +2 -0
  57. package/typings/types.d.ts +158 -18
@@ -124,6 +124,8 @@ class Protractor extends Helper {
124
124
 
125
125
  this.isRunning = false;
126
126
  this._setConfig(config);
127
+
128
+ console.log('Protractor helper is deprecated as well as Protractor itself.\nThis helper will be removed in next major release');
127
129
  }
128
130
 
129
131
  _validateConfig(config) {
@@ -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
  *
@@ -670,7 +686,7 @@ class Puppeteer extends Helper {
670
686
  assertElementExists(els);
671
687
 
672
688
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
673
- const { x, y } = await els[0]._clickablePoint();
689
+ const { x, y } = await getClickablePoint(els[0]);
674
690
  await this.page.mouse.move(x + offsetX, y + offsetY);
675
691
  return this._waitForAction();
676
692
  }
@@ -726,7 +742,7 @@ class Puppeteer extends Helper {
726
742
  const els = await this._locate(locator);
727
743
  assertElementExists(els, locator, 'Element');
728
744
  await els[0]._scrollIntoViewIfNeeded();
729
- const elementCoordinates = await els[0]._clickablePoint();
745
+ const elementCoordinates = await getClickablePoint(els[0]);
730
746
  await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY);
731
747
  } else {
732
748
  await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY);
@@ -942,7 +958,10 @@ class Puppeteer extends Helper {
942
958
  */
943
959
  async seeElement(locator) {
944
960
  let els = await this._locate(locator);
945
- els = await Promise.all(els.map(el => el.boundingBox()));
961
+ els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
962
+ // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
963
+ els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
964
+
946
965
  return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'));
947
966
  }
948
967
 
@@ -952,7 +971,10 @@ class Puppeteer extends Helper {
952
971
  */
953
972
  async dontSeeElement(locator) {
954
973
  let els = await this._locate(locator);
955
- els = await Promise.all(els.map(el => el.boundingBox()));
974
+ els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
975
+ // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
976
+ els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
977
+
956
978
  return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'));
957
979
  }
958
980
 
@@ -1239,7 +1261,7 @@ class Puppeteer extends Helper {
1239
1261
  * {{ react }}
1240
1262
  */
1241
1263
  async fillField(field, value) {
1242
- const els = await findFields.call(this, field);
1264
+ const els = await findVisibleFields.call(this, field);
1243
1265
  assertElementExists(els, field, 'Field');
1244
1266
  const el = els[0];
1245
1267
  const tag = await el.getProperty('tagName').then(el => el.jsonValue());
@@ -1266,7 +1288,7 @@ class Puppeteer extends Helper {
1266
1288
  * {{ react }}
1267
1289
  */
1268
1290
  async appendField(field, value) {
1269
- const els = await findFields.call(this, field);
1291
+ const els = await findVisibleFields.call(this, field);
1270
1292
  assertElementExists(els, field, 'Field');
1271
1293
  await els[0].press('End');
1272
1294
  await els[0].type(value, { delay: this.options.pressKeyDelay });
@@ -1308,7 +1330,7 @@ class Puppeteer extends Helper {
1308
1330
  * {{> selectOption }}
1309
1331
  */
1310
1332
  async selectOption(select, option) {
1311
- const els = await findFields.call(this, select);
1333
+ const els = await findVisibleFields.call(this, select);
1312
1334
  assertElementExists(els, select, 'Selectable field');
1313
1335
  const el = els[0];
1314
1336
  if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
@@ -1342,7 +1364,10 @@ class Puppeteer extends Helper {
1342
1364
  */
1343
1365
  async grabNumberOfVisibleElements(locator) {
1344
1366
  let els = await this._locate(locator);
1345
- els = await Promise.all(els.map(el => el.boundingBox()));
1367
+ els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
1368
+ // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1369
+ els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
1370
+
1346
1371
  return els.filter(v => v).length;
1347
1372
  }
1348
1373
 
@@ -1727,8 +1752,8 @@ class Puppeteer extends Helper {
1727
1752
  const src = await this._locate(locator);
1728
1753
  assertElementExists(src, locator, 'Slider Element');
1729
1754
 
1730
- // Note: Using private api ._clickablePoint because the .BoundingBox does not take into account iframe offsets!
1731
- const sliderSource = await src[0]._clickablePoint();
1755
+ // Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
1756
+ const sliderSource = await getClickablePoint(src[0]);
1732
1757
 
1733
1758
  // Drag start point
1734
1759
  await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
@@ -2264,7 +2289,7 @@ class Puppeteer extends Helper {
2264
2289
  module.exports = Puppeteer;
2265
2290
 
2266
2291
  async function findElements(matcher, locator) {
2267
- if (locator.react) return findReact(matcher, locator);
2292
+ if (locator.react) return findReact(matcher.executionContext(), locator);
2268
2293
  locator = new Locator(locator, 'css');
2269
2294
  if (!locator.isXPath()) return matcher.$$(locator.simplify());
2270
2295
  return matcher.$x(locator.value);
@@ -2293,7 +2318,7 @@ async function proceedClick(locator, context = null, options = {}) {
2293
2318
  }
2294
2319
 
2295
2320
  async function findClickable(matcher, locator) {
2296
- if (locator.react) return findReact(matcher, locator);
2321
+ if (locator.react) return findReact(matcher.executionContext(), locator);
2297
2322
  locator = new Locator(locator);
2298
2323
  if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
2299
2324
 
@@ -2376,6 +2401,12 @@ async function proceedIsChecked(assertType, option) {
2376
2401
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected);
2377
2402
  }
2378
2403
 
2404
+ async function findVisibleFields(locator) {
2405
+ const els = await findFields.call(this, locator);
2406
+ const visible = await Promise.all(els.map(el => el.boundingBox()));
2407
+ return els.filter((el, index) => visible[index]);
2408
+ }
2409
+
2379
2410
  async function findFields(locator) {
2380
2411
  const matchedLocator = new Locator(locator);
2381
2412
  if (!matchedLocator.isFuzzy()) {
@@ -2406,9 +2437,9 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
2406
2437
  const dst = await this._locate(destinationLocator);
2407
2438
  assertElementExists(dst, destinationLocator, 'Destination Element');
2408
2439
 
2409
- // Note: Using private api ._clickablePoint becaues the .BoundingBox does not take into account iframe offsets!
2410
- const dragSource = await src[0]._clickablePoint();
2411
- const dragDestination = await dst[0]._clickablePoint();
2440
+ // Note: Using public api .getClickablePoint becaues the .BoundingBox does not take into account iframe offsets
2441
+ const dragSource = await getClickablePoint(src[0]);
2442
+ const dragDestination = await getClickablePoint(dst[0]);
2412
2443
 
2413
2444
  // Drag start point
2414
2445
  await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 });
@@ -2422,7 +2453,7 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
2422
2453
  }
2423
2454
 
2424
2455
  async function proceedSeeInField(assertType, field, value) {
2425
- const els = await findFields.call(this, field);
2456
+ const els = await findVisibleFields.call(this, field);
2426
2457
  assertElementExists(els, field, 'Field');
2427
2458
  const el = els[0];
2428
2459
  const tag = await el.getProperty('tagName').then(el => el.jsonValue());
@@ -2555,6 +2586,13 @@ async function targetCreatedHandler(page) {
2555
2586
  }
2556
2587
  }
2557
2588
 
2589
+ // BC compatibility for Puppeteer < 10
2590
+ async function getClickablePoint(el) {
2591
+ if (el.clickablePoint) return el.clickablePoint();
2592
+ if (el._clickablePoint) return el._clickablePoint();
2593
+ return null;
2594
+ }
2595
+
2558
2596
  // List of key values to key definitions
2559
2597
  // https://github.com/GoogleChrome/puppeteer/blob/v1.20.0/lib/USKeyboardLayout.js
2560
2598
  const keyDefinitionMap = {
@@ -75,6 +75,8 @@ class REST extends Helper {
75
75
  * Executes axios request
76
76
  *
77
77
  * @param {*} request
78
+ *
79
+ * @returns {Promise<*>} response
78
80
  */
79
81
  async _executeRequest(request) {
80
82
  const _debugRequest = { ...request };
@@ -143,6 +145,8 @@ class REST extends Helper {
143
145
  *
144
146
  * @param {*} url
145
147
  * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
148
+ *
149
+ * @returns {Promise<*>} response
146
150
  */
147
151
  async sendGetRequest(url, headers = {}) {
148
152
  const request = {
@@ -166,6 +170,8 @@ class REST extends Helper {
166
170
  * @param {*} url
167
171
  * @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
168
172
  * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
173
+ *
174
+ * @returns {Promise<*>} response
169
175
  */
170
176
  async sendPostRequest(url, payload = {}, headers = {}) {
171
177
  const request = {
@@ -197,6 +203,8 @@ class REST extends Helper {
197
203
  * @param {string} url
198
204
  * @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
199
205
  * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
206
+ *
207
+ * @returns {Promise<*>} response
200
208
  */
201
209
  async sendPatchRequest(url, payload = {}, headers = {}) {
202
210
  const request = {
@@ -228,6 +236,8 @@ class REST extends Helper {
228
236
  * @param {string} url
229
237
  * @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
230
238
  * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
239
+ *
240
+ * @returns {Promise<*>} response
231
241
  */
232
242
  async sendPutRequest(url, payload = {}, headers = {}) {
233
243
  const request = {
@@ -254,6 +264,8 @@ class REST extends Helper {
254
264
  *
255
265
  * @param {*} url
256
266
  * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
267
+ *
268
+ * @returns {Promise<*>} response
257
269
  */
258
270
  async sendDeleteRequest(url, headers = {}) {
259
271
  const request = {
@@ -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
 
@@ -1252,7 +1252,6 @@ class WebDriver extends Helper {
1252
1252
 
1253
1253
  /**
1254
1254
  * {{> grabAttributeFromAll }}
1255
- * Appium: can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
1256
1255
  */
1257
1256
  async grabAttributeFromAll(locator, attr) {
1258
1257
  const res = await this._locate(locator, true);
@@ -1263,7 +1262,6 @@ class WebDriver extends Helper {
1263
1262
 
1264
1263
  /**
1265
1264
  * {{> grabAttributeFrom }}
1266
- * Appium: can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
1267
1265
  */
1268
1266
  async grabAttributeFrom(locator, attr) {
1269
1267
  const attrs = await this.grabAttributeFromAll(locator, attr);
@@ -1,7 +1,7 @@
1
1
  function ConnectionRefused(err) {
2
2
  this.message = "Can't connect to WebDriver.\n";
3
3
  this.message += `${err}\n\n`;
4
- this.message += 'Please make sure Selenium Server (ChromeDriver or PhantomJS) is running and accessible';
4
+ this.message += 'Please make sure Selenium Server is running and accessible';
5
5
  this.stack = err.stack;
6
6
  }
7
7
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
  class Popup {
5
5
  constructor(popup, defaultAction) {
6
- this._popup = popup || {};
6
+ this._popup = popup || null;
7
7
  this._actionType = '';
8
8
  this._defaultAction = defaultAction || '';
9
9
  }
@@ -4,50 +4,62 @@ let resqScript;
4
4
 
5
5
  module.exports = async function findReact(matcher, locator) {
6
6
  if (!resqScript) resqScript = fs.readFileSync(require.resolve('resq'));
7
- await matcher.executionContext().evaluate(resqScript.toString());
8
- await matcher.executionContext().evaluate(() => window.resq.waitToLoadReact());
9
- const arrayHandle = await matcher.executionContext().evaluateHandle((selector, props, state) => {
10
- let elements = window.resq.resq$$(selector);
11
- if (Object.keys(props).length) {
12
- elements = elements.byProps(props);
13
- }
14
- if (Object.keys(state).length) {
15
- elements = elements.byState(state);
16
- }
7
+ await matcher.evaluate(resqScript.toString());
8
+ await matcher
9
+ .evaluate(() => window.resq.waitToLoadReact());
10
+ const arrayHandle = await matcher.evaluateHandle(
11
+ (obj) => {
12
+ const { selector, props, state } = obj;
17
13
 
18
- if (!elements.length) {
19
- return [];
20
- }
14
+ let elements = window.resq.resq$$(selector);
15
+ if (Object.keys(props).length) {
16
+ elements = elements.byProps(props);
17
+ }
18
+ if (Object.keys(state).length) {
19
+ elements = elements.byState(state);
20
+ }
21
21
 
22
- // resq returns an array of HTMLElements if the React component is a fragment
23
- // this avoids having nested arrays of nodes which the driver does not understand
24
- // [[div, div], [div, div]] => [div, div, div, div]
25
- let nodes = [];
22
+ if (!elements.length) {
23
+ return [];
24
+ }
26
25
 
27
- elements.forEach((element) => {
28
- let { node, isFragment } = element;
26
+ // resq returns an array of HTMLElements if the React component is a fragment
27
+ // this avoids having nested arrays of nodes which the driver does not understand
28
+ // [[div, div], [div, div]] => [div, div, div, div]
29
+ let nodes = [];
29
30
 
30
- if (!node) {
31
- isFragment = true;
32
- node = element.children;
33
- }
31
+ elements.forEach((element) => {
32
+ let { node, isFragment } = element;
34
33
 
35
- if (isFragment) {
36
- nodes = nodes.concat(node);
37
- } else {
38
- nodes.push(node);
39
- }
40
- });
34
+ if (!node) {
35
+ isFragment = true;
36
+ node = element.children;
37
+ }
41
38
 
42
- return [...nodes];
43
- }, locator.react, locator.props || {}, locator.state || {}, locator.children || {}, matcher);
39
+ if (isFragment) {
40
+ nodes = nodes.concat(node);
41
+ } else {
42
+ nodes.push(node);
43
+ }
44
+ });
45
+
46
+ return [...nodes];
47
+ },
48
+ {
49
+ selector: locator.react,
50
+ props: locator.props || {},
51
+ state: locator.state || {},
52
+ },
53
+ );
44
54
 
45
55
  const properties = await arrayHandle.getProperties();
46
56
  await arrayHandle.dispose();
47
57
  const result = [];
48
58
  for (const property of properties.values()) {
49
59
  const elementHandle = property.asElement();
50
- if (elementHandle) { result.push(elementHandle); }
60
+ if (elementHandle) {
61
+ result.push(elementHandle);
62
+ }
51
63
  }
52
64
 
53
65
  return result;
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} */
@@ -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));
@@ -83,6 +87,7 @@ module.exports = (text) => {
83
87
  for (const index in example.cells) {
84
88
  const placeholder = fields[index];
85
89
  const value = transform('gherkin.examples', example.cells[index].value);
90
+ example.cells[index].value = value;
86
91
  current[placeholder] = value;
87
92
  exampleSteps = exampleSteps.map((step) => {
88
93
  step = { ...step };
@@ -94,6 +99,7 @@ module.exports = (text) => {
94
99
  const title = `${child.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
95
100
  const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)));
96
101
  test.tags = suite.tags.concat(tags);
102
+ test.file = file;
97
103
  suite.addTest(scenario.test(test));
98
104
  }
99
105
  }
@@ -103,6 +109,7 @@ module.exports = (text) => {
103
109
  const title = `${child.name} ${tags.join(' ')}`.trim();
104
110
  const test = new Test(title, async () => runSteps(child.steps));
105
111
  test.tags = suite.tags.concat(tags);
112
+ test.file = file;
106
113
  suite.addTest(scenario.test(test));
107
114
  }
108
115
 
@@ -18,13 +18,11 @@ module.exports = function () {
18
18
  failedTests = failedTests.filter(failed => id !== failed);
19
19
  });
20
20
 
21
- event.dispatcher.on(event.all.result, () => {
21
+ process.on('beforeExit', (code) => {
22
22
  if (failedTests.length) {
23
- process.exitCode = 1;
23
+ code = 1;
24
24
  }
25
- });
26
25
 
27
- process.on('beforeExit', (code) => {
28
26
  if (code) {
29
27
  process.exit(code);
30
28
  }
@@ -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');
@@ -11,11 +12,10 @@ module.exports = function () {
11
12
 
12
13
  const runHelpersHook = (hook, param) => {
13
14
  if (store.dryRun) return;
14
- Object.keys(helpers).forEach((key) => {
15
- if (!helpers[key][hook]) {
16
- return;
15
+ Object.values(helpers).forEach((helper) => {
16
+ if (helper[hook]) {
17
+ helper[hook](param);
17
18
  }
18
- helpers[key][hook](param);
19
19
  });
20
20
  };
21
21
 
package/lib/locator.js CHANGED
@@ -150,6 +150,13 @@ class Locator {
150
150
  return this.isFuzzy() && this.value[0] === '~';
151
151
  }
152
152
 
153
+ /**
154
+ * @returns {boolean}
155
+ */
156
+ isBasic() {
157
+ return this.isCSS() || this.isXPath();
158
+ }
159
+
153
160
  /**
154
161
  * @returns {string}
155
162
  */
@@ -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
 
package/lib/output.js CHANGED
@@ -235,8 +235,8 @@ function print(...msg) {
235
235
  }
236
236
 
237
237
  function truncate(msg, gap = 0) {
238
- if (msg.indexOf('\n') > 0) {
239
- return msg; // don't cut multi line steps
238
+ if (msg.indexOf('\n') > 0 || outputLevel >= 3) {
239
+ return msg; // don't cut multi line steps or on verbose log level
240
240
  }
241
241
  const width = (process.stdout.columns || 200) - gap - 4;
242
242
  if (msg.length > width) {
@@ -83,7 +83,6 @@ module.exports = (config) => {
83
83
 
84
84
  let currentMetaStep = [];
85
85
  let currentStep;
86
- let isHookSteps = false;
87
86
 
88
87
  reporter.pendingCase = function (testName, timestamp, opts = {}) {
89
88
  reporter.startCase(testName, timestamp);
@@ -191,14 +190,6 @@ module.exports = (config) => {
191
190
  }
192
191
  });
193
192
 
194
- event.dispatcher.on(event.hook.started, () => {
195
- isHookSteps = true;
196
- });
197
-
198
- event.dispatcher.on(event.hook.passed, () => {
199
- isHookSteps = false;
200
- });
201
-
202
193
  event.dispatcher.on(event.suite.after, () => {
203
194
  reporter.endSuite();
204
195
  });
@@ -258,15 +249,13 @@ module.exports = (config) => {
258
249
  });
259
250
 
260
251
  event.dispatcher.on(event.step.started, (step) => {
261
- if (isHookSteps === false) {
262
- startMetaStep(step.metaStep);
263
- if (currentStep !== step) {
264
- // In multi-session scenarios, actors' names will be highlighted with ANSI
265
- // escape sequences which are invalid XML values
266
- step.actor = step.actor.replace(ansiRegExp(), '');
267
- reporter.startStep(step.toString());
268
- currentStep = step;
269
- }
252
+ startMetaStep(step.metaStep);
253
+ if (currentStep !== step) {
254
+ // In multi-session scenarios, actors' names will be highlighted with ANSI
255
+ // escape sequences which are invalid XML values
256
+ step.actor = step.actor.replace(ansiRegExp(), '');
257
+ reporter.startStep(step.toString());
258
+ currentStep = step;
270
259
  }
271
260
  });
272
261
 
@@ -41,7 +41,7 @@ const defaultGlobalName = '__';
41
41
  * ### Config
42
42
  *
43
43
  * * `enabled` - (default: false) enable a plugin
44
- * * `regusterGlobal` - (default: false) register `__` template literal function globally. You can override function global name by providing a name as a value.
44
+ * * `registerGlobal` - (default: false) register `__` template literal function globally. You can override function global name by providing a name as a value.
45
45
  *
46
46
  * ### Examples
47
47
  *