codeceptjs 3.0.6 → 3.1.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +92 -8
  2. package/README.md +9 -1
  3. package/bin/codecept.js +28 -17
  4. package/docs/build/Appium.js +69 -0
  5. package/docs/build/GraphQL.js +9 -10
  6. package/docs/build/Playwright.js +271 -63
  7. package/docs/build/Protractor.js +2 -0
  8. package/docs/build/Puppeteer.js +56 -18
  9. package/docs/build/REST.js +16 -3
  10. package/docs/build/WebDriver.js +82 -16
  11. package/docs/changelog.md +93 -9
  12. package/docs/configuration.md +15 -2
  13. package/docs/email.md +8 -8
  14. package/docs/examples.md +3 -3
  15. package/docs/helpers/Appium.md +66 -68
  16. package/docs/helpers/MockRequest.md +3 -3
  17. package/docs/helpers/Playwright.md +269 -203
  18. package/docs/helpers/Puppeteer.md +17 -1
  19. package/docs/helpers/REST.md +23 -9
  20. package/docs/helpers/WebDriver.md +3 -2
  21. package/docs/locators.md +27 -0
  22. package/docs/mobile.md +2 -1
  23. package/docs/nightmare.md +0 -5
  24. package/docs/parallel.md +14 -7
  25. package/docs/playwright.md +178 -11
  26. package/docs/plugins.md +61 -69
  27. package/docs/react.md +1 -1
  28. package/docs/reports.md +5 -4
  29. package/lib/actor.js +1 -2
  30. package/lib/codecept.js +13 -2
  31. package/lib/command/definitions.js +8 -1
  32. package/lib/command/interactive.js +4 -2
  33. package/lib/command/run-multiple/collection.js +4 -0
  34. package/lib/container.js +3 -3
  35. package/lib/helper/Appium.js +41 -0
  36. package/lib/helper/GraphQL.js +9 -10
  37. package/lib/helper/Playwright.js +218 -70
  38. package/lib/helper/Protractor.js +2 -0
  39. package/lib/helper/Puppeteer.js +56 -18
  40. package/lib/helper/REST.js +12 -0
  41. package/lib/helper/WebDriver.js +82 -16
  42. package/lib/helper/errors/ConnectionRefused.js +1 -1
  43. package/lib/helper/extras/Popup.js +1 -1
  44. package/lib/helper/extras/React.js +44 -32
  45. package/lib/interfaces/gherkin.js +1 -0
  46. package/lib/listener/exit.js +2 -4
  47. package/lib/listener/helpers.js +3 -4
  48. package/lib/locator.js +7 -0
  49. package/lib/mochaFactory.js +11 -6
  50. package/lib/output.js +5 -2
  51. package/lib/plugin/allure.js +7 -18
  52. package/lib/plugin/commentStep.js +1 -1
  53. package/lib/plugin/{puppeteerCoverage.js → coverage.js} +10 -22
  54. package/lib/plugin/customLocator.js +2 -2
  55. package/lib/plugin/screenshotOnFail.js +5 -0
  56. package/lib/plugin/subtitles.js +88 -0
  57. package/lib/plugin/tryTo.js +1 -1
  58. package/lib/step.js +4 -2
  59. package/lib/ui.js +6 -2
  60. package/package.json +5 -4
  61. package/typings/index.d.ts +44 -21
  62. package/typings/types.d.ts +137 -16
