codeceptjs 3.4.1 → 3.5.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 (75) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +11 -9
  3. package/bin/codecept.js +1 -1
  4. package/docs/ai.md +248 -0
  5. package/docs/build/Appium.js +47 -7
  6. package/docs/build/JSONResponse.js +4 -4
  7. package/docs/build/Nightmare.js +3 -1
  8. package/docs/build/OpenAI.js +122 -0
  9. package/docs/build/Playwright.js +234 -54
  10. package/docs/build/Protractor.js +3 -1
  11. package/docs/build/Puppeteer.js +101 -12
  12. package/docs/build/REST.js +15 -5
  13. package/docs/build/TestCafe.js +61 -2
  14. package/docs/build/WebDriver.js +85 -5
  15. package/docs/changelog.md +85 -0
  16. package/docs/helpers/Appium.md +152 -147
  17. package/docs/helpers/JSONResponse.md +4 -4
  18. package/docs/helpers/Nightmare.md +2 -0
  19. package/docs/helpers/OpenAI.md +70 -0
  20. package/docs/helpers/Playwright.md +228 -151
  21. package/docs/helpers/Puppeteer.md +153 -101
  22. package/docs/helpers/REST.md +6 -5
  23. package/docs/helpers/TestCafe.md +97 -49
  24. package/docs/helpers/WebDriver.md +159 -107
  25. package/docs/mobile.md +49 -2
  26. package/docs/parallel.md +56 -0
  27. package/docs/plugins.md +87 -33
  28. package/docs/secrets.md +6 -0
  29. package/docs/tutorial.md +2 -2
  30. package/docs/webapi/appendField.mustache +2 -0
  31. package/docs/webapi/blur.mustache +17 -0
  32. package/docs/webapi/focus.mustache +12 -0
  33. package/docs/webapi/type.mustache +3 -0
  34. package/lib/ai.js +171 -0
  35. package/lib/cli.js +10 -2
  36. package/lib/codecept.js +4 -0
  37. package/lib/command/dryRun.js +9 -1
  38. package/lib/command/generate.js +46 -3
  39. package/lib/command/init.js +23 -1
  40. package/lib/command/interactive.js +15 -1
  41. package/lib/command/run-workers.js +2 -1
  42. package/lib/container.js +13 -3
  43. package/lib/event.js +2 -0
  44. package/lib/helper/Appium.js +45 -7
  45. package/lib/helper/JSONResponse.js +4 -4
  46. package/lib/helper/Nightmare.js +1 -1
  47. package/lib/helper/OpenAI.js +122 -0
  48. package/lib/helper/Playwright.js +200 -45
  49. package/lib/helper/Protractor.js +1 -1
  50. package/lib/helper/Puppeteer.js +67 -12
  51. package/lib/helper/REST.js +15 -5
  52. package/lib/helper/TestCafe.js +30 -2
  53. package/lib/helper/WebDriver.js +51 -5
  54. package/lib/helper/scripts/blurElement.js +17 -0
  55. package/lib/helper/scripts/focusElement.js +17 -0
  56. package/lib/helper/scripts/highlightElement.js +20 -0
  57. package/lib/html.js +258 -0
  58. package/lib/interfaces/gherkin.js +8 -0
  59. package/lib/listener/retry.js +2 -1
  60. package/lib/pause.js +73 -17
  61. package/lib/plugin/debugErrors.js +67 -0
  62. package/lib/plugin/fakerTransform.js +4 -6
  63. package/lib/plugin/heal.js +177 -0
  64. package/lib/plugin/screenshotOnFail.js +11 -2
  65. package/lib/recorder.js +11 -8
  66. package/lib/secret.js +5 -4
  67. package/lib/step.js +6 -1
  68. package/lib/ui.js +4 -3
  69. package/lib/utils.js +17 -0
  70. package/lib/workers.js +57 -9
  71. package/package.json +25 -16
  72. package/translations/ja-JP.js +9 -9
  73. package/typings/index.d.ts +43 -9
  74. package/typings/promiseBasedTypes.d.ts +242 -25
  75. package/typings/types.d.ts +260 -35
@@ -2,7 +2,9 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
 
4
4
  const Helper = require('@codeceptjs/helper');
