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.
- package/CHANGELOG.md +65 -0
- package/README.md +9 -7
- package/bin/codecept.js +1 -1
- package/docs/ai.md +246 -0
- package/docs/build/Appium.js +47 -7
- package/docs/build/JSONResponse.js +4 -4
- package/docs/build/Nightmare.js +3 -1
- package/docs/build/OpenAI.js +122 -0
- package/docs/build/Playwright.js +193 -45
- package/docs/build/Protractor.js +3 -1
- package/docs/build/Puppeteer.js +45 -12
- package/docs/build/REST.js +15 -5
- package/docs/build/TestCafe.js +3 -1
- package/docs/build/WebDriver.js +30 -5
- package/docs/changelog.md +65 -0
- package/docs/helpers/Appium.md +152 -147
- package/docs/helpers/JSONResponse.md +4 -4
- package/docs/helpers/Nightmare.md +2 -0
- package/docs/helpers/OpenAI.md +70 -0
- package/docs/helpers/Playwright.md +194 -152
- package/docs/helpers/Puppeteer.md +6 -0
- package/docs/helpers/REST.md +6 -5
- package/docs/helpers/TestCafe.md +2 -0
- package/docs/helpers/WebDriver.md +10 -4
- package/docs/mobile.md +49 -2
- package/docs/parallel.md +56 -0
- package/docs/plugins.md +87 -33
- package/docs/secrets.md +6 -0
- package/docs/tutorial.md +2 -2
- package/docs/webapi/appendField.mustache +2 -0
- package/docs/webapi/type.mustache +3 -0
- package/lib/ai.js +171 -0
- package/lib/cli.js +1 -1
- package/lib/codecept.js +4 -0
- package/lib/command/dryRun.js +9 -1
- package/lib/command/generate.js +46 -3
- package/lib/command/init.js +13 -1
- package/lib/command/interactive.js +15 -1
- package/lib/command/run-workers.js +2 -1
- package/lib/container.js +13 -3
- package/lib/helper/Appium.js +45 -7
- package/lib/helper/JSONResponse.js +4 -4
- package/lib/helper/Nightmare.js +1 -1
- package/lib/helper/OpenAI.js +122 -0
- package/lib/helper/Playwright.js +190 -38
- package/lib/helper/Protractor.js +1 -1
- package/lib/helper/Puppeteer.js +40 -12
- package/lib/helper/REST.js +15 -5
- package/lib/helper/TestCafe.js +1 -1
- package/lib/helper/WebDriver.js +25 -5
- package/lib/helper/scripts/highlightElement.js +20 -0
- package/lib/html.js +258 -0
- package/lib/listener/retry.js +2 -1
- package/lib/pause.js +73 -17
- package/lib/plugin/debugErrors.js +67 -0
- package/lib/plugin/fakerTransform.js +4 -6
- package/lib/plugin/heal.js +179 -0
- package/lib/plugin/screenshotOnFail.js +11 -2
- package/lib/recorder.js +4 -4
- package/lib/secret.js +5 -4
- package/lib/step.js +6 -1
- package/lib/ui.js +4 -3
- package/lib/utils.js +4 -0
- package/lib/workers.js +57 -9
- package/package.json +25 -13
- package/translations/ja-JP.js +9 -9
- package/typings/index.d.ts +43 -9
- package/typings/promiseBasedTypes.d.ts +124 -24
- 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
|
+
}
|
package/docs/build/Playwright.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
* >
|
|
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
|
|
963
|
-
const src = new Locator(srcElement
|
|
964
|
-
const dst = new Locator(destElement
|
|
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
|
-
|
|
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} [
|
|
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,
|
|
1478
|
-
return proceedClick.call(this, locator, context,
|
|
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
|
-
*
|
|
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('
|
|
1872
|
-
*
|
|
1873
|
-
*
|
|
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(
|
|
1880
|
-
|
|
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
|
-
|
|
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
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
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
|
|
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
|
|
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 {*}
|
|
3496
|
+
* @param {*} options
|
|
3390
3497
|
*/
|
|
3391
|
-
async waitForNavigation(
|
|
3392
|
-
|
|
3498
|
+
async waitForNavigation(options = {}) {
|
|
3499
|
+
options = {
|
|
3393
3500
|
timeout: this.options.getPageTimeout,
|
|
3394
3501
|
waitUntil: this.options.waitForNavigation,
|
|
3395
|
-
...
|
|
3502
|
+
...options,
|
|
3396
3503
|
};
|
|
3397
|
-
return this.page.waitForNavigation(
|
|
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}${
|
|
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}${
|
|
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
|
+
}
|
package/docs/build/Protractor.js
CHANGED
|
@@ -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
|
/**
|