codeceptjs 3.5.4-beta.1 → 3.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +368 -0
- package/README.md +0 -2
- package/docs/build/Appium.js +48 -7
- package/docs/build/GraphQL.js +25 -0
- package/docs/build/Nightmare.js +15 -6
- package/docs/build/Playwright.js +436 -197
- package/docs/build/Protractor.js +17 -8
- package/docs/build/Puppeteer.js +37 -20
- package/docs/build/TestCafe.js +19 -10
- package/docs/build/WebDriver.js +45 -37
- package/docs/changelog.md +375 -0
- package/docs/community-helpers.md +8 -4
- package/docs/examples.md +8 -2
- package/docs/helpers/Appium.md +39 -2
- package/docs/helpers/GraphQL.md +21 -0
- package/docs/helpers/Nightmare.md +1260 -0
- package/docs/helpers/Playwright.md +223 -119
- package/docs/helpers/Protractor.md +1711 -0
- package/docs/helpers/Puppeteer.md +31 -29
- package/docs/helpers/TestCafe.md +18 -17
- package/docs/helpers/WebDriver.md +34 -32
- package/docs/playwright.md +24 -1
- package/docs/webapi/dontSeeInField.mustache +1 -1
- package/docs/webapi/executeAsyncScript.mustache +2 -0
- package/docs/webapi/executeScript.mustache +2 -0
- 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/codecept.js +1 -0
- package/lib/command/definitions.js +2 -7
- package/lib/command/init.js +40 -4
- package/lib/command/run-multiple/collection.js +17 -5
- package/lib/command/run-workers.js +4 -0
- package/lib/command/run.js +6 -0
- package/lib/helper/Appium.js +46 -5
- package/lib/helper/GraphQL.js +25 -0
- package/lib/helper/Nightmare.js +1415 -0
- package/lib/helper/Playwright.js +336 -62
- package/lib/helper/Protractor.js +1837 -0
- package/lib/helper/Puppeteer.js +31 -18
- package/lib/helper/TestCafe.js +15 -8
- package/lib/helper/WebDriver.js +39 -35
- package/lib/helper/clientscripts/nightmare.js +213 -0
- package/lib/helper/errors/ElementNotFound.js +2 -1
- 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 +6 -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 +87 -87
- package/typings/promiseBasedTypes.d.ts +163 -126
- package/typings/types.d.ts +183 -144
- 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 = {
|
|
@@ -473,6 +482,7 @@ class Playwright extends Helper {
|
|
|
473
482
|
contextOptions.httpCredentials = this.options.basicAuth;
|
|
474
483
|
this.isAuthenticated = true;
|
|
475
484
|
}
|
|
485
|
+
if (this.options.bypassCSP) contextOptions.bypassCSP = this.options.bypassCSP;
|
|
476
486
|
if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo;
|
|
477
487
|
if (this.storageState) contextOptions.storageState = this.storageState;
|
|
478
488
|
if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent;
|
|
@@ -487,8 +497,17 @@ class Playwright extends Helper {
|
|
|
487
497
|
if (this.isElectron) {
|
|
488
498
|
mainPage = await this.browser.firstWindow();
|
|
489
499
|
} else {
|
|
490
|
-
|
|
491
|
-
|
|
500
|
+
try {
|
|
501
|
+
const existingPages = await this.browserContext.pages();
|
|
502
|
+
mainPage = existingPages[0] || await this.browserContext.newPage();
|
|
503
|
+
} catch (e) {
|
|
504
|
+
if (this.playwrightOptions.userDataDir) {
|
|
505
|
+
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
|
|
506
|
+
this.browserContext = this.browser;
|
|
507
|
+
const existingPages = await this.browserContext.pages();
|
|
508
|
+
mainPage = existingPages[0];
|
|
509
|
+
}
|
|
510
|
+
}
|
|
492
511
|
}
|
|
493
512
|
await targetCreatedHandler.call(this, mainPage);
|
|
494
513
|
|
|
@@ -519,13 +538,15 @@ class Playwright extends Helper {
|
|
|
519
538
|
|
|
520
539
|
// close other sessions
|
|
521
540
|
try {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
this.
|
|
526
|
-
|
|
541
|
+
if ((await this.browser)._type === 'Browser') {
|
|
542
|
+
const contexts = await this.browser.contexts();
|
|
543
|
+
const currentContext = contexts[0];
|
|
544
|
+
if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
|
|
545
|
+
this.storageState = await currentContext.storageState();
|
|
546
|
+
}
|
|
527
547
|
|
|
528
|
-
|
|
548
|
+
await Promise.all(contexts.map(c => c.close()));
|
|
549
|
+
}
|
|
529
550
|
} catch (e) {
|
|
530
551
|
console.log(e);
|
|
531
552
|
}
|
|
@@ -555,8 +576,16 @@ class Playwright extends Helper {
|
|
|
555
576
|
browserContext = browser.context();
|
|
556
577
|
page = await browser.firstWindow();
|
|
557
578
|
} else {
|
|
558
|
-
|
|
559
|
-
|
|
579
|
+
try {
|
|
580
|
+
browserContext = await this.browser.newContext(Object.assign(this.options, config));
|
|
581
|
+
page = await browserContext.newPage();
|
|
582
|
+
} catch (e) {
|
|
583
|
+
if (this.playwrightOptions.userDataDir) {
|
|
584
|
+
browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions);
|
|
585
|
+
this.browser = browserContext;
|
|
586
|
+
page = await browserContext.pages()[0];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
560
589
|
}
|
|
561
590
|
|
|
562
591
|
if (this.options.trace) await browserContext.tracing.start({ screenshots: true, snapshots: true });
|
|
@@ -569,10 +598,12 @@ class Playwright extends Helper {
|
|
|
569
598
|
// is closed by _after
|
|
570
599
|
},
|
|
571
600
|
loadVars: async (context) => {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
601
|
+
if (context) {
|
|
602
|
+
this.browserContext = context;
|
|
603
|
+
const existingPages = await context.pages();
|
|
604
|
+
this.sessionPages[this.activeSessionName] = existingPages[0];
|
|
605
|
+
return this._setPage(this.sessionPages[this.activeSessionName]);
|
|
606
|
+
}
|
|
576
607
|
},
|
|
577
608
|
restoreVars: async (session) => {
|
|
578
609
|
this.withinLocator = null;
|
|
@@ -771,7 +802,7 @@ class Playwright extends Helper {
|
|
|
771
802
|
}
|
|
772
803
|
throw err;
|
|
773
804
|
}
|
|
774
|
-
} else if (this.userDataDir) {
|
|
805
|
+
} else if (this.playwrightOptions.userDataDir) {
|
|
775
806
|
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
|
|
776
807
|
} else {
|
|
777
808
|
this.browser = await playwright[this.options.browser].launch(this.playwrightOptions);
|
|
@@ -827,14 +858,14 @@ class Playwright extends Helper {
|
|
|
827
858
|
await this.switchTo(null);
|
|
828
859
|
return frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve());
|
|
829
860
|
}
|
|
830
|
-
await this.switchTo(
|
|
831
|
-
this.withinLocator = new Locator(
|
|
861
|
+
await this.switchTo(frame);
|
|
862
|
+
this.withinLocator = new Locator(frame);
|
|
832
863
|
return;
|
|
833
864
|
}
|
|
834
865
|
|
|
835
|
-
const
|
|
836
|
-
assertElementExists(
|
|
837
|
-
this.context =
|
|
866
|
+
const el = await this._locateElement(locator);
|
|
867
|
+
assertElementExists(el, locator);
|
|
868
|
+
this.context = el;
|
|
838
869
|
this.contextLocator = locator;
|
|
839
870
|
|
|
840
871
|
this.withinLocator = new Locator(locator);
|
|
@@ -965,11 +996,11 @@ class Playwright extends Helper {
|
|
|
965
996
|
*
|
|
966
997
|
*/
|
|
967
998
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
968
|
-
const
|
|
969
|
-
assertElementExists(
|
|
999
|
+
const el = await this._locateElement(locator);
|
|
1000
|
+
assertElementExists(el, locator);
|
|
970
1001
|
|
|
971
1002
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
972
|
-
const { x, y } = await clickablePoint(
|
|
1003
|
+
const { x, y } = await clickablePoint(el);
|
|
973
1004
|
await this.page.mouse.move(x + offsetX, y + offsetY);
|
|
974
1005
|
return this._waitForAction();
|
|
975
1006
|
}
|
|
@@ -991,9 +1022,8 @@ class Playwright extends Helper {
|
|
|
991
1022
|
*
|
|
992
1023
|
*/
|
|
993
1024
|
async focus(locator, options = {}) {
|
|
994
|
-
const
|
|
995
|
-
assertElementExists(
|
|
996
|
-
const el = els[0];
|
|
1025
|
+
const el = await this._locateElement(locator);
|
|
1026
|
+
assertElementExists(el, locator, 'Element to focus');
|
|
997
1027
|
|
|
998
1028
|
await el.focus(options);
|
|
999
1029
|
return this._waitForAction();
|
|
@@ -1021,12 +1051,10 @@ class Playwright extends Helper {
|
|
|
1021
1051
|
*
|
|
1022
1052
|
*/
|
|
1023
1053
|
async blur(locator, options = {}) {
|
|
1024
|
-
const
|
|
1025
|
-
assertElementExists(
|
|
1026
|
-
// TODO: locator change required after #3677 implementation
|
|
1027
|
-
const elXpath = await getXPathForElement(els[0]);
|
|
1054
|
+
const el = await this._locateElement(locator);
|
|
1055
|
+
assertElementExists(el, locator, 'Element to blur');
|
|
1028
1056
|
|
|
1029
|
-
await
|
|
1057
|
+
await el.blur(options);
|
|
1030
1058
|
return this._waitForAction();
|
|
1031
1059
|
}
|
|
1032
1060
|
|
|
@@ -1133,8 +1161,11 @@ class Playwright extends Helper {
|
|
|
1133
1161
|
const body = document.body;
|
|
1134
1162
|
const html = document.documentElement;
|
|
1135
1163
|
window.scrollTo(0, Math.max(
|
|
1136
|
-
body.scrollHeight,
|
|
1137
|
-
|
|
1164
|
+
body.scrollHeight,
|
|
1165
|
+
body.offsetHeight,
|
|
1166
|
+
html.clientHeight,
|
|
1167
|
+
html.scrollHeight,
|
|
1168
|
+
html.offsetHeight,
|
|
1138
1169
|
));
|
|
1139
1170
|
});
|
|
1140
1171
|
}
|
|
@@ -1162,10 +1193,10 @@ class Playwright extends Helper {
|
|
|
1162
1193
|
}
|
|
1163
1194
|
|
|
1164
1195
|
if (locator) {
|
|
1165
|
-
const
|
|
1166
|
-
assertElementExists(
|
|
1167
|
-
await
|
|
1168
|
-
const elementCoordinates = await clickablePoint(
|
|
1196
|
+
const el = await this._locateElement(locator);
|
|
1197
|
+
assertElementExists(el, locator, 'Element');
|
|
1198
|
+
await el.scrollIntoViewIfNeeded();
|
|
1199
|
+
const elementCoordinates = await clickablePoint(el);
|
|
1169
1200
|
await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { offsetX: elementCoordinates.x + offsetX, offsetY: elementCoordinates.y + offsetY });
|
|
1170
1201
|
} else {
|
|
1171
1202
|
await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY });
|
|
@@ -1272,7 +1303,20 @@ class Playwright extends Helper {
|
|
|
1272
1303
|
}
|
|
1273
1304
|
|
|
1274
1305
|
/**
|
|
1275
|
-
*
|
|
1306
|
+
* Get the first element by different locator types, including strict locator
|
|
1307
|
+
* Should be used in custom helpers:
|
|
1308
|
+
*
|
|
1309
|
+
* ```js
|
|
1310
|
+
* const element = await this.helpers['Playwright']._locateElement({name: 'password'});
|
|
1311
|
+
* ```
|
|
1312
|
+
*/
|
|
1313
|
+
async _locateElement(locator) {
|
|
1314
|
+
const context = await this.context || await this._getContext();
|
|
1315
|
+
return findElement(context, locator);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Find a checkbox by providing human-readable text:
|
|
1276
1320
|
* NOTE: Assumes the checkable element exists
|
|
1277
1321
|
*
|
|
1278
1322
|
* ```js
|
|
@@ -1287,7 +1331,7 @@ class Playwright extends Helper {
|
|
|
1287
1331
|
}
|
|
1288
1332
|
|
|
1289
1333
|
/**
|
|
1290
|
-
* Find a clickable element by providing human
|
|
1334
|
+
* Find a clickable element by providing human-readable text:
|
|
1291
1335
|
*
|
|
1292
1336
|
* ```js
|
|
1293
1337
|
* this.helpers['Playwright']._locateClickable('Next page').then // ...
|
|
@@ -1299,7 +1343,7 @@ class Playwright extends Helper {
|
|
|
1299
1343
|
}
|
|
1300
1344
|
|
|
1301
1345
|
/**
|
|
1302
|
-
* Find field elements by providing human
|
|
1346
|
+
* Find field elements by providing human-readable text:
|
|
1303
1347
|
*
|
|
1304
1348
|
* ```js
|
|
1305
1349
|
* this.helpers['Playwright']._locateFields('Your email').then // ...
|
|
@@ -1962,15 +2006,10 @@ class Playwright extends Helper {
|
|
|
1962
2006
|
const els = await findFields.call(this, field);
|
|
1963
2007
|
assertElementExists(els, field, 'Field');
|
|
1964
2008
|
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
2009
|
|
|
1973
|
-
|
|
2010
|
+
await el.clear();
|
|
2011
|
+
|
|
2012
|
+
highlightActiveElement.call(this, el, await this._getContext());
|
|
1974
2013
|
|
|
1975
2014
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1976
2015
|
|
|
@@ -1995,21 +2034,16 @@ class Playwright extends Helper {
|
|
|
1995
2034
|
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
|
|
1996
2035
|
*/
|
|
1997
2036
|
async clearField(locator, options = {}) {
|
|
1998
|
-
|
|
1999
|
-
|
|
2037
|
+
const els = await findFields.call(this, locator);
|
|
2038
|
+
assertElementExists(els, locator, 'Field to clear');
|
|
2000
2039
|
|
|
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]);
|
|
2040
|
+
const el = els[0];
|
|
2006
2041
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
return result;
|
|
2042
|
+
highlightActiveElement.call(this, el, this.page);
|
|
2043
|
+
|
|
2044
|
+
await el.clear();
|
|
2045
|
+
|
|
2046
|
+
return this._waitForAction();
|
|
2013
2047
|
}
|
|
2014
2048
|
|
|
2015
2049
|
/**
|
|
@@ -2031,7 +2065,7 @@ class Playwright extends Helper {
|
|
|
2031
2065
|
async appendField(field, value) {
|
|
2032
2066
|
const els = await findFields.call(this, field);
|
|
2033
2067
|
assertElementExists(els, field, 'Field');
|
|
2034
|
-
highlightActiveElement.call(this, els[0], this.
|
|
2068
|
+
highlightActiveElement.call(this, els[0], await this._getContext());
|
|
2035
2069
|
await els[0].press('End');
|
|
2036
2070
|
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
2037
2071
|
return this._waitForAction();
|
|
@@ -2048,12 +2082,13 @@ class Playwright extends Helper {
|
|
|
2048
2082
|
* I.seeInField('#searchform input','Search');
|
|
2049
2083
|
* ```
|
|
2050
2084
|
* @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
|
|
2051
|
-
* @param {
|
|
2085
|
+
* @param {CodeceptJS.StringOrSecret} value value to check.
|
|
2052
2086
|
* ⚠️ returns a _promise_ which is synchronized internally by recorder
|
|
2053
2087
|
*
|
|
2054
2088
|
*/
|
|
2055
2089
|
async seeInField(field, value) {
|
|
2056
|
-
|
|
2090
|
+
const _value = (typeof value === 'boolean') ? value : value.toString();
|
|
2091
|
+
return proceedSeeInField.call(this, 'assert', field, _value);
|
|
2057
2092
|
}
|
|
2058
2093
|
|
|
2059
2094
|
/**
|
|
@@ -2066,12 +2101,13 @@ class Playwright extends Helper {
|
|
|
2066
2101
|
* ```
|
|
2067
2102
|
*
|
|
2068
2103
|
* @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
|
|
2069
|
-
* @param {
|
|
2104
|
+
* @param {CodeceptJS.StringOrSecret} value value to check.
|
|
2070
2105
|
* ⚠️ returns a _promise_ which is synchronized internally by recorder
|
|
2071
2106
|
*
|
|
2072
2107
|
*/
|
|
2073
2108
|
async dontSeeInField(field, value) {
|
|
2074
|
-
|
|
2109
|
+
const _value = (typeof value === 'boolean') ? value : value.toString();
|
|
2110
|
+
return proceedSeeInField.call(this, 'negate', field, _value);
|
|
2075
2111
|
}
|
|
2076
2112
|
|
|
2077
2113
|
/**
|
|
@@ -2130,29 +2166,12 @@ class Playwright extends Helper {
|
|
|
2130
2166
|
const els = await findFields.call(this, select);
|
|
2131
2167
|
assertElementExists(els, select, 'Selectable field');
|
|
2132
2168
|
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
2169
|
|
|
2139
|
-
|
|
2140
|
-
const opt = xpathLocator.literal(option[key]);
|
|
2141
|
-
let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) });
|
|
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);
|
|
2170
|
+
highlightActiveElement.call(this, el, await this._getContext());
|
|
2155
2171
|
|
|
2172
|
+
if (!Array.isArray(option)) option = [option];
|
|
2173
|
+
|
|
2174
|
+
await el.selectOption(option);
|
|
2156
2175
|
return this._waitForAction();
|
|
2157
2176
|
}
|
|
2158
2177
|
|
|
@@ -2598,7 +2617,7 @@ class Playwright extends Helper {
|
|
|
2598
2617
|
const els = await this._locate(locator);
|
|
2599
2618
|
const texts = [];
|
|
2600
2619
|
for (const el of els) {
|
|
2601
|
-
texts.push(await (await el.
|
|
2620
|
+
texts.push(await (await el.innerText()));
|
|
2602
2621
|
}
|
|
2603
2622
|
this.debug(`Matched ${els.length} elements`);
|
|
2604
2623
|
return texts;
|
|
@@ -2637,7 +2656,7 @@ class Playwright extends Helper {
|
|
|
2637
2656
|
async grabValueFromAll(locator) {
|
|
2638
2657
|
const els = await findFields.call(this, locator);
|
|
2639
2658
|
this.debug(`Matched ${els.length} elements`);
|
|
2640
|
-
return Promise.all(els.map(el => el.
|
|
2659
|
+
return Promise.all(els.map(el => el.inputValue()));
|
|
2641
2660
|
}
|
|
2642
2661
|
|
|
2643
2662
|
/**
|
|
@@ -2675,7 +2694,7 @@ class Playwright extends Helper {
|
|
|
2675
2694
|
async grabHTMLFromAll(locator) {
|
|
2676
2695
|
const els = await this._locate(locator);
|
|
2677
2696
|
this.debug(`Matched ${els.length} elements`);
|
|
2678
|
-
return Promise.all(els.map(el => el
|
|
2697
|
+
return Promise.all(els.map(el => el.innerHTML()));
|
|
2679
2698
|
}
|
|
2680
2699
|
|
|
2681
2700
|
/**
|
|
@@ -2717,7 +2736,7 @@ class Playwright extends Helper {
|
|
|
2717
2736
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
2718
2737
|
const els = await this._locate(locator);
|
|
2719
2738
|
this.debug(`Matched ${els.length} elements`);
|
|
2720
|
-
const cssValues = await Promise.all(els.map(el => el
|
|
2739
|
+
const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
|
|
2721
2740
|
|
|
2722
2741
|
return cssValues;
|
|
2723
2742
|
}
|
|
@@ -2742,21 +2761,20 @@ class Playwright extends Helper {
|
|
|
2742
2761
|
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
|
|
2743
2762
|
const elemAmount = res.length;
|
|
2744
2763
|
const commands = [];
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
}));
|
|
2764
|
+
let props = [];
|
|
2765
|
+
|
|
2766
|
+
for (const element of res) {
|
|
2767
|
+
const cssProperties = await element.evaluate((el) => getComputedStyle(el));
|
|
2768
|
+
|
|
2769
|
+
Object.keys(cssPropertiesCamelCase).forEach(prop => {
|
|
2770
|
+
if (isColorProperty(prop)) {
|
|
2771
|
+
props.push(convertColorToRGBA(cssProperties[prop]));
|
|
2772
|
+
} else {
|
|
2773
|
+
props.push(cssProperties[prop]);
|
|
2774
|
+
}
|
|
2757
2775
|
});
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2760
2778
|
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
|
|
2761
2779
|
if (!Array.isArray(props)) props = [props];
|
|
2762
2780
|
let chunked = chunkArray(props, values.length);
|
|
@@ -2791,7 +2809,7 @@ class Playwright extends Helper {
|
|
|
2791
2809
|
res.forEach((el) => {
|
|
2792
2810
|
Object.keys(attributes).forEach((prop) => {
|
|
2793
2811
|
commands.push(el
|
|
2794
|
-
|
|
2812
|
+
.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop));
|
|
2795
2813
|
});
|
|
2796
2814
|
});
|
|
2797
2815
|
let attrs = await Promise.all(commands);
|
|
@@ -2823,11 +2841,11 @@ class Playwright extends Helper {
|
|
|
2823
2841
|
*
|
|
2824
2842
|
*/
|
|
2825
2843
|
async dragSlider(locator, offsetX = 0) {
|
|
2826
|
-
const src = await this.
|
|
2844
|
+
const src = await this._locateElement(locator);
|
|
2827
2845
|
assertElementExists(src, locator, 'Slider Element');
|
|
2828
2846
|
|
|
2829
2847
|
// Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets!
|
|
2830
|
-
const sliderSource = await clickablePoint(src
|
|
2848
|
+
const sliderSource = await clickablePoint(src);
|
|
2831
2849
|
|
|
2832
2850
|
// Drag start point
|
|
2833
2851
|
await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
|
|
@@ -2880,8 +2898,7 @@ class Playwright extends Helper {
|
|
|
2880
2898
|
const array = [];
|
|
2881
2899
|
|
|
2882
2900
|
for (let index = 0; index < els.length; index++) {
|
|
2883
|
-
|
|
2884
|
-
array.push(await a.jsonValue());
|
|
2901
|
+
array.push(await els[index].getAttribute(attr));
|
|
2885
2902
|
}
|
|
2886
2903
|
|
|
2887
2904
|
return array;
|
|
@@ -2904,10 +2921,9 @@ class Playwright extends Helper {
|
|
|
2904
2921
|
async saveElementScreenshot(locator, fileName) {
|
|
2905
2922
|
const outputFile = screenshotOutputFolder(fileName);
|
|
2906
2923
|
|
|
2907
|
-
const res = await this.
|
|
2924
|
+
const res = await this._locateElement(locator);
|
|
2908
2925
|
assertElementExists(res, locator);
|
|
2909
|
-
|
|
2910
|
-
const elem = res[0];
|
|
2926
|
+
const elem = res;
|
|
2911
2927
|
this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
|
|
2912
2928
|
return elem.screenshot({ path: outputFile, type: 'png' });
|
|
2913
2929
|
}
|
|
@@ -3472,22 +3488,35 @@ class Playwright extends Helper {
|
|
|
3472
3488
|
}
|
|
3473
3489
|
return;
|
|
3474
3490
|
}
|
|
3491
|
+
let contentFrame;
|
|
3492
|
+
|
|
3475
3493
|
if (!locator) {
|
|
3476
|
-
this.context = this.page;
|
|
3494
|
+
this.context = await this.page.frames()[0];
|
|
3477
3495
|
this.contextLocator = null;
|
|
3478
3496
|
return;
|
|
3479
3497
|
}
|
|
3480
3498
|
|
|
3481
3499
|
// iframe by selector
|
|
3482
3500
|
const els = await this._locate(locator);
|
|
3483
|
-
|
|
3484
|
-
|
|
3501
|
+
if (!els[0]) {
|
|
3502
|
+
throw new Error(`Element ${JSON.stringify(locator)} was not found by text|CSS|XPath`);
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
// get content of the first iframe
|
|
3506
|
+
locator = new Locator(locator, 'css');
|
|
3507
|
+
if ((locator.frame && locator.frame === 'iframe') || locator.value.toLowerCase() === 'iframe') {
|
|
3508
|
+
contentFrame = await this.page.frames()[1];
|
|
3509
|
+
// get content of the iframe using its name
|
|
3510
|
+
} else if (locator.value.toLowerCase().includes('name=')) {
|
|
3511
|
+
const frameName = locator.value.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
|
|
3512
|
+
contentFrame = await this.page.frame(frameName);
|
|
3513
|
+
}
|
|
3485
3514
|
|
|
3486
3515
|
if (contentFrame) {
|
|
3487
3516
|
this.context = contentFrame;
|
|
3488
3517
|
this.contextLocator = null;
|
|
3489
3518
|
} else {
|
|
3490
|
-
this.context =
|
|
3519
|
+
this.context = this.page.frame(this.page.frames()[1].name());
|
|
3491
3520
|
this.contextLocator = locator;
|
|
3492
3521
|
}
|
|
3493
3522
|
}
|
|
@@ -3527,13 +3556,15 @@ class Playwright extends Helper {
|
|
|
3527
3556
|
}
|
|
3528
3557
|
|
|
3529
3558
|
/**
|
|
3530
|
-
* Waits for navigation to finish. By default takes configured `waitForNavigation` option.
|
|
3559
|
+
* Waits for navigation to finish. By default, it takes configured `waitForNavigation` option.
|
|
3531
3560
|
*
|
|
3532
3561
|
* See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
|
|
3533
3562
|
*
|
|
3534
3563
|
* @param {*} options
|
|
3535
3564
|
*/
|
|
3536
3565
|
async waitForNavigation(options = {}) {
|
|
3566
|
+
console.log(`waitForNavigation deprecated:
|
|
3567
|
+
* This method is inherently racy, please use 'waitForURL' instead.`);
|
|
3537
3568
|
options = {
|
|
3538
3569
|
timeout: this.options.getPageTimeout,
|
|
3539
3570
|
waitUntil: this.options.waitForNavigation,
|
|
@@ -3542,6 +3573,23 @@ class Playwright extends Helper {
|
|
|
3542
3573
|
return this.page.waitForNavigation(options);
|
|
3543
3574
|
}
|
|
3544
3575
|
|
|
3576
|
+
/**
|
|
3577
|
+
* Waits for page navigates to a new URL or reloads. By default, it takes configured `waitForNavigation` option.
|
|
3578
|
+
*
|
|
3579
|
+
* See [Playwright's reference](https://playwright.dev/docs/api/class-page#page-wait-for-url)
|
|
3580
|
+
*
|
|
3581
|
+
* @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.
|
|
3582
|
+
* @param {*} options
|
|
3583
|
+
*/
|
|
3584
|
+
async waitForURL(url, options = {}) {
|
|
3585
|
+
options = {
|
|
3586
|
+
timeout: this.options.getPageTimeout,
|
|
3587
|
+
waitUntil: this.options.waitForNavigation,
|
|
3588
|
+
...options,
|
|
3589
|
+
};
|
|
3590
|
+
return this.page.waitForURL(url, options);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3545
3593
|
async waitUntilExists(locator, sec) {
|
|
3546
3594
|
console.log(`waitUntilExists deprecated:
|
|
3547
3595
|
* use 'waitForElement' to wait for element to be attached
|
|
@@ -3636,9 +3684,9 @@ class Playwright extends Helper {
|
|
|
3636
3684
|
*
|
|
3637
3685
|
*/
|
|
3638
3686
|
async grabElementBoundingRect(locator, prop) {
|
|
3639
|
-
const
|
|
3640
|
-
assertElementExists(
|
|
3641
|
-
const rect = await
|
|
3687
|
+
const el = await this._locateElement(locator);
|
|
3688
|
+
assertElementExists(el, locator);
|
|
3689
|
+
const rect = await el.boundingBox();
|
|
3642
3690
|
if (prop) return rect[prop];
|
|
3643
3691
|
return rect;
|
|
3644
3692
|
}
|
|
@@ -3675,16 +3723,16 @@ class Playwright extends Helper {
|
|
|
3675
3723
|
}
|
|
3676
3724
|
|
|
3677
3725
|
/**
|
|
3678
|
-
* Starts recording
|
|
3726
|
+
* Starts recording the network traffics.
|
|
3679
3727
|
* This also resets recorded network requests.
|
|
3680
3728
|
*
|
|
3681
3729
|
* ```js
|
|
3682
3730
|
* I.startRecordingTraffic();
|
|
3683
3731
|
* ```
|
|
3684
3732
|
*
|
|
3685
|
-
* @return {
|
|
3733
|
+
* @return {void}
|
|
3686
3734
|
*/
|
|
3687
|
-
|
|
3735
|
+
startRecordingTraffic() {
|
|
3688
3736
|
this.flushNetworkTraffics();
|
|
3689
3737
|
this.recording = true;
|
|
3690
3738
|
this.recordedAtLeastOnce = true;
|
|
@@ -3695,31 +3743,62 @@ class Playwright extends Helper {
|
|
|
3695
3743
|
method: request.method(),
|
|
3696
3744
|
requestHeaders: request.headers(),
|
|
3697
3745
|
requestPostData: request.postData(),
|
|
3746
|
+
response: request.response(),
|
|
3698
3747
|
};
|
|
3699
3748
|
|
|
3700
3749
|
this.debugSection('REQUEST: ', JSON.stringify(information));
|
|
3701
3750
|
|
|
3702
|
-
information.requestPostData
|
|
3751
|
+
if (typeof information.requestPostData === 'object') {
|
|
3752
|
+
information.requestPostData = JSON.parse(information.requestPostData);
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3703
3755
|
this.requests.push(information);
|
|
3704
|
-
return this._waitForAction();
|
|
3705
3756
|
});
|
|
3706
3757
|
}
|
|
3707
3758
|
|
|
3708
3759
|
/**
|
|
3709
3760
|
* Grab the recording network traffics
|
|
3710
3761
|
*
|
|
3711
|
-
*
|
|
3762
|
+
* ```js
|
|
3763
|
+
* const traffics = await I.grabRecordedNetworkTraffics();
|
|
3764
|
+
* expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1');
|
|
3765
|
+
* expect(traffics[0].response.status).to.equal(200);
|
|
3766
|
+
* expect(traffics[0].response.body).to.contain({ name: 'this was mocked' });
|
|
3767
|
+
* ```
|
|
3768
|
+
*
|
|
3769
|
+
* @return { Promise<Array<any>> }
|
|
3712
3770
|
*
|
|
3713
3771
|
*/
|
|
3714
|
-
grabRecordedNetworkTraffics() {
|
|
3772
|
+
async grabRecordedNetworkTraffics() {
|
|
3715
3773
|
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
3716
3774
|
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
3717
3775
|
}
|
|
3776
|
+
|
|
3777
|
+
const requests = await this.requests;
|
|
3778
|
+
const promises = requests.map(async (request) => request.response.then(
|
|
3779
|
+
async (response) => {
|
|
3780
|
+
let body;
|
|
3781
|
+
try {
|
|
3782
|
+
// There's no 'body' for some requests (redirect etc...)
|
|
3783
|
+
body = JSON.parse((await response.body()).toString());
|
|
3784
|
+
} catch (e) {
|
|
3785
|
+
// only interested in JSON, not HTML responses.
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
request.response = {
|
|
3789
|
+
status: response.status(),
|
|
3790
|
+
statusText: response.statusText(),
|
|
3791
|
+
body,
|
|
3792
|
+
};
|
|
3793
|
+
},
|
|
3794
|
+
));
|
|
3795
|
+
await Promise.all(promises);
|
|
3796
|
+
|
|
3718
3797
|
return this.requests;
|
|
3719
3798
|
}
|
|
3720
3799
|
|
|
3721
3800
|
/**
|
|
3722
|
-
* Blocks traffic
|
|
3801
|
+
* Blocks traffic of a given URL or a list of URLs.
|
|
3723
3802
|
*
|
|
3724
3803
|
* Examples:
|
|
3725
3804
|
*
|
|
@@ -3730,16 +3809,30 @@ class Playwright extends Helper {
|
|
|
3730
3809
|
* I.blockTraffic(/\.css$/);
|
|
3731
3810
|
* ```
|
|
3732
3811
|
*
|
|
3733
|
-
*
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3812
|
+
* ```js
|
|
3813
|
+
* I.blockTraffic(['http://example.com/css/style.css', 'http://example.com/css/*.css']);
|
|
3814
|
+
* ```
|
|
3815
|
+
*
|
|
3816
|
+
* @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.
|
|
3817
|
+
*/
|
|
3818
|
+
blockTraffic(urls) {
|
|
3819
|
+
if (Array.isArray(urls)) {
|
|
3820
|
+
urls.forEach(url => {
|
|
3821
|
+
this.page.route(url, (route) => {
|
|
3822
|
+
route
|
|
3823
|
+
.abort()
|
|
3824
|
+
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
3825
|
+
.catch((e) => {});
|
|
3826
|
+
});
|
|
3827
|
+
});
|
|
3828
|
+
} else {
|
|
3829
|
+
this.page.route(urls, (route) => {
|
|
3830
|
+
route
|
|
3831
|
+
.abort()
|
|
3832
|
+
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
3833
|
+
.catch((e) => {});
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3743
3836
|
}
|
|
3744
3837
|
|
|
3745
3838
|
/**
|
|
@@ -3758,7 +3851,7 @@ class Playwright extends Helper {
|
|
|
3758
3851
|
* @param responseString string The string to return in fake response's body.
|
|
3759
3852
|
* @param contentType Content type of fake response. If not specified default value 'application/json' is used.
|
|
3760
3853
|
*/
|
|
3761
|
-
|
|
3854
|
+
mockTraffic(urls, responseString, contentType = 'application/json') {
|
|
3762
3855
|
// Required to mock cross-domain requests
|
|
3763
3856
|
const headers = { 'access-control-allow-origin': '*' };
|
|
3764
3857
|
|
|
@@ -3780,7 +3873,6 @@ class Playwright extends Helper {
|
|
|
3780
3873
|
});
|
|
3781
3874
|
});
|
|
3782
3875
|
});
|
|
3783
|
-
return this._waitForAction();
|
|
3784
3876
|
}
|
|
3785
3877
|
|
|
3786
3878
|
/**
|
|
@@ -3852,7 +3944,7 @@ class Playwright extends Helper {
|
|
|
3852
3944
|
}
|
|
3853
3945
|
|
|
3854
3946
|
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
3855
|
-
throw new Error('Failure in test automation. You use "I.
|
|
3947
|
+
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
3856
3948
|
}
|
|
3857
3949
|
|
|
3858
3950
|
for (let i = 0; i <= timeout * 2; i++) {
|
|
@@ -3860,7 +3952,9 @@ class Playwright extends Helper {
|
|
|
3860
3952
|
if (found) {
|
|
3861
3953
|
return true;
|
|
3862
3954
|
}
|
|
3863
|
-
await new Promise((done) =>
|
|
3955
|
+
await new Promise((done) => {
|
|
3956
|
+
setTimeout(done, 1000);
|
|
3957
|
+
});
|
|
3864
3958
|
}
|
|
3865
3959
|
|
|
3866
3960
|
// check request post data
|
|
@@ -3997,6 +4091,163 @@ class Playwright extends Helper {
|
|
|
3997
4091
|
});
|
|
3998
4092
|
return dumpedTraffic;
|
|
3999
4093
|
}
|
|
4094
|
+
|
|
4095
|
+
/**
|
|
4096
|
+
* Starts recording of websocket messages.
|
|
4097
|
+
* This also resets recorded websocket messages.
|
|
4098
|
+
*
|
|
4099
|
+
* ```js
|
|
4100
|
+
* await I.startRecordingWebSocketMessages();
|
|
4101
|
+
* ```
|
|
4102
|
+
*
|
|
4103
|
+
*/
|
|
4104
|
+
async startRecordingWebSocketMessages() {
|
|
4105
|
+
this.flushWebSocketMessages();
|
|
4106
|
+
this.recordingWebSocketMessages = true;
|
|
4107
|
+
this.recordedWebSocketMessagesAtLeastOnce = true;
|
|
4108
|
+
|
|
4109
|
+
this.cdpSession = await this.getNewCDPSession();
|
|
4110
|
+
await this.cdpSession.send('Network.enable');
|
|
4111
|
+
await this.cdpSession.send('Page.enable');
|
|
4112
|
+
|
|
4113
|
+
this.cdpSession.on(
|
|
4114
|
+
'Network.webSocketFrameReceived',
|
|
4115
|
+
(payload) => {
|
|
4116
|
+
this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
|
|
4117
|
+
},
|
|
4118
|
+
);
|
|
4119
|
+
|
|
4120
|
+
this.cdpSession.on(
|
|
4121
|
+
'Network.webSocketFrameSent',
|
|
4122
|
+
(payload) => {
|
|
4123
|
+
this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
|
|
4124
|
+
},
|
|
4125
|
+
);
|
|
4126
|
+
|
|
4127
|
+
this.cdpSession.on(
|
|
4128
|
+
'Network.webSocketFrameError',
|
|
4129
|
+
(payload) => {
|
|
4130
|
+
this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload));
|
|
4131
|
+
},
|
|
4132
|
+
);
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
/**
|
|
4136
|
+
* Stops recording WS messages. Recorded WS messages is not flashed.
|
|
4137
|
+
*
|
|
4138
|
+
* ```js
|
|
4139
|
+
* await I.stopRecordingWebSocketMessages();
|
|
4140
|
+
* ```
|
|
4141
|
+
*/
|
|
4142
|
+
async stopRecordingWebSocketMessages() {
|
|
4143
|
+
await this.cdpSession.send('Network.disable');
|
|
4144
|
+
await this.cdpSession.send('Page.disable');
|
|
4145
|
+
this.page.removeAllListeners('Network');
|
|
4146
|
+
this.recordingWebSocketMessages = false;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
/**
|
|
4150
|
+
* Grab the recording WS messages
|
|
4151
|
+
*
|
|
4152
|
+
* @return { Array<any> }
|
|
4153
|
+
*
|
|
4154
|
+
*/
|
|
4155
|
+
grabWebSocketMessages() {
|
|
4156
|
+
if (!this.recordingWebSocketMessages) {
|
|
4157
|
+
if (!this.recordedWebSocketMessagesAtLeastOnce) {
|
|
4158
|
+
throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.');
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
return this.webSocketMessages;
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
/**
|
|
4165
|
+
* Resets all recorded WS messages.
|
|
4166
|
+
*/
|
|
4167
|
+
flushWebSocketMessages() {
|
|
4168
|
+
this.webSocketMessages = [];
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
/**
|
|
4172
|
+
* Return a performance metric from the chrome cdp session.
|
|
4173
|
+
* Note: Chrome-only
|
|
4174
|
+
*
|
|
4175
|
+
* Examples:
|
|
4176
|
+
*
|
|
4177
|
+
* ```js
|
|
4178
|
+
* const metrics = await I.grabMetrics();
|
|
4179
|
+
*
|
|
4180
|
+
* // returned metrics
|
|
4181
|
+
*
|
|
4182
|
+
* [
|
|
4183
|
+
* { name: 'Timestamp', value: 1584904.203473 },
|
|
4184
|
+
* { name: 'AudioHandlers', value: 0 },
|
|
4185
|
+
* { name: 'AudioWorkletProcessors', value: 0 },
|
|
4186
|
+
* { name: 'Documents', value: 22 },
|
|
4187
|
+
* { name: 'Frames', value: 10 },
|
|
4188
|
+
* { name: 'JSEventListeners', value: 366 },
|
|
4189
|
+
* { name: 'LayoutObjects', value: 1240 },
|
|
4190
|
+
* { name: 'MediaKeySessions', value: 0 },
|
|
4191
|
+
* { name: 'MediaKeys', value: 0 },
|
|
4192
|
+
* { name: 'Nodes', value: 4505 },
|
|
4193
|
+
* { name: 'Resources', value: 141 },
|
|
4194
|
+
* { name: 'ContextLifecycleStateObservers', value: 34 },
|
|
4195
|
+
* { name: 'V8PerContextDatas', value: 4 },
|
|
4196
|
+
* { name: 'WorkerGlobalScopes', value: 0 },
|
|
4197
|
+
* { name: 'UACSSResources', value: 0 },
|
|
4198
|
+
* { name: 'RTCPeerConnections', value: 0 },
|
|
4199
|
+
* { name: 'ResourceFetchers', value: 22 },
|
|
4200
|
+
* { name: 'AdSubframes', value: 0 },
|
|
4201
|
+
* { name: 'DetachedScriptStates', value: 2 },
|
|
4202
|
+
* { name: 'ArrayBufferContents', value: 1 },
|
|
4203
|
+
* { name: 'LayoutCount', value: 0 },
|
|
4204
|
+
* { name: 'RecalcStyleCount', value: 0 },
|
|
4205
|
+
* { name: 'LayoutDuration', value: 0 },
|
|
4206
|
+
* { name: 'RecalcStyleDuration', value: 0 },
|
|
4207
|
+
* { name: 'DevToolsCommandDuration', value: 0.000013 },
|
|
4208
|
+
* { name: 'ScriptDuration', value: 0 },
|
|
4209
|
+
* { name: 'V8CompileDuration', value: 0 },
|
|
4210
|
+
* { name: 'TaskDuration', value: 0.000014 },
|
|
4211
|
+
* { name: 'TaskOtherDuration', value: 0.000001 },
|
|
4212
|
+
* { name: 'ThreadTime', value: 0.000046 },
|
|
4213
|
+
* { name: 'ProcessTime', value: 0.616852 },
|
|
4214
|
+
* { name: 'JSHeapUsedSize', value: 19004908 },
|
|
4215
|
+
* { name: 'JSHeapTotalSize', value: 26820608 },
|
|
4216
|
+
* { name: 'FirstMeaningfulPaint', value: 0 },
|
|
4217
|
+
* { name: 'DomContentLoaded', value: 1584903.690491 },
|
|
4218
|
+
* { name: 'NavigationStart', value: 1584902.841845 }
|
|
4219
|
+
* ]
|
|
4220
|
+
*
|
|
4221
|
+
* ```
|
|
4222
|
+
*
|
|
4223
|
+
* @return {Promise<Array<Object>>}
|
|
4224
|
+
*/
|
|
4225
|
+
async grabMetrics() {
|
|
4226
|
+
const client = await this.page.context().newCDPSession(this.page);
|
|
4227
|
+
await client.send('Performance.enable');
|
|
4228
|
+
const perfMetricObject = await client.send('Performance.getMetrics');
|
|
4229
|
+
return perfMetricObject?.metrics;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
_getWebSocketMessage(payload) {
|
|
4233
|
+
if (payload.errorMessage) {
|
|
4234
|
+
return payload.errorMessage;
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
return payload.response.payloadData;
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
_getWebSocketLog(prefix, payload) {
|
|
4241
|
+
return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`;
|
|
4242
|
+
}
|
|
4243
|
+
|
|
4244
|
+
async getNewCDPSession() {
|
|
4245
|
+
return this.page.context().newCDPSession(this.page);
|
|
4246
|
+
}
|
|
4247
|
+
|
|
4248
|
+
_logWebsocketMessages(message) {
|
|
4249
|
+
this.webSocketMessages += message;
|
|
4250
|
+
}
|
|
4000
4251
|
}
|
|
4001
4252
|
|
|
4002
4253
|
module.exports = Playwright;
|
|
@@ -4009,42 +4260,19 @@ function buildLocatorString(locator) {
|
|
|
4009
4260
|
}
|
|
4010
4261
|
return locator.simplify();
|
|
4011
4262
|
}
|
|
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
4263
|
|
|
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
|
-
}
|
|
4264
|
+
async function findElements(matcher, locator) {
|
|
4265
|
+
if (locator.react) return findReact(matcher, locator);
|
|
4266
|
+
locator = new Locator(locator, 'css');
|
|
4040
4267
|
|
|
4041
|
-
return
|
|
4268
|
+
return matcher.locator(buildLocatorString(locator)).all();
|
|
4042
4269
|
}
|
|
4043
4270
|
|
|
4044
|
-
async function
|
|
4271
|
+
async function findElement(matcher, locator) {
|
|
4045
4272
|
if (locator.react) return findReact(matcher, locator);
|
|
4046
4273
|
locator = new Locator(locator, 'css');
|
|
4047
|
-
|
|
4274
|
+
|
|
4275
|
+
return matcher.locator(buildLocatorString(locator));
|
|
4048
4276
|
}
|
|
4049
4277
|
|
|
4050
4278
|
async function getVisibleElements(elements) {
|
|
@@ -4074,8 +4302,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4074
4302
|
assertElementExists(els, locator, 'Clickable element');
|
|
4075
4303
|
}
|
|
4076
4304
|
|
|
4077
|
-
|
|
4078
|
-
highlightActiveElement.call(this, els[0], this.page);
|
|
4305
|
+
highlightActiveElement.call(this, els[0], await this._getContext());
|
|
4079
4306
|
|
|
4080
4307
|
/*
|
|
4081
4308
|
using the force true options itself but instead dispatching a click
|
|
@@ -4088,7 +4315,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4088
4315
|
}
|
|
4089
4316
|
const promises = [];
|
|
4090
4317
|
if (options.waitForNavigation) {
|
|
4091
|
-
promises.push(this.waitForNavigation
|
|
4318
|
+
promises.push(this.waitForURL(/.*/, { waitUntil: options.waitForNavigation }));
|
|
4092
4319
|
}
|
|
4093
4320
|
promises.push(this._waitForAction());
|
|
4094
4321
|
|
|
@@ -4123,28 +4350,28 @@ async function findClickable(matcher, locator) {
|
|
|
4123
4350
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
4124
4351
|
let description;
|
|
4125
4352
|
let allText;
|
|
4353
|
+
|
|
4126
4354
|
if (!context) {
|
|
4127
4355
|
let el = await this.context;
|
|
4128
|
-
|
|
4129
4356
|
if (el && !el.getProperty) {
|
|
4130
4357
|
// Fallback to body
|
|
4131
|
-
el = await this.
|
|
4358
|
+
el = await this.page.$('body');
|
|
4132
4359
|
}
|
|
4133
4360
|
|
|
4134
|
-
allText = [await el.
|
|
4361
|
+
allText = [await el.innerText()];
|
|
4135
4362
|
description = 'web application';
|
|
4136
4363
|
} else {
|
|
4137
4364
|
const locator = new Locator(context, 'css');
|
|
4138
4365
|
description = `element ${locator.toString()}`;
|
|
4139
4366
|
const els = await this._locate(locator);
|
|
4140
4367
|
assertElementExists(els, locator.toString());
|
|
4141
|
-
allText = await Promise.all(els.map(el => el.
|
|
4368
|
+
allText = await Promise.all(els.map(el => el.innerText()));
|
|
4142
4369
|
}
|
|
4143
4370
|
|
|
4144
4371
|
if (strict) {
|
|
4145
4372
|
return allText.map(elText => equals(description)[assertType](text, elText));
|
|
4146
4373
|
}
|
|
4147
|
-
return stringIncludes(description)[assertType](text, allText.join(' | '));
|
|
4374
|
+
return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
|
|
4148
4375
|
}
|
|
4149
4376
|
|
|
4150
4377
|
async function findCheckable(locator, context) {
|
|
@@ -4206,15 +4433,15 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
4206
4433
|
const els = await findFields.call(this, field);
|
|
4207
4434
|
assertElementExists(els, field, 'Field');
|
|
4208
4435
|
const el = els[0];
|
|
4209
|
-
const tag = await el.
|
|
4210
|
-
const fieldType = await el.
|
|
4436
|
+
const tag = await el.evaluate(e => e.tagName);
|
|
4437
|
+
const fieldType = await el.getAttribute('type');
|
|
4211
4438
|
|
|
4212
4439
|
const proceedMultiple = async (elements) => {
|
|
4213
4440
|
const fields = Array.isArray(elements) ? elements : [elements];
|
|
4214
4441
|
|
|
4215
4442
|
const elementValues = [];
|
|
4216
4443
|
for (const element of fields) {
|
|
4217
|
-
elementValues.push(await element.
|
|
4444
|
+
elementValues.push(await element.inputValue());
|
|
4218
4445
|
}
|
|
4219
4446
|
|
|
4220
4447
|
if (typeof value === 'boolean') {
|
|
@@ -4228,8 +4455,8 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
4228
4455
|
};
|
|
4229
4456
|
|
|
4230
4457
|
if (tag === 'SELECT') {
|
|
4231
|
-
if (await el.
|
|
4232
|
-
const selectedOptions = await el
|
|
4458
|
+
if (await el.getAttribute('multiple')) {
|
|
4459
|
+
const selectedOptions = await el.all('option:checked');
|
|
4233
4460
|
if (!selectedOptions.length) return null;
|
|
4234
4461
|
|
|
4235
4462
|
const options = await filterFieldsByValue(selectedOptions, value, true);
|
|
@@ -4253,14 +4480,23 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
4253
4480
|
return proceedMultiple(els[0]);
|
|
4254
4481
|
}
|
|
4255
4482
|
|
|
4256
|
-
|
|
4483
|
+
let fieldVal;
|
|
4484
|
+
|
|
4485
|
+
try {
|
|
4486
|
+
fieldVal = await el.inputValue();
|
|
4487
|
+
} catch (e) {
|
|
4488
|
+
if (e.message.includes('Error: Node is not an <input>, <textarea> or <select> element')) {
|
|
4489
|
+
fieldVal = await el.innerText();
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4257
4493
|
return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal);
|
|
4258
4494
|
}
|
|
4259
4495
|
|
|
4260
4496
|
async function filterFieldsByValue(elements, value, onlySelected) {
|
|
4261
4497
|
const matches = [];
|
|
4262
4498
|
for (const element of elements) {
|
|
4263
|
-
const val = await element.
|
|
4499
|
+
const val = await element.getAttribute('value');
|
|
4264
4500
|
let isSelected = true;
|
|
4265
4501
|
if (onlySelected) {
|
|
4266
4502
|
isSelected = await elementSelected(element);
|
|
@@ -4284,17 +4520,20 @@ async function filterFieldsBySelectionState(elements, state) {
|
|
|
4284
4520
|
}
|
|
4285
4521
|
|
|
4286
4522
|
async function elementSelected(element) {
|
|
4287
|
-
const type = await element.
|
|
4523
|
+
const type = await element.getAttribute('type');
|
|
4288
4524
|
|
|
4289
4525
|
if (type === 'checkbox' || type === 'radio') {
|
|
4290
4526
|
return element.isChecked();
|
|
4291
4527
|
}
|
|
4292
|
-
return element.
|
|
4528
|
+
return element.getAttribute('selected');
|
|
4293
4529
|
}
|
|
4294
4530
|
|
|
4295
4531
|
function isFrameLocator(locator) {
|
|
4296
4532
|
locator = new Locator(locator);
|
|
4297
|
-
if (locator.isFrame())
|
|
4533
|
+
if (locator.isFrame()) {
|
|
4534
|
+
const _locator = new Locator(locator.value);
|
|
4535
|
+
return _locator.value;
|
|
4536
|
+
}
|
|
4298
4537
|
return false;
|
|
4299
4538
|
}
|
|
4300
4539
|
|
|
@@ -4490,7 +4729,7 @@ async function saveTraceForContext(context, name) {
|
|
|
4490
4729
|
}
|
|
4491
4730
|
|
|
4492
4731
|
function highlightActiveElement(element, context) {
|
|
4493
|
-
if (!this.options.
|
|
4732
|
+
if (!this.options.highlightElement && !store.debugMode) return;
|
|
4494
4733
|
|
|
4495
4734
|
highlightElement(element, context);
|
|
4496
4735
|
}
|