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
@@ -0,0 +1,122 @@
1
+ const Helper = require('@codeceptjs/helper');
2
+ const AiAssistant = require('../ai');
3
+ const standardActingHelpers = require('../plugin/standardActingHelpers');
4
+ const Container = require('../container');
5
+ const { splitByChunks, minifyHtml } = require('../html');
6
+
7
+ /**
8
+ * OpenAI Helper for CodeceptJS.
9
+ *
10
+ * This helper class provides integration with the OpenAI GPT-3.5 or 4 language model for generating responses to questions or prompts within the context of web pages. It allows you to interact with the GPT-3.5 model to obtain intelligent responses based on HTML fragments or general prompts.
11
+ * This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDrvier to ensure the HTML context is available.
12
+ *
13
+ * ## Configuration
14
+ *
15
+ * This helper should be configured in codecept.json or codecept.conf.js
16
+ *
17
+ * * `chunkSize`: (optional, default: 80000) - The maximum number of characters to send to the OpenAI API at once. We split HTML fragments by 8000 chars to not exceed token limit. Increase this value if you use GPT-4.
18
+ */
19
+ class OpenAI extends Helper {
20
+ constructor(config) {
21
+ super(config);
22
+ this.aiAssistant = new AiAssistant();
23
+
24
+ this.options = {
25
+ chunkSize: 80000,
26
+ };
27
+ this.options = { ...this.options, ...config };
28
+
29
+ const helpers = Container.helpers();
30
+
31
+ for (const helperName of standardActingHelpers) {
32
+ if (Object.keys(helpers).indexOf(helperName) > -1) {
33
+ this.helper = helpers[helperName];
34
+ break;
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Asks the OpenAI GPT language model a question based on the provided prompt within the context of the current page's HTML.
41
+ *
42
+ * ```js
43
+ * I.askGptOnPage('what does this page do?');
44
+ * ```
45
+ *
46
+ * @async
47
+ * @param {string} prompt - The question or prompt to ask the GPT model.
48
+ * @returns {Promise<string>} - A Promise that resolves to the generated responses from the GPT model, joined by newlines.
49
+ */
50
+ async askGptOnPage(prompt) {
51
+ const html = await this.helper.grabSource();
52
+
53
+ const htmlChunks = splitByChunks(html, this.options.chunkSize);
54
+
55
+ if (htmlChunks.length > 1) this.debug(`Splitting HTML into ${htmlChunks.length} chunks`);
56
+
57
+ const responses = [];
58
+
59
+ for (const chunk of htmlChunks) {
60
+ const messages = [
61
+ { role: 'user', content: prompt },
62
+ { role: 'user', content: `Within this HTML: ${minifyHtml(chunk)}` },
63
+ ];
64
+
65
+ if (htmlChunks.length > 1) messages.push({ role: 'user', content: 'If action is not possible on this page, do not propose anything, I will send another HTML fragment' });
66
+
67
+ const response = await this.aiAssistant.createCompletion(messages);
68
+
69
+ console.log(response);
70
+
71
+ responses.push(response);
72
+ }
73
+
74
+ return responses.join('\n\n');
75
+ }
76
+
77
+ /**
78
+ * Asks the OpenAI GPT-3.5 language model a question based on the provided prompt within the context of a specific HTML fragment on the current page.
79
+ *
80
+ * ```js
81
+ * I.askGptOnPageFragment('describe features of this screen', '.screen');
82
+ * ```
83
+ *
84
+ * @async
85
+ * @param {string} prompt - The question or prompt to ask the GPT-3.5 model.
86
+ * @param {string} locator - The locator or selector used to identify the HTML fragment on the page.
87
+ * @returns {Promise<string>} - A Promise that resolves to the generated response from the GPT model.
88
+ */
89
+ async askGptOnPageFragment(prompt, locator) {
90
+ const html = await this.helper.grabHTMLFrom(locator);
91
+
92
+ const messages = [
93
+ { role: 'user', content: prompt },
94
+ { role: 'user', content: `Within this HTML: ${minifyHtml(html)}` },
95
+ ];
96
+
97
+ const response = await this.aiAssistant.createCompletion(messages);
98
+
99
+ console.log(response);
100
+
101
+ return response;
102
+ }
103
+
104
+ /**
105
+ * Send a general request to ChatGPT and return response.
106
+ * @param {string} prompt
107
+ * @returns {Promise<string>} - A Promise that resolves to the generated response from the GPT model.
108
+ */
109
+ async askGptGeneralPrompt(prompt) {
110
+ const messages = [
111
+ { role: 'user', content: prompt },
112
+ ];
113
+
114
+ const completion = await this.aiAssistant.createCompletion(messages);
115
+
116
+ const response = completion?.data?.choices[0]?.message?.content;
117
+
118
+ console.log(response);
119
+
120
+ return response;
121
+ }
122
+ }
@@ -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
 
@@ -938,6 +968,62 @@ class Playwright extends Helper {
938
968
  return this._waitForAction();
939
969
  }
940
970
 
971
+ /**
972
+ * Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the matching element.
973
+ *
974
+ * Examples:
975
+ *
976
+ * ```js
977
+ * I.dontSee('#add-to-cart-btn');
978
+ * I.focus('#product-tile')
979
+ * I.see('#add-to-cart-bnt');
980
+ * ```
981
+ *
982
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
983
+ * @param {any} [options] Playwright only: [Additional options](https://playwright.dev/docs/api/class-locator#locator-focus) for available options object as 2nd argument.
984
+ *
985
+ *
986
+ */
987
+ async focus(locator, options = {}) {
988
+ const els = await this._locate(locator);
989
+ assertElementExists(els, locator, 'Element to focus');
990
+ const el = els[0];
991
+
992
+ await el.focus(options);
993
+ return this._waitForAction();
994
+ }
995
+
996
+ /**
997
+ * Remove focus from a text input, button, etc.
998
+ * Calls [blur](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the element.
999
+ *
1000
+ * Examples:
1001
+ *
1002
+ * ```js
1003
+ * I.blur('.text-area')
1004
+ * ```
1005
+ * ```js
1006
+ * //element `#product-tile` is focused
1007
+ * I.see('#add-to-cart-btn');
1008
+ * I.blur('#product-tile')
1009
+ * I.dontSee('#add-to-cart-btn');
1010
+ * ```
1011
+ *
1012
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
1013
+ * @param {any} [options] Playwright only: [Additional options](https://playwright.dev/docs/api/class-locator#locator-blur) for available options object as 2nd argument.
1014
+ *
1015
+ *
1016
+ */
1017
+ async blur(locator, options = {}) {
1018
+ const els = await this._locate(locator);
1019
+ assertElementExists(els, locator, 'Element to blur');
1020
+ // TODO: locator change required after #3677 implementation
1021
+ const elXpath = await getXPathForElement(els[0]);
1022
+
1023
+ await this.page.locator(elXpath).blur(options);
1024
+ return this._waitForAction();
1025
+ }
1026
+
941
1027
  /**
942
1028
  * Drag an item to a destination element.
943
1029
  *
@@ -949,7 +1035,6 @@ class Playwright extends Helper {
949
1035
  * @param {LocatorOrString} destElement located by CSS|XPath|strict locator.
950
1036
  * ⚠️ returns a _promise_ which is synchronized internally by recorder
951
1037
  *
952
- *
953
1038
  * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-page#page-drag-and-drop) can be passed as 3rd argument.
954
1039
  *
955
1040
  * ```js
@@ -957,13 +1042,27 @@ class Playwright extends Helper {
957
1042
  * I.dragAndDrop('img.src', 'img.dst', { sourcePosition: {x: 10, y: 10} })
958
1043
  * ```
959
1044
  *
960
- * > By default option `force: true` is set
1045
+ * > 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`
961
1046
  */
962
- async dragAndDrop(srcElement, destElement, options = { force: true }) {
963
- const src = new Locator(srcElement, 'css');
964
- const dst = new Locator(destElement, 'css');
1047
+ async dragAndDrop(srcElement, destElement, options) {
1048
+ const src = new Locator(srcElement);
1049
+ const dst = new Locator(destElement);
1050
+
1051
+ if (options) {
1052
+ return this.page.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options);
1053
+ }
1054
+
1055
+ const _smallWaitInMs = 600;
1056
+ await this.page.locator(buildLocatorString(src)).hover();
1057
+ await this.page.mouse.down();
1058
+ await this.page.waitForTimeout(_smallWaitInMs);
1059
+
1060
+ const destElBox = await this.page.locator(buildLocatorString(dst)).boundingBox();
965
1061
 
966
- return this.page.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options);
1062
+ await this.page.mouse.move(destElBox.x + destElBox.width / 2, destElBox.y + destElBox.height / 2);
1063
+ await this.page.locator(buildLocatorString(dst)).hover({ position: { x: 10, y: 10 } });
1064
+ await this.page.waitForTimeout(_smallWaitInMs);
1065
+ await this.page.mouse.up();
967
1066
  }
968
1067
 
969
1068
  /**
@@ -1461,7 +1560,7 @@ class Playwright extends Helper {
1461
1560
  * ⚠️ returns a _promise_ which is synchronized internally by recorder
1462
1561
  *
1463
1562
  *
1464
- * @param {any} [opts] [Additional options](https://playwright.dev/docs/api/class-page#page-click) for click available as 3rd argument.
1563
+ * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-page#page-click) for click available as 3rd argument.
1465
1564
  *
1466
1565
  * Examples:
1467
1566
  *
@@ -1474,8 +1573,8 @@ class Playwright extends Helper {
1474
1573
  * ```
1475
1574
  *
1476
1575
  */
1477
- async click(locator, context = null, opts = {}) {
1478
- return proceedClick.call(this, locator, context, opts);
1576
+ async click(locator, context = null, options = {}) {
1577
+ return proceedClick.call(this, locator, context, options);
1479
1578
  }
1480
1579
 
1481
1580
  /**
@@ -1811,6 +1910,9 @@ class Playwright extends Helper {
1811
1910
  *
1812
1911
  * // passing in an array
1813
1912
  * I.type(['T', 'E', 'X', 'T']);
1913
+ *
1914
+ * // passing a secret
1915
+ * I.type(secret('123456'));
1814
1916
  * ```
1815
1917
  *
1816
1918
  * @param {string|string[]} key or array of keys to type.
@@ -1820,6 +1922,7 @@ class Playwright extends Helper {
1820
1922
  */
1821
1923
  async type(keys, delay = null) {
1822
1924
  if (!Array.isArray(keys)) {
1925
+ keys = keys.toString();
1823
1926
  keys = keys.split('');
1824
1927
  }
1825
1928
 
@@ -1860,24 +1963,47 @@ class Playwright extends Helper {
1860
1963
  } else if (editable) {
1861
1964
  await this._evaluateHandeInContext(el => el.innerHTML = '', el);
1862
1965
  }
1966
+
1967
+ highlightActiveElement.call(this, el, this.page);
1968
+
1863
1969
  await el.type(value.toString(), { delay: this.options.pressKeyDelay });
1970
+
1864
1971
  return this._waitForAction();
1865
1972
  }
1866
1973
 
1867
1974
  /**
1868
- * Clears a `<textarea>` or text `<input>` element's value.
1869
- *
1870
- * ```js
1871
- * I.clearField('Email');
1872
- * I.clearField('user[email]');
1873
- * I.clearField('#email');
1874
- * ```
1875
- * @param {LocatorOrString} editable field located by label|name|CSS|XPath|strict locator.
1876
- * ⚠️ returns a _promise_ which is synchronized internally by recorder.
1877
- *
1975
+ * Clears the text input element: `<input>`, `<textarea>` or `[contenteditable]` .
1976
+ *
1977
+ *
1978
+ * Examples:
1979
+ *
1980
+ * ```js
1981
+ * I.clearField('.text-area')
1982
+ *
1983
+ * // if this doesn't work use force option
1984
+ * I.clearField('#submit', { force: true })
1985
+ * ```
1986
+ * Use `force` to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
1987
+ *
1988
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
1989
+ * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
1878
1990
  */
1879
- async clearField(field) {
1880
- return this.fillField(field, '');
1991
+ async clearField(locator, options = {}) {
1992
+ let result;
1993
+ const isNewClearMethodPresent = typeof this.page.locator().clear === 'function';
1994
+
1995
+ if (isNewClearMethodPresent) {
1996
+ const els = await findFields.call(this, locator);
1997
+ assertElementExists(els, locator, 'Field to clear');
1998
+ // TODO: locator change required after #3677 implementation
1999
+ const elXpath = await getXPathForElement(els[0]);
2000
+
2001
+ await this.page.locator(elXpath).clear(options);
2002
+ result = await this._waitForAction();
2003
+ } else {
2004
+ result = await this.fillField(locator, '');
2005
+ }
2006
+ return result;
1881
2007
  }
1882
2008
 
1883
2009
  /**
@@ -1886,6 +2012,8 @@ class Playwright extends Helper {
1886
2012
  *
1887
2013
  * ```js
1888
2014
  * I.appendField('#myTextField', 'appended');
2015
+ * // typing secret
2016
+ * I.appendField('password', secret('123456'));
1889
2017
  * ```
1890
2018
  * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator
1891
2019
  * @param {string} value text value to append.
@@ -1897,8 +2025,9 @@ class Playwright extends Helper {
1897
2025
  async appendField(field, value) {
1898
2026
  const els = await findFields.call(this, field);
1899
2027
  assertElementExists(els, field, 'Field');
2028
+ highlightActiveElement.call(this, els[0], this.page);
1900
2029
  await els[0].press('End');
1901
- await els[0].type(value, { delay: this.options.pressKeyDelay });
2030
+ await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
1902
2031
  return this._waitForAction();
1903
2032
  }
1904
2033
 
@@ -1998,6 +2127,7 @@ class Playwright extends Helper {
1998
2127
  if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
1999
2128
  throw new Error('Element is not <select>');
2000
2129
  }
2130
+ highlightActiveElement.call(this, el, this.page);
2001
2131
  if (!Array.isArray(option)) option = [option];
2002
2132
 
2003
2133
  for (const key in option) {
@@ -2793,23 +2923,32 @@ class Playwright extends Helper {
2793
2923
  */
2794
2924
  async saveScreenshot(fileName, fullPage) {
2795
2925
  const fullPageOption = fullPage || this.options.fullPageScreenshots;
2796
- const outputFile = screenshotOutputFolder(fileName);
2926
+ let outputFile = screenshotOutputFolder(fileName);
2797
2927
 
2798
2928
  this.debug(`Screenshot is saving to ${outputFile}`);
2799
2929
 
2930
+ await this.page.screenshot({
2931
+ path: outputFile,
2932
+ fullPage: fullPageOption,
2933
+ type: 'png',
2934
+ });
2935
+
2800
2936
  if (this.activeSessionName) {
2801
- const activeSessionPage = this.sessionPages[this.activeSessionName];
2802
-
2803
- if (activeSessionPage) {
2804
- return activeSessionPage.screenshot({
2805
- path: outputFile,
2806
- fullPage: fullPageOption,
2807
- type: 'png',
2808
- });
2937
+ for (const sessionName in this.sessionPages) {
2938
+ const activeSessionPage = this.sessionPages[sessionName];
2939
+ outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`);
2940
+
2941
+ this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`);
2942
+
2943
+ if (activeSessionPage) {
2944
+ await activeSessionPage.screenshot({
2945
+ path: outputFile,
2946
+ fullPage: fullPageOption,
2947
+ type: 'png',
2948
+ });
2949
+ }
2809
2950
  }
2810
2951
  }
2811
-
2812
- return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
2813
2952
  }
2814
2953
 
2815
2954
  /**
@@ -2877,7 +3016,7 @@ class Playwright extends Helper {
2877
3016
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`);
2878
3017
  for (const sessionName in this.sessionPages) {
2879
3018
  if (!this.sessionPages[sessionName].context) continue;
2880
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context(), `${test.title}_${sessionName}.failed`);
3019
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`);
2881
3020
  }
2882
3021
  }
2883
3022
  }
@@ -2900,7 +3039,7 @@ class Playwright extends Helper {
2900
3039
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`);
2901
3040
  for (const sessionName in this.sessionPages) {
2902
3041
  if (!this.sessionPages[sessionName].context) continue;
2903
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context(), `${test.title}_${sessionName}.passed`);
3042
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`);
2904
3043
  }
2905
3044
  }
2906
3045
  } else {
@@ -3386,15 +3525,15 @@ class Playwright extends Helper {
3386
3525
  *
3387
3526
  * See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
3388
3527
  *
3389
- * @param {*} opts
3528
+ * @param {*} options
3390
3529
  */
3391
- async waitForNavigation(opts = {}) {
3392
- opts = {
3530
+ async waitForNavigation(options = {}) {
3531
+ options = {
3393
3532
  timeout: this.options.getPageTimeout,
3394
3533
  waitUntil: this.options.waitForNavigation,
3395
- ...opts,
3534
+ ...options,
3396
3535
  };
3397
- return this.page.waitForNavigation(opts);
3536
+ return this.page.waitForNavigation(options);
3398
3537
  }
3399
3538
 
3400
3539
  async waitUntilExists(locator, sec) {
@@ -3536,11 +3675,41 @@ function buildLocatorString(locator) {
3536
3675
  if (locator.isCustom()) {
3537
3676
  return `${locator.type}=${locator.value}`;
3538
3677
  } if (locator.isXPath()) {
3539
- // dont rely on heuristics of playwright for figuring out xpath
3540
3678
  return `xpath=${locator.value}`;
3541
3679
  }
3542
3680
  return locator.simplify();
3543
3681
  }
3682
+ // TODO: locator change required after #3677 implementation. Temporary solution before migration. Should be deleted after #3677 implementation
3683
+ async function getXPathForElement(elementHandle) {
3684
+ function calculateIndex(node) {
3685
+ let index = 1;
3686
+ let sibling = node.previousElementSibling;
3687
+ while (sibling) {
3688
+ if (sibling.tagName === node.tagName) {
3689
+ index++;
3690
+ }
3691
+ sibling = sibling.previousElementSibling;
3692
+ }
3693
+ return index;
3694
+ }
3695
+
3696
+ function generateXPath(node) {
3697
+ const segments = [];
3698
+ while (node && node.nodeType === Node.ELEMENT_NODE) {
3699
+ if (node.hasAttribute('id')) {
3700
+ segments.unshift(`*[@id="${node.getAttribute('id')}"]`);
3701
+ break;
3702
+ } else {
3703
+ const index = calculateIndex(node);
3704
+ segments.unshift(`${node.localName}[${index}]`);
3705
+ node = node.parentNode;
3706
+ }
3707
+ }
3708
+ return `//${segments.join('/')}`;
3709
+ }
3710
+
3711
+ return elementHandle.evaluate(generateXPath);
3712
+ }
3544
3713
 
3545
3714
  async function findElements(matcher, locator) {
3546
3715
  if (locator.react) return findReact(matcher, locator);
@@ -3574,6 +3743,10 @@ async function proceedClick(locator, context = null, options = {}) {
3574
3743
  } else {
3575
3744
  assertElementExists(els, locator, 'Clickable element');
3576
3745
  }
3746
+
3747
+ const element = els[0];
3748
+ highlightActiveElement.call(this, els[0], this.page);
3749
+
3577
3750
  /*
3578
3751
  using the force true options itself but instead dispatching a click
3579
3752
  */
@@ -3588,6 +3761,7 @@ async function proceedClick(locator, context = null, options = {}) {
3588
3761
  promises.push(this.waitForNavigation());
3589
3762
  }
3590
3763
  promises.push(this._waitForAction());
3764
+
3591
3765
  return Promise.all(promises);
3592
3766
  }
3593
3767
 
@@ -3969,7 +4143,7 @@ async function refreshContextSession() {
3969
4143
 
3970
4144
  async function saveVideoForPage(page, name) {
3971
4145
  if (!page.video()) return null;
3972
- const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${Date.now()}_${clearString(name)}`.slice(0, 245)}.webm`;
4146
+ const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`;
3973
4147
  page.video().saveAs(fileName).then(() => {
3974
4148
  if (!page) return;
3975
4149
  page.video().delete().catch(e => {});
@@ -3980,7 +4154,13 @@ async function saveVideoForPage(page, name) {
3980
4154
  async function saveTraceForContext(context, name) {
3981
4155
  if (!context) return;
3982
4156
  if (!context.tracing) return;
3983
- const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${Date.now()}_${clearString(name)}`.slice(0, 245)}.zip`;
4157
+ const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`;
3984
4158
  await context.tracing.stop({ path: fileName });
3985
4159
  return fileName;
3986
4160
  }
4161
+
4162
+ function highlightActiveElement(element, context) {
4163
+ if (!this.options.enableHighlight && !store.debugMode) return;
4164
+
4165
+ highlightElement(element, context);
4166
+ }
@@ -853,6 +853,8 @@ class Protractor extends Helper {
853
853
  *
854
854
  * ```js
855
855
  * I.appendField('#myTextField', 'appended');
856
+ * // typing secret
857
+ * I.appendField('password', secret('123456'));
856
858
  * ```
857
859
  * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator
858
860
  * @param {string} value text value to append.
@@ -862,7 +864,7 @@ class Protractor extends Helper {
862
864
  async appendField(field, value) {
863
865
  const els = await findFields(this.browser, field);
864
866
  assertElementExists(els, field, 'Field');
865
- return els[0].sendKeys(value);
867
+ return els[0].sendKeys(value.toString());
866
868
  }
867
869
 
868
870
  /**