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/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 = {
|
|
@@ -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;
|
|
@@ -763,7 +794,7 @@ class Playwright extends Helper {
|
|
|
763
794
|
}
|
|
764
795
|
throw err;
|
|
765
796
|
}
|
|
766
|
-
} else if (this.userDataDir) {
|
|
797
|
+
} else if (this.playwrightOptions.userDataDir) {
|
|
767
798
|
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
|
|
768
799
|
} else {
|
|
769
800
|
this.browser = await playwright[this.options.browser].launch(this.playwrightOptions);
|
|
@@ -819,8 +850,8 @@ class Playwright extends Helper {
|
|
|
819
850
|
await this.switchTo(null);
|
|
820
851
|
return frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve());
|
|
821
852
|
}
|
|
822
|
-
await this.switchTo(
|
|
823
|
-
this.withinLocator = new Locator(
|
|
853
|
+
await this.switchTo(frame);
|
|
854
|
+
this.withinLocator = new Locator(frame);
|
|
824
855
|
return;
|
|
825
856
|
}
|
|
826
857
|
|
|
@@ -1038,8 +1069,11 @@ class Playwright extends Helper {
|
|
|
1038
1069
|
const body = document.body;
|
|
1039
1070
|
const html = document.documentElement;
|
|
1040
1071
|
window.scrollTo(0, Math.max(
|
|
1041
|
-
body.scrollHeight,
|
|
1042
|
-
|
|
1072
|
+
body.scrollHeight,
|
|
1073
|
+
body.offsetHeight,
|
|
1074
|
+
html.clientHeight,
|
|
1075
|
+
html.scrollHeight,
|
|
1076
|
+
html.offsetHeight,
|
|
1043
1077
|
));
|
|
1044
1078
|
});
|
|
1045
1079
|
}
|
|
@@ -1545,7 +1579,7 @@ class Playwright extends Helper {
|
|
|
1545
1579
|
|
|
1546
1580
|
await el.clear();
|
|
1547
1581
|
|
|
1548
|
-
highlightActiveElement.call(this, el, this.
|
|
1582
|
+
highlightActiveElement.call(this, el, await this._getContext());
|
|
1549
1583
|
|
|
1550
1584
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1551
1585
|
|
|
@@ -1590,7 +1624,7 @@ class Playwright extends Helper {
|
|
|
1590
1624
|
async appendField(field, value) {
|
|
1591
1625
|
const els = await findFields.call(this, field);
|
|
1592
1626
|
assertElementExists(els, field, 'Field');
|
|
1593
|
-
highlightActiveElement.call(this, els[0], this.
|
|
1627
|
+
highlightActiveElement.call(this, els[0], await this._getContext());
|
|
1594
1628
|
await els[0].press('End');
|
|
1595
1629
|
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1596
1630
|
return this._waitForAction();
|
|
@@ -1600,14 +1634,16 @@ class Playwright extends Helper {
|
|
|
1600
1634
|
* {{> seeInField }}
|
|
1601
1635
|
*/
|
|
1602
1636
|
async seeInField(field, value) {
|
|
1603
|
-
|
|
1637
|
+
const _value = (typeof value === 'boolean') ? value : value.toString();
|
|
1638
|
+
return proceedSeeInField.call(this, 'assert', field, _value);
|
|
1604
1639
|
}
|
|
1605
1640
|
|
|
1606
1641
|
/**
|
|
1607
1642
|
* {{> dontSeeInField }}
|
|
1608
1643
|
*/
|
|
1609
1644
|
async dontSeeInField(field, value) {
|
|
1610
|
-
|
|
1645
|
+
const _value = (typeof value === 'boolean') ? value : value.toString();
|
|
1646
|
+
return proceedSeeInField.call(this, 'negate', field, _value);
|
|
1611
1647
|
}
|
|
1612
1648
|
|
|
1613
1649
|
/**
|
|
@@ -1634,7 +1670,8 @@ class Playwright extends Helper {
|
|
|
1634
1670
|
assertElementExists(els, select, 'Selectable field');
|
|
1635
1671
|
const el = els[0];
|
|
1636
1672
|
|
|
1637
|
-
highlightActiveElement.call(this, el, this.
|
|
1673
|
+
highlightActiveElement.call(this, el, await this._getContext());
|
|
1674
|
+
|
|
1638
1675
|
if (!Array.isArray(option)) option = [option];
|
|
1639
1676
|
|
|
1640
1677
|
await el.selectOption(option);
|
|
@@ -2515,14 +2552,17 @@ class Playwright extends Helper {
|
|
|
2515
2552
|
|
|
2516
2553
|
// iframe by selector
|
|
2517
2554
|
const els = await this._locate(locator);
|
|
2518
|
-
|
|
2555
|
+
if (!els[0]) {
|
|
2556
|
+
throw new Error(`Element ${JSON.stringify(locator)} was not found by text|CSS|XPath`);
|
|
2557
|
+
}
|
|
2519
2558
|
|
|
2520
2559
|
// get content of the first iframe
|
|
2521
|
-
|
|
2560
|
+
locator = new Locator(locator, 'css');
|
|
2561
|
+
if ((locator.frame && locator.frame === 'iframe') || locator.value.toLowerCase() === 'iframe') {
|
|
2522
2562
|
contentFrame = await this.page.frames()[1];
|
|
2523
2563
|
// get content of the iframe using its name
|
|
2524
|
-
} else if (locator.toLowerCase().includes('name=')) {
|
|
2525
|
-
const frameName = locator.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
|
|
2564
|
+
} else if (locator.value.toLowerCase().includes('name=')) {
|
|
2565
|
+
const frameName = locator.value.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
|
|
2526
2566
|
contentFrame = await this.page.frame(frameName);
|
|
2527
2567
|
}
|
|
2528
2568
|
|
|
@@ -2530,7 +2570,7 @@ class Playwright extends Helper {
|
|
|
2530
2570
|
this.context = contentFrame;
|
|
2531
2571
|
this.contextLocator = null;
|
|
2532
2572
|
} else {
|
|
2533
|
-
this.context =
|
|
2573
|
+
this.context = this.page.frame(this.page.frames()[1].name());
|
|
2534
2574
|
this.contextLocator = locator;
|
|
2535
2575
|
}
|
|
2536
2576
|
}
|
|
@@ -2553,13 +2593,15 @@ class Playwright extends Helper {
|
|
|
2553
2593
|
}
|
|
2554
2594
|
|
|
2555
2595
|
/**
|
|
2556
|
-
* Waits for navigation to finish. By default takes configured `waitForNavigation` option.
|
|
2596
|
+
* Waits for navigation to finish. By default, it takes configured `waitForNavigation` option.
|
|
2557
2597
|
*
|
|
2558
2598
|
* See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
|
|
2559
2599
|
*
|
|
2560
2600
|
* @param {*} options
|
|
2561
2601
|
*/
|
|
2562
2602
|
async waitForNavigation(options = {}) {
|
|
2603
|
+
console.log(`waitForNavigation deprecated:
|
|
2604
|
+
* This method is inherently racy, please use 'waitForURL' instead.`);
|
|
2563
2605
|
options = {
|
|
2564
2606
|
timeout: this.options.getPageTimeout,
|
|
2565
2607
|
waitUntil: this.options.waitForNavigation,
|
|
@@ -2568,6 +2610,23 @@ class Playwright extends Helper {
|
|
|
2568
2610
|
return this.page.waitForNavigation(options);
|
|
2569
2611
|
}
|
|
2570
2612
|
|
|
2613
|
+
/**
|
|
2614
|
+
* Waits for page navigates to a new URL or reloads. By default, it takes configured `waitForNavigation` option.
|
|
2615
|
+
*
|
|
2616
|
+
* See [Playwright's reference](https://playwright.dev/docs/api/class-page#page-wait-for-url)
|
|
2617
|
+
*
|
|
2618
|
+
* @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.
|
|
2619
|
+
* @param {*} options
|
|
2620
|
+
*/
|
|
2621
|
+
async waitForURL(url, options = {}) {
|
|
2622
|
+
options = {
|
|
2623
|
+
timeout: this.options.getPageTimeout,
|
|
2624
|
+
waitUntil: this.options.waitForNavigation,
|
|
2625
|
+
...options,
|
|
2626
|
+
};
|
|
2627
|
+
return this.page.waitForURL(url, options);
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2571
2630
|
async waitUntilExists(locator, sec) {
|
|
2572
2631
|
console.log(`waitUntilExists deprecated:
|
|
2573
2632
|
* use 'waitForElement' to wait for element to be attached
|
|
@@ -2652,16 +2711,16 @@ class Playwright extends Helper {
|
|
|
2652
2711
|
}
|
|
2653
2712
|
|
|
2654
2713
|
/**
|
|
2655
|
-
* Starts recording
|
|
2714
|
+
* Starts recording the network traffics.
|
|
2656
2715
|
* This also resets recorded network requests.
|
|
2657
2716
|
*
|
|
2658
2717
|
* ```js
|
|
2659
2718
|
* I.startRecordingTraffic();
|
|
2660
2719
|
* ```
|
|
2661
2720
|
*
|
|
2662
|
-
* @return {
|
|
2721
|
+
* @return {void}
|
|
2663
2722
|
*/
|
|
2664
|
-
|
|
2723
|
+
startRecordingTraffic() {
|
|
2665
2724
|
this.flushNetworkTraffics();
|
|
2666
2725
|
this.recording = true;
|
|
2667
2726
|
this.recordedAtLeastOnce = true;
|
|
@@ -2672,31 +2731,62 @@ class Playwright extends Helper {
|
|
|
2672
2731
|
method: request.method(),
|
|
2673
2732
|
requestHeaders: request.headers(),
|
|
2674
2733
|
requestPostData: request.postData(),
|
|
2734
|
+
response: request.response(),
|
|
2675
2735
|
};
|
|
2676
2736
|
|
|
2677
2737
|
this.debugSection('REQUEST: ', JSON.stringify(information));
|
|
2678
2738
|
|
|
2679
|
-
information.requestPostData
|
|
2739
|
+
if (typeof information.requestPostData === 'object') {
|
|
2740
|
+
information.requestPostData = JSON.parse(information.requestPostData);
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2680
2743
|
this.requests.push(information);
|
|
2681
|
-
return this._waitForAction();
|
|
2682
2744
|
});
|
|
2683
2745
|
}
|
|
2684
2746
|
|
|
2685
2747
|
/**
|
|
2686
2748
|
* Grab the recording network traffics
|
|
2687
2749
|
*
|
|
2688
|
-
*
|
|
2750
|
+
* ```js
|
|
2751
|
+
* const traffics = await I.grabRecordedNetworkTraffics();
|
|
2752
|
+
* expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1');
|
|
2753
|
+
* expect(traffics[0].response.status).to.equal(200);
|
|
2754
|
+
* expect(traffics[0].response.body).to.contain({ name: 'this was mocked' });
|
|
2755
|
+
* ```
|
|
2756
|
+
*
|
|
2757
|
+
* @return { Promise<Array<any>> }
|
|
2689
2758
|
*
|
|
2690
2759
|
*/
|
|
2691
|
-
grabRecordedNetworkTraffics() {
|
|
2760
|
+
async grabRecordedNetworkTraffics() {
|
|
2692
2761
|
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
2693
2762
|
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
2694
2763
|
}
|
|
2764
|
+
|
|
2765
|
+
const requests = await this.requests;
|
|
2766
|
+
const promises = requests.map(async (request) => request.response.then(
|
|
2767
|
+
async (response) => {
|
|
2768
|
+
let body;
|
|
2769
|
+
try {
|
|
2770
|
+
// There's no 'body' for some requests (redirect etc...)
|
|
2771
|
+
body = JSON.parse((await response.body()).toString());
|
|
2772
|
+
} catch (e) {
|
|
2773
|
+
// only interested in JSON, not HTML responses.
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
request.response = {
|
|
2777
|
+
status: response.status(),
|
|
2778
|
+
statusText: response.statusText(),
|
|
2779
|
+
body,
|
|
2780
|
+
};
|
|
2781
|
+
},
|
|
2782
|
+
));
|
|
2783
|
+
await Promise.all(promises);
|
|
2784
|
+
|
|
2695
2785
|
return this.requests;
|
|
2696
2786
|
}
|
|
2697
2787
|
|
|
2698
2788
|
/**
|
|
2699
|
-
* Blocks traffic
|
|
2789
|
+
* Blocks traffic of a given URL or a list of URLs.
|
|
2700
2790
|
*
|
|
2701
2791
|
* Examples:
|
|
2702
2792
|
*
|
|
@@ -2707,16 +2797,30 @@ class Playwright extends Helper {
|
|
|
2707
2797
|
* I.blockTraffic(/\.css$/);
|
|
2708
2798
|
* ```
|
|
2709
2799
|
*
|
|
2710
|
-
*
|
|
2800
|
+
* ```js
|
|
2801
|
+
* I.blockTraffic(['http://example.com/css/style.css', 'http://example.com/css/*.css']);
|
|
2802
|
+
* ```
|
|
2803
|
+
*
|
|
2804
|
+
* @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.
|
|
2711
2805
|
*/
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
.
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2806
|
+
blockTraffic(urls) {
|
|
2807
|
+
if (Array.isArray(urls)) {
|
|
2808
|
+
urls.forEach(url => {
|
|
2809
|
+
this.page.route(url, (route) => {
|
|
2810
|
+
route
|
|
2811
|
+
.abort()
|
|
2812
|
+
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
2813
|
+
.catch((e) => {});
|
|
2814
|
+
});
|
|
2815
|
+
});
|
|
2816
|
+
} else {
|
|
2817
|
+
this.page.route(urls, (route) => {
|
|
2818
|
+
route
|
|
2819
|
+
.abort()
|
|
2820
|
+
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
2821
|
+
.catch((e) => {});
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2720
2824
|
}
|
|
2721
2825
|
|
|
2722
2826
|
/**
|
|
@@ -2735,7 +2839,7 @@ class Playwright extends Helper {
|
|
|
2735
2839
|
* @param responseString string The string to return in fake response's body.
|
|
2736
2840
|
* @param contentType Content type of fake response. If not specified default value 'application/json' is used.
|
|
2737
2841
|
*/
|
|
2738
|
-
|
|
2842
|
+
mockTraffic(urls, responseString, contentType = 'application/json') {
|
|
2739
2843
|
// Required to mock cross-domain requests
|
|
2740
2844
|
const headers = { 'access-control-allow-origin': '*' };
|
|
2741
2845
|
|
|
@@ -2757,7 +2861,6 @@ class Playwright extends Helper {
|
|
|
2757
2861
|
});
|
|
2758
2862
|
});
|
|
2759
2863
|
});
|
|
2760
|
-
return this._waitForAction();
|
|
2761
2864
|
}
|
|
2762
2865
|
|
|
2763
2866
|
/**
|
|
@@ -2829,7 +2932,7 @@ class Playwright extends Helper {
|
|
|
2829
2932
|
}
|
|
2830
2933
|
|
|
2831
2934
|
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
2832
|
-
throw new Error('Failure in test automation. You use "I.
|
|
2935
|
+
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
2833
2936
|
}
|
|
2834
2937
|
|
|
2835
2938
|
for (let i = 0; i <= timeout * 2; i++) {
|
|
@@ -2837,7 +2940,9 @@ class Playwright extends Helper {
|
|
|
2837
2940
|
if (found) {
|
|
2838
2941
|
return true;
|
|
2839
2942
|
}
|
|
2840
|
-
await new Promise((done) =>
|
|
2943
|
+
await new Promise((done) => {
|
|
2944
|
+
setTimeout(done, 1000);
|
|
2945
|
+
});
|
|
2841
2946
|
}
|
|
2842
2947
|
|
|
2843
2948
|
// check request post data
|
|
@@ -2974,6 +3079,163 @@ class Playwright extends Helper {
|
|
|
2974
3079
|
});
|
|
2975
3080
|
return dumpedTraffic;
|
|
2976
3081
|
}
|
|
3082
|
+
|
|
3083
|
+
/**
|
|
3084
|
+
* Starts recording of websocket messages.
|
|
3085
|
+
* This also resets recorded websocket messages.
|
|
3086
|
+
*
|
|
3087
|
+
* ```js
|
|
3088
|
+
* await I.startRecordingWebSocketMessages();
|
|
3089
|
+
* ```
|
|
3090
|
+
*
|
|
3091
|
+
*/
|
|
3092
|
+
async startRecordingWebSocketMessages() {
|
|
3093
|
+
this.flushWebSocketMessages();
|
|
3094
|
+
this.recordingWebSocketMessages = true;
|
|
3095
|
+
this.recordedWebSocketMessagesAtLeastOnce = true;
|
|
3096
|
+
|
|
3097
|
+
this.cdpSession = await this.getNewCDPSession();
|
|
3098
|
+
await this.cdpSession.send('Network.enable');
|
|
3099
|
+
await this.cdpSession.send('Page.enable');
|
|
3100
|
+
|
|
3101
|
+
this.cdpSession.on(
|
|
3102
|
+
'Network.webSocketFrameReceived',
|
|
3103
|
+
(payload) => {
|
|
3104
|
+
this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
|
|
3105
|
+
},
|
|
3106
|
+
);
|
|
3107
|
+
|
|
3108
|
+
this.cdpSession.on(
|
|
3109
|
+
'Network.webSocketFrameSent',
|
|
3110
|
+
(payload) => {
|
|
3111
|
+
this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
|
|
3112
|
+
},
|
|
3113
|
+
);
|
|
3114
|
+
|
|
3115
|
+
this.cdpSession.on(
|
|
3116
|
+
'Network.webSocketFrameError',
|
|
3117
|
+
(payload) => {
|
|
3118
|
+
this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload));
|
|
3119
|
+
},
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
/**
|
|
3124
|
+
* Stops recording WS messages. Recorded WS messages is not flashed.
|
|
3125
|
+
*
|
|
3126
|
+
* ```js
|
|
3127
|
+
* await I.stopRecordingWebSocketMessages();
|
|
3128
|
+
* ```
|
|
3129
|
+
*/
|
|
3130
|
+
async stopRecordingWebSocketMessages() {
|
|
3131
|
+
await this.cdpSession.send('Network.disable');
|
|
3132
|
+
await this.cdpSession.send('Page.disable');
|
|
3133
|
+
this.page.removeAllListeners('Network');
|
|
3134
|
+
this.recordingWebSocketMessages = false;
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
/**
|
|
3138
|
+
* Grab the recording WS messages
|
|
3139
|
+
*
|
|
3140
|
+
* @return { Array<any> }
|
|
3141
|
+
*
|
|
3142
|
+
*/
|
|
3143
|
+
grabWebSocketMessages() {
|
|
3144
|
+
if (!this.recordingWebSocketMessages) {
|
|
3145
|
+
if (!this.recordedWebSocketMessagesAtLeastOnce) {
|
|
3146
|
+
throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.');
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
return this.webSocketMessages;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
/**
|
|
3153
|
+
* Resets all recorded WS messages.
|
|
3154
|
+
*/
|
|
3155
|
+
flushWebSocketMessages() {
|
|
3156
|
+
this.webSocketMessages = [];
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
/**
|
|
3160
|
+
* Return a performance metric from the chrome cdp session.
|
|
3161
|
+
* Note: Chrome-only
|
|
3162
|
+
*
|
|
3163
|
+
* Examples:
|
|
3164
|
+
*
|
|
3165
|
+
* ```js
|
|
3166
|
+
* const metrics = await I.grabMetrics();
|
|
3167
|
+
*
|
|
3168
|
+
* // returned metrics
|
|
3169
|
+
*
|
|
3170
|
+
* [
|
|
3171
|
+
* { name: 'Timestamp', value: 1584904.203473 },
|
|
3172
|
+
* { name: 'AudioHandlers', value: 0 },
|
|
3173
|
+
* { name: 'AudioWorkletProcessors', value: 0 },
|
|
3174
|
+
* { name: 'Documents', value: 22 },
|
|
3175
|
+
* { name: 'Frames', value: 10 },
|
|
3176
|
+
* { name: 'JSEventListeners', value: 366 },
|
|
3177
|
+
* { name: 'LayoutObjects', value: 1240 },
|
|
3178
|
+
* { name: 'MediaKeySessions', value: 0 },
|
|
3179
|
+
* { name: 'MediaKeys', value: 0 },
|
|
3180
|
+
* { name: 'Nodes', value: 4505 },
|
|
3181
|
+
* { name: 'Resources', value: 141 },
|
|
3182
|
+
* { name: 'ContextLifecycleStateObservers', value: 34 },
|
|
3183
|
+
* { name: 'V8PerContextDatas', value: 4 },
|
|
3184
|
+
* { name: 'WorkerGlobalScopes', value: 0 },
|
|
3185
|
+
* { name: 'UACSSResources', value: 0 },
|
|
3186
|
+
* { name: 'RTCPeerConnections', value: 0 },
|
|
3187
|
+
* { name: 'ResourceFetchers', value: 22 },
|
|
3188
|
+
* { name: 'AdSubframes', value: 0 },
|
|
3189
|
+
* { name: 'DetachedScriptStates', value: 2 },
|
|
3190
|
+
* { name: 'ArrayBufferContents', value: 1 },
|
|
3191
|
+
* { name: 'LayoutCount', value: 0 },
|
|
3192
|
+
* { name: 'RecalcStyleCount', value: 0 },
|
|
3193
|
+
* { name: 'LayoutDuration', value: 0 },
|
|
3194
|
+
* { name: 'RecalcStyleDuration', value: 0 },
|
|
3195
|
+
* { name: 'DevToolsCommandDuration', value: 0.000013 },
|
|
3196
|
+
* { name: 'ScriptDuration', value: 0 },
|
|
3197
|
+
* { name: 'V8CompileDuration', value: 0 },
|
|
3198
|
+
* { name: 'TaskDuration', value: 0.000014 },
|
|
3199
|
+
* { name: 'TaskOtherDuration', value: 0.000001 },
|
|
3200
|
+
* { name: 'ThreadTime', value: 0.000046 },
|
|
3201
|
+
* { name: 'ProcessTime', value: 0.616852 },
|
|
3202
|
+
* { name: 'JSHeapUsedSize', value: 19004908 },
|
|
3203
|
+
* { name: 'JSHeapTotalSize', value: 26820608 },
|
|
3204
|
+
* { name: 'FirstMeaningfulPaint', value: 0 },
|
|
3205
|
+
* { name: 'DomContentLoaded', value: 1584903.690491 },
|
|
3206
|
+
* { name: 'NavigationStart', value: 1584902.841845 }
|
|
3207
|
+
* ]
|
|
3208
|
+
*
|
|
3209
|
+
* ```
|
|
3210
|
+
*
|
|
3211
|
+
* @return {Promise<Array<Object>>}
|
|
3212
|
+
*/
|
|
3213
|
+
async grabMetrics() {
|
|
3214
|
+
const client = await this.page.context().newCDPSession(this.page);
|
|
3215
|
+
await client.send('Performance.enable');
|
|
3216
|
+
const perfMetricObject = await client.send('Performance.getMetrics');
|
|
3217
|
+
return perfMetricObject?.metrics;
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
_getWebSocketMessage(payload) {
|
|
3221
|
+
if (payload.errorMessage) {
|
|
3222
|
+
return payload.errorMessage;
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
return payload.response.payloadData;
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
_getWebSocketLog(prefix, payload) {
|
|
3229
|
+
return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`;
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
async getNewCDPSession() {
|
|
3233
|
+
return this.page.context().newCDPSession(this.page);
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
_logWebsocketMessages(message) {
|
|
3237
|
+
this.webSocketMessages += message;
|
|
3238
|
+
}
|
|
2977
3239
|
}
|
|
2978
3240
|
|
|
2979
3241
|
module.exports = Playwright;
|
|
@@ -3028,7 +3290,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
3028
3290
|
assertElementExists(els, locator, 'Clickable element');
|
|
3029
3291
|
}
|
|
3030
3292
|
|
|
3031
|
-
highlightActiveElement.call(this, els[0], this.
|
|
3293
|
+
highlightActiveElement.call(this, els[0], await this._getContext());
|
|
3032
3294
|
|
|
3033
3295
|
/*
|
|
3034
3296
|
using the force true options itself but instead dispatching a click
|
|
@@ -3041,7 +3303,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
3041
3303
|
}
|
|
3042
3304
|
const promises = [];
|
|
3043
3305
|
if (options.waitForNavigation) {
|
|
3044
|
-
promises.push(this.waitForNavigation
|
|
3306
|
+
promises.push(this.waitForURL(/.*/, { waitUntil: options.waitForNavigation }));
|
|
3045
3307
|
}
|
|
3046
3308
|
promises.push(this._waitForAction());
|
|
3047
3309
|
|
|
@@ -3097,7 +3359,7 @@ async function proceedSee(assertType, text, context, strict = false) {
|
|
|
3097
3359
|
if (strict) {
|
|
3098
3360
|
return allText.map(elText => equals(description)[assertType](text, elText));
|
|
3099
3361
|
}
|
|
3100
|
-
return stringIncludes(description)[assertType](text, allText.join(' | '));
|
|
3362
|
+
return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
|
|
3101
3363
|
}
|
|
3102
3364
|
|
|
3103
3365
|
async function findCheckable(locator, context) {
|
|
@@ -3206,7 +3468,16 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
3206
3468
|
return proceedMultiple(els[0]);
|
|
3207
3469
|
}
|
|
3208
3470
|
|
|
3209
|
-
|
|
3471
|
+
let fieldVal;
|
|
3472
|
+
|
|
3473
|
+
try {
|
|
3474
|
+
fieldVal = await el.inputValue();
|
|
3475
|
+
} catch (e) {
|
|
3476
|
+
if (e.message.includes('Error: Node is not an <input>, <textarea> or <select> element')) {
|
|
3477
|
+
fieldVal = await el.innerText();
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3210
3481
|
return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal);
|
|
3211
3482
|
}
|
|
3212
3483
|
|
|
@@ -3247,7 +3518,10 @@ async function elementSelected(element) {
|
|
|
3247
3518
|
|
|
3248
3519
|
function isFrameLocator(locator) {
|
|
3249
3520
|
locator = new Locator(locator);
|
|
3250
|
-
if (locator.isFrame())
|
|
3521
|
+
if (locator.isFrame()) {
|
|
3522
|
+
const _locator = new Locator(locator.value);
|
|
3523
|
+
return _locator.value;
|
|
3524
|
+
}
|
|
3251
3525
|
return false;
|
|
3252
3526
|
}
|
|
3253
3527
|
|
|
@@ -3443,7 +3717,7 @@ async function saveTraceForContext(context, name) {
|
|
|
3443
3717
|
}
|
|
3444
3718
|
|
|
3445
3719
|
function highlightActiveElement(element, context) {
|
|
3446
|
-
if (!this.options.
|
|
3720
|
+
if (!this.options.highlightElement && !store.debugMode) return;
|
|
3447
3721
|
|
|
3448
3722
|
highlightElement(element, context);
|
|
3449
3723
|
}
|