5
+ const { v4: uuidv4 } = require('uuid');
5
6
  const Locator = require('../locator');
7
+ const store = require('../store');
6
8
  const recorder = require('../recorder');
7
9
  const stringIncludes = require('../assert/include').includes;
8
10
  const { urlEquals } = require('../assert/equal');
@@ -43,19 +45,20 @@ const {
43
45
  setRestartStrategy, restartsSession, restartsContext, restartsBrowser,
44
46
  } = require('./extras/PlaywrightRestartOpts');
45
47
  const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine');
48
+ const { highlightElement } = require('./scripts/highlightElement');
46
49
 
47
50
  const pathSeparator = path.sep;
48
51
 
49
52
  /**
50
53
  * ## Configuration
51
54
  *
52
- * This helper should be configured in codecept.conf.js
55
+ * This helper should be configured in codecept.conf.(js|ts)
53
56
  *
54
57
  * @typedef PlaywrightConfig
55
58
  * @type {object}
56
- * @prop {string} url - base url of website to be tested
57
- * @prop {string} [browser] - a browser to test on, either: `chromium`, `firefox`, `webkit`, `electron`. Default: chromium.
58
- * @prop {boolean} [show=false] - show browser window.
59
+ * @prop {string} [url] - base url of website to be tested
60
+ * @prop {'chromium' | 'firefox'| 'webkit' | 'electron'} [browser='chromium'] - a browser to test on, either: `chromium`, `firefox`, `webkit`, `electron`. Default: chromium.
61
+ * @prop {boolean} [show=true] - show browser window.
59
62
  * @prop {string|boolean} [restart=false] - restart strategy between tests. Possible values:
60
63
  * * 'context' or **false** - restarts [browser context](https://playwright.dev/docs/api/class-browsercontext) but keeps running browser. Recommended by Playwright team to keep tests isolated.
61
64
  * * 'browser' or **true** - closes browser and opens it again between tests.
@@ -72,13 +75,13 @@ const pathSeparator = path.sep;
72
75
  * @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to 'session'.
73
76
  * @prop {boolean} [keepCookies=false] - keep cookies between tests when `restart` is set to 'session'.
74
77
  * @prop {number} [waitForAction] - how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
75
- * @prop {string} [waitForNavigation] - When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle`. Choose one of those options is possible. See [Playwright API](https://playwright.dev/docs/api/class-page#page-wait-for-navigation).
78
+ * @prop {'load' | 'domcontentloaded' | 'networkidle'} [waitForNavigation] - When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle`. Choose one of those options is possible. See [Playwright API](https://playwright.dev/docs/api/class-page#page-wait-for-navigation).
76
79
  * @prop {number} [pressKeyDelay=10] - Delay between key presses in ms. Used when calling Playwrights page.type(...) in fillField/appendField
77
80
  * @prop {number} [getPageTimeout] - config option to set maximum navigation time in milliseconds.
78
81
  * @prop {number} [waitForTimeout] - default wait* timeout in ms. Default: 1000.
79
82
  * @prop {object} [basicAuth] - the basic authentication to pass to base url. Example: {username: 'username', password: 'password'}
80
83
  * @prop {string} [windowSize] - default window size. Set a dimension like `640x480`.
81
- * @prop {string} [colorScheme] - default color scheme. Possible values: `dark` | `light` | `no-preference`.
84
+ * @prop {'dark' | 'light' | 'no-preference'} [colorScheme] - default color scheme. Possible values: `dark` | `light` | `no-preference`.
82
85
  * @prop {string} [userAgent] - user-agent string.
83
86
  * @prop {string} [locale] - locale string. Example: 'en-GB', 'de-DE', 'fr-FR', ...
84
87
  * @prop {boolean} [manualStart] - do not start browser before a test, start it manually inside a helper with `this.helpers["Playwright"]._startBrowser()`.
@@ -88,6 +91,8 @@ const pathSeparator = path.sep;
88
91
  * @prop {any} [channel] - (While Playwright can operate against the stock Google Chrome and Microsoft Edge browsers available on the machine. In particular, current Playwright version will support Stable and Beta channels of these browsers. See [Google Chrome & Microsoft Edge](https://playwright.dev/docs/browsers/#google-chrome--microsoft-edge).
89
92
  * @prop {string[]} [ignoreLog] - An array with console message types that are not logged to debug log. Default value is `['warning', 'log']`. E.g. you can set `[]` to log all messages. See all possible [values](https://playwright.dev/docs/api/class-consolemessage#console-message-type).
90
93
  * @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
94
+ * @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
95
+ * @prop {boolean} [highlightElement] - highlight the interacting elements
91
96
  */
