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
@@ -903,8 +908,59 @@ class Playwright extends Helper {
903
908
  }
904
909
 
905
910
  /**
906
- * {{> dragAndDrop }}
911
+ * Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the matching element.
912
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
913
+ * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-focus) for available options object as 2nd argument.
914
+ *
915
+ * Examples:
916
+ *
917
+ * ```js
918
+ * I.dontSee('#add-to-cart-btn');
919
+ * I.focus('#product-tile')
920
+ * I.see('#add-to-cart-bnt');
921
+ * ```
907
922
  *
923
+ */
924
+ async focus(locator, options = {}) {
925
+ const els = await this._locate(locator);
926
+ assertElementExists(els, locator, 'Element to focus');
927
+ const el = els[0];
928
+
929
+ await el.focus(options);
930
+ return this._waitForAction();
931
+ }
932
+
933
+ /**
934
+ * Remove focus from a text input, button, etc
935
+ * Calls [blur](https://playwright.dev/docs/api/class-locator#locator-blur) on the element.
936
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
937
+ * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-blur) for available options object as 2nd argument.
938
+ *
939
+ * Examples:
940
+ *
941
+ * ```js
942
+ * I.blur('.text-area')
943
+ * ```
944
+ * ```js
945
+ * //element `#product-tile` is focused
946
+ * I.see('#add-to-cart-btn');
947
+ * I.blur('#product-tile')
948
+ * I.dontSee('#add-to-cart-btn');
949
+ * ```
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);
920
976
 
921
- return this.page.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options);
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);
985
+
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,44 @@ 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
+ * Clear the <input>, <textarea> or [contenteditable] .
1546
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
1547
+ * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
1548
+ *
1549
+ * Examples:
1550
+ *
1551
+ * ```js
1552
+ * I.clearField('.text-area')
1553
+ * ```
1554
+ * ```js
1555
+ * I.clearField('#submit', { force: true }) // force to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
1556
+ * ```
1471
1557
  */
1472
- async clearField(field) {
1473
- return this.fillField(field, '');
1558
+ async clearField(locator, options = {}) {
1559
+ let result;
1560
+ const isNewClearMethodPresent = typeof this.page.locator().clear === 'function';
1561
+
1562
+ if (isNewClearMethodPresent) {
1563
+ const els = await findFields.call(this, locator);
1564
+ assertElementExists(els, locator, 'Field to clear');
1565
+ // TODO: locator change required after #3677 implementation
1566
+ const elXpath = await getXPathForElement(els[0]);
1567
+
1568
+ await this.page.locator(elXpath).clear(options);
1569
+ result = await this._waitForAction();
1570
+ } else {
1571
+ result = await this.fillField(locator, '');
1572
+ }
1573
+ return result;
1474
1574
  }
1475
1575
 
1476
1576
  /**
@@ -1481,8 +1581,9 @@ class Playwright extends Helper {
1481
1581
  async appendField(field, value) {
1482
1582
  const els = await findFields.call(this, field);
1483
1583
  assertElementExists(els, field, 'Field');
1584
+ highlightActiveElement.call(this, els[0], this.page);
1484
1585
  await els[0].press('End');
1485
- await els[0].type(value, { delay: this.options.pressKeyDelay });
1586
+ await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
1486
1587
  return this._waitForAction();
1487
1588
  }
1488
1589
 
@@ -1526,6 +1627,7 @@ class Playwright extends Helper {
1526
1627
  if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
1527
1628
  throw new Error('Element is not <select>');
1528
1629
  }
1630
+ highlightActiveElement.call(this, el, this.page);
1529
1631
  if (!Array.isArray(option)) option = [option];
1530
1632
 
1531
1633
  for (const key in option) {
@@ -1999,23 +2101,32 @@ class Playwright extends Helper {
1999
2101
  */
2000
2102
  async saveScreenshot(fileName, fullPage) {
2001
2103
  const fullPageOption = fullPage || this.options.fullPageScreenshots;
2002
- const outputFile = screenshotOutputFolder(fileName);
2104
+ let outputFile = screenshotOutputFolder(fileName);
2003
2105
 
2004
2106
  this.debug(`Screenshot is saving to ${outputFile}`);
2005
2107
 
2108
+ await this.page.screenshot({
2109
+ path: outputFile,
2110
+ fullPage: fullPageOption,
2111
+ type: 'png',
2112
+ });
2113
+
2006
2114
  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
- });
2115
+ for (const sessionName in this.sessionPages) {
2116
+ const activeSessionPage = this.sessionPages[sessionName];
2117
+ outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`);
2118
+
2119
+ this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`);
2120
+
2121
+ if (activeSessionPage) {
2122
+ await activeSessionPage.screenshot({
2123
+ path: outputFile,
2124
+ fullPage: fullPageOption,
2125
+ type: 'png',
2126
+ });
2127
+ }
2015
2128
  }
2016
2129
  }
2017
-
2018
- return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
2019
2130
  }
2020
2131
 
