codeceptjs 3.4.1 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/README.md +11 -9
- package/bin/codecept.js +1 -1
- package/docs/ai.md +248 -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 +234 -54
- package/docs/build/Protractor.js +3 -1
- package/docs/build/Puppeteer.js +101 -12
- package/docs/build/REST.js +15 -5
- package/docs/build/TestCafe.js +61 -2
- package/docs/build/WebDriver.js +85 -5
- package/docs/changelog.md +85 -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 +228 -151
- package/docs/helpers/Puppeteer.md +153 -101
- package/docs/helpers/REST.md +6 -5
- package/docs/helpers/TestCafe.md +97 -49
- package/docs/helpers/WebDriver.md +159 -107
- 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/blur.mustache +17 -0
- package/docs/webapi/focus.mustache +12 -0
- package/docs/webapi/type.mustache +3 -0
- package/lib/ai.js +171 -0
- package/lib/cli.js +10 -2
- 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 +23 -1
- package/lib/command/interactive.js +15 -1
- package/lib/command/run-workers.js +2 -1
- package/lib/container.js +13 -3
- package/lib/event.js +2 -0
- 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 +200 -45
- package/lib/helper/Protractor.js +1 -1
- package/lib/helper/Puppeteer.js +67 -12
- package/lib/helper/REST.js +15 -5
- package/lib/helper/TestCafe.js +30 -2
- package/lib/helper/WebDriver.js +51 -5
- package/lib/helper/scripts/blurElement.js +17 -0
- package/lib/helper/scripts/focusElement.js +17 -0
- package/lib/helper/scripts/highlightElement.js +20 -0
- package/lib/html.js +258 -0
- package/lib/interfaces/gherkin.js +8 -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 +177 -0
- package/lib/plugin/screenshotOnFail.js +11 -2
- package/lib/recorder.js +11 -8
- package/lib/secret.js +5 -4
- package/lib/step.js +6 -1
- package/lib/ui.js +4 -3
- package/lib/utils.js +17 -0
- package/lib/workers.js +57 -9
- package/package.json +25 -16
- package/translations/ja-JP.js +9 -9
- package/typings/index.d.ts +43 -9
- package/typings/promiseBasedTypes.d.ts +242 -25
- package/typings/types.d.ts +260 -35
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,19 +45,20 @@ const {
|
|
|
43
45
|
setRestartStrategy, restartsSession, restartsContext, restartsBrowser,
|
|
44
46
|
} = require('./extras/PlaywrightRestartOpts');
|
|
45
47
|
const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine');
|
|
48
|
+
const { highlightElement } = require('./scripts/highlightElement');
|
|
46
49
|
|
|
47
50
|
const pathSeparator = path.sep;
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
53
|
* ## Configuration
|
|
51
54
|
*
|
|
52
|
-
* This helper should be configured in codecept.conf.js
|
|
55
|
+
* This helper should be configured in codecept.conf.(js|ts)
|
|
53
56
|
*
|
|
54
57
|
* @typedef PlaywrightConfig
|
|
55
58
|
* @type {object}
|
|
56
|
-
* @prop {string} url - base url of website to be tested
|
|
57
|
-
* @prop {
|
|
58
|
-
* @prop {boolean} [show=
|
|
59
|
+
* @prop {string} [url] - base url of website to be tested
|
|
60
|
+
* @prop {'chromium' | 'firefox'| 'webkit' | 'electron'} [browser='chromium'] - a browser to test on, either: `chromium`, `firefox`, `webkit`, `electron`. Default: chromium.
|
|
61
|
+
* @prop {boolean} [show=true] - show browser window.
|
|
59
62
|
* @prop {string|boolean} [restart=false] - restart strategy between tests. Possible values:
|
|
60
63
|
* * 'context' or **false** - restarts [browser context](https://playwright.dev/docs/api/class-browsercontext) but keeps running browser. Recommended by Playwright team to keep tests isolated.
|
|
61
64
|
* * 'browser' or **true** - closes browser and opens it again between tests.
|
|
@@ -72,13 +75,13 @@ const pathSeparator = path.sep;
|
|
|
72
75
|
* @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to 'session'.
|
|
73
76
|
* @prop {boolean} [keepCookies=false] - keep cookies between tests when `restart` is set to 'session'.
|
|
74
77
|
* @prop {number} [waitForAction] - how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
|
|
75
|
-
* @prop {
|
|
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
|
|
@@ -255,6 +260,22 @@ const config = {};
|
|
|
255
260
|
* }
|
|
256
261
|
* ```
|
|
257
262
|
*
|
|
263
|
+
* * #### Example #9: Launch electron test
|
|
264
|
+
*
|
|
265
|
+
* ```js
|
|
266
|
+
* {
|
|
267
|
+
* helpers: {
|
|
268
|
+
* Playwright: {
|
|
269
|
+
* browser: 'electron',
|
|
270
|
+
* electron: {
|
|
271
|
+
* executablePath: require("electron"),
|
|
272
|
+
* args: [path.join('../', "main.js")],
|
|
273
|
+
* },
|
|
274
|
+
* }
|
|
275
|
+
* },
|
|
276
|
+
* }
|
|
277
|
+
* ```
|
|
278
|
+
*
|
|
258
279
|
* Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
|
|
259
280
|
*
|
|
260
281
|
* ## Access From Helpers
|
|
@@ -368,15 +389,24 @@ class Playwright extends Helper {
|
|
|
368
389
|
|
|
369
390
|
static _config() {
|
|
370
391
|
return [
|
|
371
|
-
{ name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
|
|
372
|
-
{
|
|
373
|
-
name: 'show', message: 'Show browser window', default: true, type: 'confirm',
|
|
374
|
-
},
|
|
375
392
|
{
|
|
376
393
|
name: 'browser',
|
|
377
394
|
message: 'Browser in which testing will be performed. Possible options: chromium, firefox, webkit or electron',
|
|
378
395
|
default: 'chromium',
|
|
379
396
|
},
|
|
397
|
+
{
|
|
398
|
+
name: 'url',
|
|
399
|
+
message: 'Base url of site to be tested',
|
|
400
|
+
default: 'http://localhost',
|
|
401
|
+
when: (answers) => answers.Playwright_browser !== 'electron',
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: 'show',
|
|
405
|
+
message: 'Show browser window',
|
|
406
|
+
default: true,
|
|
407
|
+
type: 'confirm',
|
|
408
|
+
when: (answers) => answers.Playwright_browser !== 'electron',
|
|
409
|
+
},
|
|
380
410
|
];
|
|
381
411
|
}
|
|
382
412
|
|
|
@@ -903,8 +933,34 @@ class Playwright extends Helper {
|
|
|
903
933
|
}
|
|
904
934
|
|
|
905
935
|
/**
|
|
906
|
-
* {{>
|
|
936
|
+
* {{> focus }}
|
|
907
937
|
*
|
|
938
|
+
*/
|
|
939
|
+
async focus(locator, options = {}) {
|
|
940
|
+
const els = await this._locate(locator);
|
|
941
|
+
assertElementExists(els, locator, 'Element to focus');
|
|
942
|
+
const el = els[0];
|
|
943
|
+
|
|
944
|
+
await el.focus(options);
|
|
945
|
+
return this._waitForAction();
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* {{> blur }}
|
|
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);
|
|
976
|
+
|
|
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);
|
|
920
985
|
|
|
921
|
-
|
|
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,47 @@ 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
|
+
* Clears the text input element: `<input>`, `<textarea>` or `[contenteditable]` .
|
|
1546
|
+
*
|
|
1547
|
+
*
|
|
1548
|
+
* Examples:
|
|
1549
|
+
*
|
|
1550
|
+
* ```js
|
|
1551
|
+
* I.clearField('.text-area')
|
|
1552
|
+
*
|
|
1553
|
+
* // if this doesn't work use force option
|
|
1554
|
+
* I.clearField('#submit', { force: true })
|
|
1555
|
+
* ```
|
|
1556
|
+
* Use `force` to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
|
|
1557
|
+
*
|
|
1558
|
+
* @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
|
|
1559
|
+
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
|
|
1471
1560
|
*/
|
|
1472
|
-
async clearField(
|
|
1473
|
-
|
|
1561
|
+
async clearField(locator, options = {}) {
|
|
1562
|
+
let result;
|
|
1563
|
+
const isNewClearMethodPresent = typeof this.page.locator().clear === 'function';
|
|
1564
|
+
|
|
1565
|
+
if (isNewClearMethodPresent) {
|
|
1566
|
+
const els = await findFields.call(this, locator);
|
|
1567
|
+
assertElementExists(els, locator, 'Field to clear');
|
|
1568
|
+
// TODO: locator change required after #3677 implementation
|
|
1569
|
+
const elXpath = await getXPathForElement(els[0]);
|
|
1570
|
+
|
|
1571
|
+
await this.page.locator(elXpath).clear(options);
|
|
1572
|
+
result = await this._waitForAction();
|
|
1573
|
+
} else {
|
|
1574
|
+
result = await this.fillField(locator, '');
|
|
1575
|
+
}
|
|
1576
|
+
return result;
|
|
1474
1577
|
}
|
|
1475
1578
|
|
|
1476
1579
|
/**
|
|
@@ -1481,8 +1584,9 @@ class Playwright extends Helper {
|
|
|
1481
1584
|
async appendField(field, value) {
|
|
1482
1585
|
const els = await findFields.call(this, field);
|
|
1483
1586
|
assertElementExists(els, field, 'Field');
|
|
1587
|
+
highlightActiveElement.call(this, els[0], this.page);
|
|
1484
1588
|
await els[0].press('End');
|
|
1485
|
-
await els[0].type(value, { delay: this.options.pressKeyDelay });
|
|
1589
|
+
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1486
1590
|
return this._waitForAction();
|
|
1487
1591
|
}
|
|
1488
1592
|
|
|
@@ -1526,6 +1630,7 @@ class Playwright extends Helper {
|
|
|
1526
1630
|
if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
|
|
1527
1631
|
throw new Error('Element is not <select>');
|
|
1528
1632
|
}
|
|
1633
|
+
highlightActiveElement.call(this, el, this.page);
|
|
1529
1634
|
if (!Array.isArray(option)) option = [option];
|
|
1530
1635
|
|
|
1531
1636
|
for (const key in option) {
|
|
@@ -1999,23 +2104,32 @@ class Playwright extends Helper {
|
|
|
1999
2104
|
*/
|
|
2000
2105
|
async saveScreenshot(fileName, fullPage) {
|
|
2001
2106
|
const fullPageOption = fullPage || this.options.fullPageScreenshots;
|
|
2002
|
-
|
|
2107
|
+
let outputFile = screenshotOutputFolder(fileName);
|
|
2003
2108
|
|
|
2004
2109
|
this.debug(`Screenshot is saving to ${outputFile}`);
|
|
2005
2110
|
|
|
2111
|
+
await this.page.screenshot({
|
|
2112
|
+
path: outputFile,
|
|
2113
|
+
fullPage: fullPageOption,
|
|
2114
|
+
type: 'png',
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2006
2117
|
if (this.activeSessionName) {
|
|
2007
|
-
const
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2118
|
+
for (const sessionName in this.sessionPages) {
|
|
2119
|
+
const activeSessionPage = this.sessionPages[sessionName];
|
|
2120
|
+
outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`);
|
|
2121
|
+
|
|
2122
|
+
this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`);
|
|
2123
|
+
|
|
2124
|
+
if (activeSessionPage) {
|
|
2125
|
+
await activeSessionPage.screenshot({
|
|
2126
|
+
path: outputFile,
|
|
2127
|
+
fullPage: fullPageOption,
|
|
2128
|
+
type: 'png',
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2015
2131
|
}
|
|
2016
2132
|
}
|
|
2017
|
-
|
|
2018
|
-
return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
|
|
2019
2133
|
}
|
|
2020
2134
|
|
|
2021
2135
|
/**
|
|
@@ -2083,7 +2197,7 @@ class Playwright extends Helper {
|
|
|
2083
2197
|
test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`);
|
|
2084
2198
|
for (const sessionName in this.sessionPages) {
|
|
2085
2199
|
if (!this.sessionPages[sessionName].context) continue;
|
|
2086
|
-
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context
|
|
2200
|
+
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`);
|
|
2087
2201
|
}
|
|
2088
2202
|
}
|
|
2089
2203
|
}
|
|
@@ -2106,7 +2220,7 @@ class Playwright extends Helper {
|
|
|
2106
2220
|
test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`);
|
|
2107
2221
|
for (const sessionName in this.sessionPages) {
|
|
2108
2222
|
if (!this.sessionPages[sessionName].context) continue;
|
|
2109
|
-
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context
|
|
2223
|
+
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`);
|
|
2110
2224
|
}
|
|
2111
2225
|
}
|
|
2112
2226
|
} else {
|
|
@@ -2448,15 +2562,15 @@ class Playwright extends Helper {
|
|
|
2448
2562
|
*
|
|
2449
2563
|
* See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
|
|
2450
2564
|
*
|
|
2451
|
-
* @param {*}
|
|
2565
|
+
* @param {*} options
|
|
2452
2566
|
*/
|
|
2453
|
-
async waitForNavigation(
|
|
2454
|
-
|
|
2567
|
+
async waitForNavigation(options = {}) {
|
|
2568
|
+
options = {
|
|
2455
2569
|
timeout: this.options.getPageTimeout,
|
|
2456
2570
|
waitUntil: this.options.waitForNavigation,
|
|
2457
|
-
...
|
|
2571
|
+
...options,
|
|
2458
2572
|
};
|
|
2459
|
-
return this.page.waitForNavigation(
|
|
2573
|
+
return this.page.waitForNavigation(options);
|
|
2460
2574
|
}
|
|
2461
2575
|
|
|
2462
2576
|
async waitUntilExists(locator, sec) {
|
|
@@ -2549,11 +2663,41 @@ function buildLocatorString(locator) {
|
|
|
2549
2663
|
if (locator.isCustom()) {
|
|
2550
2664
|
return `${locator.type}=${locator.value}`;
|
|
2551
2665
|
} if (locator.isXPath()) {
|
|
2552
|
-
// dont rely on heuristics of playwright for figuring out xpath
|
|
2553
2666
|
return `xpath=${locator.value}`;
|
|
2554
2667
|
}
|
|
2555
2668
|
return locator.simplify();
|
|
2556
2669
|
}
|
|
2670
|
+
// TODO: locator change required after #3677 implementation. Temporary solution before migration. Should be deleted after #3677 implementation
|
|
2671
|
+
async function getXPathForElement(elementHandle) {
|
|
2672
|
+
function calculateIndex(node) {
|
|
2673
|
+
let index = 1;
|
|
2674
|
+
let sibling = node.previousElementSibling;
|
|
2675
|
+
while (sibling) {
|
|
2676
|
+
if (sibling.tagName === node.tagName) {
|
|
2677
|
+
index++;
|
|
2678
|
+
}
|
|
2679
|
+
sibling = sibling.previousElementSibling;
|
|
2680
|
+
}
|
|
2681
|
+
return index;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
function generateXPath(node) {
|
|
2685
|
+
const segments = [];
|
|
2686
|
+
while (node && node.nodeType === Node.ELEMENT_NODE) {
|
|
2687
|
+
if (node.hasAttribute('id')) {
|
|
2688
|
+
segments.unshift(`*[@id="${node.getAttribute('id')}"]`);
|
|
2689
|
+
break;
|
|
2690
|
+
} else {
|
|
2691
|
+
const index = calculateIndex(node);
|
|
2692
|
+
segments.unshift(`${node.localName}[${index}]`);
|
|
2693
|
+
node = node.parentNode;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
return `//${segments.join('/')}`;
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
return elementHandle.evaluate(generateXPath);
|
|
2700
|
+
}
|
|
2557
2701
|
|
|
2558
2702
|
async function findElements(matcher, locator) {
|
|
2559
2703
|
if (locator.react) return findReact(matcher, locator);
|
|
@@ -2587,6 +2731,10 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2587
2731
|
} else {
|
|
2588
2732
|
assertElementExists(els, locator, 'Clickable element');
|
|
2589
2733
|
}
|
|
2734
|
+
|
|
2735
|
+
const element = els[0];
|
|
2736
|
+
highlightActiveElement.call(this, els[0], this.page);
|
|
2737
|
+
|
|
2590
2738
|
/*
|
|
2591
2739
|
using the force true options itself but instead dispatching a click
|
|
2592
2740
|
*/
|
|
@@ -2601,6 +2749,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2601
2749
|
promises.push(this.waitForNavigation());
|
|
2602
2750
|
}
|
|
2603
2751
|
promises.push(this._waitForAction());
|
|
2752
|
+
|
|
2604
2753
|
return Promise.all(promises);
|
|
2605
2754
|
}
|
|
2606
2755
|
|
|
@@ -2982,7 +3131,7 @@ async function refreshContextSession() {
|
|
|
2982
3131
|
|
|
2983
3132
|
async function saveVideoForPage(page, name) {
|
|
2984
3133
|
if (!page.video()) return null;
|
|
2985
|
-
const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${
|
|
3134
|
+
const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`;
|
|
2986
3135
|
page.video().saveAs(fileName).then(() => {
|
|
2987
3136
|
if (!page) return;
|
|
2988
3137
|
page.video().delete().catch(e => {});
|
|
@@ -2993,7 +3142,13 @@ async function saveVideoForPage(page, name) {
|
|
|
2993
3142
|
async function saveTraceForContext(context, name) {
|
|
2994
3143
|
if (!context) return;
|
|
2995
3144
|
if (!context.tracing) return;
|
|
2996
|
-
const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${
|
|
3145
|
+
const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`;
|
|
2997
3146
|
await context.tracing.stop({ path: fileName });
|
|
2998
3147
|
return fileName;
|
|
2999
3148
|
}
|
|
3149
|
+
|
|
3150
|
+
function highlightActiveElement(element, context) {
|
|
3151
|
+
if (!this.options.enableHighlight && !store.debugMode) return;
|
|
3152
|
+
|
|
3153
|
+
highlightElement(element, context);
|
|
3154
|
+
}
|
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,9 @@ 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');
|
|
38
|
+
const { blurElement } = require('./scripts/blurElement');
|
|
39
|
+
const { focusElement } = require('./scripts/focusElement');
|
|
36
40
|
|
|
37
41
|
let puppeteer;
|
|
38
42
|
let perfTiming;
|
|
@@ -65,6 +69,7 @@ const consoleLogStore = new Console();
|
|
|
65
69
|
* @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["Puppeteer"]._startBrowser()`.
|
|
66
70
|
* @prop {string} [browser=chrome] - can be changed to `firefox` when using [puppeteer-firefox](https://codecept.io/helpers/Puppeteer-firefox).
|
|
67
71
|
* @prop {object} [chrome] - pass additional [Puppeteer run options](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions).
|
|
72
|
+
* @prop {boolean} [highlightElement] - highlight the interacting elements
|
|
68
73
|
*/
|
|
69
74
|
const config = {};
|
|
70
75
|
|
|
@@ -704,6 +709,31 @@ class Puppeteer extends Helper {
|
|
|
704
709
|
return this._waitForAction();
|
|
705
710
|
}
|
|
706
711
|
|
|
712
|
+
/**
|
|
713
|
+
* {{> focus }}
|
|
714
|
+
*
|
|
715
|
+
*/
|
|
716
|
+
async focus(locator) {
|
|
717
|
+
const els = await this._locate(locator);
|
|
718
|
+
assertElementExists(els, locator, 'Element to focus');
|
|
719
|
+
const el = els[0];
|
|
720
|
+
|
|
721
|
+
await focusElement(el, this.page);
|
|
722
|
+
return this._waitForAction();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* {{> blur }}
|
|
727
|
+
*
|
|
728
|
+
*/
|
|
729
|
+
async blur(locator) {
|
|
730
|
+
const els = await this._locate(locator);
|
|
731
|
+
assertElementExists(els, locator, 'Element to blur');
|
|
732
|
+
|
|
733
|
+
await blurElement(els[0], this.page);
|
|
734
|
+
return this._waitForAction();
|
|
735
|
+
}
|
|
736
|
+
|
|
707
737
|
/**
|
|
708
738
|
* {{> dragAndDrop }}
|
|
709
739
|
*/
|
|
@@ -1262,6 +1292,7 @@ class Puppeteer extends Helper {
|
|
|
1262
1292
|
*/
|
|
1263
1293
|
async type(keys, delay = null) {
|
|
1264
1294
|
if (!Array.isArray(keys)) {
|
|
1295
|
+
keys = keys.toString();
|
|
1265
1296
|
keys = keys.split('');
|
|
1266
1297
|
}
|
|
1267
1298
|
|
|
@@ -1286,7 +1317,10 @@ class Puppeteer extends Helper {
|
|
|
1286
1317
|
} else if (editable) {
|
|
1287
1318
|
await this._evaluateHandeInContext(el => el.innerHTML = '', el);
|
|
1288
1319
|
}
|
|
1320
|
+
|
|
1321
|
+
highlightActiveElement.call(this, el, this.page);
|
|
1289
1322
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1323
|
+
|
|
1290
1324
|
return this._waitForAction();
|
|
1291
1325
|
}
|
|
1292
1326
|
|
|
@@ -1305,8 +1339,9 @@ class Puppeteer extends Helper {
|
|
|
1305
1339
|
async appendField(field, value) {
|
|
1306
1340
|
const els = await findVisibleFields.call(this, field);
|
|
1307
1341
|
assertElementExists(els, field, 'Field');
|
|
1342
|
+
highlightActiveElement.call(this, els[0], this.page);
|
|
1308
1343
|
await els[0].press('End');
|
|
1309
|
-
await els[0].type(value, { delay: this.options.pressKeyDelay });
|
|
1344
|
+
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1310
1345
|
return this._waitForAction();
|
|
1311
1346
|
}
|
|
1312
1347
|
|
|
@@ -1351,6 +1386,7 @@ class Puppeteer extends Helper {
|
|
|
1351
1386
|
if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
|
|
1352
1387
|
throw new Error('Element is not <select>');
|
|
1353
1388
|
}
|
|
1389
|
+
highlightActiveElement.call(this, els[0], this.page);
|
|
1354
1390
|
if (!Array.isArray(option)) option = [option];
|
|
1355
1391
|
|
|
1356
1392
|
for (const key in option) {
|
|
@@ -1828,23 +1864,32 @@ class Puppeteer extends Helper {
|
|
|
1828
1864
|
*/
|
|
1829
1865
|
async saveScreenshot(fileName, fullPage) {
|
|
1830
1866
|
const fullPageOption = fullPage || this.options.fullPageScreenshots;
|
|
1831
|
-
|
|
1867
|
+
let outputFile = screenshotOutputFolder(fileName);
|
|
1832
1868
|
|
|
1833
1869
|
this.debug(`Screenshot is saving to ${outputFile}`);
|
|
1834
1870
|
|
|
1871
|
+
await this.page.screenshot({
|
|
1872
|
+
path: outputFile,
|
|
1873
|
+
fullPage: fullPageOption,
|
|
1874
|
+
type: 'png',
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1835
1877
|
if (this.activeSessionName) {
|
|
1836
|
-
const
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1878
|
+
for (const sessionName in this.sessionPages) {
|
|
1879
|
+
const activeSessionPage = this.sessionPages[sessionName];
|
|
1880
|
+
outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`);
|
|
1881
|
+
|
|
1882
|
+
this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`);
|
|
1883
|
+
|
|
1884
|
+
if (activeSessionPage) {
|
|
1885
|
+
await activeSessionPage.screenshot({
|
|
1886
|
+
path: outputFile,
|
|
1887
|
+
fullPage: fullPageOption,
|
|
1888
|
+
type: 'png',
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1844
1891
|
}
|
|
1845
1892
|
}
|
|
1846
|
-
|
|
1847
|
-
return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
|
|
1848
1893
|
}
|
|
1849
1894
|
|
|
1850
1895
|
async _failed() {
|
|
@@ -2313,12 +2358,16 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2313
2358
|
} else {
|
|
2314
2359
|
assertElementExists(els, locator, 'Clickable element');
|
|
2315
2360
|
}
|
|
2361
|
+
|
|
2362
|
+
highlightActiveElement.call(this, els[0], this.page);
|
|
2363
|
+
|
|
2316
2364
|
await els[0].click(options);
|
|
2317
2365
|
const promises = [];
|
|
2318
2366
|
if (options.waitForNavigation) {
|
|
2319
2367
|
promises.push(this.waitForNavigation());
|
|
2320
2368
|
}
|
|
2321
2369
|
promises.push(this._waitForAction());
|
|
2370
|
+
|
|
2322
2371
|
return Promise.all(promises);
|
|
2323
2372
|
}
|
|
2324
2373
|
|
|
@@ -2663,3 +2712,9 @@ function getNormalizedKey(key) {
|
|
|
2663
2712
|
}
|
|
2664
2713
|
return normalizedKey;
|
|
2665
2714
|
}
|
|
2715
|
+
|
|
2716
|
+
function highlightActiveElement(element, context) {
|
|
2717
|
+
if (!this.options.enableHighlight && !store.debugMode) return;
|
|
2718
|
+
|
|
2719
|
+
highlightElement(element, context);
|
|
2720
|
+
}
|
package/lib/helper/REST.js
CHANGED
|
@@ -34,7 +34,8 @@ const config = {};
|
|
|
34
34
|
* endpoint: 'http://site.com/api',
|
|
35
35
|
* prettyPrintJson: true,
|
|
36
36
|
* onRequest: (request) => {
|
|
37
|
-
*
|
|
37
|
+
* request.headers.auth = '123';
|
|
38
|
+
* }
|
|
38
39
|
* }
|
|
39
40
|
* }
|
|
40
41
|
*}
|
|
@@ -136,6 +137,15 @@ class REST extends Helper {
|
|
|
136
137
|
request.auth = this.headers.auth;
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
if (typeof request.data === 'object') {
|
|
141
|
+
const returnedValue = {};
|
|
142
|
+
for (const [key, value] of Object.entries(request.data)) {
|
|
143
|
+
returnedValue[key] = value;
|
|
144
|
+
if (value instanceof Secret) returnedValue[key] = value.getMasked();
|
|
145
|
+
}
|
|
146
|
+
_debugRequest.data = returnedValue;
|
|
147
|
+
}
|
|
148
|
+
|
|
139
149
|
if (request.data instanceof Secret) {
|
|
140
150
|
_debugRequest.data = '*****';
|
|
141
151
|
request.data = (typeof request.data === 'object' && !(request.data instanceof Secret)) ? { ...request.data.toString() } : request.data.toString();
|
|
@@ -198,7 +208,7 @@ class REST extends Helper {
|
|
|
198
208
|
* ```
|
|
199
209
|
*
|
|
200
210
|
* @param {*} url
|
|
201
|
-
* @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
|
|
211
|
+
* @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
|
|
202
212
|
*
|
|
203
213
|
* @returns {Promise<*>} response
|
|
204
214
|
*/
|
|
@@ -222,8 +232,8 @@ class REST extends Helper {
|
|
|
222
232
|
* ```
|
|
223
233
|
*
|
|
224
234
|
* @param {*} url
|
|
225
|
-
* @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
|
|
226
|
-
* @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
|
|
235
|
+
* @param {*} [payload={}] - the payload to be sent. By default, it is sent as an empty object
|
|
236
|
+
* @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
|
|
227
237
|
*
|
|
228
238
|
* @returns {Promise<*>} response
|
|
229
239
|
*/
|
|
@@ -317,7 +327,7 @@ class REST extends Helper {
|
|
|
317
327
|
* ```
|
|
318
328
|
*
|
|
319
329
|
* @param {*} url
|
|
320
|
-
* @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
|
|
330
|
+
* @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
|
|
321
331
|
*
|
|
322
332
|
* @returns {Promise<*>} response
|
|
323
333
|
*/
|