codeceptjs 3.4.1 → 3.5.0

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 (69) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +9 -7
  3. package/bin/codecept.js +1 -1
  4. package/docs/ai.md +246 -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 +193 -45
  10. package/docs/build/Protractor.js +3 -1
  11. package/docs/build/Puppeteer.js +45 -12
  12. package/docs/build/REST.js +15 -5
  13. package/docs/build/TestCafe.js +3 -1
  14. package/docs/build/WebDriver.js +30 -5
  15. package/docs/changelog.md +65 -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 +194 -152
  21. package/docs/helpers/Puppeteer.md +6 -0
  22. package/docs/helpers/REST.md +6 -5
  23. package/docs/helpers/TestCafe.md +2 -0
  24. package/docs/helpers/WebDriver.md +10 -4
  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/type.mustache +3 -0
  32. package/lib/ai.js +171 -0
  33. package/lib/cli.js +1 -1
  34. package/lib/codecept.js +4 -0
  35. package/lib/command/dryRun.js +9 -1
  36. package/lib/command/generate.js +46 -3
  37. package/lib/command/init.js +13 -1
  38. package/lib/command/interactive.js +15 -1
  39. package/lib/command/run-workers.js +2 -1
  40. package/lib/container.js +13 -3
  41. package/lib/helper/Appium.js +45 -7
  42. package/lib/helper/JSONResponse.js +4 -4
  43. package/lib/helper/Nightmare.js +1 -1
  44. package/lib/helper/OpenAI.js +122 -0
  45. package/lib/helper/Playwright.js +190 -38
  46. package/lib/helper/Protractor.js +1 -1
  47. package/lib/helper/Puppeteer.js +40 -12
  48. package/lib/helper/REST.js +15 -5
  49. package/lib/helper/TestCafe.js +1 -1
  50. package/lib/helper/WebDriver.js +25 -5
  51. package/lib/helper/scripts/highlightElement.js +20 -0
  52. package/lib/html.js +258 -0
  53. package/lib/listener/retry.js +2 -1
  54. package/lib/pause.js +73 -17
  55. package/lib/plugin/debugErrors.js +67 -0
  56. package/lib/plugin/fakerTransform.js +4 -6
  57. package/lib/plugin/heal.js +179 -0
  58. package/lib/plugin/screenshotOnFail.js +11 -2
  59. package/lib/recorder.js +4 -4
  60. package/lib/secret.js +5 -4
  61. package/lib/step.js +6 -1
  62. package/lib/ui.js +4 -3
  63. package/lib/utils.js +4 -0
  64. package/lib/workers.js +57 -9
  65. package/package.json +25 -13
  66. package/translations/ja-JP.js +9 -9
  67. package/typings/index.d.ts +43 -9
  68. package/typings/promiseBasedTypes.d.ts +124 -24
  69. package/typings/types.d.ts +138 -30
@@ -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,6 +45,7 @@ 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
 
@@ -54,7 +57,7 @@ const pathSeparator = path.sep;
54
57
  * @typedef PlaywrightConfig
55
58
  * @type {object}
56
59
  * @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.
60
+ * @prop {'chromium' | 'firefox'| 'webkit' | 'electron'} [browser='chromium'] - a browser to test on, either: `chromium`, `firefox`, `webkit`, `electron`. Default: chromium.
58
61
  * @prop {boolean} [show=false] - 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.
@@ -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
@@ -938,6 +943,58 @@ class Playwright extends Helper {
938
943
  return this._waitForAction();
939
944
  }
940
945
 
946
+ /**
947
+ * Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the matching element.
948
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
949
+ * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-focus) for available options object as 2nd argument.
950
+ *
951
+ * Examples:
952
+ *
953
+ * ```js
954
+ * I.dontSee('#add-to-cart-btn');
955
+ * I.focus('#product-tile')
956
+ * I.see('#add-to-cart-bnt');
957
+ * ```
958
+ *
959
+ */
960
+ async focus(locator, options = {}) {
961
+ const els = await this._locate(locator);
962
+ assertElementExists(els, locator, 'Element to focus');
963
+ const el = els[0];
964
+
965
+ await el.focus(options);
966
+ return this._waitForAction();
967
+ }
968
+
969
+ /**
970
+ * Remove focus from a text input, button, etc
971
+ * Calls [blur](https://playwright.dev/docs/api/class-locator#locator-blur) on the element.
972
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
973
+ * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-blur) for available options object as 2nd argument.
974
+ *
975
+ * Examples:
976
+ *
977
+ * ```js
978
+ * I.blur('.text-area')
979
+ * ```
980
+ * ```js
981
+ * //element `#product-tile` is focused
982
+ * I.see('#add-to-cart-btn');
983
+ * I.blur('#product-tile')
984
+ * I.dontSee('#add-to-cart-btn');
985
+ * ```
986
+ *
987
+ */
988
+ async blur(locator, options = {}) {
989
+ const els = await this._locate(locator);
990
+ assertElementExists(els, locator, 'Element to blur');
991
+ // TODO: locator change required after #3677 implementation
992
+ const elXpath = await getXPathForElement(els[0]);
993
+
994
+ await this.page.locator(elXpath).blur(options);
995
+ return this._waitForAction();
996
+ }
997
+
941
998
  /**
942
999
  * Drag an item to a destination element.
943
1000
  *
@@ -949,7 +1006,6 @@ class Playwright extends Helper {
949
1006
  * @param {LocatorOrString} destElement located by CSS|XPath|strict locator.
950
1007
  * ⚠️ returns a _promise_ which is synchronized internally by recorder
951
1008
  *
952
- *
953
1009
  * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-page#page-drag-and-drop) can be passed as 3rd argument.
954
1010
  *
955
1011
  * ```js
@@ -957,13 +1013,27 @@ class Playwright extends Helper {
957
1013
  * I.dragAndDrop('img.src', 'img.dst', { sourcePosition: {x: 10, y: 10} })
958
1014
  * ```
959
1015
  *
960
- * > By default option `force: true` is set
1016
+ * > 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
1017
  */