92
97
  const config = {};
93
98
 
@@ -125,7 +130,7 @@ const config = {};
125
130
  *
126
131
  * #### Trace Recording Customization
127
132
  *
128
- * Trace recording provides a complete information on test execution and includes DOM snapshots, screenshots, and network requests logged during run.
133
+ * Trace recording provides complete information on test execution and includes DOM snapshots, screenshots, and network requests logged during run.
129
134
  * Traces will be saved to `output/trace`
130
135
  *
131
136
  * * `trace`: enables trace recording for failed tests; trace are saved into `output/trace` folder
@@ -255,6 +260,22 @@ const config = {};
255
260
  * }
256
261
  * ```
257
262
  *
263
+ * * #### Example #9: Launch electron test
264
+ *
265
+ * ```js
266
+ * {
267
+ * helpers: {
268
+ * Playwright: {
269
+ * browser: 'electron',
270
+ * electron: {
271
+ * executablePath: require("electron"),
272
+ * args: [path.join('../', "main.js")],
273
+ * },
274
+ * }
275
+ * },
276
+ * }
277
+ * ```
278
+ *
258
279
  * Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
259
280
  *
260
281
  * ## Access From Helpers
@@ -368,15 +389,24 @@ class Playwright extends Helper {
368
389
 
369
390
  static _config() {
370
391
  return [
371
- { name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
372
- {
373
- name: 'show', message: 'Show browser window', default: true, type: 'confirm',
374
- },
375
392
  {
376
393
  name: 'browser',
377
394
  message: 'Browser in which testing will be performed. Possible options: chromium, firefox, webkit or electron',
378
395
  default: 'chromium',
379
396
  },
397
+ {
398
+ name: 'url',
399
+ message: 'Base url of site to be tested',
400
+ default: 'http://localhost',
401
+ when: (answers) => answers.Playwright_browser !== 'electron',
402
+ },
403
+ {
404
+ name: 'show',
405
+ message: 'Show browser window',
406
+ default: true,
407
+ type: 'confirm',
408
+ when: (answers) => answers.Playwright_browser !== 'electron',
409
+ },
380
410
  ];
381
411
  }
382
412
 
@@ -903,8 +933,34 @@ class Playwright extends Helper {
903
933
  }
904
934
 
905
935
  /**
906
- * {{> dragAndDrop }}
936
+ * {{> focus }}
907
937
  *
938
+ */
939
+ async focus(locator, options = {}) {
940
+ const els = await this._locate(locator);
941
+ assertElementExists(els, locator, 'Element to focus');
942
+ const el = els[0];
943
+
944
+ await el.focus(options);
945
+ return this._waitForAction();
946
+ }
947
+
948
+ /**
949
+ * {{> blur }}
950
+ *
951
+ */
952
+ async blur(locator, options = {}) {
953
+ const els = await this._locate(locator);
954
+ assertElementExists(els, locator, 'Element to blur');
955
+ // TODO: locator change required after #3677 implementation
956
+ const elXpath = await getXPathForElement(els[0]);
957
+
958
+ await this.page.locator(elXpath).blur(options);
959
+ return this._waitForAction();
960
+ }
961
+
962
+ /**
963
+ * {{> dragAndDrop }}
908
964
  * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-page#page-drag-and-drop) can be passed as 3rd argument.
909
965
  *
910
966
  * ```js
