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/lib/helper/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;
|
|
@@ -763,7 +793,7 @@ class Playwright extends Helper {
|
|
|
763
793
|
}
|
|
764
794
|
throw err;
|
|
765
795
|
}
|
|
766
|
-
} else if (this.userDataDir) {
|
|
796
|
+
} else if (this.playwrightOptions.userDataDir) {
|
|
767
797
|
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
|
|
768
798
|
} else {
|
|
769
799
|
this.browser = await playwright[this.options.browser].launch(this.playwrightOptions);
|
|
@@ -824,9 +854,9 @@ class Playwright extends Helper {
|
|
|
824
854
|
return;
|
|
825
855
|
}
|
|
826
856
|
|
|
827
|
-
const
|
|
828
|
-
assertElementExists(
|
|
829
|
-
this.context =
|
|
857
|
+
const el = await this._locateElement(locator);
|
|
858
|
+
assertElementExists(el, locator);
|
|
859
|
+
this.context = el;
|
|
830
860
|
this.contextLocator = locator;
|
|
831
861
|
|
|
832
862
|
this.withinLocator = new Locator(locator);
|
|
@@ -929,11 +959,11 @@ class Playwright extends Helper {
|
|
|
929
959
|
*
|
|
930
960
|
*/
|
|
931
961
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
932
|
-
const
|
|
933
|
-
assertElementExists(
|
|
962
|
+
const el = await this._locateElement(locator);
|
|
963
|
+
assertElementExists(el, locator);
|
|
934
964
|
|
|
935
965
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
936
|
-
const { x, y } = await clickablePoint(
|
|
966
|
+
const { x, y } = await clickablePoint(el);
|
|
937
967
|
await this.page.mouse.move(x + offsetX, y + offsetY);
|
|
938
968
|
return this._waitForAction();
|
|
939
969
|
}
|
|
@@ -943,9 +973,8 @@ class Playwright extends Helper {
|
|
|
943
973
|
*
|
|
944
974
|
*/
|
|
945
975
|
async focus(locator, options = {}) {
|
|
946
|
-
const
|
|
947
|
-
assertElementExists(
|
|
948
|
-
const el = els[0];
|
|
976
|
+
const el = await this._locateElement(locator);
|
|
977
|
+
assertElementExists(el, locator, 'Element to focus');
|
|
949
978
|
|
|
950
979
|
await el.focus(options);
|
|
951
980
|
return this._waitForAction();
|
|
@@ -956,12 +985,10 @@ class Playwright extends Helper {
|
|
|
956
985
|
*
|
|
957
986
|
*/
|
|
958
987
|
async blur(locator, options = {}) {
|
|
959
|
-
const
|
|
960
|
-
assertElementExists(
|
|
961
|
-
// TODO: locator change required after #3677 implementation
|
|
962
|
-
const elXpath = await getXPathForElement(els[0]);
|
|
988
|
+
const el = await this._locateElement(locator);
|
|
989
|
+
assertElementExists(el, locator, 'Element to blur');
|
|
963
990
|
|
|
964
|
-
await
|
|
991
|
+
await el.blur(options);
|
|
965
992
|
return this._waitForAction();
|
|
966
993
|
}
|
|
967
994
|
|
|
@@ -1041,8 +1068,11 @@ class Playwright extends Helper {
|
|
|
1041
1068
|
const body = document.body;
|
|
1042
1069
|
const html = document.documentElement;
|
|
1043
1070
|
window.scrollTo(0, Math.max(
|
|
1044
|
-
body.scrollHeight,
|
|
1045
|
-
|
|
1071
|
+
body.scrollHeight,
|
|
1072
|
+
body.offsetHeight,
|
|
1073
|
+
html.clientHeight,
|
|
1074
|
+
html.scrollHeight,
|
|
1075
|
+
html.offsetHeight,
|
|
1046
1076
|
));
|
|
1047
1077
|
});
|
|
1048
1078
|
}
|
|
@@ -1058,10 +1088,10 @@ class Playwright extends Helper {
|
|
|
1058
1088
|
}
|
|
1059
1089
|
|
|
1060
1090
|
if (locator) {
|
|
1061
|
-
const
|
|
1062
|
-
assertElementExists(
|
|
1063
|
-
await
|
|
1064
|
-
const elementCoordinates = await clickablePoint(
|
|
1091
|
+
const el = await this._locateElement(locator);
|
|
1092
|
+
assertElementExists(el, locator, 'Element');
|
|
1093
|
+
await el.scrollIntoViewIfNeeded();
|
|
1094
|
+
const elementCoordinates = await clickablePoint(el);
|
|
1065
1095
|
await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { offsetX: elementCoordinates.x + offsetX, offsetY: elementCoordinates.y + offsetY });
|
|
1066
1096
|
} else {
|
|
1067
1097
|
await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY });
|
|
@@ -1129,7 +1159,20 @@ class Playwright extends Helper {
|
|
|
1129
1159
|
}
|
|
1130
1160
|
|
|
1131
1161
|
/**
|
|
1132
|
-
*
|
|
1162
|
+
* Get the first element by different locator types, including strict locator
|
|
1163
|
+
* Should be used in custom helpers:
|
|
1164
|
+
*
|
|
1165
|
+
* ```js
|
|
1166
|
+
* const element = await this.helpers['Playwright']._locateElement({name: 'password'});
|
|
1167
|
+
* ```
|
|
1168
|
+
*/
|
|
1169
|
+
async _locateElement(locator) {
|
|
1170
|
+
const context = await this.context || await this._getContext();
|
|
1171
|
+
return findElement(context, locator);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Find a checkbox by providing human-readable text:
|
|
1133
1176
|
* NOTE: Assumes the checkable element exists
|
|
1134
1177
|
*
|
|
1135
1178
|
* ```js
|
|
@@ -1144,7 +1187,7 @@ class Playwright extends Helper {
|
|
|
1144
1187
|
}
|
|
1145
1188
|
|
|
1146
1189
|
/**
|
|
1147
|
-
* Find a clickable element by providing human
|
|
1190
|
+
* Find a clickable element by providing human-readable text:
|
|
1148
1191
|
*
|
|
1149
1192
|
* ```js
|
|
1150
1193
|
* this.helpers['Playwright']._locateClickable('Next page').then // ...
|
|
@@ -1156,7 +1199,7 @@ class Playwright extends Helper {
|
|
|
1156
1199
|
}
|
|
1157
1200
|
|
|
1158
1201
|
/**
|
|
1159
|
-
* Find field elements by providing human
|
|
1202
|
+
* Find field elements by providing human-readable text:
|
|
1160
1203
|
*
|
|
1161
1204
|
* ```js
|
|
1162
1205
|
* this.helpers['Playwright']._locateFields('Your email').then // ...
|
|
@@ -1532,15 +1575,10 @@ class Playwright extends Helper {
|
|
|
1532
1575
|
const els = await findFields.call(this, field);
|
|
1533
1576
|
assertElementExists(els, field, 'Field');
|
|
1534
1577
|
const el = els[0];
|
|
1535
|
-
const tag = await el.getProperty('tagName').then(el => el.jsonValue());
|
|
1536
|
-
const editable = await el.getProperty('contenteditable').then(el => el.jsonValue());
|
|
1537
|
-
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
1538
|
-
await this._evaluateHandeInContext(el => el.value = '', el);
|
|
1539
|
-
} else if (editable) {
|
|
1540
|
-
await this._evaluateHandeInContext(el => el.innerHTML = '', el);
|
|
1541
|
-
}
|
|
1542
1578
|
|
|
1543
|
-
|
|
1579
|
+
await el.clear();
|
|
1580
|
+
|
|
1581
|
+
highlightActiveElement.call(this, el, await this._getContext());
|
|
1544
1582
|
|
|
1545
1583
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1546
1584
|
|
|
@@ -1565,21 +1603,16 @@ class Playwright extends Helper {
|
|
|
1565
1603
|
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
|
|
1566
1604
|
*/
|
|
1567
1605
|
async clearField(locator, options = {}) {
|
|
1568
|
-
|
|
1569
|
-
|
|
1606
|
+
const els = await findFields.call(this, locator);
|
|
1607
|
+
assertElementExists(els, locator, 'Field to clear');
|
|
1570
1608
|
|
|
1571
|
-
|
|
1572
|
-
const els = await findFields.call(this, locator);
|
|
1573
|
-
assertElementExists(els, locator, 'Field to clear');
|
|
1574
|
-
// TODO: locator change required after #3677 implementation
|
|
1575
|
-
const elXpath = await getXPathForElement(els[0]);
|
|
1609
|
+
const el = els[0];
|
|
1576
1610
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
return result;
|
|
1611
|
+
highlightActiveElement.call(this, el, this.page);
|
|
1612
|
+
|
|
1613
|
+
await el.clear();
|
|
1614
|
+
|
|
1615
|
+
return this._waitForAction();
|
|
1583
1616
|
}
|
|
1584
1617
|
|
|
1585
1618
|
/**
|
|
@@ -1590,7 +1623,7 @@ class Playwright extends Helper {
|
|
|
1590
1623
|
async appendField(field, value) {
|
|
1591
1624
|
const els = await findFields.call(this, field);
|
|
1592
1625
|
assertElementExists(els, field, 'Field');
|
|
1593
|
-
highlightActiveElement.call(this, els[0], this.
|
|
1626
|
+
highlightActiveElement.call(this, els[0], await this._getContext());
|
|
1594
1627
|
await els[0].press('End');
|
|
1595
1628
|
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1596
1629
|
return this._waitForAction();
|
|
@@ -1600,14 +1633,16 @@ class Playwright extends Helper {
|
|
|
1600
1633
|
* {{> seeInField }}
|
|
1601
1634
|
*/
|
|
1602
1635
|
async seeInField(field, value) {
|
|
1603
|
-
|
|
1636
|
+
const _value = (typeof value === 'boolean') ? value : value.toString();
|
|
1637
|
+
return proceedSeeInField.call(this, 'assert', field, _value);
|
|
1604
1638
|
}
|
|
1605
1639
|
|
|
1606
1640
|
/**
|
|
1607
1641
|
* {{> dontSeeInField }}
|
|
1608
1642
|
*/
|
|
1609
1643
|
async dontSeeInField(field, value) {
|
|
1610
|
-
|
|
1644
|
+
const _value = (typeof value === 'boolean') ? value : value.toString();
|
|
1645
|
+
return proceedSeeInField.call(this, 'negate', field, _value);
|
|
1611
1646
|
}
|
|
1612
1647
|
|
|
1613
1648
|
/**
|
|
@@ -1633,29 +1668,12 @@ class Playwright extends Helper {
|
|
|
1633
1668
|
const els = await findFields.call(this, select);
|
|
1634
1669
|
assertElementExists(els, select, 'Selectable field');
|
|
1635
1670
|
const el = els[0];
|
|
1636
|
-
if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
|
|
1637
|
-
throw new Error('Element is not <select>');
|
|
1638
|
-
}
|
|
1639
|
-
highlightActiveElement.call(this, el, this.page);
|
|
1640
|
-
if (!Array.isArray(option)) option = [option];
|
|
1641
1671
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
if (optEl.length) {
|
|
1646
|
-
this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
|
|
1647
|
-
continue;
|
|
1648
|
-
}
|
|
1649
|
-
optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) });
|
|
1650
|
-
if (optEl.length) {
|
|
1651
|
-
this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
await this._evaluateHandeInContext((element) => {
|
|
1655
|
-
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1656
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1657
|
-
}, el);
|
|
1672
|
+
highlightActiveElement.call(this, el, await this._getContext());
|
|
1673
|
+
|
|
1674
|
+
if (!Array.isArray(option)) option = [option];
|
|
1658
1675
|
|
|
1676
|
+
await el.selectOption(option);
|
|
1659
1677
|
return this._waitForAction();
|
|
1660
1678
|
}
|
|
1661
1679
|
|
|
@@ -1906,7 +1924,7 @@ class Playwright extends Helper {
|
|
|
1906
1924
|
const els = await this._locate(locator);
|
|
1907
1925
|
const texts = [];
|
|
1908
1926
|
for (const el of els) {
|
|
1909
|
-
texts.push(await (await el.
|
|
1927
|
+
texts.push(await (await el.innerText()));
|
|
1910
1928
|
}
|
|
1911
1929
|
this.debug(`Matched ${els.length} elements`);
|
|
1912
1930
|
return texts;
|
|
@@ -1928,7 +1946,7 @@ class Playwright extends Helper {
|
|
|
1928
1946
|
async grabValueFromAll(locator) {
|
|
1929
1947
|
const els = await findFields.call(this, locator);
|
|
1930
1948
|
this.debug(`Matched ${els.length} elements`);
|
|
1931
|
-
return Promise.all(els.map(el => el.
|
|
1949
|
+
return Promise.all(els.map(el => el.inputValue()));
|
|
1932
1950
|
}
|
|
1933
1951
|
|
|
1934
1952
|
/**
|
|
@@ -1947,7 +1965,7 @@ class Playwright extends Helper {
|
|
|
1947
1965
|
async grabHTMLFromAll(locator) {
|
|
1948
1966
|
const els = await this._locate(locator);
|
|
1949
1967
|
this.debug(`Matched ${els.length} elements`);
|
|
1950
|
-
return Promise.all(els.map(el => el
|
|
1968
|
+
return Promise.all(els.map(el => el.innerHTML()));
|
|
1951
1969
|
}
|
|
1952
1970
|
|
|
1953
1971
|
/**
|
|
@@ -1968,7 +1986,7 @@ class Playwright extends Helper {
|
|
|
1968
1986
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
1969
1987
|
const els = await this._locate(locator);
|
|
1970
1988
|
this.debug(`Matched ${els.length} elements`);
|
|
1971
|
-
const cssValues = await Promise.all(els.map(el => el
|
|
1989
|
+
const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
|
|
1972
1990
|
|
|
1973
1991
|
return cssValues;
|
|
1974
1992
|
}
|
|
@@ -1984,21 +2002,20 @@ class Playwright extends Helper {
|
|
|
1984
2002
|
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
|
|
1985
2003
|
const elemAmount = res.length;
|
|
1986
2004
|
const commands = [];
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
}));
|
|
2005
|
+
let props = [];
|
|
2006
|
+
|
|
2007
|
+
for (const element of res) {
|
|
2008
|
+
const cssProperties = await element.evaluate((el) => getComputedStyle(el));
|
|
2009
|
+
|
|
2010
|
+
Object.keys(cssPropertiesCamelCase).forEach(prop => {
|
|
2011
|
+
if (isColorProperty(prop)) {
|
|
2012
|
+
props.push(convertColorToRGBA(cssProperties[prop]));
|
|
2013
|
+
} else {
|
|
2014
|
+
props.push(cssProperties[prop]);
|
|
2015
|
+
}
|
|
1999
2016
|
});
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2002
2019
|
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
|
|
2003
2020
|
if (!Array.isArray(props)) props = [props];
|
|
2004
2021
|
let chunked = chunkArray(props, values.length);
|
|
@@ -2024,7 +2041,7 @@ class Playwright extends Helper {
|
|
|
2024
2041
|
res.forEach((el) => {
|
|
2025
2042
|
Object.keys(attributes).forEach((prop) => {
|
|
2026
2043
|
commands.push(el
|
|
2027
|
-
|
|
2044
|
+
.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop));
|
|
2028
2045
|
});
|
|
2029
2046
|
});
|
|
2030
2047
|
let attrs = await Promise.all(commands);
|
|
@@ -2045,11 +2062,11 @@ class Playwright extends Helper {
|
|
|
2045
2062
|
*
|
|
2046
2063
|
*/
|
|
2047
2064
|
async dragSlider(locator, offsetX = 0) {
|
|
2048
|
-
const src = await this.
|
|
2065
|
+
const src = await this._locateElement(locator);
|
|
2049
2066
|
assertElementExists(src, locator, 'Slider Element');
|
|
2050
2067
|
|
|
2051
2068
|
// Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets!
|
|
2052
|
-
const sliderSource = await clickablePoint(src
|
|
2069
|
+
const sliderSource = await clickablePoint(src);
|
|
2053
2070
|
|
|
2054
2071
|
// Drag start point
|
|
2055
2072
|
await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
|
|
@@ -2083,8 +2100,7 @@ class Playwright extends Helper {
|
|
|
2083
2100
|
const array = [];
|
|
2084
2101
|
|
|
2085
2102
|
for (let index = 0; index < els.length; index++) {
|
|
2086
|
-
|
|
2087
|
-
array.push(await a.jsonValue());
|
|
2103
|
+
array.push(await els[index].getAttribute(attr));
|
|
2088
2104
|
}
|
|
2089
2105
|
|
|
2090
2106
|
return array;
|
|
@@ -2097,10 +2113,9 @@ class Playwright extends Helper {
|
|
|
2097
2113
|
async saveElementScreenshot(locator, fileName) {
|
|
2098
2114
|
const outputFile = screenshotOutputFolder(fileName);
|
|
2099
2115
|
|
|
2100
|
-
const res = await this.
|
|
2116
|
+
const res = await this._locateElement(locator);
|
|
2101
2117
|
assertElementExists(res, locator);
|
|
2102
|
-
|
|
2103
|
-
const elem = res[0];
|
|
2118
|
+
const elem = res;
|
|
2104
2119
|
this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
|
|
2105
2120
|
return elem.screenshot({ path: outputFile, type: 'png' });
|
|
2106
2121
|
}
|
|
@@ -2526,16 +2541,26 @@ class Playwright extends Helper {
|
|
|
2526
2541
|
}
|
|
2527
2542
|
return;
|
|
2528
2543
|
}
|
|
2544
|
+
let contentFrame;
|
|
2545
|
+
|
|
2529
2546
|
if (!locator) {
|
|
2530
|
-
this.context = this.page;
|
|
2547
|
+
this.context = await this.page.frames()[0];
|
|
2531
2548
|
this.contextLocator = null;
|
|
2532
2549
|
return;
|
|
2533
2550
|
}
|
|
2534
2551
|
|
|
2535
2552
|
// iframe by selector
|
|
2536
2553
|
const els = await this._locate(locator);
|
|
2537
|
-
assertElementExists(els, locator);
|
|
2538
|
-
|
|
2554
|
+
// assertElementExists(els, locator);
|
|
2555
|
+
|
|
2556
|
+
// get content of the first iframe
|
|
2557
|
+
if ((locator.frame && locator.frame === 'iframe') || locator.toLowerCase() === 'iframe') {
|
|
2558
|
+
contentFrame = await this.page.frames()[1];
|
|
2559
|
+
// get content of the iframe using its name
|
|
2560
|
+
} else if (locator.toLowerCase().includes('name=')) {
|
|
2561
|
+
const frameName = locator.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
|
|
2562
|
+
contentFrame = await this.page.frame(frameName);
|
|
2563
|
+
}
|
|
2539
2564
|
|
|
2540
2565
|
if (contentFrame) {
|
|
2541
2566
|
this.context = contentFrame;
|
|
@@ -2564,13 +2589,15 @@ class Playwright extends Helper {
|
|
|
2564
2589
|
}
|
|
2565
2590
|
|
|
2566
2591
|
/**
|
|
2567
|
-
* Waits for navigation to finish. By default takes configured `waitForNavigation` option.
|
|
2592
|
+
* Waits for navigation to finish. By default, it takes configured `waitForNavigation` option.
|
|
2568
2593
|
*
|
|
2569
2594
|
* See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
|
|
2570
2595
|
*
|
|
2571
2596
|
* @param {*} options
|
|
2572
2597
|
*/
|
|
2573
2598
|
async waitForNavigation(options = {}) {
|
|
2599
|
+
console.log(`waitForNavigation deprecated:
|
|
2600
|
+
* This method is inherently racy, please use 'waitForURL' instead.`);
|
|
2574
2601
|
options = {
|
|
2575
2602
|
timeout: this.options.getPageTimeout,
|
|
2576
2603
|
waitUntil: this.options.waitForNavigation,
|
|
@@ -2579,6 +2606,23 @@ class Playwright extends Helper {
|
|
|
2579
2606
|
return this.page.waitForNavigation(options);
|
|
2580
2607
|
}
|
|
2581
2608
|
|
|
2609
|
+
/**
|
|
2610
|
+
* Waits for page navigates to a new URL or reloads. By default, it takes configured `waitForNavigation` option.
|
|
2611
|
+
*
|
|
2612
|
+
* See [Playwright's reference](https://playwright.dev/docs/api/class-page#page-wait-for-url)
|
|
2613
|
+
*
|
|
2614
|
+
* @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.
|
|
2615
|
+
* @param {*} options
|
|
2616
|
+
*/
|
|
2617
|
+
async waitForURL(url, options = {}) {
|
|
2618
|
+
options = {
|
|
2619
|
+
timeout: this.options.getPageTimeout,
|
|
2620
|
+
waitUntil: this.options.waitForNavigation,
|
|
2621
|
+
...options,
|
|
2622
|
+
};
|
|
2623
|
+
return this.page.waitForURL(url, options);
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2582
2626
|
async waitUntilExists(locator, sec) {
|
|
2583
2627
|
console.log(`waitUntilExists deprecated:
|
|
2584
2628
|
* use 'waitForElement' to wait for element to be attached
|
|
@@ -2624,9 +2668,9 @@ class Playwright extends Helper {
|
|
|
2624
2668
|
* {{> grabElementBoundingRect }}
|
|
2625
2669
|
*/
|
|
2626
2670
|
async grabElementBoundingRect(locator, prop) {
|
|
2627
|
-
const
|
|
2628
|
-
assertElementExists(
|
|
2629
|
-
const rect = await
|
|
2671
|
+
const el = await this._locateElement(locator);
|
|
2672
|
+
assertElementExists(el, locator);
|
|
2673
|
+
const rect = await el.boundingBox();
|
|
2630
2674
|
if (prop) return rect[prop];
|
|
2631
2675
|
return rect;
|
|
2632
2676
|
}
|
|
@@ -2663,16 +2707,16 @@ class Playwright extends Helper {
|
|
|
2663
2707
|
}
|
|
2664
2708
|
|
|
2665
2709
|
/**
|
|
2666
|
-
* Starts recording
|
|
2710
|
+
* Starts recording the network traffics.
|
|
2667
2711
|
* This also resets recorded network requests.
|
|
2668
2712
|
*
|
|
2669
2713
|
* ```js
|
|
2670
2714
|
* I.startRecordingTraffic();
|
|
2671
2715
|
* ```
|
|
2672
2716
|
*
|
|
2673
|
-
* @return {
|
|
2717
|
+
* @return {void}
|
|
2674
2718
|
*/
|
|
2675
|
-
|
|
2719
|
+
startRecordingTraffic() {
|
|
2676
2720
|
this.flushNetworkTraffics();
|
|
2677
2721
|
this.recording = true;
|
|
2678
2722
|
this.recordedAtLeastOnce = true;
|
|
@@ -2683,31 +2727,62 @@ class Playwright extends Helper {
|
|
|
2683
2727
|
method: request.method(),
|
|
2684
2728
|
requestHeaders: request.headers(),
|
|
2685
2729
|
requestPostData: request.postData(),
|
|
2730
|
+
response: request.response(),
|
|
2686
2731
|
};
|
|
2687
2732
|
|
|
2688
2733
|
this.debugSection('REQUEST: ', JSON.stringify(information));
|
|
2689
2734
|
|
|
2690
|
-
information.requestPostData
|
|
2735
|
+
if (typeof information.requestPostData === 'object') {
|
|
2736
|
+
information.requestPostData = JSON.parse(information.requestPostData);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2691
2739
|
this.requests.push(information);
|
|
2692
|
-
return this._waitForAction();
|
|
2693
2740
|
});
|
|
2694
2741
|
}
|
|
2695
2742
|
|
|
2696
2743
|
/**
|
|
2697
2744
|
* Grab the recording network traffics
|
|
2698
2745
|
*
|
|
2699
|
-
*
|
|
2746
|
+
* ```js
|
|
2747
|
+
* const traffics = await I.grabRecordedNetworkTraffics();
|
|
2748
|
+
* expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1');
|
|
2749
|
+
* expect(traffics[0].response.status).to.equal(200);
|
|
2750
|
+
* expect(traffics[0].response.body).to.contain({ name: 'this was mocked' });
|
|
2751
|
+
* ```
|
|
2752
|
+
*
|
|
2753
|
+
* @return { Promise<Array<any>> }
|
|
2700
2754
|
*
|
|
2701
2755
|
*/
|
|
2702
|
-
grabRecordedNetworkTraffics() {
|
|
2756
|
+
async grabRecordedNetworkTraffics() {
|
|
2703
2757
|
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
2704
2758
|
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
2705
2759
|
}
|
|
2760
|
+
|
|
2761
|
+
const requests = await this.requests;
|
|
2762
|
+
const promises = requests.map(async (request) => request.response.then(
|
|
2763
|
+
async (response) => {
|
|
2764
|
+
let body;
|
|
2765
|
+
try {
|
|
2766
|
+
// There's no 'body' for some requests (redirect etc...)
|
|
2767
|
+
body = JSON.parse((await response.body()).toString());
|
|
2768
|
+
} catch (e) {
|
|
2769
|
+
// only interested in JSON, not HTML responses.
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
request.response = {
|
|
2773
|
+
status: response.status(),
|
|
2774
|
+
statusText: response.statusText(),
|
|
2775
|
+
body,
|
|
2776
|
+
};
|
|
2777
|
+
},
|
|
2778
|
+
));
|
|
2779
|
+
await Promise.all(promises);
|
|
2780
|
+
|
|
2706
2781
|
return this.requests;
|
|
2707
2782
|
}
|
|
2708
2783
|
|
|
2709
2784
|
/**
|
|
2710
|
-
* Blocks traffic
|
|
2785
|
+
* Blocks traffic of a given URL or a list of URLs.
|
|
2711
2786
|
*
|
|
2712
2787
|
* Examples:
|
|
2713
2788
|
*
|
|
@@ -2718,16 +2793,30 @@ class Playwright extends Helper {
|
|
|
2718
2793
|
* I.blockTraffic(/\.css$/);
|
|
2719
2794
|
* ```
|
|
2720
2795
|
*
|
|
2721
|
-
*
|
|
2796
|
+
* ```js
|
|
2797
|
+
* I.blockTraffic(['http://example.com/css/style.css', 'http://example.com/css/*.css']);
|
|
2798
|
+
* ```
|
|
2799
|
+
*
|
|
2800
|
+
* @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.
|
|
2722
2801
|
*/
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
.
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2802
|
+
blockTraffic(urls) {
|
|
2803
|
+
if (Array.isArray(urls)) {
|
|
2804
|
+
urls.forEach(url => {
|
|
2805
|
+
this.page.route(url, (route) => {
|
|
2806
|
+
route
|
|
2807
|
+
.abort()
|
|
2808
|
+
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
2809
|
+
.catch((e) => {});
|
|
2810
|
+
});
|
|
2811
|
+
});
|
|
2812
|
+
} else {
|
|
2813
|
+
this.page.route(urls, (route) => {
|
|
2814
|
+
route
|
|
2815
|
+
.abort()
|
|
2816
|
+
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
2817
|
+
.catch((e) => {});
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2731
2820
|
}
|
|
2732
2821
|
|
|
2733
2822
|
/**
|
|
@@ -2746,7 +2835,7 @@ class Playwright extends Helper {
|
|
|
2746
2835
|
* @param responseString string The string to return in fake response's body.
|
|
2747
2836
|
* @param contentType Content type of fake response. If not specified default value 'application/json' is used.
|
|
2748
2837
|
*/
|
|
2749
|
-
|
|
2838
|
+
mockTraffic(urls, responseString, contentType = 'application/json') {
|
|
2750
2839
|
// Required to mock cross-domain requests
|
|
2751
2840
|
const headers = { 'access-control-allow-origin': '*' };
|
|
2752
2841
|
|
|
@@ -2768,7 +2857,6 @@ class Playwright extends Helper {
|
|
|
2768
2857
|
});
|
|
2769
2858
|
});
|
|
2770
2859
|
});
|
|
2771
|
-
return this._waitForAction();
|
|
2772
2860
|
}
|
|
2773
2861
|
|
|
2774
2862
|
/**
|
|
@@ -2840,7 +2928,7 @@ class Playwright extends Helper {
|
|
|
2840
2928
|
}
|
|
2841
2929
|
|
|
2842
2930
|
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
2843
|
-
throw new Error('Failure in test automation. You use "I.
|
|
2931
|
+
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
2844
2932
|
}
|
|
2845
2933
|
|
|
2846
2934
|
for (let i = 0; i <= timeout * 2; i++) {
|
|
@@ -2848,7 +2936,9 @@ class Playwright extends Helper {
|
|
|
2848
2936
|
if (found) {
|
|
2849
2937
|
return true;
|
|
2850
2938
|
}
|
|
2851
|
-
await new Promise((done) =>
|
|
2939
|
+
await new Promise((done) => {
|
|
2940
|
+
setTimeout(done, 1000);
|
|
2941
|
+
});
|
|
2852
2942
|
}
|
|
2853
2943
|
|
|
2854
2944
|
// check request post data
|
|
@@ -2985,6 +3075,163 @@ class Playwright extends Helper {
|
|
|
2985
3075
|
});
|
|
2986
3076
|
return dumpedTraffic;
|
|
2987
3077
|
}
|
|
3078
|
+
|
|
3079
|
+
/**
|
|
3080
|
+
* Starts recording of websocket messages.
|
|
3081
|
+
* This also resets recorded websocket messages.
|
|
3082
|
+
*
|
|
3083
|
+
* ```js
|
|
3084
|
+
* await I.startRecordingWebSocketMessages();
|
|
3085
|
+
* ```
|
|
3086
|
+
*
|
|
3087
|
+
*/
|
|
3088
|
+
async startRecordingWebSocketMessages() {
|
|
3089
|
+
this.flushWebSocketMessages();
|
|
3090
|
+
this.recordingWebSocketMessages = true;
|
|
3091
|
+
this.recordedWebSocketMessagesAtLeastOnce = true;
|
|
3092
|
+
|
|
3093
|
+
this.cdpSession = await this.getNewCDPSession();
|
|
3094
|
+
await this.cdpSession.send('Network.enable');
|
|
3095
|
+
await this.cdpSession.send('Page.enable');
|
|
3096
|
+
|
|
3097
|
+
this.cdpSession.on(
|
|
3098
|
+
'Network.webSocketFrameReceived',
|
|
3099
|
+
(payload) => {
|
|
3100
|
+
this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
|
|
3101
|
+
},
|
|
3102
|
+
);
|
|
3103
|
+
|
|
3104
|
+
this.cdpSession.on(
|
|
3105
|
+
'Network.webSocketFrameSent',
|
|
3106
|
+
(payload) => {
|
|
3107
|
+
this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
|
|
3108
|
+
},
|
|
3109
|
+
);
|
|
3110
|
+
|
|
3111
|
+
this.cdpSession.on(
|
|
3112
|
+
'Network.webSocketFrameError',
|
|
3113
|
+
(payload) => {
|
|
3114
|
+
this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload));
|
|
3115
|
+
},
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
/**
|
|
3120
|
+
* Stops recording WS messages. Recorded WS messages is not flashed.
|
|
3121
|
+
*
|
|
3122
|
+
* ```js
|
|
3123
|
+
* await I.stopRecordingWebSocketMessages();
|
|
3124
|
+
* ```
|
|
3125
|
+
*/
|
|
3126
|
+
async stopRecordingWebSocketMessages() {
|
|
3127
|
+
await this.cdpSession.send('Network.disable');
|
|
3128
|
+
await this.cdpSession.send('Page.disable');
|
|
3129
|
+
this.page.removeAllListeners('Network');
|
|
3130
|
+
this.recordingWebSocketMessages = false;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
/**
|
|
3134
|
+
* Grab the recording WS messages
|
|
3135
|
+
*
|
|
3136
|
+
* @return { Array<any> }
|
|
3137
|
+
*
|
|
3138
|
+
*/
|
|
3139
|
+
grabWebSocketMessages() {
|
|
3140
|
+
if (!this.recordingWebSocketMessages) {
|
|
3141
|
+
if (!this.recordedWebSocketMessagesAtLeastOnce) {
|
|
3142
|
+
throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.');
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
return this.webSocketMessages;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
/**
|
|
3149
|
+
* Resets all recorded WS messages.
|
|
3150
|
+
*/
|
|
3151
|
+
flushWebSocketMessages() {
|
|
3152
|
+
this.webSocketMessages = [];
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
/**
|
|
3156
|
+
* Return a performance metric from the chrome cdp session.
|
|
3157
|
+
* Note: Chrome-only
|
|
3158
|
+
*
|
|
3159
|
+
* Examples:
|
|
3160
|
+
*
|
|
3161
|
+
* ```js
|
|
3162
|
+
* const metrics = await I.grabMetrics();
|
|
3163
|
+
*
|
|
3164
|
+
* // returned metrics
|
|
3165
|
+
*
|
|
3166
|
+
* [
|
|
3167
|
+
* { name: 'Timestamp', value: 1584904.203473 },
|
|
3168
|
+
* { name: 'AudioHandlers', value: 0 },
|
|
3169
|
+
* { name: 'AudioWorkletProcessors', value: 0 },
|
|
3170
|
+
* { name: 'Documents', value: 22 },
|
|
3171
|
+
* { name: 'Frames', value: 10 },
|
|
3172
|
+
* { name: 'JSEventListeners', value: 366 },
|
|
3173
|
+
* { name: 'LayoutObjects', value: 1240 },
|
|
3174
|
+
* { name: 'MediaKeySessions', value: 0 },
|
|
3175
|
+
* { name: 'MediaKeys', value: 0 },
|
|
3176
|
+
* { name: 'Nodes', value: 4505 },
|
|
3177
|
+
* { name: 'Resources', value: 141 },
|
|
3178
|
+
* { name: 'ContextLifecycleStateObservers', value: 34 },
|
|
3179
|
+
* { name: 'V8PerContextDatas', value: 4 },
|
|
3180
|
+
* { name: 'WorkerGlobalScopes', value: 0 },
|
|
3181
|
+
* { name: 'UACSSResources', value: 0 },
|
|
3182
|
+
* { name: 'RTCPeerConnections', value: 0 },
|
|
3183
|
+
* { name: 'ResourceFetchers', value: 22 },
|
|
3184
|
+
* { name: 'AdSubframes', value: 0 },
|
|
3185
|
+
* { name: 'DetachedScriptStates', value: 2 },
|
|
3186
|
+
* { name: 'ArrayBufferContents', value: 1 },
|
|
3187
|
+
* { name: 'LayoutCount', value: 0 },
|
|
3188
|
+
* { name: 'RecalcStyleCount', value: 0 },
|
|
3189
|
+
* { name: 'LayoutDuration', value: 0 },
|
|
3190
|
+
* { name: 'RecalcStyleDuration', value: 0 },
|
|
3191
|
+
* { name: 'DevToolsCommandDuration', value: 0.000013 },
|
|
3192
|
+
* { name: 'ScriptDuration', value: 0 },
|
|
3193
|
+
* { name: 'V8CompileDuration', value: 0 },
|
|
3194
|
+
* { name: 'TaskDuration', value: 0.000014 },
|
|
3195
|
+
* { name: 'TaskOtherDuration', value: 0.000001 },
|
|
3196
|
+
* { name: 'ThreadTime', value: 0.000046 },
|
|
3197
|
+
* { name: 'ProcessTime', value: 0.616852 },
|
|
3198
|
+
* { name: 'JSHeapUsedSize', value: 19004908 },
|
|
3199
|
+
* { name: 'JSHeapTotalSize', value: 26820608 },
|
|
3200
|
+
* { name: 'FirstMeaningfulPaint', value: 0 },
|
|
3201
|
+
* { name: 'DomContentLoaded', value: 1584903.690491 },
|
|
3202
|
+
* { name: 'NavigationStart', value: 1584902.841845 }
|
|
3203
|
+
* ]
|
|
3204
|
+
*
|
|
3205
|
+
* ```
|
|
3206
|
+
*
|
|
3207
|
+
* @return {Promise<Array<Object>>}
|
|
3208
|
+
*/
|
|
3209
|
+
async grabMetrics() {
|
|
3210
|
+
const client = await this.page.context().newCDPSession(this.page);
|
|
3211
|
+
await client.send('Performance.enable');
|
|
3212
|
+
const perfMetricObject = await client.send('Performance.getMetrics');
|
|
3213
|
+
return perfMetricObject?.metrics;
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
_getWebSocketMessage(payload) {
|
|
3217
|
+
if (payload.errorMessage) {
|
|
3218
|
+
return payload.errorMessage;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
return payload.response.payloadData;
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
_getWebSocketLog(prefix, payload) {
|
|
3225
|
+
return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`;
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
async getNewCDPSession() {
|
|
3229
|
+
return this.page.context().newCDPSession(this.page);
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
_logWebsocketMessages(message) {
|
|
3233
|
+
this.webSocketMessages += message;
|
|
3234
|
+
}
|
|
2988
3235
|
}
|
|
2989
3236
|
|
|
2990
3237
|
module.exports = Playwright;
|
|
@@ -2997,42 +3244,19 @@ function buildLocatorString(locator) {
|
|
|
2997
3244
|
}
|
|
2998
3245
|
return locator.simplify();
|
|
2999
3246
|
}
|
|
3000
|
-
// TODO: locator change required after #3677 implementation. Temporary solution before migration. Should be deleted after #3677 implementation
|
|
3001
|
-
async function getXPathForElement(elementHandle) {
|
|
3002
|
-
function calculateIndex(node) {
|
|
3003
|
-
let index = 1;
|
|
3004
|
-
let sibling = node.previousElementSibling;
|
|
3005
|
-
while (sibling) {
|
|
3006
|
-
if (sibling.tagName === node.tagName) {
|
|
3007
|
-
index++;
|
|
3008
|
-
}
|
|
3009
|
-
sibling = sibling.previousElementSibling;
|
|
3010
|
-
}
|
|
3011
|
-
return index;
|
|
3012
|
-
}
|
|
3013
3247
|
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
if (node.hasAttribute('id')) {
|
|
3018
|
-
segments.unshift(`*[@id="${node.getAttribute('id')}"]`);
|
|
3019
|
-
break;
|
|
3020
|
-
} else {
|
|
3021
|
-
const index = calculateIndex(node);
|
|
3022
|
-
segments.unshift(`${node.localName}[${index}]`);
|
|
3023
|
-
node = node.parentNode;
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
return `//${segments.join('/')}`;
|
|
3027
|
-
}
|
|
3248
|
+
async function findElements(matcher, locator) {
|
|
3249
|
+
if (locator.react) return findReact(matcher, locator);
|
|
3250
|
+
locator = new Locator(locator, 'css');
|
|
3028
3251
|
|
|
3029
|
-
return
|
|
3252
|
+
return matcher.locator(buildLocatorString(locator)).all();
|
|
3030
3253
|
}
|
|
3031
3254
|
|
|
3032
|
-
async function
|
|
3255
|
+
async function findElement(matcher, locator) {
|
|
3033
3256
|
if (locator.react) return findReact(matcher, locator);
|
|
3034
3257
|
locator = new Locator(locator, 'css');
|
|
3035
|
-
|
|
3258
|
+
|
|
3259
|
+
return matcher.locator(buildLocatorString(locator));
|
|
3036
3260
|
}
|
|
3037
3261
|
|
|
3038
3262
|
async function getVisibleElements(elements) {
|
|
@@ -3062,8 +3286,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
3062
3286
|
assertElementExists(els, locator, 'Clickable element');
|
|
3063
3287
|
}
|
|
3064
3288
|
|
|
3065
|
-
|
|
3066
|
-
highlightActiveElement.call(this, els[0], this.page);
|
|
3289
|
+
highlightActiveElement.call(this, els[0], await this._getContext());
|
|
3067
3290
|
|
|
3068
3291
|
/*
|
|
3069
3292
|
using the force true options itself but instead dispatching a click
|
|
@@ -3076,7 +3299,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
3076
3299
|
}
|
|
3077
3300
|
const promises = [];
|
|
3078
3301
|
if (options.waitForNavigation) {
|
|
3079
|
-
promises.push(this.waitForNavigation
|
|
3302
|
+
promises.push(this.waitForURL(/.*/, { waitUntil: options.waitForNavigation }));
|
|
3080
3303
|
}
|
|
3081
3304
|
promises.push(this._waitForAction());
|
|
3082
3305
|
|
|
@@ -3111,28 +3334,28 @@ async function findClickable(matcher, locator) {
|
|
|
3111
3334
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
3112
3335
|
let description;
|
|
3113
3336
|
let allText;
|
|
3337
|
+
|
|
3114
3338
|
if (!context) {
|
|
3115
3339
|
let el = await this.context;
|
|
3116
|
-
|
|
3117
3340
|
if (el && !el.getProperty) {
|
|
3118
3341
|
// Fallback to body
|
|
3119
|
-
el = await this.
|
|
3342
|
+
el = await this.page.$('body');
|
|
3120
3343
|
}
|
|
3121
3344
|
|
|
3122
|
-
allText = [await el.
|
|
3345
|
+
allText = [await el.innerText()];
|
|
3123
3346
|
description = 'web application';
|
|
3124
3347
|
} else {
|
|
3125
3348
|
const locator = new Locator(context, 'css');
|
|
3126
3349
|
description = `element ${locator.toString()}`;
|
|
3127
3350
|
const els = await this._locate(locator);
|
|
3128
3351
|
assertElementExists(els, locator.toString());
|
|
3129
|
-
allText = await Promise.all(els.map(el => el.
|
|
3352
|
+
allText = await Promise.all(els.map(el => el.innerText()));
|
|
3130
3353
|
}
|
|
3131
3354
|
|
|
3132
3355
|
if (strict) {
|
|
3133
3356
|
return allText.map(elText => equals(description)[assertType](text, elText));
|
|
3134
3357
|
}
|
|
3135
|
-
return stringIncludes(description)[assertType](text, allText.join(' | '));
|
|
3358
|
+
return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
|
|
3136
3359
|
}
|
|
3137
3360
|
|
|
3138
3361
|
async function findCheckable(locator, context) {
|
|
@@ -3194,15 +3417,15 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
3194
3417
|
const els = await findFields.call(this, field);
|
|
3195
3418
|
assertElementExists(els, field, 'Field');
|
|
3196
3419
|
const el = els[0];
|
|
3197
|
-
const tag = await el.
|
|
3198
|
-
const fieldType = await el.
|
|
3420
|
+
const tag = await el.evaluate(e => e.tagName);
|
|
3421
|
+
const fieldType = await el.getAttribute('type');
|
|
3199
3422
|
|
|
3200
3423
|
const proceedMultiple = async (elements) => {
|
|
3201
3424
|
const fields = Array.isArray(elements) ? elements : [elements];
|
|
3202
3425
|
|
|
3203
3426
|
const elementValues = [];
|
|
3204
3427
|
for (const element of fields) {
|
|
3205
|
-
elementValues.push(await element.
|
|
3428
|
+
elementValues.push(await element.inputValue());
|
|
3206
3429
|
}
|
|
3207
3430
|
|
|
3208
3431
|
if (typeof value === 'boolean') {
|
|
@@ -3216,8 +3439,8 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
3216
3439
|
};
|
|
3217
3440
|
|
|
3218
3441
|
if (tag === 'SELECT') {
|
|
3219
|
-
if (await el.
|
|
3220
|
-
const selectedOptions = await el
|
|
3442
|
+
if (await el.getAttribute('multiple')) {
|
|
3443
|
+
const selectedOptions = await el.all('option:checked');
|
|
3221
3444
|
if (!selectedOptions.length) return null;
|
|
3222
3445
|
|
|
3223
3446
|
const options = await filterFieldsByValue(selectedOptions, value, true);
|
|
@@ -3241,14 +3464,23 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
3241
3464
|
return proceedMultiple(els[0]);
|
|
3242
3465
|
}
|
|
3243
3466
|
|
|
3244
|
-
|
|
3467
|
+
let fieldVal;
|
|
3468
|
+
|
|
3469
|
+
try {
|
|
3470
|
+
fieldVal = await el.inputValue();
|
|
3471
|
+
} catch (e) {
|
|
3472
|
+
if (e.message.includes('Error: Node is not an <input>, <textarea> or <select> element')) {
|
|
3473
|
+
fieldVal = await el.innerText();
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3245
3477
|
return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal);
|
|
3246
3478
|
}
|
|
3247
3479
|
|
|
3248
3480
|
async function filterFieldsByValue(elements, value, onlySelected) {
|
|
3249
3481
|
const matches = [];
|
|
3250
3482
|
for (const element of elements) {
|
|
3251
|
-
const val = await element.
|
|
3483
|
+
const val = await element.getAttribute('value');
|
|
3252
3484
|
let isSelected = true;
|
|
3253
3485
|
if (onlySelected) {
|
|
3254
3486
|
isSelected = await elementSelected(element);
|
|
@@ -3272,12 +3504,12 @@ async function filterFieldsBySelectionState(elements, state) {
|
|
|
3272
3504
|
}
|
|
3273
3505
|
|
|
3274
3506
|
async function elementSelected(element) {
|
|
3275
|
-
const type = await element.
|
|
3507
|
+
const type = await element.getAttribute('type');
|
|
3276
3508
|
|
|
3277
3509
|
if (type === 'checkbox' || type === 'radio') {
|
|
3278
3510
|
return element.isChecked();
|
|
3279
3511
|
}
|
|
3280
|
-
return element.
|
|
3512
|
+
return element.getAttribute('selected');
|
|
3281
3513
|
}
|
|
3282
3514
|
|
|
3283
3515
|
function isFrameLocator(locator) {
|
|
@@ -3478,7 +3710,7 @@ async function saveTraceForContext(context, name) {
|
|
|
3478
3710
|
}
|
|
3479
3711
|
|
|
3480
3712
|
function highlightActiveElement(element, context) {
|
|
3481
|
-
if (!this.options.
|
|
3713
|
+
if (!this.options.highlightElement && !store.debugMode) return;
|
|
3482
3714
|
|
|
3483
3715
|
highlightElement(element, context);
|
|
3484
3716
|
}
|