962
- async dragAndDrop(srcElement, destElement, options = { force: true }) {
963
- const src = new Locator(srcElement, 'css');
964
- const dst = new Locator(destElement, 'css');
1018
+ async dragAndDrop(srcElement, destElement, options) {
1019
+ const src = new Locator(srcElement);
1020
+ const dst = new Locator(destElement);
1021
+
1022
+ if (options) {
1023
+ return this.page.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options);
1024
+ }
965
1025
 
966
- return this.page.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options);
1026
+ const _smallWaitInMs = 600;
1027
+ await this.page.locator(buildLocatorString(src)).hover();
1028
+ await this.page.mouse.down();
1029
+ await this.page.waitForTimeout(_smallWaitInMs);
1030
+
1031
+ const destElBox = await this.page.locator(buildLocatorString(dst)).boundingBox();
1032
+
1033
+ await this.page.mouse.move(destElBox.x + destElBox.width / 2, destElBox.y + destElBox.height / 2);
1034
+ await this.page.locator(buildLocatorString(dst)).hover({ position: { x: 10, y: 10 } });
1035
+ await this.page.waitForTimeout(_smallWaitInMs);
1036
+ await this.page.mouse.up();
967
1037
  }
968
1038
 
969
1039
  /**
@@ -1461,7 +1531,7 @@ class Playwright extends Helper {
1461
1531
  * ⚠️ returns a _promise_ which is synchronized internally by recorder
1462
1532
  *
1463
1533
  *
1464
- * @param {any} [opts] [Additional options](https://playwright.dev/docs/api/class-page#page-click) for click available as 3rd argument.
1534
+ * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-page#page-click) for click available as 3rd argument.
1465
1535
  *
1466
1536
  * Examples:
1467
1537
  *
@@ -1474,8 +1544,8 @@ class Playwright extends Helper {
1474
1544
  * ```
1475
1545
  *
1476
1546
  */
