codeceptjs 3.5.3 → 3.5.4
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/README.md +0 -2
- package/docs/build/Appium.js +8 -6
- package/docs/build/GraphQL.js +25 -0
- package/docs/build/Nightmare.js +11 -6
- package/docs/build/Playwright.js +425 -193
- package/docs/build/Protractor.js +13 -8
- package/docs/build/Puppeteer.js +20 -14
- package/docs/build/TestCafe.js +17 -10
- package/docs/build/WebDriver.js +41 -37
- package/docs/changelog.md +170 -1
- package/docs/community-helpers.md +8 -4
- package/docs/examples.md +8 -2
- package/docs/helpers/Appium.md +2 -2
- package/docs/helpers/GraphQL.md +21 -0
- package/docs/helpers/Nightmare.md +2 -2
- package/docs/helpers/Playwright.md +239 -122
- package/docs/helpers/Protractor.md +2 -2
- package/docs/helpers/Puppeteer.md +3 -3
- package/docs/helpers/TestCafe.md +2 -2
- package/docs/helpers/WebDriver.md +3 -3
- package/docs/playwright.md +24 -1
- package/docs/webapi/dontSeeInField.mustache +1 -1
- package/docs/webapi/seeInField.mustache +1 -1
- package/docs/wiki/Books-&-Posts.md +0 -0
- package/docs/wiki/Community-Helpers-&-Plugins.md +8 -4
- package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +46 -14
- package/docs/wiki/Examples.md +8 -2
- package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +0 -0
- package/docs/wiki/Home.md +0 -0
- package/docs/wiki/Migration-to-Appium-v2---CodeceptJS.md +83 -0
- package/docs/wiki/Release-Process.md +0 -0
- package/docs/wiki/Roadmap.md +0 -0
- package/docs/wiki/Tests.md +0 -0
- package/docs/wiki/Upgrading-to-CodeceptJS-3.md +0 -0
- package/docs/wiki/Videos.md +0 -0
- package/lib/command/definitions.js +2 -7
- package/lib/command/run-multiple/collection.js +17 -5
- package/lib/helper/Appium.js +6 -4
- package/lib/helper/GraphQL.js +25 -0
- package/lib/helper/Nightmare.js +9 -4
- package/lib/helper/Playwright.js +422 -190
- package/lib/helper/Protractor.js +11 -6
- package/lib/helper/Puppeteer.js +18 -12
- package/lib/helper/TestCafe.js +15 -8
- package/lib/helper/WebDriver.js +39 -35
- package/lib/helper/errors/ElementNotFound.js +2 -1
- package/lib/helper/extras/PlaywrightReact.js +9 -0
- package/lib/helper/scripts/highlightElement.js +1 -1
- package/lib/interfaces/bdd.js +1 -1
- package/lib/mochaFactory.js +2 -1
- package/lib/pause.js +5 -4
- package/lib/plugin/heal.js +2 -3
- package/lib/plugin/selenoid.js +6 -1
- package/lib/step.js +27 -10
- package/lib/utils.js +4 -0
- package/lib/workers.js +3 -1
- package/package.json +14 -14
- package/typings/promiseBasedTypes.d.ts +145 -126
- package/typings/types.d.ts +152 -133
- package/CHANGELOG.md +0 -2563
- package/docs/build/Polly.js +0 -42
- package/docs/build/SeleniumWebdriver.js +0 -76
package/docs/build/Playwright.js
CHANGED
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
isModifierKey,
|
|
24
24
|
clearString,
|
|
25
25
|
requireWithFallback,
|
|
26
|
+
normalizeSpacesInString,
|
|
26
27
|
} = require('../utils');
|
|
27
28
|
const {
|
|
28
29
|
isColorProperty,
|
|
@@ -76,7 +77,7 @@ const pathSeparator = path.sep;
|
|
|
76
77
|
* @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to 'session'.
|
|
77
78
|
* @prop {boolean} [keepCookies=false] - keep cookies between tests when `restart` is set to 'session'.
|
|
78
79
|
* @prop {number} [waitForAction] - how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
|
|
79
|
-
* @prop {'load' | 'domcontentloaded' | '
|
|
80
|
+
* @prop {'load' | 'domcontentloaded' | 'commit'} [waitForNavigation] - When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `commit`. Choose one of those options is possible. See [Playwright API](https://playwright.dev/docs/api/class-page#page-wait-for-url).
|
|
80
81
|
* @prop {number} [pressKeyDelay=10] - Delay between key presses in ms. Used when calling Playwrights page.type(...) in fillField/appendField
|
|
81
82
|
* @prop {number} [getPageTimeout] - config option to set maximum navigation time in milliseconds.
|
|
82
83
|
* @prop {number} [waitForTimeout] - default wait* timeout in ms. Default: 1000.
|
|
@@ -93,7 +94,7 @@ const pathSeparator = path.sep;
|
|
|
93
94
|
* @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).
|
|
94
95
|
* @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
|
|
95
96
|
* @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
|
|
96
|
-
* @prop {boolean} [highlightElement] - highlight the interacting elements
|
|
97
|
+
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false
|
|
97
98
|
*/
|
|
98
99
|
const config = {};
|
|
99
100
|
|
|
@@ -207,6 +208,7 @@ const config = {};
|
|
|
207
208
|
* url: "http://localhost",
|
|
208
209
|
* show: true // headless mode not supported for extensions
|
|
209
210
|
* chromium: {
|
|
211
|
+
* // Note: due to this would launch persistent context, so to avoid the error when running tests with run-workers a timestamp would be appended to the defined folder name. For instance: playwright-tmp_1692715649511
|
|
210
212
|
* userDataDir: '/tmp/playwright-tmp', // necessary to launch the browser in normal mode instead of incognito,
|
|
211
213
|
* args: [
|
|
212
214
|
* `--disable-extensions-except=${pathToExtension}`,
|
|
@@ -317,6 +319,12 @@ class Playwright extends Helper {
|
|
|
317
319
|
this.recording = false;
|
|
318
320
|
this.recordedAtLeastOnce = false;
|
|
319
321
|
|
|
322
|
+
// for websocket messages
|
|
323
|
+
this.webSocketMessages = [];
|
|
324
|
+
this.recordingWebSocketMessages = false;
|
|
325
|
+
this.recordedWebSocketMessagesAtLeastOnce = false;
|
|
326
|
+
this.cdpSession = null;
|
|
327
|
+
|
|
320
328
|
// override defaults with config
|
|
321
329
|
this._setConfig(config);
|
|
322
330
|
}
|
|
@@ -343,7 +351,8 @@ class Playwright extends Helper {
|
|
|
343
351
|
show: false,
|
|
344
352
|
defaultPopupAction: 'accept',
|
|
345
353
|
use: { actionTimeout: 0 },
|
|
346
|
-
ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors
|
|
354
|
+
ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors,
|
|
355
|
+
highlightElement: false,
|
|
347
356
|
};
|
|
348
357
|
|
|
349
358
|
config = Object.assign(defaults, config);
|
|
@@ -388,7 +397,7 @@ class Playwright extends Helper {
|
|
|
388
397
|
}
|
|
389
398
|
this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint;
|
|
390
399
|
this.isElectron = this.options.browser === 'electron';
|
|
391
|
-
this.userDataDir = this.playwrightOptions.userDataDir;
|
|
400
|
+
this.userDataDir = this.playwrightOptions.userDataDir ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` : undefined;
|
|
392
401
|
this.isCDPConnection = this.playwrightOptions.cdpConnection;
|
|
393
402
|
popupStore.defaultAction = this.options.defaultPopupAction;
|
|
394
403
|
}
|
|
@@ -461,7 +470,7 @@ class Playwright extends Helper {
|
|
|
461
470
|
this.isAuthenticated = false;
|
|
462
471
|
if (this.isElectron) {
|
|
463
472
|
this.browserContext = this.browser.context();
|
|
464
|
-
} else if (this.userDataDir) {
|
|
473
|
+
} else if (this.playwrightOptions.userDataDir) {
|
|
465
474
|
this.browserContext = this.browser;
|
|
466
475
|
} else {
|
|
467
476
|
const contextOptions = {
|
|
@@ -487,8 +496,17 @@ class Playwright extends Helper {
|
|
|
487
496
|
if (this.isElectron) {
|
|
488
497
|
mainPage = await this.browser.firstWindow();
|
|
489
498
|
} else {
|
|
490
|
-
|
|
491
|
-
|
|
499
|
+
try {
|
|
500
|
+
const existingPages = await this.browserContext.pages();
|
|
501
|
+
mainPage = existingPages[0] || await this.browserContext.newPage();
|
|
502
|
+
} catch (e) {
|
|
503
|
+
if (this.playwrightOptions.userDataDir) {
|
|
504
|
+
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
|
|
505
|
+
this.browserContext = this.browser;
|
|
506
|
+
const existingPages = await this.browserContext.pages();
|
|
507
|
+
mainPage = existingPages[0];
|
|
508
|
+
}
|
|
509
|
+
}
|
|
492
510
|
}
|
|
493
511
|
await targetCreatedHandler.call(this, mainPage);
|
|
494
512
|
|
|
@@ -519,13 +537,15 @@ class Playwright extends Helper {
|
|
|
519
537
|
|
|
520
538
|
// close other sessions
|
|
521
539
|
try {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
this.
|
|
526
|
-
|
|
540
|
+
if ((await this.browser)._type === 'Browser') {
|
|
541
|
+
const contexts = await this.browser.contexts();
|
|
542
|
+
const currentContext = contexts[0];
|
|
543
|
+
if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
|
|
544
|
+
this.storageState = await currentContext.storageState();
|
|
545
|
+
}
|
|
527
546
|
|
|
528
|
-
|
|
547
|
+
await Promise.all(contexts.map(c => c.close()));
|
|
548
|
+
}
|
|
529
549
|
} catch (e) {
|
|
530
550
|
console.log(e);
|
|
531
551
|
}
|
|
@@ -555,8 +575,16 @@ class Playwright extends Helper {
|
|
|
555
575
|
browserContext = browser.context();
|
|
556
576
|
page = await browser.firstWindow();
|
|
557
577
|
} else {
|
|
558
|
-
|
|
559
|
-
|
|
578
|
+
try {
|
|
579
|
+
browserContext = await this.browser.newContext(Object.assign(this.options, config));
|
|
580
|
+
page = await browserContext.newPage();
|
|
581
|
+
} catch (e) {
|
|
582
|
+
if (this.playwrightOptions.userDataDir) {
|
|
583
|
+
browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions);
|
|
584
|
+
this.browser = browserContext;
|
|
585
|
+
page = await browserContext.pages()[0];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
560
588
|
}
|
|
561
589
|
|
|
562
590
|
if (this.options.trace) await browserContext.tracing.start({ screenshots: true, snapshots: true });
|
|
@@ -569,10 +597,12 @@ class Playwright extends Helper {
|
|
|
569
597
|
// is closed by _after
|
|
570
598
|
},
|
|
571
599
|
loadVars: async (context) => {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
600
|
+
if (context) {
|
|
601
|
+
this.browserContext = context;
|
|
602
|
+
const existingPages = await context.pages();
|
|
603
|
+
this.sessionPages[this.activeSessionName] = existingPages[0];
|
|
604
|
+
return this._setPage(this.sessionPages[this.activeSessionName]);
|
|
605
|
+
}
|
|
576
606
|
},
|
|
577
607
|
restoreVars: async (session) => {
|
|
578
608
|
this.withinLocator = null;
|
|
@@ -771,7 +801,7 @@ class Playwright extends Helper {
|
|
|
771
801
|
}
|
|
772
802
|
throw err;
|
|
773
803
|
}
|
|
774
|
-
} else if (this.userDataDir) {
|
|
804
|
+
} else if (this.playwrightOptions.userDataDir) {
|
|
775
805
|
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
|
|
776
806
|
} else {
|
|
777
807
|
this.browser = await playwright[this.options.browser].launch(this.playwrightOptions);
|
|
@@ -832,9 +862,9 @@ class Playwright extends Helper {
|
|
|
832
862
|
return;
|
|
833
863
|
}
|
|
834
864
|
|
|
835
|
-
const
|
|
836
|
-
assertElementExists(
|
|
837
|
-
this.context =
|
|
865
|
+
const el = await this._locateElement(locator);
|
|
866
|
+
assertElementExists(el, locator);
|
|
867
|
+
this.context = el;
|
|
838
868
|
this.contextLocator = locator;
|
|
839
869
|
|
|
840
870
|
this.withinLocator = new Locator(locator);
|
|
@@ -965,11 +995,11 @@ class Playwright extends Helper {
|
|
|
965
995
|
*
|
|
966
996
|
*/
|
|
967
997
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
968
|
-
const
|
|
969
|
-
assertElementExists(
|
|
998
|
+
const el = await this._locateElement(locator);
|
|
999
|
+
assertElementExists(el, locator);
|
|
970
1000
|
|
|
971
1001
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
972
|
-
const { x, y } = await clickablePoint(
|
|
1002
|
+
const { x, y } = await clickablePoint(el);
|
|
973
1003
|
await this.page.mouse.move(x + offsetX, y + offsetY);
|
|
974
1004
|
return this._waitForAction();
|
|
975
1005
|
}
|
|
@@ -991,9 +1021,8 @@ class Playwright extends Helper {
|
|
|
991
1021
|
*
|
|
992
1022
|
*/
|
|
993
1023
|
async focus(locator, options = {}) {
|
|
994
|
-
const
|
|
995
|
-
assertElementExists(
|
|
996
|
-
const el = els[0];
|
|
1024
|
+
const el = await this._locateElement(locator);
|
|
1025
|
+
assertElementExists(el, locator, 'Element to focus');
|
|
997
1026
|
|
|
998
1027
|
await el.focus(options);
|
|
999
1028
|
return this._waitForAction();
|
|
@@ -1021,12 +1050,10 @@ class Playwright extends Helper {
|
|
|
1021
1050
|
*
|
|
1022
1051
|
*/
|
|
1023
1052
|
async blur(locator, options = {}) {
|
|
1024
|
-
const
|
|
1025
|
-
assertElementExists(
|
|
1026
|
-
// TODO: locator change required after #3677 implementation
|
|
1027
|
-
const elXpath = await getXPathForElement(els[0]);
|
|
1053
|
+
const el = await this._locateElement(locator);
|
|
1054
|
+
assertElementExists(el, locator, 'Element to blur');
|
|
1028
1055
|
|
|
1029
|
-
await
|
|
1056
|
+
await el.blur(options);
|
|
1030
1057
|
return this._waitForAction();
|
|
1031
1058
|
}
|
|
1032
1059
|
|
|
@@ -1133,8 +1160,11 @@ class Playwright extends Helper {
|
|
|
1133
1160
|
const body = document.body;
|
|
1134
1161
|
const html = document.documentElement;
|
|
1135
1162
|
window.scrollTo(0, Math.max(
|
|
1136
|
-
body.scrollHeight,
|
|
1137
|
-
|
|
1163
|
+
body.scrollHeight,
|
|
1164
|
+
body.offsetHeight,
|
|
1165
|
+
html.clientHeight,
|
|
1166
|
+
html.scrollHeight,
|
|
1167
|
+
html.offsetHeight,
|
|
1138
1168
|
));
|
|
1139
1169
|
});
|
|
1140
1170
|
}
|
|
@@ -1162,10 +1192,10 @@ class Playwright extends Helper {
|
|
|
1162
1192
|
}
|
|
1163
1193
|
|
|
1164
1194
|
if (locator) {
|
|
1165
|
-
const
|
|
1166
|
-
assertElementExists(
|
|
1167
|
-
await
|
|
1168
|
-
const elementCoordinates = await clickablePoint(
|
|
1195
|
+
const el = await this._locateElement(locator);
|
|
1196
|
+
assertElementExists(el, locator, 'Element');
|
|
1197
|
+
await el.scrollIntoViewIfNeeded();
|
|
1198
|
+
const elementCoordinates = await clickablePoint(el);
|
|
1169
1199
|
await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { offsetX: elementCoordinates.x + offsetX, offsetY: elementCoordinates.y + offsetY });
|
|
1170
1200
|
} else {
|
|
1171
1201
|
await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY });
|
|
@@ -1272,7 +1302,20 @@ class Playwright extends Helper {
|
|
|
1272
1302
|
}
|
|
1273
1303
|
|
|
1274
1304
|
/**
|
|
1275
|
-
*
|
|
1305
|
+
* Get the first element by different locator types, including strict locator
|
|
1306
|
+
* Should be used in custom helpers:
|
|
1307
|
+
*
|
|
1308
|
+
* ```js
|
|
1309
|
+
* const element = await this.helpers['Playwright']._locateElement({name: 'password'});
|
|
1310
|
+
* ```
|
|
1311
|
+
*/
|
|
1312
|
+
async _locateElement(locator) {
|
|
1313
|
+
const context = await this.context || await this._getContext();
|
|
1314
|
+
return findElement(context, locator);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Find a checkbox by providing human-readable text:
|
|
1276
1319
|
* NOTE: Assumes the checkable element exists
|
|
1277
1320
|
*
|
|
1278
1321
|
* ```js
|
|
@@ -1287,7 +1330,7 @@ class Playwright extends Helper {
|
|
|
1287
1330
|
}
|
|
1288
1331
|
|
|
1289
1332
|
/**
|
|
1290
|
-
* Find a clickable element by providing human
|
|
1333
|
+
* Find a clickable element by providing human-readable text:
|
|
1291
1334
|
*
|
|
1292
1335
|
* ```js
|
|
1293
1336
|
* this.helpers['Playwright']._locateClickable('Next page').then // ...
|
|
@@ -1299,7 +1342,7 @@ class Playwright extends Helper {
|
|
|
1299
1342
|
}
|
|
1300
1343
|
|
|
1301
1344
|
/**
|
|
1302
|
-
* Find field elements by providing human
|
|
1345
|
+
* Find field elements by providing human-readable text:
|
|
1303
1346
|
*
|
|
1304
1347
|
* ```js
|
|
1305
1348
|
* this.helpers['Playwright']._locateFields('Your email').then // ...
|
|
@@ -1962,15 +2005,10 @@ class Playwright extends Helper {
|
|
|
1962
2005
|
const els = await findFields.call(this, field);
|
|
1963
2006
|
assertElementExists(els, field, 'Field');
|
|
1964
2007
|
const el = els[0];
|
|
1965
|
-
const tag = await el.getProperty('tagName').then(el => el.jsonValue());
|
|
1966
|
-
const editable = await el.getProperty('contenteditable').then(el => el.jsonValue());
|
|
1967
|
-
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
1968
|
-
await this._evaluateHandeInContext(el => el.value = '', el);
|
|
1969
|
-
} else if (editable) {
|
|
1970
|
-
await this._evaluateHandeInContext(el => el.innerHTML = '', el);
|
|
1971
|
-
}
|
|
1972
2008
|
|
|
1973
|
-
|
|
2009
|
+
await el.clear();
|
|
2010
|
+
|
|
2011
|
+
highlightActiveElement.call(this, el, await this._getContext());
|
|
1974
2012
|
|
|
1975
2013
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1976
2014
|
|
|
@@ -1995,21 +2033,16 @@ class Playwright extends Helper {
|
|
|
1995
2033
|
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
|
|
1996
2034
|
*/
|
|
1997
2035
|
async clearField(locator, options = {}) {
|
|
1998
|
-
|
|
1999
|
-
|
|
2036
|
+
const els = await findFields.call(this, locator);
|
|
2037
|
+
assertElementExists(els, locator, 'Field to clear');
|
|
2000
2038
|
|
|
2001
|
-
|
|
2002
|
-
const els = await findFields.call(this, locator);
|
|
2003
|
-
assertElementExists(els, locator, 'Field to clear');
|
|
2004
|
-
// TODO: locator change required after #3677 implementation
|
|
2005
|
-
const elXpath = await getXPathForElement(els[0]);
|
|
2039
|
+
const el = els[0];
|
|
2006
2040
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
return result;
|
|
2041
|
+
highlightActiveElement.call(this, el, this.page);
|
|
2042
|
+
|
|
2043
|
+
await el.clear();
|
|
2044
|
+
|
|
2045
|
+
return this._waitForAction();
|
|
2013
2046
|
}
|
|
2014
2047
|
|
|
2015
2048
|
/**
|
|
@@ -2031,7 +2064,7 @@ class Playwright extends Helper {
|
|
|
2031
2064
|
async appendField(field, value) {
|
|
2032
2065
|
const els = await findFields.call(this, field);
|
|
2033
2066
|
assertElementExists(els, field, 'Field');
|
|
2034
|
-
highlightActiveElement.call(this, els[0], this.
|
|
2067
|
+
highlightActiveElement.call(this, els[0], await this._getContext());
|
|
2035
2068
|
await els[0].press('End');
|
|
2036
2069
|
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
2037
2070
|
return this._waitForAction();
|
|
@@ -2048,12 +2081,13 @@ class Playwright extends Helper {
|
|
|
2048
2081
|
* I.seeInField('#searchform input','Search');
|
|
2049
2082
|
* ```
|
|
2050
2083
|
* @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
|
|
2051
|
-
* @param {
|
|
2084
|
+
* @param {CodeceptJS.StringOrSecret} value value to check.
|
|
2052
2085
|
* ⚠️ returns a _promise_ which is synchronized internally by recorder
|
|
2053
2086
|
*
|
|
2054
2087
|
*/
|
|
2055
2088
|
async seeInField(field, value) {
|
|
2056
|
-
|
|
2089
|
+
const _value = (typeof value === 'boolean') ? value : value.toString();
|
|
2090
|
+
return proceedSeeInField.call(this, 'assert', field, _value);
|
|
2057
2091
|
}
|
|
2058
2092
|
|
|
2059
2093
|
/**
|
|
@@ -2066,12 +2100,13 @@ class Playwright extends Helper {
|
|
|
2066
2100
|
* ```
|
|
2067
2101
|
*
|
|
2068
2102
|
* @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
|
|
2069
|
-
* @param {
|
|
2103
|
+
* @param {CodeceptJS.StringOrSecret} value value to check.
|
|
2070
2104
|
* ⚠️ returns a _promise_ which is synchronized internally by recorder
|
|
2071
2105
|
*
|
|
2072
2106
|
*/
|
|
2073
2107
|
async dontSeeInField(field, value) {
|
|
2074
|
-
|
|
2108
|
+
const _value = (typeof value === 'boolean') ? value : value.toString();
|
|
2109
|
+
return proceedSeeInField.call(this, 'negate', field, _value);
|
|
2075
2110
|
}
|
|
2076
2111
|
|
|
2077
2112
|
/**
|
|
@@ -2130,29 +2165,12 @@ class Playwright extends Helper {
|
|
|
2130
2165
|
const els = await findFields.call(this, select);
|
|
2131
2166
|
assertElementExists(els, select, 'Selectable field');
|
|
2132
2167
|
const el = els[0];
|
|
2133
|
-
if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
|
|
2134
|
-
throw new Error('Element is not <select>');
|
|
2135
|
-
}
|
|
2136
|
-
highlightActiveElement.call(this, el, this.page);
|
|
2137
|
-
if (!Array.isArray(option)) option = [option];
|
|
2138
2168
|
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
if (optEl.length) {
|
|
2143
|
-
this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
|
|
2144
|
-
continue;
|
|
2145
|
-
}
|
|
2146
|
-
optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) });
|
|
2147
|
-
if (optEl.length) {
|
|
2148
|
-
this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2151
|
-
await this._evaluateHandeInContext((element) => {
|
|
2152
|
-
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2153
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2154
|
-
}, el);
|
|
2169
|
+
highlightActiveElement.call(this, el, await this._getContext());
|
|
2170
|
+
|
|
2171
|
+
if (!Array.isArray(option)) option = [option];
|
|
2155
2172
|
|
|
2173
|
+
await el.selectOption(option);
|
|
2156
2174
|
return this._waitForAction();
|
|
2157
2175
|
}
|
|
2158
2176
|
|
|
@@ -2598,7 +2616,7 @@ class Playwright extends Helper {
|
|
|
2598
2616
|
const els = await this._locate(locator);
|
|
2599
2617
|
const texts = [];
|
|
2600
2618
|
for (const el of els) {
|
|
2601
|
-
texts.push(await (await el.
|
|
2619
|
+
texts.push(await (await el.innerText()));
|
|
2602
2620
|
}
|
|
2603
2621
|
this.debug(`Matched ${els.length} elements`);
|
|
2604
2622
|
return texts;
|
|
@@ -2637,7 +2655,7 @@ class Playwright extends Helper {
|
|
|
2637
2655
|
async grabValueFromAll(locator) {
|
|
2638
2656
|
const els = await findFields.call(this, locator);
|
|
2639
2657
|
this.debug(`Matched ${els.length} elements`);
|
|
2640
|
-
return Promise.all(els.map(el => el.
|
|
2658
|
+
return Promise.all(els.map(el => el.inputValue()));
|
|
2641
2659
|
}
|
|
2642
2660
|
|
|
2643
2661
|
/**
|
|
@@ -2675,7 +2693,7 @@ class Playwright extends Helper {
|
|
|
2675
2693
|
async grabHTMLFromAll(locator) {
|
|
2676
2694
|
const els = await this._locate(locator);
|
|
2677
2695
|
this.debug(`Matched ${els.length} elements`);
|
|
2678
|
-
return Promise.all(els.map(el => el
|
|
2696
|
+
return Promise.all(els.map(el => el.innerHTML()));
|
|
2679
2697
|
}
|
|
2680
2698
|
|
|
2681
2699
|
/**
|
|
@@ -2717,7 +2735,7 @@ class Playwright extends Helper {
|
|
|
2717
2735
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
2718
2736
|
const els = await this._locate(locator);
|
|
2719
2737
|
this.debug(`Matched ${els.length} elements`);
|
|
2720
|
-
const cssValues = await Promise.all(els.map(el => el
|
|
2738
|
+
const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
|
|
2721
2739
|
|
|
2722
2740
|
return cssValues;
|
|
2723
2741
|
}
|
|
@@ -2742,21 +2760,20 @@ class Playwright extends Helper {
|
|
|
2742
2760
|
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
|
|
2743
2761
|
const elemAmount = res.length;
|
|
2744
2762
|
const commands = [];
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
}));
|
|
2763
|
+
let props = [];
|
|
2764
|
+
|
|
2765
|
+
for (const element of res) {
|
|
2766
|
+
const cssProperties = await element.evaluate((el) => getComputedStyle(el));
|
|
2767
|
+
|
|
2768
|
+
Object.keys(cssPropertiesCamelCase).forEach(prop => {
|
|
2769
|
+
if (isColorProperty(prop)) {
|
|
2770
|
+
props.push(convertColorToRGBA(cssProperties[prop]));
|
|
2771
|
+
} else {
|
|
2772
|
+
props.push(cssProperties[prop]);
|
|
2773
|
+
}
|
|
2757
2774
|
});
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2760
2777
|
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
|
|
2761
2778
|
if (!Array.isArray(props)) props = [props];
|
|
2762
2779
|
let chunked = chunkArray(props, values.length);
|
|
@@ -2791,7 +2808,7 @@ class Playwright extends Helper {
|
|
|
2791
2808
|
res.forEach((el) => {
|
|
2792
2809
|
Object.keys(attributes).forEach((prop) => {
|
|
2793
2810
|
commands.push(el
|
|
2794
|
-
|
|
2811
|
+
.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop));
|
|
2795
2812
|
});
|
|
2796
2813
|
});
|
|
2797
2814
|
let attrs = await Promise.all(commands);
|
|
@@ -2823,11 +2840,11 @@ class Playwright extends Helper {
|
|
|
2823
2840
|
*
|
|
2824
2841
|
*/
|
|
2825
2842
|
async dragSlider(locator, offsetX = 0) {
|
|
2826
|
-
const src = await this.
|
|
2843
|
+
const src = await this._locateElement(locator);
|
|
2827
2844
|
assertElementExists(src, locator, 'Slider Element');
|
|
2828
2845
|
|
|
2829
2846
|
// Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets!
|
|
2830
|
-
const sliderSource = await clickablePoint(src
|
|
2847
|
+
const sliderSource = await clickablePoint(src);
|
|
2831
2848
|
|
|
2832
2849
|
// Drag start point
|
|
2833
2850
|
await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
|
|
@@ -2880,8 +2897,7 @@ class Playwright extends Helper {
|
|
|
2880
2897
|
const array = [];
|
|
2881
2898
|
|
|
2882
2899
|
for (let index = 0; index < els.length; index++) {
|
|
2883
|
-
|
|
2884
|
-
array.push(await a.jsonValue());
|
|
2900
|
+
array.push(await els[index].getAttribute(attr));
|
|
2885
2901
|
}
|
|
2886
2902
|
|
|
2887
2903
|
return array;
|
|
@@ -2904,10 +2920,9 @@ class Playwright extends Helper {
|
|
|
2904
2920
|
async saveElementScreenshot(locator, fileName) {
|
|
2905
2921
|
const outputFile = screenshotOutputFolder(fileName);
|
|
2906
2922
|
|
|
2907
|
-
const res = await this.
|
|
2923
|
+
const res = await this._locateElement(locator);
|
|
2908
2924
|
assertElementExists(res, locator);
|
|
2909
|
-
|
|
2910
|
-
const elem = res[0];
|
|
2925
|
+
const elem = res;
|
|
2911
2926
|
this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
|
|
2912
2927
|
return elem.screenshot({ path: outputFile, type: 'png' });
|
|
2913
2928
|
}
|
|
@@ -3472,16 +3487,26 @@ class Playwright extends Helper {
|
|
|
3472
3487
|
}
|
|
3473
3488
|
return;
|
|
3474
3489
|
}
|
|
3490
|
+
let contentFrame;
|
|
3491
|
+
|
|
3475
3492
|
if (!locator) {
|
|
3476
|
-
this.context = this.page;
|
|
3493
|
+
this.context = await this.page.frames()[0];
|
|
3477
3494
|
this.contextLocator = null;
|
|
3478
3495
|
return;
|
|
3479
3496
|
}
|
|
3480
3497
|
|
|
3481
3498
|
// iframe by selector
|
|
3482
3499
|
const els = await this._locate(locator);
|
|
3483
|
-
assertElementExists(els, locator);
|
|
3484
|
-
|
|
3500
|
+
// assertElementExists(els, locator);
|
|
3501
|
+
|
|
3502
|
+
// get content of the first iframe
|
|
3503
|
+
if ((locator.frame && locator.frame === 'iframe') || locator.toLowerCase() === 'iframe') {
|
|
3504
|
+
contentFrame = await this.page.frames()[1];
|
|
3505
|
+
// get content of the iframe using its name
|
|
3506
|
+
} else if (locator.toLowerCase().includes('name=')) {
|
|
3507
|
+
const frameName = locator.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
|
|
3508
|
+
contentFrame = await this.page.frame(frameName);
|
|
3509
|
+
}
|
|
3485
3510
|
|
|
3486
3511
|
if (contentFrame) {
|
|
3487
3512
|
this.context = contentFrame;
|
|
@@ -3527,13 +3552,15 @@ class Playwright extends Helper {
|
|
|
3527
3552
|
}
|
|
3528
3553
|
|
|
3529
3554
|
/**
|
|
3530
|
-
* Waits for navigation to finish. By default takes configured `waitForNavigation` option.
|
|
3555
|
+
* Waits for navigation to finish. By default, it takes configured `waitForNavigation` option.
|
|
3531
3556
|
*
|
|
3532
3557
|
* See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
|
|
3533
3558
|
*
|
|
3534
3559
|
* @param {*} options
|
|
3535
3560
|
*/
|
|
3536
3561
|
async waitForNavigation(options = {}) {
|
|
3562
|
+
console.log(`waitForNavigation deprecated:
|
|
3563
|
+
* This method is inherently racy, please use 'waitForURL' instead.`);
|
|
3537
3564
|
options = {
|
|
3538
3565
|
timeout: this.options.getPageTimeout,
|
|
3539
3566
|
waitUntil: this.options.waitForNavigation,
|
|
@@ -3542,6 +3569,23 @@ class Playwright extends Helper {
|
|
|
3542
3569
|
return this.page.waitForNavigation(options);
|
|
3543
3570
|
}
|
|
3544
3571
|
|
|
3572
|
+
/**
|
|
3573
|
+
* Waits for page navigates to a new URL or reloads. By default, it takes configured `waitForNavigation` option.
|
|
3574
|
+
*
|
|
3575
|
+
* See [Playwright's reference](https://playwright.dev/docs/api/class-page#page-wait-for-url)
|
|
3576
|
+
*
|
|
3577
|
+
* @param {string|RegExp} url - A glob pattern, regex pattern or predicate receiving URL to match while waiting for the navigation. Note that if the parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to the string.
|
|
3578
|
+
* @param {*} options
|
|
3579
|
+
*/
|
|
3580
|
+
async waitForURL(url, options = {}) {
|
|
3581
|
+
options = {
|
|
3582
|
+
timeout: this.options.getPageTimeout,
|
|
3583
|
+
waitUntil: this.options.waitForNavigation,
|
|
3584
|
+
...options,
|
|
3585
|
+
};
|
|
3586
|
+
return this.page.waitForURL(url, options);
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3545
3589
|
async waitUntilExists(locator, sec) {
|
|
3546
3590
|
console.log(`waitUntilExists deprecated:
|
|
3547
3591
|
* use 'waitForElement' to wait for element to be attached
|
|
@@ -3636,9 +3680,9 @@ class Playwright extends Helper {
|
|
|
3636
3680
|
*
|
|
3637
3681
|
*/
|
|
3638
3682
|
async grabElementBoundingRect(locator, prop) {
|
|
3639
|
-
const
|
|
3640
|
-
assertElementExists(
|
|
3641
|
-
const rect = await
|
|
3683
|
+
const el = await this._locateElement(locator);
|
|
3684
|
+
assertElementExists(el, locator);
|
|
3685
|
+
const rect = await el.boundingBox();
|
|
3642
3686
|
if (prop) return rect[prop];
|
|
3643
3687
|
return rect;
|
|
3644
3688
|
}
|
|
@@ -3675,16 +3719,16 @@ class Playwright extends Helper {
|
|
|
3675
3719
|
}
|
|
3676
3720
|
|
|
3677
3721
|
/**
|
|
3678
|
-
* Starts recording
|
|
3722
|
+
* Starts recording the network traffics.
|
|
3679
3723
|
* This also resets recorded network requests.
|
|
3680
3724
|
*
|
|
3681
3725
|
* ```js
|
|
3682
3726
|
* I.startRecordingTraffic();
|
|
3683
3727
|
* ```
|
|
3684
3728
|
*
|
|
3685
|
-
* @return {
|
|
3729
|
+
* @return {void}
|
|
3686
3730
|
*/
|
|
3687
|
-
|
|
3731
|
+
startRecordingTraffic() {
|
|
3688
3732
|
this.flushNetworkTraffics();
|
|
3689
3733
|
this.recording = true;
|
|
3690
3734
|
this.recordedAtLeastOnce = true;
|
|
@@ -3695,31 +3739,62 @@ class Playwright extends Helper {
|
|
|
3695
3739
|
method: request.method(),
|
|
3696
3740
|
requestHeaders: request.headers(),
|
|
3697
3741
|
requestPostData: request.postData(),
|
|
3742
|
+
response: request.response(),
|
|
3698
3743
|
};
|
|
3699
3744
|
|
|
3700
3745
|
this.debugSection('REQUEST: ', JSON.stringify(information));
|
|
3701
3746
|
|
|
3702
|
-
information.requestPostData
|
|
3747
|
+
if (typeof information.requestPostData === 'object') {
|
|
3748
|
+
information.requestPostData = JSON.parse(information.requestPostData);
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3703
3751
|
this.requests.push(information);
|
|
3704
|
-
return this._waitForAction();
|
|
3705
3752
|
});
|
|
3706
3753
|
}
|
|
3707
3754
|
|
|
3708
3755
|
/**
|
|
3709
3756
|
* Grab the recording network traffics
|
|
3710
3757
|
*
|
|
3711
|
-
*
|
|
3758
|
+
* ```js
|
|
3759
|
+
* const traffics = await I.grabRecordedNetworkTraffics();
|
|
3760
|
+
* expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1');
|
|
3761
|
+
* expect(traffics[0].response.status).to.equal(200);
|
|
3762
|
+
* expect(traffics[0].response.body).to.contain({ name: 'this was mocked' });
|
|
3763
|
+
* ```
|
|
3764
|
+
*
|
|
3765
|
+
* @return { Promise<Array<any>> }
|
|
3712
3766
|
*
|
|
3713
3767
|
*/
|
|
3714
|
-
grabRecordedNetworkTraffics() {
|
|
3768
|
+
async grabRecordedNetworkTraffics() {
|
|
3715
3769
|
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
3716
3770
|
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
3717
3771
|
}
|
|
3772
|
+
|
|
3773
|
+
const requests = await this.requests;
|
|
3774
|
+
const promises = requests.map(async (request) => request.response.then(
|
|
3775
|
+
async (response) => {
|
|
3776
|
+
let body;
|
|
3777
|
+
try {
|
|
3778
|
+
// There's no 'body' for some requests (redirect etc...)
|
|
3779
|
+
body = JSON.parse((await response.body()).toString());
|
|
3780
|
+
} catch (e) {
|
|
3781
|
+
// only interested in JSON, not HTML responses.
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
request.response = {
|
|
3785
|
+
status: response.status(),
|
|
3786
|
+
statusText: response.statusText(),
|
|
3787
|
+
body,
|
|
3788
|
+
};
|
|
3789
|
+
},
|
|
3790
|
+
));
|
|
3791
|
+
await Promise.all(promises);
|
|
3792
|
+
|
|
3718
3793
|
return this.requests;
|
|
3719
3794
|
}
|
|
3720
3795
|
|
|
3721
3796
|
/**
|
|
3722
|
-
* Blocks traffic
|
|
3797
|
+
* Blocks traffic of a given URL or a list of URLs.
|
|
3723
3798
|
*
|
|
3724
3799
|
* Examples:
|
|
3725
3800
|
*
|
|
@@ -3730,16 +3805,30 @@ class Playwright extends Helper {
|
|
|
3730
3805
|
* I.blockTraffic(/\.css$/);
|
|
3731
3806
|
* ```
|
|
3732
3807
|
*
|
|
3733
|
-
*
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3808
|
+
* ```js
|
|
3809
|
+
* I.blockTraffic(['http://example.com/css/style.css', 'http://example.com/css/*.css']);
|
|
3810
|
+
* ```
|
|
3811
|
+
*
|
|
3812
|
+
* @param {string|Array|RegExp} urls URL or a list of URLs to block . URL can contain * for wildcards. Example: https://www.example.com** to block all traffic for that domain. Regexp are also supported.
|
|
3813
|
+
*/
|
|
3814
|
+
blockTraffic(urls) {
|
|
3815
|
+
if (Array.isArray(urls)) {
|
|
3816
|
+
urls.forEach(url => {
|
|
3817
|
+
this.page.route(url, (route) => {
|
|
3818
|
+
route
|
|
3819
|
+
.abort()
|
|
3820
|
+
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
3821
|
+
.catch((e) => {});
|
|
3822
|
+
});
|
|
3823
|
+
});
|
|
3824
|
+
} else {
|
|
3825
|
+
this.page.route(urls, (route) => {
|
|
3826
|
+
route
|
|
3827
|
+
.abort()
|
|
3828
|
+
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
3829
|
+
.catch((e) => {});
|
|
3830
|
+
});
|
|
3831
|
+
}
|
|
3743
3832
|
}
|
|
3744
3833
|
|
|
3745
3834
|
/**
|
|
@@ -3758,7 +3847,7 @@ class Playwright extends Helper {
|
|
|
3758
3847
|
* @param responseString string The string to return in fake response's body.
|
|
3759
3848
|
* @param contentType Content type of fake response. If not specified default value 'application/json' is used.
|
|
3760
3849
|
*/
|
|
3761
|
-
|
|
3850
|
+
mockTraffic(urls, responseString, contentType = 'application/json') {
|
|
3762
3851
|
// Required to mock cross-domain requests
|
|
3763
3852
|
const headers = { 'access-control-allow-origin': '*' };
|
|
3764
3853
|
|
|
@@ -3780,7 +3869,6 @@ class Playwright extends Helper {
|
|
|
3780
3869
|
});
|
|
3781
3870
|
});
|
|
3782
3871
|
});
|
|
3783
|
-
return this._waitForAction();
|
|
3784
3872
|
}
|
|
3785
3873
|
|
|
3786
3874
|
/**
|
|
@@ -3852,7 +3940,7 @@ class Playwright extends Helper {
|
|
|
3852
3940
|
}
|
|
3853
3941
|
|
|
3854
3942
|
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
3855
|
-
throw new Error('Failure in test automation. You use "I.
|
|
3943
|
+
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
3856
3944
|
}
|
|
3857
3945
|
|
|
3858
3946
|
for (let i = 0; i <= timeout * 2; i++) {
|
|
@@ -3860,7 +3948,9 @@ class Playwright extends Helper {
|
|
|
3860
3948
|
if (found) {
|
|
3861
3949
|
return true;
|
|
3862
3950
|
}
|
|
3863
|
-
await new Promise((done) =>
|
|
3951
|
+
await new Promise((done) => {
|
|
3952
|
+
setTimeout(done, 1000);
|
|
3953
|
+
});
|
|
3864
3954
|
}
|
|
3865
3955
|
|
|
3866
3956
|
// check request post data
|
|
@@ -3997,6 +4087,163 @@ class Playwright extends Helper {
|
|
|
3997
4087
|
});
|
|
3998
4088
|
return dumpedTraffic;
|
|
3999
4089
|
}
|
|
4090
|
+
|
|
4091
|
+
/**
|
|
4092
|
+
* Starts recording of websocket messages.
|
|
4093
|
+
* This also resets recorded websocket messages.
|
|
4094
|
+
*
|
|
4095
|
+
* ```js
|
|
4096
|
+
* await I.startRecordingWebSocketMessages();
|
|
4097
|
+
* ```
|
|
4098
|
+
*
|
|
4099
|
+
*/
|
|
4100
|
+
async startRecordingWebSocketMessages() {
|
|
4101
|
+
this.flushWebSocketMessages();
|
|
4102
|
+
this.recordingWebSocketMessages = true;
|
|
4103
|
+
this.recordedWebSocketMessagesAtLeastOnce = true;
|
|
4104
|
+
|
|
4105
|
+
this.cdpSession = await this.getNewCDPSession();
|
|
4106
|
+
await this.cdpSession.send('Network.enable');
|
|
4107
|
+
await this.cdpSession.send('Page.enable');
|
|
4108
|
+
|
|
4109
|
+
this.cdpSession.on(
|
|
4110
|
+
'Network.webSocketFrameReceived',
|
|
4111
|
+
(payload) => {
|
|
4112
|
+
this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
|
|
4113
|
+
},
|
|
4114
|
+
);
|
|
4115
|
+
|
|
4116
|
+
this.cdpSession.on(
|
|
4117
|
+
'Network.webSocketFrameSent',
|
|
4118
|
+
(payload) => {
|
|
4119
|
+
this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
|
|
4120
|
+
},
|
|
4121
|
+
);
|
|
4122
|
+
|
|
4123
|
+
this.cdpSession.on(
|
|
4124
|
+
'Network.webSocketFrameError',
|
|
4125
|
+
(payload) => {
|
|
4126
|
+
this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload));
|
|
4127
|
+
},
|
|
4128
|
+
);
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
/**
|
|
4132
|
+
* Stops recording WS messages. Recorded WS messages is not flashed.
|
|
4133
|
+
*
|
|
4134
|
+
* ```js
|
|
4135
|
+
* await I.stopRecordingWebSocketMessages();
|
|
4136
|
+
* ```
|
|
4137
|
+
*/
|
|
4138
|
+
async stopRecordingWebSocketMessages() {
|
|
4139
|
+
await this.cdpSession.send('Network.disable');
|
|
4140
|
+
await this.cdpSession.send('Page.disable');
|
|
4141
|
+
this.page.removeAllListeners('Network');
|
|
4142
|
+
this.recordingWebSocketMessages = false;
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
/**
|
|
4146
|
+
* Grab the recording WS messages
|
|
4147
|
+
*
|
|
4148
|
+
* @return { Array<any> }
|
|
4149
|
+
*
|
|
4150
|
+
*/
|
|
4151
|
+
grabWebSocketMessages() {
|
|
4152
|
+
if (!this.recordingWebSocketMessages) {
|
|
4153
|
+
if (!this.recordedWebSocketMessagesAtLeastOnce) {
|
|
4154
|
+
throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.');
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
return this.webSocketMessages;
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
/**
|
|
4161
|
+
* Resets all recorded WS messages.
|
|
4162
|
+
*/
|
|
4163
|
+
flushWebSocketMessages() {
|
|
4164
|
+
this.webSocketMessages = [];
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
/**
|
|
4168
|
+
* Return a performance metric from the chrome cdp session.
|
|
4169
|
+
* Note: Chrome-only
|
|
4170
|
+
*
|
|
4171
|
+
* Examples:
|
|
4172
|
+
*
|
|
4173
|
+
* ```js
|
|
4174
|
+
* const metrics = await I.grabMetrics();
|
|
4175
|
+
*
|
|
4176
|
+
* // returned metrics
|
|
4177
|
+
*
|
|
4178
|
+
* [
|
|
4179
|
+
* { name: 'Timestamp', value: 1584904.203473 },
|
|
4180
|
+
* { name: 'AudioHandlers', value: 0 },
|
|
4181
|
+
* { name: 'AudioWorkletProcessors', value: 0 },
|
|
4182
|
+
* { name: 'Documents', value: 22 },
|
|
4183
|
+
* { name: 'Frames', value: 10 },
|
|
4184
|
+
* { name: 'JSEventListeners', value: 366 },
|
|
4185
|
+
* { name: 'LayoutObjects', value: 1240 },
|
|
4186
|
+
* { name: 'MediaKeySessions', value: 0 },
|
|
4187
|
+
* { name: 'MediaKeys', value: 0 },
|
|
4188
|
+
* { name: 'Nodes', value: 4505 },
|
|
4189
|
+
* { name: 'Resources', value: 141 },
|
|
4190
|
+
* { name: 'ContextLifecycleStateObservers', value: 34 },
|
|
4191
|
+
* { name: 'V8PerContextDatas', value: 4 },
|
|
4192
|
+
* { name: 'WorkerGlobalScopes', value: 0 },
|
|
4193
|
+
* { name: 'UACSSResources', value: 0 },
|
|
4194
|
+
* { name: 'RTCPeerConnections', value: 0 },
|
|
4195
|
+
* { name: 'ResourceFetchers', value: 22 },
|
|
4196
|
+
* { name: 'AdSubframes', value: 0 },
|
|
4197
|
+
* { name: 'DetachedScriptStates', value: 2 },
|
|
4198
|
+
* { name: 'ArrayBufferContents', value: 1 },
|
|
4199
|
+
* { name: 'LayoutCount', value: 0 },
|
|
4200
|
+
* { name: 'RecalcStyleCount', value: 0 },
|
|
4201
|
+
* { name: 'LayoutDuration', value: 0 },
|
|
4202
|
+
* { name: 'RecalcStyleDuration', value: 0 },
|
|
4203
|
+
* { name: 'DevToolsCommandDuration', value: 0.000013 },
|
|
4204
|
+
* { name: 'ScriptDuration', value: 0 },
|
|
4205
|
+
* { name: 'V8CompileDuration', value: 0 },
|
|
4206
|
+
* { name: 'TaskDuration', value: 0.000014 },
|
|
4207
|
+
* { name: 'TaskOtherDuration', value: 0.000001 },
|
|
4208
|
+
* { name: 'ThreadTime', value: 0.000046 },
|
|
4209
|
+
* { name: 'ProcessTime', value: 0.616852 },
|
|
4210
|
+
* { name: 'JSHeapUsedSize', value: 19004908 },
|
|
4211
|
+
* { name: 'JSHeapTotalSize', value: 26820608 },
|
|
4212
|
+
* { name: 'FirstMeaningfulPaint', value: 0 },
|
|
4213
|
+
* { name: 'DomContentLoaded', value: 1584903.690491 },
|
|
4214
|
+
* { name: 'NavigationStart', value: 1584902.841845 }
|
|
4215
|
+
* ]
|
|
4216
|
+
*
|
|
4217
|
+
* ```
|
|
4218
|
+
*
|
|
4219
|
+
* @return {Promise<Array<Object>>}
|
|
4220
|
+
*/
|
|
4221
|
+
async grabMetrics() {
|
|
4222
|
+
const client = await this.page.context().newCDPSession(this.page);
|
|
4223
|
+
await client.send('Performance.enable');
|
|
4224
|
+
const perfMetricObject = await client.send('Performance.getMetrics');
|
|
4225
|
+
return perfMetricObject?.metrics;
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
_getWebSocketMessage(payload) {
|
|
4229
|
+
if (payload.errorMessage) {
|
|
4230
|
+
return payload.errorMessage;
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
return payload.response.payloadData;
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
_getWebSocketLog(prefix, payload) {
|
|
4237
|
+
return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`;
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
async getNewCDPSession() {
|
|
4241
|
+
return this.page.context().newCDPSession(this.page);
|
|
4242
|
+
}
|
|
4243
|
+
|
|
4244
|
+
_logWebsocketMessages(message) {
|
|
4245
|
+
this.webSocketMessages += message;
|
|
4246
|
+
}
|
|
4000
4247
|
}
|
|
4001
4248
|
|
|
4002
4249
|
module.exports = Playwright;
|
|
@@ -4009,42 +4256,19 @@ function buildLocatorString(locator) {
|
|
|
4009
4256
|
}
|
|
4010
4257
|
return locator.simplify();
|
|
4011
4258
|
}
|
|
4012
|
-
// TODO: locator change required after #3677 implementation. Temporary solution before migration. Should be deleted after #3677 implementation
|
|
4013
|
-
async function getXPathForElement(elementHandle) {
|
|
4014
|
-
function calculateIndex(node) {
|
|
4015
|
-
let index = 1;
|
|
4016
|
-
let sibling = node.previousElementSibling;
|
|
4017
|
-
while (sibling) {
|
|
4018
|
-
if (sibling.tagName === node.tagName) {
|
|
4019
|
-
index++;
|
|
4020
|
-
}
|
|
4021
|
-
sibling = sibling.previousElementSibling;
|
|
4022
|
-
}
|
|
4023
|
-
return index;
|
|
4024
|
-
}
|
|
4025
4259
|
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
if (node.hasAttribute('id')) {
|
|
4030
|
-
segments.unshift(`*[@id="${node.getAttribute('id')}"]`);
|
|
4031
|
-
break;
|
|
4032
|
-
} else {
|
|
4033
|
-
const index = calculateIndex(node);
|
|
4034
|
-
segments.unshift(`${node.localName}[${index}]`);
|
|
4035
|
-
node = node.parentNode;
|
|
4036
|
-
}
|
|
4037
|
-
}
|
|
4038
|
-
return `//${segments.join('/')}`;
|
|
4039
|
-
}
|
|
4260
|
+
async function findElements(matcher, locator) {
|
|
4261
|
+
if (locator.react) return findReact(matcher, locator);
|
|
4262
|
+
locator = new Locator(locator, 'css');
|
|
4040
4263
|
|
|
4041
|
-
return
|
|
4264
|
+
return matcher.locator(buildLocatorString(locator)).all();
|
|
4042
4265
|
}
|
|
4043
4266
|
|
|
4044
|
-
async function
|
|
4267
|
+
async function findElement(matcher, locator) {
|
|
4045
4268
|
if (locator.react) return findReact(matcher, locator);
|
|
4046
4269
|
locator = new Locator(locator, 'css');
|
|
4047
|
-
|
|
4270
|
+
|
|
4271
|
+
return matcher.locator(buildLocatorString(locator));
|
|
4048
4272
|
}
|
|
4049
4273
|
|
|
4050
4274
|
async function getVisibleElements(elements) {
|
|
@@ -4074,8 +4298,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4074
4298
|
assertElementExists(els, locator, 'Clickable element');
|
|
4075
4299
|
}
|
|
4076
4300
|
|
|
4077
|
-
|
|
4078
|
-
highlightActiveElement.call(this, els[0], this.page);
|
|
4301
|
+
highlightActiveElement.call(this, els[0], await this._getContext());
|
|
4079
4302
|
|
|
4080
4303
|
/*
|
|
4081
4304
|
using the force true options itself but instead dispatching a click
|
|
@@ -4088,7 +4311,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4088
4311
|
}
|
|
4089
4312
|
const promises = [];
|
|
4090
4313
|
if (options.waitForNavigation) {
|
|
4091
|
-
promises.push(this.waitForNavigation
|
|
4314
|
+
promises.push(this.waitForURL(/.*/, { waitUntil: options.waitForNavigation }));
|
|
4092
4315
|
}
|
|
4093
4316
|
promises.push(this._waitForAction());
|
|
4094
4317
|
|
|
@@ -4123,28 +4346,28 @@ async function findClickable(matcher, locator) {
|
|
|
4123
4346
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
4124
4347
|
let description;
|
|
4125
4348
|
let allText;
|
|
4349
|
+
|
|
4126
4350
|
if (!context) {
|
|
4127
4351
|
let el = await this.context;
|
|
4128
|
-
|
|
4129
4352
|
if (el && !el.getProperty) {
|
|
4130
4353
|
// Fallback to body
|
|
4131
|
-
el = await this.
|
|
4354
|
+
el = await this.page.$('body');
|
|
4132
4355
|
}
|
|
4133
4356
|
|
|
4134
|
-
allText = [await el.
|
|
4357
|
+
allText = [await el.innerText()];
|
|
4135
4358
|
description = 'web application';
|
|
4136
4359
|
} else {
|
|
4137
4360
|
const locator = new Locator(context, 'css');
|
|
4138
4361
|
description = `element ${locator.toString()}`;
|
|
4139
4362
|
const els = await this._locate(locator);
|
|
4140
4363
|
assertElementExists(els, locator.toString());
|
|
4141
|
-
allText = await Promise.all(els.map(el => el.
|
|
4364
|
+
allText = await Promise.all(els.map(el => el.innerText()));
|
|
4142
4365
|
}
|
|
4143
4366
|
|
|
4144
4367
|
if (strict) {
|
|
4145
4368
|
return allText.map(elText => equals(description)[assertType](text, elText));
|
|
4146
4369
|
}
|
|
4147
|
-
return stringIncludes(description)[assertType](text, allText.join(' | '));
|
|
4370
|
+
return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
|
|
4148
4371
|
}
|
|
4149
4372
|
|
|
4150
4373
|
async function findCheckable(locator, context) {
|
|
@@ -4206,15 +4429,15 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
4206
4429
|
const els = await findFields.call(this, field);
|
|
4207
4430
|
assertElementExists(els, field, 'Field');
|
|
4208
4431
|
const el = els[0];
|
|
4209
|
-
const tag = await el.
|
|
4210
|
-
const fieldType = await el.
|
|
4432
|
+
const tag = await el.evaluate(e => e.tagName);
|
|
4433
|
+
const fieldType = await el.getAttribute('type');
|
|
4211
4434
|
|
|
4212
4435
|
const proceedMultiple = async (elements) => {
|
|
4213
4436
|
const fields = Array.isArray(elements) ? elements : [elements];
|
|
4214
4437
|
|
|
4215
4438
|
const elementValues = [];
|
|
4216
4439
|
for (const element of fields) {
|
|
4217
|
-
elementValues.push(await element.
|
|
4440
|
+
elementValues.push(await element.inputValue());
|
|
4218
4441
|
}
|
|
4219
4442
|
|
|
4220
4443
|
if (typeof value === 'boolean') {
|
|
@@ -4228,8 +4451,8 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
4228
4451
|
};
|
|
4229
4452
|
|
|
4230
4453
|
if (tag === 'SELECT') {
|
|
4231
|
-
if (await el.
|
|
4232
|
-
const selectedOptions = await el
|
|
4454
|
+
if (await el.getAttribute('multiple')) {
|
|
4455
|
+
const selectedOptions = await el.all('option:checked');
|
|
4233
4456
|
if (!selectedOptions.length) return null;
|
|
4234
4457
|
|
|
4235
4458
|
const options = await filterFieldsByValue(selectedOptions, value, true);
|
|
@@ -4253,14 +4476,23 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
4253
4476
|
return proceedMultiple(els[0]);
|
|
4254
4477
|
}
|
|
4255
4478
|
|
|
4256
|
-
|
|
4479
|
+
let fieldVal;
|
|
4480
|
+
|
|
4481
|
+
try {
|
|
4482
|
+
fieldVal = await el.inputValue();
|
|
4483
|
+
} catch (e) {
|
|
4484
|
+
if (e.message.includes('Error: Node is not an <input>, <textarea> or <select> element')) {
|
|
4485
|
+
fieldVal = await el.innerText();
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4257
4489
|
return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal);
|
|
4258
4490
|
}
|
|
4259
4491
|
|
|
4260
4492
|
async function filterFieldsByValue(elements, value, onlySelected) {
|
|
4261
4493
|
const matches = [];
|
|
4262
4494
|
for (const element of elements) {
|
|
4263
|
-
const val = await element.
|
|
4495
|
+
const val = await element.getAttribute('value');
|
|
4264
4496
|
let isSelected = true;
|
|
4265
4497
|
if (onlySelected) {
|
|
4266
4498
|
isSelected = await elementSelected(element);
|
|
@@ -4284,12 +4516,12 @@ async function filterFieldsBySelectionState(elements, state) {
|
|
|
4284
4516
|
}
|
|
4285
4517
|
|
|
4286
4518
|
async function elementSelected(element) {
|
|
4287
|
-
const type = await element.
|
|
4519
|
+
const type = await element.getAttribute('type');
|
|
4288
4520
|
|
|
4289
4521
|
if (type === 'checkbox' || type === 'radio') {
|
|
4290
4522
|
return element.isChecked();
|
|
4291
4523
|
}
|
|
4292
|
-
return element.
|
|
4524
|
+
return element.getAttribute('selected');
|
|
4293
4525
|
}
|
|
4294
4526
|
|
|
4295
4527
|
function isFrameLocator(locator) {
|
|
@@ -4490,7 +4722,7 @@ async function saveTraceForContext(context, name) {
|
|
|
4490
4722
|
}
|
|
4491
4723
|
|
|
4492
4724
|
function highlightActiveElement(element, context) {
|
|
4493
|
-
if (!this.options.
|
|
4725
|
+
if (!this.options.highlightElement && !store.debugMode) return;
|
|
4494
4726
|
|
|
4495
4727
|
highlightElement(element, context);
|
|
4496
4728
|
}
|