2021
2132
  /**
@@ -2083,7 +2194,7 @@ class Playwright extends Helper {
2083
2194
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`);
2084
2195
  for (const sessionName in this.sessionPages) {
2085
2196
  if (!this.sessionPages[sessionName].context) continue;
2086
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context(), `${test.title}_${sessionName}.failed`);
2197
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`);
2087
2198
  }
2088
2199
  }
2089
2200
  }
@@ -2106,7 +2217,7 @@ class Playwright extends Helper {
2106
2217
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`);
2107
2218
  for (const sessionName in this.sessionPages) {
2108
2219
  if (!this.sessionPages[sessionName].context) continue;
2109
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context(), `${test.title}_${sessionName}.passed`);
2220
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`);
2110
2221
  }
2111
2222
  }
2112
2223
  } else {
@@ -2448,15 +2559,15 @@ class Playwright extends Helper {
2448
2559
  *
2449
2560
  * See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
2450
2561
  *
2451
- * @param {*} opts
2562
+ * @param {*} options
2452
2563
  */
2453
- async waitForNavigation(opts = {}) {
2454
- opts = {
2564
+ async waitForNavigation(options = {}) {
2565
+ options = {
2455
2566
  timeout: this.options.getPageTimeout,
2456
2567
  waitUntil: this.options.waitForNavigation,
2457
- ...opts,
2568
+ ...options,
2458
2569
  };
2459
- return this.page.waitForNavigation(opts);
2570
+ return this.page.waitForNavigation(options);
2460
2571
  }
2461
2572
 
2462
2573
  async waitUntilExists(locator, sec) {
@@ -2549,11 +2660,41 @@ function buildLocatorString(locator) {
2549
2660
  if (locator.isCustom()) {
2550
2661
  return `${locator.type}=${locator.value}`;
2551
2662
  } if (locator.isXPath()) {
2552
- // dont rely on heuristics of playwright for figuring out xpath
2553
2663
  return `xpath=${locator.value}`;
2554
2664
  }
2555
2665
  return locator.simplify();
2556
2666
  }
2667
+ // TODO: locator change required after #3677 implementation. Temporary solution before migration. Should be deleted after #3677 implementation
2668
+ async function getXPathForElement(elementHandle) {
2669
+ function calculateIndex(node) {
2670
+ let index = 1;
2671
+ let sibling = node.previousElementSibling;
2672
+ while (sibling) {
2673
+ if (sibling.tagName === node.tagName) {
2674
+ index++;
2675
+ }
2676
+ sibling = sibling.previousElementSibling;
2677
+ }
2678
+ return index;
2679
+ }
2680
+
2681
+ function generateXPath(node) {
2682
+ const segments = [];
2683
+ while (node && node.nodeType === Node.ELEMENT_NODE) {
2684
+ if (node.hasAttribute('id')) {
2685
+ segments.unshift(`*[@id="${node.getAttribute('id')}"]`);
2686
+ break;
2687
+ } else {
2688
+ const index = calculateIndex(node);
2689
+ segments.unshift(`${node.localName}[${index}]`);
2690
+ node = node.parentNode;
2691
+ }
2692
+ }
2693
+ return `//${segments.join('/')}`;
2694
+ }
2695
+
2696
+ return elementHandle.evaluate(generateXPath);
2697
+ }
2557
2698
 
2558
2699
  async function findElements(matcher, locator) {
2559
2700
  if (locator.react) return findReact(matcher, locator);
@@ -2587,6 +2728,10 @@ async function proceedClick(locator, context = null, options = {}) {
2587
2728
  } else {
2588
2729
  assertElementExists(els, locator, 'Clickable element');
2589
2730
  }
2731
+
2732
+ const element = els[0];
2733
+ highlightActiveElement.call(this, els[0], this.page);
2734
+
2590
2735
  /*
2591
2736
  using the force true options itself but instead dispatching a click
2592
2737
  */
@@ -2601,6 +2746,7 @@ async function proceedClick(locator, context = null, options = {}) {
2601
2746
  promises.push(this.waitForNavigation());
2602
2747
  }
2603
2748
  promises.push(this._waitForAction());
2749
+
2604
2750
  return Promise.all(promises);
2605
2751
  }
2606
2752
 
@@ -2982,7 +3128,7 @@ async function refreshContextSession() {
2982
3128
 
2983
3129
  async function saveVideoForPage(page, name) {
2984
3130
  if (!page.video()) return null;
2985
- const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${Date.now()}_${clearString(name)}`.slice(0, 245)}.webm`;
3131
+ const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`;
2986
3132
  page.video().saveAs(fileName).then(() => {
2987
3133
  if (!page) return;
2988
3134
  page.video().delete().catch(e => {});
@@ -2993,7 +3139,13 @@ async function saveVideoForPage(page, name) {
2993
3139
  async function saveTraceForContext(context, name) {
2994
3140
  if (!context) return;
2995
3141
  if (!context.tracing) return;
2996
- const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${Date.now()}_${clearString(name)}`.slice(0, 245)}.zip`;
3142
+ const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`;
2997
3143
  await context.tracing.stop({ path: fileName });
2998
3144
  return fileName;
2999
3145
  }
3146
+
3147
+ function highlightActiveElement(element, context) {
3148
+ if (!this.options.enableHighlight && !store.debugMode) return;
3149
+
3150
+ highlightElement(element, context);
3151
+ }
@@ -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,7 @@ 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');
36
38
 
37
39
  let puppeteer;
38
40
  let perfTiming;
@@ -65,6 +67,7 @@ const consoleLogStore = new Console();
65
67
  * @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["Puppeteer"]._startBrowser()`.
66
68
  * @prop {string} [browser=chrome] - can be changed to `firefox` when using [puppeteer-firefox](https://codecept.io/helpers/Puppeteer-firefox).
67
69
  * @prop {object} [chrome] - pass additional [Puppeteer run options](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions).
70
+ * @prop {boolean} [highlightElement] - highlight the interacting elements
68
71
  */
69
72
  const config = {};
70
73
 
@@ -1262,6 +1265,7 @@ class Puppeteer extends Helper {
1262
1265
  */
1263
1266
  async type(keys, delay = null) {
1264
1267
  if (!Array.isArray(keys)) {
1268
+ keys = keys.toString();
1265
1269
  keys = keys.split('');
1266
1270
  }
1267
1271
 
@@ -1286,7 +1290,10 @@ class Puppeteer extends Helper {
1286
1290
  } else if (editable) {
1287
1291
  await this._evaluateHandeInContext(el => el.innerHTML = '', el);
1288
1292
  }
1293
+
1294
+ highlightActiveElement.call(this, el, this.page);
1289
1295
  await el.type(value.toString(), { delay: this.options.pressKeyDelay });
1296
+
1290
1297
  return this._waitForAction();
1291
1298
  }
1292
1299
 
@@ -1305,8 +1312,9 @@ class Puppeteer extends Helper {
1305
1312
  async appendField(field, value) {
1306
1313
  const els = await findVisibleFields.call(this, field);
1307
1314
  assertElementExists(els, field, 'Field');
1315
+ highlightActiveElement.call(this, els[0], this.page);
1308
1316
  await els[0].press('End');
1309
- await els[0].type(value, { delay: this.options.pressKeyDelay });
1317
+ await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
1310
1318
  return this._waitForAction();
1311
1319
  }
1312
1320
 
@@ -1351,6 +1359,7 @@ class Puppeteer extends Helper {
1351
1359
  if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
1352
1360
  throw new Error('Element is not <select>');
1353
1361
  }
1362
+ highlightActiveElement.call(this, els[0], this.page);
1354
1363
  if (!Array.isArray(option)) option = [option];
1355
1364
 
1356
1365
  for (const key in option) {
@@ -1828,23 +1837,32 @@ class Puppeteer extends Helper {
1828
1837
  */
1829
1838
  async saveScreenshot(fileName, fullPage) {
1830
1839
  const fullPageOption = fullPage || this.options.fullPageScreenshots;
1831
- const outputFile = screenshotOutputFolder(fileName);
1840
+ let outputFile = screenshotOutputFolder(fileName);
1832
1841
 
1833
1842
  this.debug(`Screenshot is saving to ${outputFile}`);
1834
1843
 
1844
+ await this.page.screenshot({
1845
+ path: outputFile,
1846
+ fullPage: fullPageOption,
1847
+ type: 'png',
1848
+ });
1849
+
1835
1850
  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
- });
1851
+ for (const sessionName in this.sessionPages) {
1852
+ const activeSessionPage = this.sessionPages[sessionName];
1853
+ outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`);
1854
+
1855
+ this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`);
1856
+
1857
+ if (activeSessionPage) {
1858
+ await activeSessionPage.screenshot({
1859
+ path: outputFile,
1860
+ fullPage: fullPageOption,
1861
+ type: 'png',
1862
+ });
1863
+ }
1844
1864
  }
1845
1865
  }
1846
-
1847
- return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
1848
1866
  }
1849
1867
 
1850
1868
  async _failed() {
@@ -2313,12 +2331,16 @@ async function proceedClick(locator, context = null, options = {}) {
2313
2331
  } else {
2314
2332
  assertElementExists(els, locator, 'Clickable element');
2315
2333
  }
2334
+
2335
+ highlightActiveElement.call(this, els[0], this.page);
2336
+
2316
2337
  await els[0].click(options);
2317
2338
  const promises = [];
2318
2339
  if (options.waitForNavigation) {
2319
2340
  promises.push(this.waitForNavigation());
2320
2341
  }
2321
2342
  promises.push(this._waitForAction());
2343
+
2322
2344
  return Promise.all(promises);
2323
2345
  }
2324
2346
 
@@ -2663,3 +2685,9 @@ function getNormalizedKey(key) {
2663
2685
  }
2664
2686
  return normalizedKey;
2665
2687
  }
2688
+
2689
+ function highlightActiveElement(element, context) {
2690
+ if (!this.options.enableHighlight && !store.debugMode) return;
2691
+
2692
+ highlightElement(element, context);
2693
+ }