1477
- async click(locator, context = null, opts = {}) {
1478
- return proceedClick.call(this, locator, context, opts);
1547
+ async click(locator, context = null, options = {}) {
1548
+ return proceedClick.call(this, locator, context, options);
1479
1549
  }
1480
1550
 
1481
1551
  /**
@@ -1811,6 +1881,9 @@ class Playwright extends Helper {
1811
1881
  *
1812
1882
  * // passing in an array
1813
1883
  * I.type(['T', 'E', 'X', 'T']);
1884
+ *
1885
+ * // passing a secret
1886
+ * I.type(secret('123456'));
1814
1887
  * ```
1815
1888
  *
1816
1889
  * @param {string|string[]} key or array of keys to type.
@@ -1820,6 +1893,7 @@ class Playwright extends Helper {
1820
1893
  */
1821
1894
  async type(keys, delay = null) {
1822
1895
  if (!Array.isArray(keys)) {
1896
+ keys = keys.toString();
1823
1897
  keys = keys.split('');
1824
1898
  }
1825
1899
 
@@ -1860,24 +1934,44 @@ class Playwright extends Helper {
1860
1934
  } else if (editable) {
1861
1935
  await this._evaluateHandeInContext(el => el.innerHTML = '', el);
1862
1936
  }
1937
+
1938
+ highlightActiveElement.call(this, el, this.page);
1939
+
1863
1940
  await el.type(value.toString(), { delay: this.options.pressKeyDelay });
1941
+
1864
1942
  return this._waitForAction();
1865
1943
  }
1866
1944
 
1867
1945
  /**
1868
- * Clears a `<textarea>` or text `<input>` element's value.
1869
- *
1946
+ * Clear the <input>, <textarea> or [contenteditable] .
1947
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
1948
+ * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
1949
+ *
1950
+ * Examples:
1951
+ *
1870
1952
  * ```js
1871
- * I.clearField('Email');
1872
- * I.clearField('user[email]');
1873
- * I.clearField('#email');
1953
+ * I.clearField('.text-area')
1954
+ * ```
1955
+ * ```js
1956
+ * I.clearField('#submit', { force: true }) // force to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
1874
1957
  * ```
1875
- * @param {LocatorOrString} editable field located by label|name|CSS|XPath|strict locator.
1876
- * ⚠️ returns a _promise_ which is synchronized internally by recorder.
1877
- *
1878
1958
  */
1879
- async clearField(field) {
1880
- return this.fillField(field, '');
1959
+ async clearField(locator, options = {}) {
1960
+ let result;
1961
+ const isNewClearMethodPresent = typeof this.page.locator().clear === 'function';
1962
+
1963
+ if (isNewClearMethodPresent) {
1964
+ const els = await findFields.call(this, locator);
1965
+ assertElementExists(els, locator, 'Field to clear');
1966
+ // TODO: locator change required after #3677 implementation
1967
+ const elXpath = await getXPathForElement(els[0]);
1968
+
1969
+ await this.page.locator(elXpath).clear(options);
1970
+ result = await this._waitForAction();
1971
+ } else {
1972
+ result = await this.fillField(locator, '');
1973
+ }
1974
+ return result;
1881
1975
  }
1882
1976
 
1883
1977
  /**
@@ -1886,6 +1980,8 @@ class Playwright extends Helper {
1886
1980
  *
1887
1981
  * ```js
1888
1982
  * I.appendField('#myTextField', 'appended');
1983
+ * // typing secret
1984
+ * I.appendField('password', secret('123456'));
1889
1985
  * ```
1890
1986
  * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator
1891
1987
  * @param {string} value text value to append.
@@ -1897,8 +1993,9 @@ class Playwright extends Helper {
1897
1993
  async appendField(field, value) {
1898
1994
  const els = await findFields.call(this, field);
1899
1995
  assertElementExists(els, field, 'Field');
1996
+ highlightActiveElement.call(this, els[0], this.page);
1900
1997
  await els[0].press('End');
1901
- await els[0].type(value, { delay: this.options.pressKeyDelay });
1998
+ await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
1902
1999
  return this._waitForAction();
1903
2000
  }
1904
2001
 
@@ -1998,6 +2095,7 @@ class Playwright extends Helper {
1998
2095
  if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
1999
2096
  throw new Error('Element is not <select>');
2000
2097
  }
2098
+ highlightActiveElement.call(this, el, this.page);
2001
2099
  if (!Array.isArray(option)) option = [option];
2002
2100
 
2003
2101
  for (const key in option) {
@@ -2793,23 +2891,32 @@ class Playwright extends Helper {
2793
2891
  */
2794
2892
  async saveScreenshot(fileName, fullPage) {
2795
2893
  const fullPageOption = fullPage || this.options.fullPageScreenshots;
2796
- const outputFile = screenshotOutputFolder(fileName);
2894
+ let outputFile = screenshotOutputFolder(fileName);
2797
2895
 
2798
2896
  this.debug(`Screenshot is saving to ${outputFile}`);
2799
2897
 
2898
+ await this.page.screenshot({
2899
+ path: outputFile,
2900
+ fullPage: fullPageOption,
2901
+ type: 'png',
2902
+ });
2903
+
2800
2904
  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
- });
2905
+ for (const sessionName in this.sessionPages) {
2906
+ const activeSessionPage = this.sessionPages[sessionName];
2907
+ outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`);
2908
+
2909
+ this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`);
2910
+
2911
+ if (activeSessionPage) {
2912
+ await activeSessionPage.screenshot({
2913
+ path: outputFile,
2914
+ fullPage: fullPageOption,
2915
+ type: 'png',
2916
+ });
2917
+ }
2809
2918
  }
2810
2919
  }
2811
-
2812
- return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
2813
2920
  }
2814
2921
 
2815
2922
  /**
@@ -2877,7 +2984,7 @@ class Playwright extends Helper {
2877
2984
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`);
2878
2985
  for (const sessionName in this.sessionPages) {
2879
2986
  if (!this.sessionPages[sessionName].context) continue;
2880
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context(), `${test.title}_${sessionName}.failed`);
2987
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`);
2881
2988
  }
2882
2989
  }
2883
2990
  }
@@ -2900,7 +3007,7 @@ class Playwright extends Helper {
2900
3007
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`);
2901
3008
  for (const sessionName in this.sessionPages) {
2902
3009
  if (!this.sessionPages[sessionName].context) continue;
2903
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context(), `${test.title}_${sessionName}.passed`);
3010
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`);
2904
3011
  }
2905
3012
  }
2906
3013
  } else {
@@ -3386,15 +3493,15 @@ class Playwright extends Helper {
3386
3493
  *
3387
3494
  * See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
3388
3495
  *
3389
- * @param {*} opts
3496
+ * @param {*} options
3390
3497
  */
