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/REST.js
CHANGED
|
@@ -72,9 +72,12 @@ class REST extends Helper {
|
|
|
72
72
|
this.options.maxBodyLength = maxContentLength;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
// override defaults with config
|
|
76
|
+
this._setConfig(config);
|
|
77
|
+
|
|
76
78
|
this.headers = { ...this.options.defaultHeaders };
|
|
77
79
|
this.axios = axios.create();
|
|
80
|
+
// @ts-ignore
|
|
78
81
|
this.axios.defaults.headers = this.options.defaultHeaders;
|
|
79
82
|
}
|
|
80
83
|
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -2,10 +2,9 @@ let webdriverio;
|
|
|
2
2
|
|
|
3
3
|
const assert = require('assert');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
5
|
|
|
7
6
|
const Helper = require('@codeceptjs/helper');
|
|
8
|
-
const
|
|
7
|
+
const promiseRetry = require('promise-retry');
|
|
9
8
|
const stringIncludes = require('../assert/include').includes;
|
|
10
9
|
const { urlEquals, equals } = require('../assert/equal');
|
|
11
10
|
const { debug } = require('../output');
|
|
@@ -29,9 +28,14 @@ const ElementNotFound = require('./errors/ElementNotFound');
|
|
|
29
28
|
const ConnectionRefused = require('./errors/ConnectionRefused');
|
|
30
29
|
const Locator = require('../locator');
|
|
31
30
|
const { highlightElement } = require('./scripts/highlightElement');
|
|
32
|
-
const store = require('../store');
|
|
33
31
|
const { focusElement } = require('./scripts/focusElement');
|
|
34
32
|
const { blurElement } = require('./scripts/blurElement');
|
|
33
|
+
const {
|
|
34
|
+
dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError,
|
|
35
|
+
} = require('./errors/ElementAssertion');
|
|
36
|
+
const {
|
|
37
|
+
dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics,
|
|
38
|
+
} = require('./network/actions');
|
|
35
39
|
|
|
36
40
|
const SHADOW = 'shadow';
|
|
37
41
|
const webRoot = 'body';
|
|
@@ -448,6 +452,11 @@ class WebDriver extends Helper {
|
|
|
448
452
|
this.activeSessionName = '';
|
|
449
453
|
this.customLocatorStrategies = config.customLocatorStrategies;
|
|
450
454
|
|
|
455
|
+
// for network stuff
|
|
456
|
+
this.requests = [];
|
|
457
|
+
this.recording = false;
|
|
458
|
+
this.recordedAtLeastOnce = false;
|
|
459
|
+
|
|
451
460
|
this._setConfig(config);
|
|
452
461
|
|
|
453
462
|
Locator.addFilter((locator, result) => {
|
|
@@ -487,8 +496,9 @@ class WebDriver extends Helper {
|
|
|
487
496
|
if (config.host) {
|
|
488
497
|
// webdriverio spec
|
|
489
498
|
config.hostname = config.host;
|
|
490
|
-
config.path = '/wd/hub';
|
|
499
|
+
config.path = config.path ? config.path : '/wd/hub';
|
|
491
500
|
}
|
|
501
|
+
|
|
492
502
|
config.baseUrl = config.url || config.baseUrl;
|
|
493
503
|
if (config.desiredCapabilities && Object.keys(config.desiredCapabilities).length) {
|
|
494
504
|
config.capabilities = config.desiredCapabilities;
|
|
@@ -629,6 +639,11 @@ class WebDriver extends Helper {
|
|
|
629
639
|
if (this.browser.capabilities && this.browser.capabilities.platformName) {
|
|
630
640
|
this.browser.capabilities.platformName = this.browser.capabilities.platformName.toLowerCase();
|
|
631
641
|
}
|
|
642
|
+
|
|
643
|
+
if (this.options.automationProtocol) {
|
|
644
|
+
this.puppeteerBrowser = await this.browser.getPuppeteer();
|
|
645
|
+
}
|
|
646
|
+
|
|
632
647
|
return this.browser;
|
|
633
648
|
}
|
|
634
649
|
|
|
@@ -961,12 +976,12 @@ class WebDriver extends Helper {
|
|
|
961
976
|
*/
|
|
962
977
|
amOnPage(url) {
|
|
963
978
|
let split_url;
|
|
964
|
-
if (this.
|
|
979
|
+
if (this.options.basicAuth) {
|
|
965
980
|
if (url.startsWith('/')) {
|
|
966
|
-
url = this.
|
|
981
|
+
url = this.options.url + url;
|
|
967
982
|
}
|
|
968
983
|
split_url = url.split('//');
|
|
969
|
-
url = `${split_url[0]}//${this.
|
|
984
|
+
url = `${split_url[0]}//${this.options.basicAuth.username}:${this.options.basicAuth.password}@${split_url[1]}`;
|
|
970
985
|
}
|
|
971
986
|
return this.browser.url(url);
|
|
972
987
|
}
|
|
@@ -1461,7 +1476,11 @@ class WebDriver extends Helper {
|
|
|
1461
1476
|
const res = await this._locate(locator, true);
|
|
1462
1477
|
assertElementExists(res, locator);
|
|
1463
1478
|
const selected = await forEachAsync(res, async el => el.isDisplayed());
|
|
1464
|
-
|
|
1479
|
+
try {
|
|
1480
|
+
return truth(`elements of ${(new Locator(locator))}`, 'to be seen').assert(selected);
|
|
1481
|
+
} catch (e) {
|
|
1482
|
+
dontSeeElementError(locator);
|
|
1483
|
+
}
|
|
1465
1484
|
}
|
|
1466
1485
|
|
|
1467
1486
|
/**
|
|
@@ -1474,7 +1493,11 @@ class WebDriver extends Helper {
|
|
|
1474
1493
|
return truth(`elements of ${(new Locator(locator))}`, 'to be seen').negate(false);
|
|
1475
1494
|
}
|
|
1476
1495
|
const selected = await forEachAsync(res, async el => el.isDisplayed());
|
|
1477
|
-
|
|
1496
|
+
try {
|
|
1497
|
+
return truth(`elements of ${(new Locator(locator))}`, 'to be seen').negate(selected);
|
|
1498
|
+
} catch (e) {
|
|
1499
|
+
seeElementError(locator);
|
|
1500
|
+
}
|
|
1478
1501
|
}
|
|
1479
1502
|
|
|
1480
1503
|
/**
|
|
@@ -1483,7 +1506,11 @@ class WebDriver extends Helper {
|
|
|
1483
1506
|
*/
|
|
1484
1507
|
async seeElementInDOM(locator) {
|
|
1485
1508
|
const res = await this._res(locator);
|
|
1486
|
-
|
|
1509
|
+
try {
|
|
1510
|
+
return empty('elements').negate(res);
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
dontSeeElementInDOMError(locator);
|
|
1513
|
+
}
|
|
1487
1514
|
}
|
|
1488
1515
|
|
|
1489
1516
|
/**
|
|
@@ -1492,7 +1519,11 @@ class WebDriver extends Helper {
|
|
|
1492
1519
|
*/
|
|
1493
1520
|
async dontSeeElementInDOM(locator) {
|
|
1494
1521
|
const res = await this._res(locator);
|
|
1495
|
-
|
|
1522
|
+
try {
|
|
1523
|
+
return empty('elements').assert(res);
|
|
1524
|
+
} catch (e) {
|
|
1525
|
+
seeElementInDOMError(locator);
|
|
1526
|
+
}
|
|
1496
1527
|
}
|
|
1497
1528
|
|
|
1498
1529
|
/**
|
|
@@ -1612,6 +1643,8 @@ class WebDriver extends Helper {
|
|
|
1612
1643
|
for (let i = 0; i < val.length; ++i) {
|
|
1613
1644
|
const _actual = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
|
|
1614
1645
|
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
1646
|
+
// the attribute could be a boolean
|
|
1647
|
+
if (typeof _actual === 'boolean') return _actual === _expected;
|
|
1615
1648
|
if (_actual !== _expected) return false;
|
|
1616
1649
|
}
|
|
1617
1650
|
return true;
|
|
@@ -1839,6 +1872,37 @@ class WebDriver extends Helper {
|
|
|
1839
1872
|
return cookie[0];
|
|
1840
1873
|
}
|
|
1841
1874
|
|
|
1875
|
+
/**
|
|
1876
|
+
* {{> waitForCookie }}
|
|
1877
|
+
*/
|
|
1878
|
+
async waitForCookie(name, sec) {
|
|
1879
|
+
// by default, we will retry 3 times
|
|
1880
|
+
let retries = 3;
|
|
1881
|
+
const waitTimeout = sec || this.options.waitForTimeoutInSeconds;
|
|
1882
|
+
|
|
1883
|
+
if (sec) {
|
|
1884
|
+
retries = sec;
|
|
1885
|
+
} else {
|
|
1886
|
+
retries = waitTimeout - 1;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
return promiseRetry(async (retry, number) => {
|
|
1890
|
+
const _grabCookie = async (name) => {
|
|
1891
|
+
const cookie = await this.browser.getCookies([name]);
|
|
1892
|
+
if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`);
|
|
1893
|
+
};
|
|
1894
|
+
|
|
1895
|
+
this.debugSection('Wait for cookie: ', name);
|
|
1896
|
+
if (number > 1) this.debugSection('Retrying... Attempt #', number);
|
|
1897
|
+
|
|
1898
|
+
try {
|
|
1899
|
+
await _grabCookie(name);
|
|
1900
|
+
} catch (e) {
|
|
1901
|
+
retry(e);
|
|
1902
|
+
}
|
|
1903
|
+
}, { retries, maxTimeout: 1000 });
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1842
1906
|
/**
|
|
1843
1907
|
* Accepts the active JavaScript native popup window, as created by window.alert|window.confirm|window.prompt.
|
|
1844
1908
|
* Don't confuse popups with modal windows, as created by [various
|
|
@@ -2550,9 +2614,9 @@ class WebDriver extends Helper {
|
|
|
2550
2614
|
return;
|
|
2551
2615
|
}
|
|
2552
2616
|
this.geoLocation = { latitude, longitude };
|
|
2553
|
-
|
|
2617
|
+
|
|
2554
2618
|
await this.browser.call(async () => {
|
|
2555
|
-
const pages = await puppeteerBrowser.pages();
|
|
2619
|
+
const pages = await this.puppeteerBrowser.pages();
|
|
2556
2620
|
await pages[0].setGeolocation({ latitude, longitude });
|
|
2557
2621
|
});
|
|
2558
2622
|
}
|
|
@@ -2614,6 +2678,118 @@ class WebDriver extends Helper {
|
|
|
2614
2678
|
runInWeb(fn) {
|
|
2615
2679
|
return fn();
|
|
2616
2680
|
}
|
|
2681
|
+
|
|
2682
|
+
/**
|
|
2683
|
+
*
|
|
2684
|
+
* _Note:_ Only works when devtoolsProtocol is enabled.
|
|
2685
|
+
*
|
|
2686
|
+
* {{> flushNetworkTraffics }}
|
|
2687
|
+
*/
|
|
2688
|
+
flushNetworkTraffics() {
|
|
2689
|
+
if (!this.options.automationProtocol) {
|
|
2690
|
+
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
this.requests = [];
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
/**
|
|
2697
|
+
*
|
|
2698
|
+
* _Note:_ Only works when devtoolsProtocol is enabled.
|
|
2699
|
+
*
|
|
2700
|
+
* {{> stopRecordingTraffic }}
|
|
2701
|
+
*/
|
|
2702
|
+
stopRecordingTraffic() {
|
|
2703
|
+
if (!this.options.automationProtocol) {
|
|
2704
|
+
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
this.page.removeAllListeners('request');
|
|
2708
|
+
this.recording = false;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
/**
|
|
2712
|
+
*
|
|
2713
|
+
* _Note:_ Only works when devtoolsProtocol is enabled.
|
|
2714
|
+
*
|
|
2715
|
+
* {{> startRecordingTraffic }}
|
|
2716
|
+
*
|
|
2717
|
+
*/
|
|
2718
|
+
async startRecordingTraffic() {
|
|
2719
|
+
if (!this.options.automationProtocol) {
|
|
2720
|
+
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
this.flushNetworkTraffics();
|
|
2724
|
+
this.recording = true;
|
|
2725
|
+
this.recordedAtLeastOnce = true;
|
|
2726
|
+
|
|
2727
|
+
this.page = (await this.puppeteerBrowser.pages())[0];
|
|
2728
|
+
await this.page.setRequestInterception(true);
|
|
2729
|
+
|
|
2730
|
+
this.page.on('request', (request) => {
|
|
2731
|
+
const information = {
|
|
2732
|
+
url: request.url(),
|
|
2733
|
+
method: request.method(),
|
|
2734
|
+
requestHeaders: request.headers(),
|
|
2735
|
+
requestPostData: request.postData(),
|
|
2736
|
+
response: request.response(),
|
|
2737
|
+
};
|
|
2738
|
+
|
|
2739
|
+
this.debugSection('REQUEST: ', JSON.stringify(information));
|
|
2740
|
+
|
|
2741
|
+
if (typeof information.requestPostData === 'object') {
|
|
2742
|
+
information.requestPostData = JSON.parse(information.requestPostData);
|
|
2743
|
+
}
|
|
2744
|
+
request.continue();
|
|
2745
|
+
this.requests.push(information);
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
/**
|
|
2750
|
+
*
|
|
2751
|
+
* _Note:_ Only works when devtoolsProtocol is enabled.
|
|
2752
|
+
*
|
|
2753
|
+
* {{> grabRecordedNetworkTraffics }}
|
|
2754
|
+
*/
|
|
2755
|
+
async grabRecordedNetworkTraffics() {
|
|
2756
|
+
if (!this.options.automationProtocol) {
|
|
2757
|
+
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
return grabRecordedNetworkTraffics.call(this);
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
/**
|
|
2764
|
+
*
|
|
2765
|
+
* _Note:_ Only works when devtoolsProtocol is enabled.
|
|
2766
|
+
*
|
|
2767
|
+
* {{> seeTraffic }}
|
|
2768
|
+
*/
|
|
2769
|
+
async seeTraffic({
|
|
2770
|
+
name, url, parameters, requestPostData, timeout = 10,
|
|
2771
|
+
}) {
|
|
2772
|
+
if (!this.options.automationProtocol) {
|
|
2773
|
+
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
await seeTraffic.call(this, ...arguments);
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
/**
|
|
2780
|
+
*
|
|
2781
|
+
* _Note:_ Only works when devtoolsProtocol is enabled.
|
|
2782
|
+
*
|
|
2783
|
+
* {{> dontSeeTraffic }}
|
|
2784
|
+
*
|
|
2785
|
+
*/
|
|
2786
|
+
dontSeeTraffic({ name, url }) {
|
|
2787
|
+
if (!this.options.automationProtocol) {
|
|
2788
|
+
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
dontSeeTraffic.call(this, ...arguments);
|
|
2792
|
+
}
|
|
2617
2793
|
}
|
|
2618
2794
|
|
|
2619
2795
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const Locator = require('../../locator');
|
|
2
|
+
|
|
3
|
+
const prefixMessage = 'Element';
|
|
4
|
+
|
|
5
|
+
function seeElementError(locator) {
|
|
6
|
+
if (typeof locator === 'object') {
|
|
7
|
+
locator = JSON.stringify(locator);
|
|
8
|
+
}
|
|
9
|
+
throw new Error(`${prefixMessage} "${(new Locator(locator))}" is still visible on page.`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function seeElementInDOMError(locator) {
|
|
13
|
+
if (typeof locator === 'object') {
|
|
14
|
+
locator = JSON.stringify(locator);
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`${prefixMessage} "${(new Locator(locator))}" is still seen in DOM.`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function dontSeeElementError(locator) {
|
|
20
|
+
if (typeof locator === 'object') {
|
|
21
|
+
locator = JSON.stringify(locator);
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`${prefixMessage} "${(new Locator(locator))}" is not visible on page.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function dontSeeElementInDOMError(locator) {
|
|
27
|
+
if (typeof locator === 'object') {
|
|
28
|
+
locator = JSON.stringify(locator);
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`${prefixMessage} "${(new Locator(locator))}" is not seen in DOM.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
seeElementError,
|
|
35
|
+
dontSeeElementError,
|
|
36
|
+
seeElementInDOMError,
|
|
37
|
+
dontSeeElementInDOMError,
|
|
38
|
+
};
|
|
@@ -20,6 +20,11 @@ async function findVue(matcher, locator) {
|
|
|
20
20
|
return matcher.locator(_locator).all();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
async function findByPlaywrightLocator(matcher, locator) {
|
|
24
|
+
if (locator && locator.toString().includes(process.env.testIdAttribute)) return matcher.getByTestId(locator.pw.value.split('=')[1]);
|
|
25
|
+
return matcher.locator(locator.pw).all();
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
function propBuilder(props) {
|
|
24
29
|
let _props = '';
|
|
25
30
|
|
|
@@ -35,4 +40,4 @@ function propBuilder(props) {
|
|
|
35
40
|
return _props;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
module.exports = { findReact, findVue };
|
|
43
|
+
module.exports = { findReact, findVue, findByPlaywrightLocator };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const { isInTraffic, createAdvancedTestResults, getTrafficDump } = require('./utils');
|
|
3
|
+
|
|
4
|
+
function dontSeeTraffic({ name, url }) {
|
|
5
|
+
if (!this.recordedAtLeastOnce) {
|
|
6
|
+
throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (!name) {
|
|
10
|
+
throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!url) {
|
|
14
|
+
throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (isInTraffic.call(this, url)) {
|
|
18
|
+
assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function seeTraffic({
|
|
23
|
+
name, url, parameters, requestPostData, timeout = 10,
|
|
24
|
+
}) {
|
|
25
|
+
if (!name) {
|
|
26
|
+
throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!url) {
|
|
30
|
+
throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
34
|
+
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i <= timeout * 2; i++) {
|
|
38
|
+
const found = isInTraffic.call(this, url, parameters);
|
|
39
|
+
if (found) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
await new Promise((done) => {
|
|
43
|
+
setTimeout(done, 1000);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// check request post data
|
|
48
|
+
if (requestPostData && isInTraffic.call(this, url)) {
|
|
49
|
+
const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);
|
|
50
|
+
|
|
51
|
+
assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
|
|
52
|
+
} else if (parameters && isInTraffic.call(this, url)) {
|
|
53
|
+
const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);
|
|
54
|
+
|
|
55
|
+
assert.fail(
|
|
56
|
+
`Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
|
|
57
|
+
+ `${advancedTestResults}`,
|
|
58
|
+
);
|
|
59
|
+
} else {
|
|
60
|
+
assert.fail(
|
|
61
|
+
`Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
|
|
62
|
+
+ `Expected url: ${url}.\n`
|
|
63
|
+
+ `Recorded traffic:\n${getTrafficDump.call(this)}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function grabRecordedNetworkTraffics() {
|
|
69
|
+
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
70
|
+
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const promises = this.requests.map(async (request) => {
|
|
74
|
+
const resp = await request.response;
|
|
75
|
+
|
|
76
|
+
if (!resp) {
|
|
77
|
+
return {
|
|
78
|
+
url: '',
|
|
79
|
+
response: {
|
|
80
|
+
status: '',
|
|
81
|
+
statusText: '',
|
|
82
|
+
body: '',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let body;
|
|
88
|
+
try {
|
|
89
|
+
// There's no 'body' for some requests (redirect etc...)
|
|
90
|
+
body = JSON.parse((await resp.body()).toString());
|
|
91
|
+
} catch (e) {
|
|
92
|
+
// only interested in JSON, not HTML responses.
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
url: resp.url(),
|
|
97
|
+
response: {
|
|
98
|
+
status: resp.status(),
|
|
99
|
+
statusText: resp.statusText(),
|
|
100
|
+
body,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
return Promise.all(promises);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stopRecordingTraffic() {
|
|
108
|
+
// @ts-ignore
|
|
109
|
+
this.page.removeAllListeners('request');
|
|
110
|
+
this.recording = false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function flushNetworkTraffics() {
|
|
114
|
+
this.requests = [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
dontSeeTraffic,
|
|
119
|
+
seeTraffic,
|
|
120
|
+
grabRecordedNetworkTraffics,
|
|
121
|
+
stopRecordingTraffic,
|
|
122
|
+
flushNetworkTraffics,
|
|
123
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const createAdvancedTestResults = (url, dataToCheck, requests) => {
|
|
2
|
+
// Creates advanced test results for a network traffic check.
|
|
3
|
+
// Advanced test results only applies when expected parameters are set
|
|
4
|
+
if (!dataToCheck) return '';
|
|
5
|
+
|
|
6
|
+
let urlFound = false;
|
|
7
|
+
let advancedResults;
|
|
8
|
+
requests.forEach((request) => {
|
|
9
|
+
// url not found in this request. continue with next request
|
|
10
|
+
if (urlFound || !request.url.match(new RegExp(url))) return;
|
|
11
|
+
urlFound = true;
|
|
12
|
+
|
|
13
|
+
// Url found. Now we create advanced test report for that URL and show which parameters failed
|
|
14
|
+
if (!request.requestPostData) {
|
|
15
|
+
advancedResults = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), dataToCheck);
|
|
16
|
+
} else if (request.requestPostData) {
|
|
17
|
+
advancedResults = allRequestPostDataValuePairsMatchExtreme(request.requestPostData, dataToCheck);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return advancedResults;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const extractQueryObjects = (queryString) => {
|
|
24
|
+
// Converts a string of GET parameters into an array of parameter objects. Each parameter object contains the properties "name" and "value".
|
|
25
|
+
if (queryString.indexOf('?') === -1) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const queryObjects = [];
|
|
29
|
+
|
|
30
|
+
const queryPart = queryString.split('?')[1];
|
|
31
|
+
|
|
32
|
+
const queryParameters = queryPart.split('&');
|
|
33
|
+
|
|
34
|
+
queryParameters.forEach((queryParameter) => {
|
|
35
|
+
const keyValue = queryParameter.split('=');
|
|
36
|
+
const queryObject = {};
|
|
37
|
+
// eslint-disable-next-line prefer-destructuring
|
|
38
|
+
queryObject.name = keyValue[0];
|
|
39
|
+
queryObject.value = decodeURIComponent(keyValue[1]);
|
|
40
|
+
queryObjects.push(queryObject);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return queryObjects;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const allParameterValuePairsMatchExtreme = (queryStringObject, advancedExpectedParameterValuePairs) => {
|
|
47
|
+
// More advanced check if all request parameters match with the expectations
|
|
48
|
+
let littleReport = '\nQuery parameters:\n';
|
|
49
|
+
let success = true;
|
|
50
|
+
|
|
51
|
+
for (const expectedKey in advancedExpectedParameterValuePairs) {
|
|
52
|
+
if (!Object.prototype.hasOwnProperty.call(advancedExpectedParameterValuePairs, expectedKey)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
let parameterFound = false;
|
|
56
|
+
const expectedValue = advancedExpectedParameterValuePairs[expectedKey];
|
|
57
|
+
|
|
58
|
+
for (const queryParameter of queryStringObject) {
|
|
59
|
+
if (queryParameter.name === expectedKey) {
|
|
60
|
+
parameterFound = true;
|
|
61
|
+
if (expectedValue === undefined) {
|
|
62
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
|
|
63
|
+
} else if (typeof expectedValue === 'object' && expectedValue.base64) {
|
|
64
|
+
const decodedActualValue = Buffer.from(queryParameter.value, 'base64').toString('utf8');
|
|
65
|
+
if (decodedActualValue === expectedValue.base64) {
|
|
66
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
|
|
67
|
+
} else {
|
|
68
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
|
|
69
|
+
success = false;
|
|
70
|
+
}
|
|
71
|
+
} else if (queryParameter.value === expectedValue) {
|
|
72
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
|
|
73
|
+
} else {
|
|
74
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${queryParameter.value}"\n`;
|
|
75
|
+
success = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (parameterFound === false) {
|
|
81
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> parameter not found in request\n`;
|
|
82
|
+
success = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return success ? true : littleReport;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const allRequestPostDataValuePairsMatchExtreme = (RequestPostDataObject, advancedExpectedRequestPostValuePairs) => {
|
|
90
|
+
// More advanced check if all request post data match with the expectations
|
|
91
|
+
let littleReport = '\nRequest Post Data:\n';
|
|
92
|
+
let success = true;
|
|
93
|
+
|
|
94
|
+
for (const expectedKey in advancedExpectedRequestPostValuePairs) {
|
|
95
|
+
if (!Object.prototype.hasOwnProperty.call(advancedExpectedRequestPostValuePairs, expectedKey)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
let keyFound = false;
|
|
99
|
+
const expectedValue = advancedExpectedRequestPostValuePairs[expectedKey];
|
|
100
|
+
|
|
101
|
+
for (const [key, value] of Object.entries(RequestPostDataObject)) {
|
|
102
|
+
if (key === expectedKey) {
|
|
103
|
+
keyFound = true;
|
|
104
|
+
if (expectedValue === undefined) {
|
|
105
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
|
|
106
|
+
} else if (typeof expectedValue === 'object' && expectedValue.base64) {
|
|
107
|
+
const decodedActualValue = Buffer.from(value, 'base64').toString('utf8');
|
|
108
|
+
if (decodedActualValue === expectedValue.base64) {
|
|
109
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
|
|
110
|
+
} else {
|
|
111
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
|
|
112
|
+
success = false;
|
|
113
|
+
}
|
|
114
|
+
} else if (value === expectedValue) {
|
|
115
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
|
|
116
|
+
} else {
|
|
117
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${value}"\n`;
|
|
118
|
+
success = false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (keyFound === false) {
|
|
124
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> key not found in request\n`;
|
|
125
|
+
success = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return success ? true : littleReport;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns all URLs of all network requests recorded so far during execution of test scenario.
|
|
134
|
+
*
|
|
135
|
+
* @return {string} List of URLs recorded as a string, separated by new lines after each URL
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
function getTrafficDump() {
|
|
139
|
+
let dumpedTraffic = '';
|
|
140
|
+
this.requests.forEach((request) => {
|
|
141
|
+
dumpedTraffic += `${request.method} - ${request.url}\n`;
|
|
142
|
+
});
|
|
143
|
+
return dumpedTraffic;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
|
|
148
|
+
*
|
|
149
|
+
* @param url URL to look for.
|
|
150
|
+
* @param [parameters] Parameters that this URL needs to contain
|
|
151
|
+
* @return {boolean} Whether or not URL with parameters is part of network traffic.
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
function isInTraffic(url, parameters) {
|
|
155
|
+
let isInTraffic = false;
|
|
156
|
+
this.requests.forEach((request) => {
|
|
157
|
+
if (isInTraffic) {
|
|
158
|
+
return; // We already found traffic. Continue with next request
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!request.url.match(new RegExp(url))) {
|
|
162
|
+
return; // url not found in this request. continue with next request
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// URL has matched. Now we check the parameters
|
|
166
|
+
|
|
167
|
+
if (parameters) {
|
|
168
|
+
const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
|
|
169
|
+
if (advancedReport === true) {
|
|
170
|
+
isInTraffic = true;
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
isInTraffic = true;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return isInTraffic;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
createAdvancedTestResults,
|
|
182
|
+
extractQueryObjects,
|
|
183
|
+
allParameterValuePairsMatchExtreme,
|
|
184
|
+
allRequestPostDataValuePairsMatchExtreme,
|
|
185
|
+
getTrafficDump,
|
|
186
|
+
isInTraffic,
|
|
187
|
+
};
|