@@ -912,13 +968,27 @@ class Playwright extends Helper {
912
968
  * I.dragAndDrop('img.src', 'img.dst', { sourcePosition: {x: 10, y: 10} })
913
969
  * ```
914
970
  *
915
- * > By default option `force: true` is set
971
+ * > When no option is set, custom drag and drop would be used, to use the dragAndDrop API from Playwright, please set options, for example `force: true`
916
972
  */
917
- async dragAndDrop(srcElement, destElement, options = { force: true }) {
918
- const src = new Locator(srcElement, 'css');
919
- const dst = new Locator(destElement, 'css');
973
+ async dragAndDrop(srcElement, destElement, options) {
974
+ const src = new Locator(srcElement);
975
+ const dst = new Locator(destElement);
976
+
977
+ if (options) {
978
+ return this.page.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options);
979
+ }
980
+
981
+ const _smallWaitInMs = 600;
982
+ await this.page.locator(buildLocatorString(src)).hover();
983
+ await this.page.mouse.down();
984
+ await this.page.waitForTimeout(_smallWaitInMs);
920
985
 
921
- return this.page.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options);
986
+ const destElBox = await this.page.locator(buildLocatorString(dst)).boundingBox();
987
+
988
+ await this.page.mouse.move(destElBox.x + destElBox.width / 2, destElBox.y + destElBox.height / 2);
989
+ await this.page.locator(buildLocatorString(dst)).hover({ position: { x: 10, y: 10 } });
990
+ await this.page.waitForTimeout(_smallWaitInMs);
991
+ await this.page.mouse.up();
922
992
  }
923
993
 
924
994
  /**
@@ -1282,7 +1352,7 @@ class Playwright extends Helper {
1282
1352
  /**
1283
1353
  * {{> click }}
1284
1354
  *
1285
- * @param {any} [opts] [Additional options](https://playwright.dev/docs/api/class-page#page-click) for click available as 3rd argument.
1355
+ * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-page#page-click) for click available as 3rd argument.
1286
1356
  *
1287
1357
  * Examples:
1288
1358
  *
@@ -1295,8 +1365,8 @@ class Playwright extends Helper {
1295
1365
  * ```
1296
1366
  *
1297
1367
  */
1298
- async click(locator, context = null, opts = {}) {
1299
- return proceedClick.call(this, locator, context, opts);
1368
+ async click(locator, context = null, options = {}) {
1369
+ return proceedClick.call(this, locator, context, options);
1300
1370
  }
1301
1371
 
1302
1372
  /**
@@ -1438,6 +1508,7 @@ class Playwright extends Helper {
1438
1508
  */
1439
1509
  async type(keys, delay = null) {
1440
1510
  if (!Array.isArray(keys)) {
1511
+ keys = keys.toString();
1441
1512
  keys = keys.split('');
1442
1513
  }
1443
1514
 
@@ -1462,15 +1533,47 @@ class Playwright extends Helper {
1462
1533
  } else if (editable) {
1463
1534
  await this._evaluateHandeInContext(el => el.innerHTML = '', el);
1464
1535
  }
1536
+
1537
+ highlightActiveElement.call(this, el, this.page);
1538
+
1465
1539
  await el.type(value.toString(), { delay: this.options.pressKeyDelay });
1540
+
1466
1541
  return this._waitForAction();
1467
1542
  }
1468
1543
 
1469
1544
  /**
1470
- * {{> clearField }}
1545
+ * Clears the text input element: `<input>`, `<textarea>` or `[contenteditable]` .
1546
+ *
1547
+ *
1548
+ * Examples:
1549
+ *
1550
+ * ```js
1551
+ * I.clearField('.text-area')
1552
+ *
1553
+ * // if this doesn't work use force option
1554
+ * I.clearField('#submit', { force: true })
1555
+ * ```
1556
+ * Use `force` to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
1557
+ *
1558
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
1559
+ * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
1471
1560
  */
1472
- async clearField(field) {
1473
- return this.fillField(field, '');
1561
+ async clearField(locator, options = {}) {
1562
+ let result;
1563
+ const isNewClearMethodPresent = typeof this.page.locator().clear === 'function';
1564
+
1565
+ if (isNewClearMethodPresent) {
1566
+ const els = await findFields.call(this, locator);
1567
+ assertElementExists(els, locator, 'Field to clear');
1568
+ // TODO: locator change required after #3677 implementation
1569
+ const elXpath = await getXPathForElement(els[0]);
1570
+
1571
+ await this.page.locator(elXpath).clear(options);
1572
+ result = await this._waitForAction();
1573
+ } else {
1574
+ result = await this.fillField(locator, '');
1575
+ }
1576
+ return result;
1474
1577
  }
1475
1578
 
1476
1579
  /**
@@ -1481,8 +1584,9 @@ class Playwright extends Helper {
1481
1584
  async appendField(field, value) {
1482
1585
  const els = await findFields.call(this, field);
1483
1586
  assertElementExists(els, field, 'Field');
1587
+ highlightActiveElement.call(this, els[0], this.page);
1484
1588
  await els[0].press('End');
1485
- await els[0].type(value, { delay: this.options.pressKeyDelay });
1589
+ await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
1486
1590
  return this._waitForAction();
1487
1591
  }
1488
1592
 
@@ -1526,6 +1630,7 @@ class Playwright extends Helper {
1526
1630
  if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
1527
1631
  throw new Error('Element is not <select>');
1528
1632
  }
1633
+ highlightActiveElement.call(this, el, this.page);
1529
1634
  if (!Array.isArray(option)) option = [option];
1530
1635
 
1531
1636
  for (const key in option) {
@@ -1999,23 +2104,32 @@ class Playwright extends Helper {
1999
2104
  */
2000
2105
  async saveScreenshot(fileName, fullPage) {
2001
2106
  const fullPageOption = fullPage || this.options.fullPageScreenshots;
2002
- const outputFile = screenshotOutputFolder(fileName);
2107
+ let outputFile = screenshotOutputFolder(fileName);
2003
2108
 
2004
2109
  this.debug(`Screenshot is saving to ${outputFile}`);
2005
2110
 
2111
+ await this.page.screenshot({
2112
+ path: outputFile,
2113
+ fullPage: fullPageOption,
2114
+ type: 'png',
2115
+ });
2116
+
2006
2117
  if (this.activeSessionName) {
2007
- const activeSessionPage = this.sessionPages[this.activeSessionName];
2008
-
2009
- if (activeSessionPage) {
2010
- return activeSessionPage.screenshot({
2011
- path: outputFile,
2012
- fullPage: fullPageOption,
2013
- type: 'png',
2014
- });
2118
+ for (const sessionName in this.sessionPages) {
2119
+ const activeSessionPage = this.sessionPages[sessionName];
2120
+ outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`);
2121
+
2122
+ this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`);
2123
+
2124
+ if (activeSessionPage) {
2125
+ await activeSessionPage.screenshot({
2126
+ path: outputFile,
2127
+ fullPage: fullPageOption,
2128
+ type: 'png',
2129
+ });
2130
+ }
2015
2131
  }
2016
2132
  }
2017
-
2018
- return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
2019
2133
  }
2020
2134
 
2021
2135
  /**
@@ -2083,7 +2197,7 @@ class Playwright extends Helper {
2083
2197
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`);
2084
2198
  for (const sessionName in this.sessionPages) {
2085
2199
  if (!this.sessionPages[sessionName].context) continue;
2086
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context(), `${test.title}_${sessionName}.failed`);
2200
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`);
2087
2201
  }
2088
2202
  }
2089
2203
  }
@@ -2106,7 +2220,7 @@ class Playwright extends Helper {
2106
2220
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`);
2107
2221
  for (const sessionName in this.sessionPages) {
2108
2222
  if (!this.sessionPages[sessionName].context) continue;
2109
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context(), `${test.title}_${sessionName}.passed`);
2223
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`);
2110
2224
  }
2111
2225
  }
2112
2226
  } else {
@@ -2448,15 +2562,15 @@ class Playwright extends Helper {
2448
2562
  *
2449
2563
  * See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
2450
2564
  *
2451
- * @param {*} opts
2565
+ * @param {*} options
2452
2566
  */
2453
- async waitForNavigation(opts = {}) {
2454
- opts = {
2567
+ async waitForNavigation(options = {}) {
2568
+ options = {
2455
2569
  timeout: this.options.getPageTimeout,
2456
2570
  waitUntil: this.options.waitForNavigation,
2457
- ...opts,
2571
+ ...options,
2458
2572
  };
2459
- return this.page.waitForNavigation(opts);
2573
+ return this.page.waitForNavigation(options);
2460
2574
  }
2461
2575
 
2462
2576
  async waitUntilExists(locator, sec) {
@@ -2549,11 +2663,41 @@ function buildLocatorString(locator) {
2549
2663
  if (locator.isCustom()) {
2550
2664
  return `${locator.type}=${locator.value}`;
2551
2665
  } if (locator.isXPath()) {
2552
- // dont rely on heuristics of playwright for figuring out xpath
2553
2666
  return `xpath=${locator.value}`;
2554
2667
  }
2555
2668
  return locator.simplify();
2556
2669
  }
2670
+ // TODO: locator change required after #3677 implementation. Temporary solution before migration. Should be deleted after #3677 implementation
2671
+ async function getXPathForElement(elementHandle) {
2672
+ function calculateIndex(node) {
2673
+ let index = 1;
2674
+ let sibling = node.previousElementSibling;
2675
+ while (sibling) {
2676
+ if (sibling.tagName === node.tagName) {
2677
+ index++;
2678
+ }
2679
+ sibling = sibling.previousElementSibling;
2680
+ }
2681
+ return index;
2682
+ }
2683
+
2684
+ function generateXPath(node) {
2685
+ const segments = [];
2686
+ while (node && node.nodeType === Node.ELEMENT_NODE) {
2687
+ if (node.hasAttribute('id')) {
2688
+ segments.unshift(`*[@id="${node.getAttribute('id')}"]`);
2689
+ break;
2690
+ } else {
2691
+ const index = calculateIndex(node);
2692
+ segments.unshift(`${node.localName}[${index}]`);
2693
+ node = node.parentNode;
2694
+ }
2695
+ }
2696
+ return `//${segments.join('/')}`;
2697
+ }
2698
+
2699
+ return elementHandle.evaluate(generateXPath);
2700
+ }
2557
2701
 
2558
2702
  async function findElements(matcher, locator) {
2559
2703
  if (locator.react) return findReact(matcher, locator);
@@ -2587,6 +2731,10 @@ async function proceedClick(locator, context = null, options = {}) {
2587
2731
  } else {
2588
2732
  assertElementExists(els, locator, 'Clickable element');
2589
2733
  }
2734
+
2735
+ const element = els[0];
2736
+ highlightActiveElement.call(this, els[0], this.page);
2737
+
2590
2738
  /*
2591
2739
  using the force true options itself but instead dispatching a click
2592
2740
  */
@@ -2601,6 +2749,7 @@ async function proceedClick(locator, context = null, options = {}) {
2601
2749
  promises.push(this.waitForNavigation());
2602
2750
  }
2603
2751
  promises.push(this._waitForAction());
2752
+
2604
2753
  return Promise.all(promises);
2605
2754
  }
2606
2755
 
@@ -2982,7 +3131,7 @@ async function refreshContextSession() {
2982
3131
 
2983
3132
  async function saveVideoForPage(page, name) {
2984
3133
  if (!page.video()) return null;
2985
- const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${Date.now()}_${clearString(name)}`.slice(0, 245)}.webm`;
3134
+ const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`;
2986
3135
  page.video().saveAs(fileName).then(() => {
2987
3136
  if (!page) return;
2988
3137
  page.video().delete().catch(e => {});
@@ -2993,7 +3142,13 @@ async function saveVideoForPage(page, name) {
2993
3142
  async function saveTraceForContext(context, name) {
2994
3143
  if (!context) return;
2995
3144
  if (!context.tracing) return;
2996
- const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${Date.now()}_${clearString(name)}`.slice(0, 245)}.zip`;
3145
+ const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`;
2997
3146
  await context.tracing.stop({ path: fileName });
2998
3147
  return fileName;
2999
3148
  }
3149
+
3150
+ function highlightActiveElement(element, context) {
3151
+ if (!this.options.enableHighlight && !store.debugMode) return;
3152
+
3153
+ highlightElement(element, context);
3154
+ }
@@ -647,7 +647,7 @@ class Protractor extends Helper {
647
647
  async appendField(field, value) {
648
648
  const els = await findFields(this.browser, field);
649
649
  assertElementExists(els, field, 'Field');
650
- return els[0].sendKeys(value);
650
+ return els[0].sendKeys(value.toString());
651
651
  }
652
652
 
653
653
  /**
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const Helper = require('@codeceptjs/helper');
7
7
  const Locator = require('../locator');
8
8
  const recorder = require('../recorder');
9
+ const store = require('../store');
9
10
  const stringIncludes = require('../assert/include').includes;
10
11
  const { urlEquals } = require('../assert/equal');
11
12
  const { equals } = require('../assert/equal');
@@ -33,6 +34,9 @@ const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnection
33
34
  const Popup = require('./extras/Popup');
34
35
  const Console = require('./extras/Console');
35
36
  const findReact = require('./extras/React');
37
+ const { highlightElement } = require('./scripts/highlightElement');
38
+ const { blurElement } = require('./scripts/blurElement');
39
+ const { focusElement } = require('./scripts/focusElement');
36
40
 
37
41
  let puppeteer;
38
42
  let perfTiming;
@@ -65,6 +69,7 @@ const consoleLogStore = new Console();
65
69
  * @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["Puppeteer"]._startBrowser()`.
66
70
  * @prop {string} [browser=chrome] - can be changed to `firefox` when using [puppeteer-firefox](https://codecept.io/helpers/Puppeteer-firefox).
67
71
  * @prop {object} [chrome] - pass additional [Puppeteer run options](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions).
72
+ * @prop {boolean} [highlightElement] - highlight the interacting elements
68
73
  */
69
74
  const config = {};
70
75
 
@@ -704,6 +709,31 @@ class Puppeteer extends Helper {
704
709
  return this._waitForAction();
705
710
  }
706
711
 
712
+ /**
713
+ * {{> focus }}
714
+ *
715
+ */
716
+ async focus(locator) {
717
+ const els = await this._locate(locator);
718
+ assertElementExists(els, locator, 'Element to focus');
719
+ const el = els[0];
720
+
721
+ await focusElement(el, this.page);
722
+ return this._waitForAction();
723
+ }
724
+
725
+ /**
726
+ * {{> blur }}
727
+ *
728
+ */
729
+ async blur(locator) {
730
+ const els = await this._locate(locator);
731
+ assertElementExists(els, locator, 'Element to blur');
732
+
733
+ await blurElement(els[0], this.page);
734
+ return this._waitForAction();
735
+ }
736
+
707
737
  /**
708
738
  * {{> dragAndDrop }}
709
739
  */
@@ -1262,6 +1292,7 @@ class Puppeteer extends Helper {
1262
1292
  */
1263
1293
  async type(keys, delay = null) {
1264
1294
  if (!Array.isArray(keys)) {
1295
+ keys = keys.toString();
1265
1296
  keys = keys.split('');
1266
1297
  }
1267
1298
 
@@ -1286,7 +1317,10 @@ class Puppeteer extends Helper {
1286
1317
  } else if (editable) {
1287
1318
  await this._evaluateHandeInContext(el => el.innerHTML = '', el);
1288
1319
  }
1320
+
1321
+ highlightActiveElement.call(this, el, this.page);
1289
1322
  await el.type(value.toString(), { delay: this.options.pressKeyDelay });
1323
+
1290
1324
  return this._waitForAction();
1291
1325
  }
1292
1326
 
@@ -1305,8 +1339,9 @@ class Puppeteer extends Helper {
1305
1339
  async appendField(field, value) {
1306
1340
  const els = await findVisibleFields.call(this, field);
1307
1341
  assertElementExists(els, field, 'Field');
1342
+ highlightActiveElement.call(this, els[0], this.page);
1308
1343
  await els[0].press('End');
1309
- await els[0].type(value, { delay: this.options.pressKeyDelay });
1344
+ await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
1310
1345
  return this._waitForAction();
1311
1346
  }
1312
1347
 
@@ -1351,6 +1386,7 @@ class Puppeteer extends Helper {
1351
1386
  if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
1352
1387
  throw new Error('Element is not <select>');
1353
1388
  }
1389
+ highlightActiveElement.call(this, els[0], this.page);
1354
1390
  if (!Array.isArray(option)) option = [option];
1355
1391
 
1356
1392
  for (const key in option) {
@@ -1828,23 +1864,32 @@ class Puppeteer extends Helper {
1828
1864
  */
1829
1865
  async saveScreenshot(fileName, fullPage) {
1830
1866
  const fullPageOption = fullPage || this.options.fullPageScreenshots;
1831
- const outputFile = screenshotOutputFolder(fileName);
1867
+ let outputFile = screenshotOutputFolder(fileName);
1832
1868
 
1833
1869
  this.debug(`Screenshot is saving to ${outputFile}`);
1834
1870
 
1871
+ await this.page.screenshot({
1872
+ path: outputFile,
1873
+ fullPage: fullPageOption,
1874
+ type: 'png',
1875
+ });
1876
+
1835
1877
  if (this.activeSessionName) {
1836
- const activeSessionPage = this.sessionPages[this.activeSessionName];
1837
-
1838
- if (activeSessionPage) {
1839
- return activeSessionPage.screenshot({
1840
- path: outputFile,
1841
- fullPage: fullPageOption,
1842
- type: 'png',
1843
- });
1878
+ for (const sessionName in this.sessionPages) {
1879
+ const activeSessionPage = this.sessionPages[sessionName];
1880
+ outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`);
1881
+
1882
+ this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`);
1883
+
1884
+ if (activeSessionPage) {
1885
+ await activeSessionPage.screenshot({
1886
+ path: outputFile,
1887
+ fullPage: fullPageOption,
1888
+ type: 'png',
1889
+ });
1890
+ }
1844
1891
  }
1845
1892
  }
1846
-
1847
- return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
1848
1893
  }
1849
1894
 
1850
1895
  async _failed() {
@@ -2313,12 +2358,16 @@ async function proceedClick(locator, context = null, options = {}) {
2313
2358
  } else {
2314
2359
  assertElementExists(els, locator, 'Clickable element');
2315
2360
  }
2361
+
2362
+ highlightActiveElement.call(this, els[0], this.page);
2363
+
2316
2364
  await els[0].click(options);
2317
2365
  const promises = [];
2318
2366
  if (options.waitForNavigation) {
2319
2367
  promises.push(this.waitForNavigation());
2320
2368
  }
2321
2369
  promises.push(this._waitForAction());
2370
+
2322
2371
  return Promise.all(promises);
2323
2372
  }
2324
2373
 
@@ -2663,3 +2712,9 @@ function getNormalizedKey(key) {
2663
2712
  }
2664
2713
  return normalizedKey;
2665
2714
  }
2715
+
2716
+ function highlightActiveElement(element, context) {
2717
+ if (!this.options.enableHighlight && !store.debugMode) return;
2718
+
2719
+ highlightElement(element, context);
2720
+ }
@@ -34,7 +34,8 @@ const config = {};
34
34
  * endpoint: 'http://site.com/api',
35
35
  * prettyPrintJson: true,
36
36
  * onRequest: (request) => {
37
- * request.headers.auth = '123';
37
+ * request.headers.auth = '123';
38
+ * }
38
39
  * }
39
40
  * }
40
41
  *}
@@ -136,6 +137,15 @@ class REST extends Helper {
136
137
  request.auth = this.headers.auth;
137
138
  }
138
139
 
140
+ if (typeof request.data === 'object') {
141
+ const returnedValue = {};
142
+ for (const [key, value] of Object.entries(request.data)) {
143
+ returnedValue[key] = value;
144
+ if (value instanceof Secret) returnedValue[key] = value.getMasked();
145
+ }
146
+ _debugRequest.data = returnedValue;
147
+ }
148
+
139
149
  if (request.data instanceof Secret) {
140
150
  _debugRequest.data = '*****';
141
151
  request.data = (typeof request.data === 'object' && !(request.data instanceof Secret)) ? { ...request.data.toString() } : request.data.toString();
@@ -198,7 +208,7 @@ class REST extends Helper {
198
208
  * ```
199
209
  *
200
210
  * @param {*} url
201
- * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
211
+ * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
202
212
  *
203
213
  * @returns {Promise<*>} response
204
214
  */
@@ -222,8 +232,8 @@ class REST extends Helper {
222
232
  * ```
223
233
  *
224
234
  * @param {*} url
225
- * @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
226
- * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
235
+ * @param {*} [payload={}] - the payload to be sent. By default, it is sent as an empty object
236
+ * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
227
237
  *
228
238
  * @returns {Promise<*>} response
229
239
  */
@@ -317,7 +327,7 @@ class REST extends Helper {
317
327
  * ```
318
328
  *
319
329
  * @param {*} url
320
- * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
330
+ * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
321
331
  *
322
332
  * @returns {Promise<*>} response
323
333
  */