3391
- async waitForNavigation(opts = {}) {
3392
- opts = {
3498
+ async waitForNavigation(options = {}) {
3499
+ options = {
3393
3500
  timeout: this.options.getPageTimeout,
3394
3501
  waitUntil: this.options.waitForNavigation,
3395
- ...opts,
3502
+ ...options,
3396
3503
  };
3397
- return this.page.waitForNavigation(opts);
3504
+ return this.page.waitForNavigation(options);
3398
3505
  }
3399
3506
 
3400
3507
  async waitUntilExists(locator, sec) {
@@ -3536,11 +3643,41 @@ function buildLocatorString(locator) {
3536
3643
  if (locator.isCustom()) {
3537
3644
  return `${locator.type}=${locator.value}`;
3538
3645
  } if (locator.isXPath()) {
3539
- // dont rely on heuristics of playwright for figuring out xpath
3540
3646
  return `xpath=${locator.value}`;
3541
3647
  }
3542
3648
  return locator.simplify();
3543
3649
  }
3650
+ // TODO: locator change required after #3677 implementation. Temporary solution before migration. Should be deleted after #3677 implementation
3651
+ async function getXPathForElement(elementHandle) {
3652
+ function calculateIndex(node) {
3653
+ let index = 1;
3654
+ let sibling = node.previousElementSibling;
3655
+ while (sibling) {
3656
+ if (sibling.tagName === node.tagName) {
3657
+ index++;
3658
+ }
3659
+ sibling = sibling.previousElementSibling;
3660
+ }
3661
+ return index;
3662
+ }
3663
+
3664
+ function generateXPath(node) {
3665
+ const segments = [];
3666
+ while (node && node.nodeType === Node.ELEMENT_NODE) {
3667
+ if (node.hasAttribute('id')) {
3668
+ segments.unshift(`*[@id="${node.getAttribute('id')}"]`);
3669
+ break;
3670
+ } else {
3671
+ const index = calculateIndex(node);
3672
+ segments.unshift(`${node.localName}[${index}]`);
3673
+ node = node.parentNode;
3674
+ }
3675
+ }
3676
+ return `//${segments.join('/')}`;
3677
+ }
3678
+
3679
+ return elementHandle.evaluate(generateXPath);
3680
+ }
3544
3681
 
3545
3682
  async function findElements(matcher, locator) {
3546
3683
  if (locator.react) return findReact(matcher, locator);
@@ -3574,6 +3711,10 @@ async function proceedClick(locator, context = null, options = {}) {
3574
3711
  } else {
3575
3712
  assertElementExists(els, locator, 'Clickable element');
3576
3713
  }
3714
+
3715
+ const element = els[0];
3716
+ highlightActiveElement.call(this, els[0], this.page);
3717
+
3577
3718
  /*
3578
3719
  using the force true options itself but instead dispatching a click
3579
3720
  */
@@ -3588,6 +3729,7 @@ async function proceedClick(locator, context = null, options = {}) {
3588
3729
  promises.push(this.waitForNavigation());
3589
3730
  }
3590
3731
  promises.push(this._waitForAction());
3732
+
3591
3733
  return Promise.all(promises);
3592
3734
  }
3593
3735
 
@@ -3969,7 +4111,7 @@ async function refreshContextSession() {
3969
4111
 
3970
4112
  async function saveVideoForPage(page, name) {
3971
4113
  if (!page.video()) return null;
3972
- const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${Date.now()}_${clearString(name)}`.slice(0, 245)}.webm`;
4114
+ const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`;
3973
4115
  page.video().saveAs(fileName).then(() => {
3974
4116
  if (!page) return;
3975
4117
  page.video().delete().catch(e => {});
@@ -3980,7 +4122,13 @@ async function saveVideoForPage(page, name) {
3980
4122
  async function saveTraceForContext(context, name) {
3981
4123
  if (!context) return;
3982
4124
  if (!context.tracing) return;
3983
- const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${Date.now()}_${clearString(name)}`.slice(0, 245)}.zip`;
4125
+ const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`;
3984
4126
  await context.tracing.stop({ path: fileName });
3985
4127
  return fileName;
3986
4128
  }
4129
+
4130
+ function highlightActiveElement(element, context) {
4131
+ if (!this.options.enableHighlight && !store.debugMode) return;
4132
+
4133
+ highlightElement(element, context);
4134
+ }
@@ -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
  /**