codeceptjs 3.6.0-beta.1.ai-healers → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/codecept.js +2 -1
- package/docs/webapi/dontSeeTraffic.mustache +13 -0
- package/docs/webapi/flushNetworkTraffics.mustache +5 -0
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +10 -0
- package/docs/webapi/seeTraffic.mustache +36 -0
- package/docs/webapi/startRecordingTraffic.mustache +8 -0
- package/docs/webapi/startRecordingWebSocketMessages.mustache +8 -0
- package/docs/webapi/stopRecordingTraffic.mustache +5 -0
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +7 -0
- package/docs/webapi/waitForCookie.mustache +9 -0
- package/lib/actor.js +6 -3
- package/lib/command/dryRun.js +44 -13
- package/lib/helper/Appium.js +36 -12
- package/lib/helper/Expect.js +11 -8
- package/lib/helper/JSONResponse.js +8 -8
- package/lib/helper/MockServer.js +221 -0
- package/lib/helper/Playwright.js +107 -371
- package/lib/helper/Puppeteer.js +404 -71
- package/lib/helper/REST.js +4 -1
- package/lib/helper/WebDriver.js +189 -13
- package/lib/helper/errors/ElementAssertion.js +38 -0
- package/lib/helper/extras/PlaywrightReactVueLocator.js +6 -1
- package/lib/helper/network/actions.js +123 -0
- package/lib/helper/network/utils.js +187 -0
- package/lib/locator.js +36 -5
- package/lib/pause.js +4 -9
- package/lib/plugin/coverage.js +112 -99
- package/lib/step.js +3 -1
- package/package.json +49 -38
- package/typings/index.d.ts +19 -2
- package/typings/promiseBasedTypes.d.ts +505 -41
- package/typings/types.d.ts +531 -43
- package/docs/advanced.md +0 -351
- package/docs/ai.md +0 -365
- package/docs/api.md +0 -323
- package/docs/basics.md +0 -979
- package/docs/bdd.md +0 -539
- package/docs/best.md +0 -237
- package/docs/books.md +0 -37
- package/docs/bootstrap.md +0 -135
- package/docs/build/AI.js +0 -124
- package/docs/build/ApiDataFactory.js +0 -410
- package/docs/build/Appium.js +0 -2027
- package/docs/build/Expect.js +0 -422
- package/docs/build/FileSystem.js +0 -228
- package/docs/build/GraphQL.js +0 -229
- package/docs/build/GraphQLDataFactory.js +0 -309
- package/docs/build/JSONResponse.js +0 -338
- package/docs/build/Mochawesome.js +0 -71
- package/docs/build/Nightmare.js +0 -2152
- package/docs/build/OpenAI.js +0 -126
- package/docs/build/Playwright.js +0 -5110
- package/docs/build/Protractor.js +0 -2706
- package/docs/build/Puppeteer.js +0 -3905
- package/docs/build/REST.js +0 -344
- package/docs/build/TestCafe.js +0 -2125
- package/docs/build/WebDriver.js +0 -4240
- package/docs/changelog.md +0 -2572
- package/docs/commands.md +0 -266
- package/docs/community-helpers.md +0 -58
- package/docs/configuration.md +0 -157
- package/docs/continuous-integration.md +0 -22
- package/docs/custom-helpers.md +0 -306
- package/docs/data.md +0 -379
- package/docs/detox.md +0 -235
- package/docs/docker.md +0 -136
- package/docs/email.md +0 -183
- package/docs/examples.md +0 -149
- package/docs/heal.md +0 -186
- package/docs/helpers/ApiDataFactory.md +0 -266
- package/docs/helpers/Appium.md +0 -1374
- package/docs/helpers/Detox.md +0 -586
- package/docs/helpers/Expect.md +0 -275
- package/docs/helpers/FileSystem.md +0 -152
- package/docs/helpers/GraphQL.md +0 -151
- package/docs/helpers/GraphQLDataFactory.md +0 -226
- package/docs/helpers/JSONResponse.md +0 -254
- package/docs/helpers/Mochawesome.md +0 -8
- package/docs/helpers/MockRequest.md +0 -377
- package/docs/helpers/Nightmare.md +0 -1305
- package/docs/helpers/OpenAI.md +0 -70
- package/docs/helpers/Playwright.md +0 -2759
- package/docs/helpers/Polly.md +0 -44
- package/docs/helpers/Protractor.md +0 -1769
- package/docs/helpers/Puppeteer-firefox.md +0 -86
- package/docs/helpers/Puppeteer.md +0 -2317
- package/docs/helpers/REST.md +0 -218
- package/docs/helpers/TestCafe.md +0 -1321
- package/docs/helpers/WebDriver.md +0 -2547
- package/docs/hooks.md +0 -340
- package/docs/index.md +0 -111
- package/docs/installation.md +0 -75
- package/docs/internal-api.md +0 -266
- package/docs/locators.md +0 -339
- package/docs/mobile-react-native-locators.md +0 -67
- package/docs/mobile.md +0 -338
- package/docs/pageobjects.md +0 -291
- package/docs/parallel.md +0 -400
- package/docs/playwright.md +0 -632
- package/docs/plugins.md +0 -1247
- package/docs/puppeteer.md +0 -316
- package/docs/quickstart.md +0 -162
- package/docs/react.md +0 -70
- package/docs/reports.md +0 -392
- package/docs/secrets.md +0 -36
- package/docs/shadow.md +0 -68
- package/docs/shared/keys.mustache +0 -31
- package/docs/shared/react.mustache +0 -1
- package/docs/testcafe.md +0 -174
- package/docs/translation.md +0 -247
- package/docs/tutorial.md +0 -271
- package/docs/typescript.md +0 -180
- package/docs/ui.md +0 -59
- package/docs/videos.md +0 -28
- package/docs/visual.md +0 -202
- package/docs/vue.md +0 -143
- package/docs/webdriver.md +0 -701
- package/docs/wiki/Books-&-Posts.md +0 -27
- package/docs/wiki/Community-Helpers-&-Plugins.md +0 -53
- package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +0 -61
- package/docs/wiki/Examples.md +0 -145
- package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +0 -68
- package/docs/wiki/Home.md +0 -16
- package/docs/wiki/Migration-to-Appium-v2---CodeceptJS.md +0 -83
- package/docs/wiki/Release-Process.md +0 -24
- package/docs/wiki/Roadmap.md +0 -23
- package/docs/wiki/Tests.md +0 -1393
- package/docs/wiki/Upgrading-to-CodeceptJS-3.md +0 -153
- package/docs/wiki/Videos.md +0 -19
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
|
|
6
6
|
const Helper = require('@codeceptjs/helper');
|
|
7
7
|
const { v4: uuidv4 } = require('uuid');
|
|
8
|
+
const promiseRetry = require('promise-retry');
|
|
8
9
|
const Locator = require('../locator');
|
|
9
10
|
const recorder = require('../recorder');
|
|
10
11
|
const store = require('../store');
|
|
@@ -35,10 +36,14 @@ const ElementNotFound = require('./errors/ElementNotFound');
|
|
|
35
36
|
const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused');
|
|
36
37
|
const Popup = require('./extras/Popup');
|
|
37
38
|
const Console = require('./extras/Console');
|
|
38
|
-
const findReact = require('./extras/React');
|
|
39
39
|
const { highlightElement } = require('./scripts/highlightElement');
|
|
40
40
|
const { blurElement } = require('./scripts/blurElement');
|
|
41
|
-
const {
|
|
41
|
+
const {
|
|
42
|
+
dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError,
|
|
43
|
+
} = require('./errors/ElementAssertion');
|
|
44
|
+
const {
|
|
45
|
+
dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics,
|
|
46
|
+
} = require('./network/actions');
|
|
42
47
|
|
|
43
48
|
let puppeteer;
|
|
44
49
|
let perfTiming;
|
|
@@ -74,7 +79,7 @@ const consoleLogStore = new Console();
|
|
|
74
79
|
* @prop {string} [browser=chrome] - can be changed to `firefox` when using [puppeteer-firefox](https://codecept.io/helpers/Puppeteer-firefox).
|
|
75
80
|
* @prop {object} [chrome] - pass additional [Puppeteer run options](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions).
|
|
76
81
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
77
|
-
*/
|
|
82
|
+
*/
|
|
78
83
|
const config = {};
|
|
79
84
|
|
|
80
85
|
/**
|
|
@@ -222,6 +227,17 @@ class Puppeteer extends Helper {
|
|
|
222
227
|
this.sessionPages = {};
|
|
223
228
|
this.activeSessionName = '';
|
|
224
229
|
|
|
230
|
+
// for network stuff
|
|
231
|
+
this.requests = [];
|
|
232
|
+
this.recording = false;
|
|
233
|
+
this.recordedAtLeastOnce = false;
|
|
234
|
+
|
|
235
|
+
// for websocket messages
|
|
236
|
+
this.webSocketMessages = [];
|
|
237
|
+
this.recordingWebSocketMessages = false;
|
|
238
|
+
this.recordedWebSocketMessagesAtLeastOnce = false;
|
|
239
|
+
this.cdpSession = null;
|
|
240
|
+
|
|
225
241
|
// override defaults with config
|
|
226
242
|
this._setConfig(config);
|
|
227
243
|
}
|
|
@@ -365,7 +381,7 @@ class Puppeteer extends Helper {
|
|
|
365
381
|
this.debugSection('Incognito Tab', 'opened');
|
|
366
382
|
this.activeSessionName = name;
|
|
367
383
|
|
|
368
|
-
const bc = await this.browser.
|
|
384
|
+
const bc = await this.browser.createBrowserContext();
|
|
369
385
|
await bc.newPage();
|
|
370
386
|
|
|
371
387
|
// Create a new page inside context.
|
|
@@ -482,7 +498,7 @@ class Puppeteer extends Helper {
|
|
|
482
498
|
if (!page) return;
|
|
483
499
|
page.setDefaultNavigationTimeout(this.options.getPageTimeout);
|
|
484
500
|
this.context = await this.page.$('body');
|
|
485
|
-
if (this.
|
|
501
|
+
if (this.options.browser === 'chrome') {
|
|
486
502
|
await page.bringToFront();
|
|
487
503
|
}
|
|
488
504
|
}
|
|
@@ -595,14 +611,7 @@ class Puppeteer extends Helper {
|
|
|
595
611
|
}
|
|
596
612
|
|
|
597
613
|
async _evaluateHandeInContext(...args) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (context.constructor.name === 'Frame') {
|
|
601
|
-
// Currently there is no evalateHandle for the Frame object
|
|
602
|
-
// https://github.com/GoogleChrome/puppeteer/issues/1051
|
|
603
|
-
context = await context.executionContext();
|
|
604
|
-
}
|
|
605
|
-
|
|
614
|
+
const context = await this._getContext();
|
|
606
615
|
return context.evaluateHandle(...args);
|
|
607
616
|
}
|
|
608
617
|
|
|
@@ -654,9 +663,9 @@ class Puppeteer extends Helper {
|
|
|
654
663
|
url = this.options.url + url;
|
|
655
664
|
}
|
|
656
665
|
|
|
657
|
-
if (this.
|
|
666
|
+
if (this.options.basicAuth && (this.isAuthenticated !== true)) {
|
|
658
667
|
if (url.includes(this.options.url)) {
|
|
659
|
-
await this.page.authenticate(this.
|
|
668
|
+
await this.page.authenticate(this.options.basicAuth);
|
|
660
669
|
this.isAuthenticated = true;
|
|
661
670
|
}
|
|
662
671
|
}
|
|
@@ -880,7 +889,8 @@ class Puppeteer extends Helper {
|
|
|
880
889
|
* {{ react }}
|
|
881
890
|
*/
|
|
882
891
|
async _locate(locator) {
|
|
883
|
-
|
|
892
|
+
const context = await this.context;
|
|
893
|
+
return findElements.call(this, context, locator);
|
|
884
894
|
}
|
|
885
895
|
|
|
886
896
|
/**
|
|
@@ -906,7 +916,7 @@ class Puppeteer extends Helper {
|
|
|
906
916
|
* ```
|
|
907
917
|
*/
|
|
908
918
|
async _locateClickable(locator) {
|
|
909
|
-
const context = await this.
|
|
919
|
+
const context = await this.context;
|
|
910
920
|
return findClickable.call(this, context, locator);
|
|
911
921
|
}
|
|
912
922
|
|
|
@@ -1038,8 +1048,11 @@ class Puppeteer extends Helper {
|
|
|
1038
1048
|
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
|
|
1039
1049
|
// Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
|
|
1040
1050
|
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
|
|
1041
|
-
|
|
1042
|
-
|
|
1051
|
+
try {
|
|
1052
|
+
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'));
|
|
1053
|
+
} catch (e) {
|
|
1054
|
+
dontSeeElementError(locator);
|
|
1055
|
+
}
|
|
1043
1056
|
}
|
|
1044
1057
|
|
|
1045
1058
|
/**
|
|
@@ -1051,8 +1064,11 @@ class Puppeteer extends Helper {
|
|
|
1051
1064
|
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
|
|
1052
1065
|
// Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
|
|
1053
1066
|
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
|
|
1054
|
-
|
|
1055
|
-
|
|
1067
|
+
try {
|
|
1068
|
+
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'));
|
|
1069
|
+
} catch (e) {
|
|
1070
|
+
seeElementError(locator);
|
|
1071
|
+
}
|
|
1056
1072
|
}
|
|
1057
1073
|
|
|
1058
1074
|
/**
|
|
@@ -1060,7 +1076,11 @@ class Puppeteer extends Helper {
|
|
|
1060
1076
|
*/
|
|
1061
1077
|
async seeElementInDOM(locator) {
|
|
1062
1078
|
const els = await this._locate(locator);
|
|
1063
|
-
|
|
1079
|
+
try {
|
|
1080
|
+
return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'));
|
|
1081
|
+
} catch (e) {
|
|
1082
|
+
dontSeeElementInDOMError(locator);
|
|
1083
|
+
}
|
|
1064
1084
|
}
|
|
1065
1085
|
|
|
1066
1086
|
/**
|
|
@@ -1068,7 +1088,11 @@ class Puppeteer extends Helper {
|
|
|
1068
1088
|
*/
|
|
1069
1089
|
async dontSeeElementInDOM(locator) {
|
|
1070
1090
|
const els = await this._locate(locator);
|
|
1071
|
-
|
|
1091
|
+
try {
|
|
1092
|
+
return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'));
|
|
1093
|
+
} catch (e) {
|
|
1094
|
+
seeElementInDOMError(locator);
|
|
1095
|
+
}
|
|
1072
1096
|
}
|
|
1073
1097
|
|
|
1074
1098
|
/**
|
|
@@ -1618,6 +1642,38 @@ class Puppeteer extends Helper {
|
|
|
1618
1642
|
if (cookie[0]) return cookie[0];
|
|
1619
1643
|
}
|
|
1620
1644
|
|
|
1645
|
+
/**
|
|
1646
|
+
* {{> waitForCookie }}
|
|
1647
|
+
*/
|
|
1648
|
+
async waitForCookie(name, sec) {
|
|
1649
|
+
// by default, we will retry 3 times
|
|
1650
|
+
let retries = 3;
|
|
1651
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
1652
|
+
|
|
1653
|
+
if (sec) {
|
|
1654
|
+
retries = sec;
|
|
1655
|
+
} else {
|
|
1656
|
+
retries = Math.ceil(waitTimeout / 1000) - 1;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
return promiseRetry(async (retry, number) => {
|
|
1660
|
+
const _grabCookie = async (name) => {
|
|
1661
|
+
const cookies = await this.page.cookies();
|
|
1662
|
+
const cookie = cookies.filter(c => c.name === name);
|
|
1663
|
+
if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`);
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
this.debugSection('Wait for cookie: ', name);
|
|
1667
|
+
if (number > 1) this.debugSection('Retrying... Attempt #', number);
|
|
1668
|
+
|
|
1669
|
+
try {
|
|
1670
|
+
await _grabCookie(name);
|
|
1671
|
+
} catch (e) {
|
|
1672
|
+
retry(e);
|
|
1673
|
+
}
|
|
1674
|
+
}, { retries, maxTimeout: 1000 });
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1621
1677
|
/**
|
|
1622
1678
|
* {{> clearCookie }}
|
|
1623
1679
|
*/
|
|
@@ -1637,8 +1693,8 @@ class Puppeteer extends Helper {
|
|
|
1637
1693
|
* {{> executeScript }}
|
|
1638
1694
|
*/
|
|
1639
1695
|
async executeScript(...args) {
|
|
1640
|
-
let context = this.
|
|
1641
|
-
if (this.context && this.context.constructor.name === '
|
|
1696
|
+
let context = await this._getContext();
|
|
1697
|
+
if (this.context && this.context.constructor.name === 'CdpFrame') {
|
|
1642
1698
|
context = this.context; // switching to iframe context
|
|
1643
1699
|
}
|
|
1644
1700
|
return context.evaluate.apply(context, args);
|
|
@@ -1719,7 +1775,7 @@ class Puppeteer extends Helper {
|
|
|
1719
1775
|
*/
|
|
1720
1776
|
async grabHTMLFromAll(locator) {
|
|
1721
1777
|
const els = await this._locate(locator);
|
|
1722
|
-
const values = await Promise.all(els.map(el => el.
|
|
1778
|
+
const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML, el)));
|
|
1723
1779
|
return values;
|
|
1724
1780
|
}
|
|
1725
1781
|
|
|
@@ -1742,7 +1798,7 @@ class Puppeteer extends Helper {
|
|
|
1742
1798
|
*/
|
|
1743
1799
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
1744
1800
|
const els = await this._locate(locator);
|
|
1745
|
-
const res = await Promise.all(els.map(el => el.
|
|
1801
|
+
const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el))), el)));
|
|
1746
1802
|
const cssValues = res.map(props => props[toCamelCase(cssProperty)]);
|
|
1747
1803
|
|
|
1748
1804
|
return cssValues;
|
|
@@ -1804,33 +1860,34 @@ class Puppeteer extends Helper {
|
|
|
1804
1860
|
* {{ react }}
|
|
1805
1861
|
*/
|
|
1806
1862
|
async seeAttributesOnElements(locator, attributes) {
|
|
1807
|
-
const
|
|
1808
|
-
assertElementExists(
|
|
1863
|
+
const elements = await this._locate(locator);
|
|
1864
|
+
assertElementExists(elements, locator);
|
|
1809
1865
|
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
});
|
|
1820
|
-
let attrs = await Promise.all(commands);
|
|
1821
|
-
const values = Object.keys(attributes).map(key => attributes[key]);
|
|
1822
|
-
if (!Array.isArray(attrs)) attrs = [attrs];
|
|
1823
|
-
let chunked = chunkArray(attrs, values.length);
|
|
1824
|
-
chunked = chunked.filter((val) => {
|
|
1825
|
-
for (let i = 0; i < val.length; ++i) {
|
|
1826
|
-
const _actual = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(values[i], 10);
|
|
1827
|
-
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
1828
|
-
// if the attribute doesn't exist, returns false as well
|
|
1829
|
-
if (!_actual || !_actual.includes(_expected)) return false;
|
|
1830
|
-
}
|
|
1831
|
-
return true;
|
|
1866
|
+
const expectedAttributes = Object.entries(attributes);
|
|
1867
|
+
|
|
1868
|
+
const valuesPromises = elements.map(async (element) => {
|
|
1869
|
+
const elementAttributes = {};
|
|
1870
|
+
await Promise.all(expectedAttributes.map(async ([attribute, expectedValue]) => {
|
|
1871
|
+
const actualValue = await element.evaluate((el, attr) => el[attr] || el.getAttribute(attr), attribute);
|
|
1872
|
+
elementAttributes[attribute] = actualValue;
|
|
1873
|
+
}));
|
|
1874
|
+
return elementAttributes;
|
|
1832
1875
|
});
|
|
1833
|
-
|
|
1876
|
+
|
|
1877
|
+
const actualAttributes = await Promise.all(valuesPromises);
|
|
1878
|
+
|
|
1879
|
+
const matchingElements = actualAttributes.filter((attrs) => expectedAttributes.every(([attribute, expectedValue]) => {
|
|
1880
|
+
const actualValue = attrs[attribute];
|
|
1881
|
+
if (!actualValue) return false;
|
|
1882
|
+
if (actualValue.toString().match(new RegExp(expectedValue.toString()))) return true;
|
|
1883
|
+
return expectedValue === actualValue;
|
|
1884
|
+
}));
|
|
1885
|
+
|
|
1886
|
+
const elementsCount = elements.length;
|
|
1887
|
+
const matchingCount = matchingElements.length;
|
|
1888
|
+
|
|
1889
|
+
return equals(`all elements (${(new Locator(locator))}) to have attributes ${JSON.stringify(attributes)}`)
|
|
1890
|
+
.assert(matchingCount, elementsCount);
|
|
1834
1891
|
}
|
|
1835
1892
|
|
|
1836
1893
|
/**
|
|
@@ -2033,7 +2090,6 @@ class Puppeteer extends Helper {
|
|
|
2033
2090
|
async waitNumberOfVisibleElements(locator, num, sec) {
|
|
2034
2091
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2035
2092
|
locator = new Locator(locator, 'css');
|
|
2036
|
-
await this.context;
|
|
2037
2093
|
let waiter;
|
|
2038
2094
|
const context = await this._getContext();
|
|
2039
2095
|
if (locator.isCSS()) {
|
|
@@ -2086,7 +2142,7 @@ class Puppeteer extends Helper {
|
|
|
2086
2142
|
if (locator.isCSS()) {
|
|
2087
2143
|
waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout });
|
|
2088
2144
|
} else {
|
|
2089
|
-
waiter =
|
|
2145
|
+
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout });
|
|
2090
2146
|
}
|
|
2091
2147
|
return waiter.catch((err) => {
|
|
2092
2148
|
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
@@ -2107,7 +2163,7 @@ class Puppeteer extends Helper {
|
|
|
2107
2163
|
if (locator.isCSS()) {
|
|
2108
2164
|
waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, visible: true });
|
|
2109
2165
|
} else {
|
|
2110
|
-
waiter =
|
|
2166
|
+
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, visible: true });
|
|
2111
2167
|
}
|
|
2112
2168
|
return waiter.catch((err) => {
|
|
2113
2169
|
throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
@@ -2126,7 +2182,7 @@ class Puppeteer extends Helper {
|
|
|
2126
2182
|
if (locator.isCSS()) {
|
|
2127
2183
|
waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true });
|
|
2128
2184
|
} else {
|
|
2129
|
-
waiter =
|
|
2185
|
+
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true });
|
|
2130
2186
|
}
|
|
2131
2187
|
return waiter.catch((err) => {
|
|
2132
2188
|
throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
@@ -2144,7 +2200,7 @@ class Puppeteer extends Helper {
|
|
|
2144
2200
|
if (locator.isCSS()) {
|
|
2145
2201
|
waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true });
|
|
2146
2202
|
} else {
|
|
2147
|
-
waiter =
|
|
2203
|
+
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true });
|
|
2148
2204
|
}
|
|
2149
2205
|
return waiter.catch((err) => {
|
|
2150
2206
|
throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
@@ -2170,7 +2226,7 @@ class Puppeteer extends Helper {
|
|
|
2170
2226
|
}
|
|
2171
2227
|
|
|
2172
2228
|
async _getContext() {
|
|
2173
|
-
if (this.context && this.context.constructor.name === '
|
|
2229
|
+
if (this.context && this.context.constructor.name === 'CdpFrame') {
|
|
2174
2230
|
return this.context;
|
|
2175
2231
|
}
|
|
2176
2232
|
return this.page;
|
|
@@ -2293,35 +2349,37 @@ class Puppeteer extends Helper {
|
|
|
2293
2349
|
async switchTo(locator) {
|
|
2294
2350
|
if (Number.isInteger(locator)) {
|
|
2295
2351
|
// Select by frame index of current context
|
|
2296
|
-
|
|
2297
|
-
let childFrames = null;
|
|
2352
|
+
let frames = [];
|
|
2298
2353
|
if (this.context && typeof this.context.childFrames === 'function') {
|
|
2299
|
-
|
|
2354
|
+
frames = await this.context.childFrames();
|
|
2300
2355
|
} else {
|
|
2301
|
-
|
|
2356
|
+
frames = await this.page.mainFrame().childFrames();
|
|
2302
2357
|
}
|
|
2303
2358
|
|
|
2304
|
-
if (locator >= 0 && locator <
|
|
2305
|
-
this.context =
|
|
2359
|
+
if (locator >= 0 && locator < frames.length) {
|
|
2360
|
+
this.context = frames[locator];
|
|
2306
2361
|
} else {
|
|
2307
|
-
throw new Error('
|
|
2362
|
+
throw new Error('Frame index out of bounds');
|
|
2308
2363
|
}
|
|
2309
2364
|
return;
|
|
2310
2365
|
}
|
|
2366
|
+
|
|
2311
2367
|
if (!locator) {
|
|
2312
|
-
this.context = await this.page.mainFrame()
|
|
2368
|
+
this.context = await this.page.mainFrame();
|
|
2313
2369
|
return;
|
|
2314
2370
|
}
|
|
2315
2371
|
|
|
2316
|
-
// iframe by selector
|
|
2372
|
+
// Select iframe by selector
|
|
2317
2373
|
const els = await this._locate(locator);
|
|
2318
2374
|
assertElementExists(els, locator);
|
|
2319
|
-
|
|
2375
|
+
|
|
2376
|
+
const iframeElement = els[0];
|
|
2377
|
+
const contentFrame = await iframeElement.contentFrame();
|
|
2320
2378
|
|
|
2321
2379
|
if (contentFrame) {
|
|
2322
2380
|
this.context = contentFrame;
|
|
2323
2381
|
} else {
|
|
2324
|
-
|
|
2382
|
+
throw new Error('Element "#invalidIframeSelector" was not found by text|CSS|XPath');
|
|
2325
2383
|
}
|
|
2326
2384
|
}
|
|
2327
2385
|
|
|
@@ -2412,12 +2470,220 @@ class Puppeteer extends Helper {
|
|
|
2412
2470
|
if (prop) return rect[prop];
|
|
2413
2471
|
return rect;
|
|
2414
2472
|
}
|
|
2473
|
+
|
|
2474
|
+
/**
|
|
2475
|
+
* Mocks network request using [`Request Interception`](https://pptr.dev/next/guides/request-interception)
|
|
2476
|
+
*
|
|
2477
|
+
* ```js
|
|
2478
|
+
* I.mockRoute(/(\.png$)|(\.jpg$)/, route => route.abort());
|
|
2479
|
+
* ```
|
|
2480
|
+
* This method allows intercepting and mocking requests & responses. [Learn more about it](https://pptr.dev/next/guides/request-interception)
|
|
2481
|
+
*
|
|
2482
|
+
* @param {string|RegExp} [url] URL, regex or pattern for to match URL
|
|
2483
|
+
* @param {function} [handler] a function to process request
|
|
2484
|
+
*/
|
|
2485
|
+
async mockRoute(url, handler) {
|
|
2486
|
+
await this.page.setRequestInterception(true);
|
|
2487
|
+
|
|
2488
|
+
this.page.on('request', interceptedRequest => {
|
|
2489
|
+
if (interceptedRequest.url().match(url)) {
|
|
2490
|
+
// @ts-ignore
|
|
2491
|
+
handler(interceptedRequest);
|
|
2492
|
+
} else {
|
|
2493
|
+
interceptedRequest.continue();
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
/**
|
|
2499
|
+
* Stops network mocking created by `mockRoute`.
|
|
2500
|
+
*
|
|
2501
|
+
* ```js
|
|
2502
|
+
* I.stopMockingRoute(/(\.png$)|(\.jpg$)/);
|
|
2503
|
+
* ```
|
|
2504
|
+
*
|
|
2505
|
+
* @param {string|RegExp} [url] URL, regex or pattern for to match URL
|
|
2506
|
+
*/
|
|
2507
|
+
async stopMockingRoute(url) {
|
|
2508
|
+
await this.page.setRequestInterception(true);
|
|
2509
|
+
|
|
2510
|
+
this.page.off('request');
|
|
2511
|
+
|
|
2512
|
+
// Resume normal request handling for the given URL
|
|
2513
|
+
this.page.on('request', interceptedRequest => {
|
|
2514
|
+
if (interceptedRequest.url().includes(url)) {
|
|
2515
|
+
interceptedRequest.continue();
|
|
2516
|
+
} else {
|
|
2517
|
+
interceptedRequest.continue();
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
/**
|
|
2523
|
+
*
|
|
2524
|
+
* {{> flushNetworkTraffics }}
|
|
2525
|
+
*/
|
|
2526
|
+
flushNetworkTraffics() {
|
|
2527
|
+
flushNetworkTraffics.call(this);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
/**
|
|
2531
|
+
*
|
|
2532
|
+
* {{> stopRecordingTraffic }}
|
|
2533
|
+
*/
|
|
2534
|
+
stopRecordingTraffic() {
|
|
2535
|
+
stopRecordingTraffic.call(this);
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
/**
|
|
2539
|
+
* {{> startRecordingTraffic }}
|
|
2540
|
+
*
|
|
2541
|
+
*/
|
|
2542
|
+
async startRecordingTraffic() {
|
|
2543
|
+
this.flushNetworkTraffics();
|
|
2544
|
+
this.recording = true;
|
|
2545
|
+
this.recordedAtLeastOnce = true;
|
|
2546
|
+
|
|
2547
|
+
await this.page.setRequestInterception(true);
|
|
2548
|
+
|
|
2549
|
+
this.page.on('request', (request) => {
|
|
2550
|
+
const information = {
|
|
2551
|
+
url: request.url(),
|
|
2552
|
+
method: request.method(),
|
|
2553
|
+
requestHeaders: request.headers(),
|
|
2554
|
+
requestPostData: request.postData(),
|
|
2555
|
+
response: request.response(),
|
|
2556
|
+
};
|
|
2557
|
+
|
|
2558
|
+
this.debugSection('REQUEST: ', JSON.stringify(information));
|
|
2559
|
+
|
|
2560
|
+
if (typeof information.requestPostData === 'object') {
|
|
2561
|
+
information.requestPostData = JSON.parse(information.requestPostData);
|
|
2562
|
+
}
|
|
2563
|
+
request.continue();
|
|
2564
|
+
this.requests.push(information);
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
/**
|
|
2569
|
+
*
|
|
2570
|
+
* {{> grabRecordedNetworkTraffics }}
|
|
2571
|
+
*/
|
|
2572
|
+
async grabRecordedNetworkTraffics() {
|
|
2573
|
+
return grabRecordedNetworkTraffics.call(this);
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
/**
|
|
2577
|
+
*
|
|
2578
|
+
* {{> seeTraffic }}
|
|
2579
|
+
*/
|
|
2580
|
+
async seeTraffic({
|
|
2581
|
+
name, url, parameters, requestPostData, timeout = 10,
|
|
2582
|
+
}) {
|
|
2583
|
+
await seeTraffic.call(this, ...arguments);
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
/**
|
|
2587
|
+
*
|
|
2588
|
+
* {{> dontSeeTraffic }}
|
|
2589
|
+
*
|
|
2590
|
+
*/
|
|
2591
|
+
dontSeeTraffic({ name, url }) {
|
|
2592
|
+
dontSeeTraffic.call(this, ...arguments);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
async getNewCDPSession() {
|
|
2596
|
+
const client = await this.page.target().createCDPSession();
|
|
2597
|
+
return client;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
/**
|
|
2601
|
+
* {{> startRecordingWebSocketMessages }}
|
|
2602
|
+
*/
|
|
2603
|
+
async startRecordingWebSocketMessages() {
|
|
2604
|
+
this.flushWebSocketMessages();
|
|
2605
|
+
this.recordingWebSocketMessages = true;
|
|
2606
|
+
this.recordedWebSocketMessagesAtLeastOnce = true;
|
|
2607
|
+
|
|
2608
|
+
this.cdpSession = await this.getNewCDPSession();
|
|
2609
|
+
await this.cdpSession.send('Network.enable');
|
|
2610
|
+
await this.cdpSession.send('Page.enable');
|
|
2611
|
+
|
|
2612
|
+
this.cdpSession.on(
|
|
2613
|
+
'Network.webSocketFrameReceived',
|
|
2614
|
+
(payload) => {
|
|
2615
|
+
this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
|
|
2616
|
+
},
|
|
2617
|
+
);
|
|
2618
|
+
|
|
2619
|
+
this.cdpSession.on(
|
|
2620
|
+
'Network.webSocketFrameSent',
|
|
2621
|
+
(payload) => {
|
|
2622
|
+
this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
|
|
2623
|
+
},
|
|
2624
|
+
);
|
|
2625
|
+
|
|
2626
|
+
this.cdpSession.on(
|
|
2627
|
+
'Network.webSocketFrameError',
|
|
2628
|
+
(payload) => {
|
|
2629
|
+
this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload));
|
|
2630
|
+
},
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
/**
|
|
2635
|
+
* {{> stopRecordingWebSocketMessages }}
|
|
2636
|
+
*/
|
|
2637
|
+
async stopRecordingWebSocketMessages() {
|
|
2638
|
+
await this.cdpSession.send('Network.disable');
|
|
2639
|
+
await this.cdpSession.send('Page.disable');
|
|
2640
|
+
this.page.removeAllListeners('Network');
|
|
2641
|
+
this.recordingWebSocketMessages = false;
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
/**
|
|
2645
|
+
* Grab the recording WS messages
|
|
2646
|
+
*
|
|
2647
|
+
* @return { Array<any>|undefined }
|
|
2648
|
+
*
|
|
2649
|
+
*/
|
|
2650
|
+
grabWebSocketMessages() {
|
|
2651
|
+
if (!this.recordingWebSocketMessages) {
|
|
2652
|
+
if (!this.recordedWebSocketMessagesAtLeastOnce) {
|
|
2653
|
+
throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.');
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
return this.webSocketMessages;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
/**
|
|
2660
|
+
* Resets all recorded WS messages.
|
|
2661
|
+
*/
|
|
2662
|
+
flushWebSocketMessages() {
|
|
2663
|
+
this.webSocketMessages = [];
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
_getWebSocketMessage(payload) {
|
|
2667
|
+
if (payload.errorMessage) {
|
|
2668
|
+
return payload.errorMessage;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
return payload.response.payloadData;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
_getWebSocketLog(prefix, payload) {
|
|
2675
|
+
return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
_logWebsocketMessages(message) {
|
|
2679
|
+
this.webSocketMessages += message;
|
|
2680
|
+
}
|
|
2415
2681
|
}
|
|
2416
2682
|
|
|
2417
2683
|
module.exports = Puppeteer;
|
|
2418
2684
|
|
|
2419
2685
|
async function findElements(matcher, locator) {
|
|
2420
|
-
if (locator.react) return
|
|
2686
|
+
if (locator.react) return findReactElements.call(this, locator);
|
|
2421
2687
|
locator = new Locator(locator, 'css');
|
|
2422
2688
|
if (!locator.isXPath()) return matcher.$$(locator.simplify());
|
|
2423
2689
|
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
|
|
@@ -2454,7 +2720,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2454
2720
|
}
|
|
2455
2721
|
|
|
2456
2722
|
async function findClickable(matcher, locator) {
|
|
2457
|
-
if (locator.react) return
|
|
2723
|
+
if (locator.react) return findReactElements.call(this, locator);
|
|
2458
2724
|
locator = new Locator(locator);
|
|
2459
2725
|
if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
|
|
2460
2726
|
|
|
@@ -2803,3 +3069,70 @@ function highlightActiveElement(element, context) {
|
|
|
2803
3069
|
highlightElement(element, context);
|
|
2804
3070
|
}
|
|
2805
3071
|
}
|
|
3072
|
+
|
|
3073
|
+
function _waitForElement(locator, options) {
|
|
3074
|
+
try {
|
|
3075
|
+
return this.context.waitForXPath(locator.value, options);
|
|
3076
|
+
} catch (e) {
|
|
3077
|
+
return this.context.waitForSelector(`::-p-xpath(${locator.value})`, options);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
async function findReactElements(locator, props = {}, state = {}) {
|
|
3082
|
+
const resqScript = await fs.promises.readFile(require.resolve('resq'), 'utf-8');
|
|
3083
|
+
await this.page.evaluate(resqScript.toString());
|
|
3084
|
+
|
|
3085
|
+
await this.page.evaluate(() => window.resq.waitToLoadReact());
|
|
3086
|
+
const arrayHandle = await this.page.evaluateHandle((obj) => {
|
|
3087
|
+
const { selector, props, state } = obj;
|
|
3088
|
+
let elements = window.resq.resq$$(selector);
|
|
3089
|
+
if (Object.keys(props).length) {
|
|
3090
|
+
elements = elements.byProps(props);
|
|
3091
|
+
}
|
|
3092
|
+
if (Object.keys(state).length) {
|
|
3093
|
+
elements = elements.byState(state);
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
if (!elements.length) {
|
|
3097
|
+
return [];
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
// resq returns an array of HTMLElements if the React component is a fragment
|
|
3101
|
+
// this avoids having nested arrays of nodes which the driver does not understand
|
|
3102
|
+
// [[div, div], [div, div]] => [div, div, div, div]
|
|
3103
|
+
let nodes = [];
|
|
3104
|
+
|
|
3105
|
+
elements.forEach((element) => {
|
|
3106
|
+
let { node, isFragment } = element;
|
|
3107
|
+
|
|
3108
|
+
if (!node) {
|
|
3109
|
+
isFragment = true;
|
|
3110
|
+
node = element.children;
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
if (isFragment) {
|
|
3114
|
+
nodes = nodes.concat(node);
|
|
3115
|
+
} else {
|
|
3116
|
+
nodes.push(node);
|
|
3117
|
+
}
|
|
3118
|
+
});
|
|
3119
|
+
|
|
3120
|
+
return [...nodes];
|
|
3121
|
+
}, {
|
|
3122
|
+
selector: locator.react,
|
|
3123
|
+
props: locator.props || {},
|
|
3124
|
+
state: locator.state || {},
|
|
3125
|
+
});
|
|
3126
|
+
|
|
3127
|
+
const properties = await arrayHandle.getProperties();
|
|
3128
|
+
const result = [];
|
|
3129
|
+
for (const property of properties.values()) {
|
|
3130
|
+
const elementHandle = property.asElement();
|
|
3131
|
+
if (elementHandle) {
|
|
3132
|
+
result.push(elementHandle);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
await arrayHandle.dispose();
|
|
3137
|
+
return result;
|
|
3138
|
+
}
|