@@ -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 = {
@@ -396,6 +396,7 @@ class WebDriver extends Helper {
396
396
  this.isRunning = false;
397
397
  this.sessionWindows = {};
398
398
  this.activeSessionName = '';
399
+ this.customLocatorStrategies = config.customLocatorStrategies;
399
400
 
400
401
  this._setConfig(config);
401
402
 
@@ -503,6 +504,33 @@ class WebDriver extends Helper {
503
504
  }
504
505
  }
505
506
 
507
+ _lookupCustomLocator(customStrategy) {
508
+ if (typeof (this.customLocatorStrategies) !== 'object') {
509
+ return null;
510
+ }
511
+ const strategy = this.customLocatorStrategies[customStrategy];
512
+ return typeof (strategy) === 'function' ? strategy : null;
513
+ }
514
+
515
+ _isCustomLocator(locator) {
516
+ const locatorObj = new Locator(locator);
517
+ if (locatorObj.isCustom()) {
518
+ const customLocator = this._lookupCustomLocator(locatorObj.type);
519
+ if (customLocator) {
520
+ return true;
521
+ }
522
+ throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".');
523
+ }
524
+ return false;
525
+ }
526
+
527
+ async _res(locator) {
528
+ const res = (this._isShadowLocator(locator) || this._isCustomLocator(locator))
529
+ ? await this._locate(locator)
530
+ : await this.$$(withStrictLocator(locator));
531
+ return res;
532
+ }
533
+
506
534
  async _startBrowser() {
507
535
  try {
508
536
  if (this.options.multiremote) {
@@ -530,9 +558,22 @@ class WebDriver extends Helper {
530
558
  await this._resizeWindowIfNeeded(this.browser, this.options.windowSize);
531
559
 
532
560
  this.$$ = this.browser.$$.bind(this.browser);
561
+
562
+ if (this._isCustomLocatorStrategyDefined()) {
563
+ Object.keys(this.customLocatorStrategies).forEach(async (customLocator) => {
564
+ this.debugSection('Weddriver', `adding custom locator strategy: ${customLocator}`);
565
+ const locatorFunction = this._lookupCustomLocator(customLocator);
566
+ this.browser.addLocatorStrategy(customLocator, locatorFunction);
567
+ });
568
+ }
569
+
533
570
  return this.browser;
534
571
  }
535
572
 
573
+ _isCustomLocatorStrategyDefined() {
574
+ return this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length;
575
+ }
576
+
536
577
  async _stopBrowser() {
537
578
  if (this.browser && this.isRunning) await this.browser.deleteSession();
538
579
  }
@@ -755,17 +796,34 @@ class WebDriver extends Helper {
755
796
  }
756
797
 
757
798
  if (!this.options.smartWait || !smartWait) {
799
+ if (this._isCustomLocator(locator)) {
800
+ const locatorObj = new Locator(locator);
801
+ return this.browser.custom$$(locatorObj.type, locatorObj.value);
802
+ }
803
+
758
804
  const els = await this.$$(withStrictLocator(locator));
759
805
  return els;
760
806
  }
761
807
 
762
808
  await this._smartWait(locator);
763
809
 
810
+ if (this._isCustomLocator(locator)) {
811
+ const locatorObj = new Locator(locator);
812
+ return this.browser.custom$$(locatorObj.type, locatorObj.value);
813
+ }
814
+
764
815
  const els = await this.$$(withStrictLocator(locator));
765
816
  await this.defineTimeout({ implicit: 0 });
766
817
  return els;
767
818
  }
768
819
 
820
+ _grabCustomLocator(locator) {
821
+ if (typeof locator === 'string') {
822
+ locator = new Locator(locator);
823
+ }
824
+ return locator.value ? locator.value : locator.custom;
825
+ }
826
+
769
827
  /**
770
828
  * Find a checkbox by providing human readable text:
771
829
  *
@@ -962,6 +1020,7 @@ class WebDriver extends Helper {
962
1020
  /**
963
1021
  * {{> fillField }}
964
1022
  * {{ react }}
1023
+ * {{ custom }}
965
1024
  *
966
1025
  */
967
1026
  async fillField(field, value) {
@@ -1193,7 +1252,6 @@ class WebDriver extends Helper {
1193
1252
 
1194
1253
  /**
1195
1254
  * {{> grabAttributeFromAll }}
1196
- * Appium: can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
1197
1255
  */
1198
1256
  async grabAttributeFromAll(locator, attr) {
1199
1257
  const res = await this._locate(locator, true);
@@ -1204,7 +1262,6 @@ class WebDriver extends Helper {
1204
1262
 
1205
1263
  /**
1206
1264
  * {{> grabAttributeFrom }}
1207
- * Appium: can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
1208
1265
  */
1209
1266
  async grabAttributeFrom(locator, attr) {
1210
1267
  const attrs = await this.grabAttributeFromAll(locator, attr);
@@ -1335,7 +1392,7 @@ class WebDriver extends Helper {
1335
1392
  *
1336
1393
  */
1337
1394
  async seeElementInDOM(locator) {
1338
- const res = await this.$$(withStrictLocator(locator));
1395
+ const res = await this._res(locator);
1339
1396
  return empty('elements').negate(res);
1340
1397
  }
1341
1398
 
@@ -1344,7 +1401,7 @@ class WebDriver extends Helper {
1344
1401
  *
1345
1402
  */
1346
1403
  async dontSeeElementInDOM(locator) {
1347
- const res = await this.$$(withStrictLocator(locator));
1404
+ const res = await this._res(locator);
1348
1405
  return empty('elements').assert(res);
1349
1406
  }
1350
1407
 
@@ -1991,7 +2048,7 @@ class WebDriver extends Helper {
1991
2048
  }, aSec * 1000, `element (${new Locator(locator)}) still not enabled after ${aSec} sec`);
1992
2049
  }
1993
2050
  return this.browser.waitUntil(async () => {
1994
- const res = await this.$$(withStrictLocator(locator));
2051
+ const res = await this._res(locator);
1995
2052
  if (!res || res.length === 0) {
1996
2053
  return false;
1997
2054
  }
@@ -2018,7 +2075,7 @@ class WebDriver extends Helper {
2018
2075
  }, aSec * 1000, `element (${locator}) still not present on page after ${aSec} sec`);
2019
2076
  }
2020
2077
  return this.browser.waitUntil(async () => {
2021
- const res = await this.$$(withStrictLocator(locator));
2078
+ const res = await this._res(locator);
2022
2079
  return res && res.length;
2023
2080
  }, { timeout: aSec * 1000, timeoutMsg: `element (${locator}) still not present on page after ${aSec} sec` });
2024
2081
  }
@@ -2188,9 +2245,7 @@ class WebDriver extends Helper {
2188
2245
  }, aSec * 1000, `element (${new Locator(locator)}) still not visible after ${aSec} sec`);
2189
2246
  }
2190
2247
  return this.browser.waitUntil(async () => {
2191
- const res = (this._isShadowLocator(locator))
2192
- ? await this._locate(withStrictLocator(locator))
2193
- : await this.$$(withStrictLocator(locator));
2248
+ const res = await this._res(locator);
2194
2249
  if (!res || res.length === 0) return false;
2195
2250
  const selected = await forEachAsync(res, async el => el.isDisplayed());
2196
2251
  if (Array.isArray(selected)) {
@@ -2217,7 +2272,7 @@ class WebDriver extends Helper {
2217
2272
  }, aSec * 1000, `The number of elements (${new Locator(locator)}) is not ${num} after ${aSec} sec`);
2218
2273
  }
2219
2274
  return this.browser.waitUntil(async () => {
2220
- const res = await this.$$(withStrictLocator(locator));
2275
+ const res = await this._res(locator);
2221
2276
  if (!res || res.length === 0) return false;
2222
2277
  let selected = await forEachAsync(res, async el => el.isDisplayed());
2223
2278
 
@@ -2241,7 +2296,7 @@ class WebDriver extends Helper {
2241
2296
  }, aSec * 1000, `element (${new Locator(locator)}) still visible after ${aSec} sec`);
2242
2297
  }
2243
2298
  return this.browser.waitUntil(async () => {
2244
- const res = await this.$$(withStrictLocator(locator));
2299
+ const res = await this._res(locator);
2245
2300
  if (!res || res.length === 0) return true;
2246
2301
  const selected = await forEachAsync(res, async el => el.isDisplayed());
2247
2302
  return !selected.length;
@@ -2262,7 +2317,7 @@ class WebDriver extends Helper {
2262
2317
  const aSec = sec || this.options.waitForTimeout;
2263
2318
  if (isWebDriver5()) {
2264
2319
  return this.browser.waitUntil(async () => {
2265
- const res = await this.$$(withStrictLocator(locator));
2320
+ const res = await this._res(locator);
2266
2321
  if (!res || res.length === 0) {
2267
2322
  return true;
2268
2323
  }
@@ -2270,7 +2325,7 @@ class WebDriver extends Helper {
2270
2325
  }, aSec * 1000, `element (${new Locator(locator)}) still on page after ${aSec} sec`);
2271
2326
  }
2272
2327
  return this.browser.waitUntil(async () => {
2273
- const res = await this.$$(withStrictLocator(locator));
2328
+ const res = await this._res(locator);
2274
2329
  if (!res || res.length === 0) {
2275
2330
  return true;
2276
2331
  }
@@ -2543,12 +2598,9 @@ async function proceedSee(assertType, text, context, strict = false) {
2543
2598
  }
2544
2599
 
2545
2600
  const smartWaitEnabled = assertType === 'assert';
2546
-
2547
2601
  const res = await this._locate(withStrictLocator(context), smartWaitEnabled);
2548
2602
  assertElementExists(res, context);
2549
-
2550
2603
  const selected = await forEachAsync(res, async el => this.browser.getElementText(getElementId(el)));
2551
-
2552
2604
  if (strict) {
2553
2605
  if (Array.isArray(selected) && selected.length !== 0) {
2554
2606
  return selected.map(elText => equals(description)[assertType](text, elText));
@@ -2616,6 +2668,11 @@ async function filterAsync(array, callback) {
2616
2668
 
2617
2669
  async function findClickable(locator, locateFn) {
2618
2670
  locator = new Locator(locator);
2671
+
2672
+ if (this._isCustomLocator(locator)) {
2673
+ return locateFn(locator.value);
2674
+ }
2675
+
2619
2676
  if (locator.isAccessibilityId() && !this.isWeb) return locateFn(locator, true);
2620
2677
  if (!locator.isFuzzy()) return locateFn(locator, true);
2621
2678
 
@@ -2637,6 +2694,10 @@ async function findClickable(locator, locateFn) {
2637
2694
  async function findFields(locator) {
2638
2695
  locator = new Locator(locator);
2639
2696
 
2697
+ if (this._isCustomLocator(locator)) {
2698
+ return this._locate(locator);
2699
+ }
2700
+
2640
2701
  if (locator.isAccessibilityId() && !this.isWeb) return this._locate(locator, true);
2641
2702
  if (!locator.isFuzzy()) return this._locate(locator, true);
2642
2703
 
@@ -2742,6 +2803,10 @@ async function findCheckable(locator, locateFn) {
2742
2803
  let els;
2743
2804
  locator = new Locator(locator);
2744
2805
 
2806
+ if (this._isCustomLocator(locator)) {
2807
+ return locateFn(locator.value);
2808
+ }
2809
+
2745
2810
  if (locator.isAccessibilityId() && !this.isWeb) return locateFn(locator, true);
2746
2811
  if (!locator.isFuzzy()) return locateFn(locator, true);
2747
2812
 
@@ -2787,6 +2852,7 @@ function getElementId(el) {
2787
2852
  if (el.ELEMENT) {
2788
2853
  return el.ELEMENT;
2789
2854
  }
2855
+
2790
2856
  return null;
2791
2857
  }
2792
2858
 
@@ -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;
@@ -83,6 +83,7 @@ module.exports = (text) => {
83
83
  for (const index in example.cells) {
84
84
  const placeholder = fields[index];
85
85
  const value = transform('gherkin.examples', example.cells[index].value);
86
+ example.cells[index].value = value;
86
87
  current[placeholder] = value;
87
88
  exampleSteps = exampleSteps.map((step) => {
88
89
  step = { ...step };
@@ -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
  }
@@ -11,11 +11,10 @@ module.exports = function () {
11
11
 
12
12
  const runHelpersHook = (hook, param) => {
13
13
  if (store.dryRun) return;
14
- Object.keys(helpers).forEach((key) => {
15
- if (!helpers[key][hook]) {
16
- return;
14
+ Object.values(helpers).forEach((helper) => {
15
+ if (helper[hook]) {
16
+ helper[hook](param);
17
17
  }
18
- helpers[key][hook](param);
19
18
  });
20
19
  };
21
20
 
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
  */