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/lib/helper/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
|
|
@@ -903,8 +908,59 @@ class Playwright extends Helper {
|
|
|
903
908
|
}
|
|
904
909
|
|
|
905
910
|
/**
|
|
906
|
-
*
|
|
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
|
-
* >
|
|
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
|
|
918
|
-
const src = new Locator(srcElement
|
|
919
|
-
const dst = new Locator(destElement
|
|
973
|
+
async dragAndDrop(srcElement, destElement, options) {
|
|
974
|
+
const src = new Locator(srcElement);
|
|
975
|
+
const dst = new Locator(destElement);
|
|
920
976
|
|
|
921
|
-
|
|
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} [
|
|
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,
|
|
1299
|
-
return proceedClick.call(this, locator, context,
|
|
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
|
-
*
|
|
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(
|
|
1473
|
-
|
|
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
|
-
|
|
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
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
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
|
|
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
|
|
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 {*}
|
|
2562
|
+
* @param {*} options
|
|
2452
2563
|
*/
|
|
2453
|
-
async waitForNavigation(
|
|
2454
|
-
|
|
2564
|
+
async waitForNavigation(options = {}) {
|
|
2565
|
+
options = {
|
|
2455
2566
|
timeout: this.options.getPageTimeout,
|
|
2456
2567
|
waitUntil: this.options.waitForNavigation,
|
|
2457
|
-
...
|
|
2568
|
+
...options,
|
|
2458
2569
|
};
|
|
2459
|
-
return this.page.waitForNavigation(
|
|
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}${
|
|
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}${
|
|
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
|
+
}
|
package/lib/helper/Protractor.js
CHANGED
|
@@ -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
|
/**
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
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
|
+
}
|