codeceptjs 3.6.0-beta.1.ai-healers → 3.6.0
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/plugin/coverage.js +112 -99
- package/lib/step.js +3 -1
- package/package.json +48 -37
- package/typings/index.d.ts +19 -2
- package/typings/promiseBasedTypes.d.ts +505 -41
- package/typings/types.d.ts +609 -56
- 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/Playwright.js
CHANGED
|
@@ -4,8 +4,8 @@ const fs = require('fs');
|
|
|
4
4
|
const Helper = require('@codeceptjs/helper');
|
|
5
5
|
const { v4: uuidv4 } = require('uuid');
|
|
6
6
|
const assert = require('assert');
|
|
7
|
+
const promiseRetry = require('promise-retry');
|
|
7
8
|
const Locator = require('../locator');
|
|
8
|
-
const store = require('../store');
|
|
9
9
|
const recorder = require('../recorder');
|
|
10
10
|
const stringIncludes = require('../assert/include').includes;
|
|
11
11
|
const { urlEquals } = require('../assert/equal');
|
|
@@ -33,7 +33,7 @@ const ElementNotFound = require('./errors/ElementNotFound');
|
|
|
33
33
|
const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused');
|
|
34
34
|
const Popup = require('./extras/Popup');
|
|
35
35
|
const Console = require('./extras/Console');
|
|
36
|
-
const { findReact, findVue } = require('./extras/PlaywrightReactVueLocator');
|
|
36
|
+
const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator');
|
|
37
37
|
|
|
38
38
|
let playwright;
|
|
39
39
|
let perfTiming;
|
|
@@ -47,6 +47,12 @@ const {
|
|
|
47
47
|
setRestartStrategy, restartsSession, restartsContext, restartsBrowser,
|
|
48
48
|
} = require('./extras/PlaywrightRestartOpts');
|
|
49
49
|
const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine');
|
|
50
|
+
const {
|
|
51
|
+
seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError,
|
|
52
|
+
} = require('./errors/ElementAssertion');
|
|
53
|
+
const {
|
|
54
|
+
dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics,
|
|
55
|
+
} = require('./network/actions');
|
|
50
56
|
|
|
51
57
|
const pathSeparator = path.sep;
|
|
52
58
|
|
|
@@ -95,6 +101,7 @@ const pathSeparator = path.sep;
|
|
|
95
101
|
* @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
|
|
96
102
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
97
103
|
* @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
|
|
104
|
+
* @prop {string} [testIdAttribute=data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
|
|
98
105
|
*/
|
|
99
106
|
const config = {};
|
|
100
107
|
|
|
@@ -374,6 +381,7 @@ class Playwright extends Helper {
|
|
|
374
381
|
highlightElement: false,
|
|
375
382
|
};
|
|
376
383
|
|
|
384
|
+
process.env.testIdAttribute = 'data-testid';
|
|
377
385
|
config = Object.assign(defaults, config);
|
|
378
386
|
|
|
379
387
|
if (availableBrowsers.indexOf(config.browser) < 0) {
|
|
@@ -459,6 +467,7 @@ class Playwright extends Helper {
|
|
|
459
467
|
try {
|
|
460
468
|
await playwright.selectors.register('__value', createValueEngine);
|
|
461
469
|
await playwright.selectors.register('__disabled', createDisabledEngine);
|
|
470
|
+
if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute);
|
|
462
471
|
} catch (e) {
|
|
463
472
|
console.warn(e);
|
|
464
473
|
}
|
|
@@ -745,9 +754,7 @@ class Playwright extends Helper {
|
|
|
745
754
|
});
|
|
746
755
|
this.context = await this.page;
|
|
747
756
|
this.contextLocator = null;
|
|
748
|
-
|
|
749
|
-
await page.bringToFront();
|
|
750
|
-
}
|
|
757
|
+
await page.bringToFront();
|
|
751
758
|
}
|
|
752
759
|
|
|
753
760
|
/**
|
|
@@ -1451,7 +1458,11 @@ class Playwright extends Helper {
|
|
|
1451
1458
|
async seeElement(locator) {
|
|
1452
1459
|
let els = await this._locate(locator);
|
|
1453
1460
|
els = await Promise.all(els.map(el => el.isVisible()));
|
|
1454
|
-
|
|
1461
|
+
try {
|
|
1462
|
+
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'));
|
|
1463
|
+
} catch (e) {
|
|
1464
|
+
dontSeeElementError(locator);
|
|
1465
|
+
}
|
|
1455
1466
|
}
|
|
1456
1467
|
|
|
1457
1468
|
/**
|
|
@@ -1461,7 +1472,11 @@ class Playwright extends Helper {
|
|
|
1461
1472
|
async dontSeeElement(locator) {
|
|
1462
1473
|
let els = await this._locate(locator);
|
|
1463
1474
|
els = await Promise.all(els.map(el => el.isVisible()));
|
|
1464
|
-
|
|
1475
|
+
try {
|
|
1476
|
+
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'));
|
|
1477
|
+
} catch (e) {
|
|
1478
|
+
seeElementError(locator);
|
|
1479
|
+
}
|
|
1465
1480
|
}
|
|
1466
1481
|
|
|
1467
1482
|
/**
|
|
@@ -1469,7 +1484,11 @@ class Playwright extends Helper {
|
|
|
1469
1484
|
*/
|
|
1470
1485
|
async seeElementInDOM(locator) {
|
|
1471
1486
|
const els = await this._locate(locator);
|
|
1472
|
-
|
|
1487
|
+
try {
|
|
1488
|
+
return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'));
|
|
1489
|
+
} catch (e) {
|
|
1490
|
+
dontSeeElementInDOMError(locator);
|
|
1491
|
+
}
|
|
1473
1492
|
}
|
|
1474
1493
|
|
|
1475
1494
|
/**
|
|
@@ -1477,7 +1496,11 @@ class Playwright extends Helper {
|
|
|
1477
1496
|
*/
|
|
1478
1497
|
async dontSeeElementInDOM(locator) {
|
|
1479
1498
|
const els = await this._locate(locator);
|
|
1480
|
-
|
|
1499
|
+
try {
|
|
1500
|
+
return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'));
|
|
1501
|
+
} catch (e) {
|
|
1502
|
+
seeElementInDOMError(locator);
|
|
1503
|
+
}
|
|
1481
1504
|
}
|
|
1482
1505
|
|
|
1483
1506
|
/**
|
|
@@ -1786,7 +1809,7 @@ class Playwright extends Helper {
|
|
|
1786
1809
|
let optionToSelect = '';
|
|
1787
1810
|
|
|
1788
1811
|
try {
|
|
1789
|
-
optionToSelect = await el.locator('option', { hasText: option }).textContent();
|
|
1812
|
+
optionToSelect = (await el.locator('option', { hasText: option }).textContent()).trim();
|
|
1790
1813
|
} catch (e) {
|
|
1791
1814
|
optionToSelect = option;
|
|
1792
1815
|
}
|
|
@@ -2169,6 +2192,8 @@ class Playwright extends Helper {
|
|
|
2169
2192
|
let chunked = chunkArray(attrs, values.length);
|
|
2170
2193
|
chunked = chunked.filter((val) => {
|
|
2171
2194
|
for (let i = 0; i < val.length; ++i) {
|
|
2195
|
+
// the attribute could be a boolean
|
|
2196
|
+
if (typeof val[i] === 'boolean') return val[i] === values[i];
|
|
2172
2197
|
// if the attribute doesn't exist, returns false as well
|
|
2173
2198
|
if (!val[i] || !val[i].includes(values[i])) return false;
|
|
2174
2199
|
}
|
|
@@ -2447,7 +2472,7 @@ class Playwright extends Helper {
|
|
|
2447
2472
|
async waitNumberOfVisibleElements(locator, num, sec) {
|
|
2448
2473
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2449
2474
|
locator = new Locator(locator, 'css');
|
|
2450
|
-
|
|
2475
|
+
|
|
2451
2476
|
let waiter;
|
|
2452
2477
|
const context = await this._getContext();
|
|
2453
2478
|
if (locator.isCSS()) {
|
|
@@ -2657,6 +2682,7 @@ class Playwright extends Helper {
|
|
|
2657
2682
|
*/
|
|
2658
2683
|
async waitForText(text, sec = null, context = null) {
|
|
2659
2684
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2685
|
+
const errorMessage = `Text "${text}" was not found on page after ${waitTimeout / 1000} sec.`;
|
|
2660
2686
|
let waiter;
|
|
2661
2687
|
|
|
2662
2688
|
const contextObject = await this._getContext();
|
|
@@ -2667,18 +2693,21 @@ class Playwright extends Helper {
|
|
|
2667
2693
|
try {
|
|
2668
2694
|
await contextObject.locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`).first().waitFor({ timeout: waitTimeout, state: 'visible' });
|
|
2669
2695
|
} catch (e) {
|
|
2670
|
-
|
|
2671
|
-
throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${e.message}`);
|
|
2696
|
+
throw new Error(`${errorMessage}\n${e.message}`);
|
|
2672
2697
|
}
|
|
2673
2698
|
}
|
|
2674
2699
|
|
|
2675
2700
|
if (locator.isXPath()) {
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2701
|
+
try {
|
|
2702
|
+
await contextObject.waitForFunction(([locator, text, $XPath]) => {
|
|
2703
|
+
eval($XPath); // eslint-disable-line no-eval
|
|
2704
|
+
const el = $XPath(null, locator);
|
|
2705
|
+
if (!el.length) return false;
|
|
2706
|
+
return el[0].innerText.indexOf(text) > -1;
|
|
2707
|
+
}, [locator.value, text, $XPath.toString()], { timeout: waitTimeout });
|
|
2708
|
+
} catch (e) {
|
|
2709
|
+
throw new Error(`${errorMessage}\n${e.message}`);
|
|
2710
|
+
}
|
|
2682
2711
|
}
|
|
2683
2712
|
} else {
|
|
2684
2713
|
// we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
|
|
@@ -2692,7 +2721,7 @@ class Playwright extends Helper {
|
|
|
2692
2721
|
count += 1000;
|
|
2693
2722
|
} while (count <= waitTimeout);
|
|
2694
2723
|
|
|
2695
|
-
if (!waiter) throw new Error(
|
|
2724
|
+
if (!waiter) throw new Error(`${errorMessage}`);
|
|
2696
2725
|
}
|
|
2697
2726
|
}
|
|
2698
2727
|
|
|
@@ -2869,6 +2898,38 @@ class Playwright extends Helper {
|
|
|
2869
2898
|
}
|
|
2870
2899
|
}
|
|
2871
2900
|
|
|
2901
|
+
/**
|
|
2902
|
+
* {{> waitForCookie }}
|
|
2903
|
+
*/
|
|
2904
|
+
async waitForCookie(name, sec) {
|
|
2905
|
+
// by default, we will retry 3 times
|
|
2906
|
+
let retries = 3;
|
|
2907
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2908
|
+
|
|
2909
|
+
if (sec) {
|
|
2910
|
+
retries = sec;
|
|
2911
|
+
} else {
|
|
2912
|
+
retries = Math.ceil(waitTimeout / 1000) - 1;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
return promiseRetry(async (retry, number) => {
|
|
2916
|
+
const _grabCookie = async (name) => {
|
|
2917
|
+
const cookies = await this.browserContext.cookies();
|
|
2918
|
+
const cookie = cookies.filter(c => c.name === name);
|
|
2919
|
+
if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`);
|
|
2920
|
+
};
|
|
2921
|
+
|
|
2922
|
+
this.debugSection('Wait for cookie: ', name);
|
|
2923
|
+
if (number > 1) this.debugSection('Retrying... Attempt #', number);
|
|
2924
|
+
|
|
2925
|
+
try {
|
|
2926
|
+
await _grabCookie(name);
|
|
2927
|
+
} catch (e) {
|
|
2928
|
+
retry(e);
|
|
2929
|
+
}
|
|
2930
|
+
}, { retries, maxTimeout: 1000 });
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2872
2933
|
async _waitForAction() {
|
|
2873
2934
|
return this.wait(this.options.waitForAction / 1000);
|
|
2874
2935
|
}
|
|
@@ -2900,7 +2961,7 @@ class Playwright extends Helper {
|
|
|
2900
2961
|
* This method allows intercepting and mocking requests & responses. [Learn more about it](https://playwright.dev/docs/network#handle-requests)
|
|
2901
2962
|
*
|
|
2902
2963
|
* @param {string|RegExp} [url] URL, regex or pattern for to match URL
|
|
2903
|
-
* @param {function} [handler] a function to process
|
|
2964
|
+
* @param {function} [handler] a function to process request
|
|
2904
2965
|
*/
|
|
2905
2966
|
async mockRoute(url, handler) {
|
|
2906
2967
|
return this.browserContext.route(...arguments);
|
|
@@ -2916,21 +2977,15 @@ class Playwright extends Helper {
|
|
|
2916
2977
|
* If no handler is passed, all mock requests for the rote are disabled.
|
|
2917
2978
|
*
|
|
2918
2979
|
* @param {string|RegExp} [url] URL, regex or pattern for to match URL
|
|
2919
|
-
* @param {function} [handler] a function to process
|
|
2980
|
+
* @param {function} [handler] a function to process request
|
|
2920
2981
|
*/
|
|
2921
2982
|
async stopMockingRoute(url, handler) {
|
|
2922
2983
|
return this.browserContext.unroute(...arguments);
|
|
2923
2984
|
}
|
|
2924
2985
|
|
|
2925
2986
|
/**
|
|
2926
|
-
*
|
|
2927
|
-
* This also resets recorded network requests.
|
|
2928
|
-
*
|
|
2929
|
-
* ```js
|
|
2930
|
-
* I.startRecordingTraffic();
|
|
2931
|
-
* ```
|
|
2987
|
+
* {{> flushNetworkTraffics }}
|
|
2932
2988
|
*
|
|
2933
|
-
* @return {void}
|
|
2934
2989
|
*/
|
|
2935
2990
|
startRecordingTraffic() {
|
|
2936
2991
|
this.flushNetworkTraffics();
|
|
@@ -2956,47 +3011,6 @@ class Playwright extends Helper {
|
|
|
2956
3011
|
});
|
|
2957
3012
|
}
|
|
2958
3013
|
|
|
2959
|
-
/**
|
|
2960
|
-
* Grab the recording network traffics
|
|
2961
|
-
*
|
|
2962
|
-
* ```js
|
|
2963
|
-
* const traffics = await I.grabRecordedNetworkTraffics();
|
|
2964
|
-
* expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1');
|
|
2965
|
-
* expect(traffics[0].response.status).to.equal(200);
|
|
2966
|
-
* expect(traffics[0].response.body).to.contain({ name: 'this was mocked' });
|
|
2967
|
-
* ```
|
|
2968
|
-
*
|
|
2969
|
-
* @return { Promise<Array<any>> }
|
|
2970
|
-
*
|
|
2971
|
-
*/
|
|
2972
|
-
async grabRecordedNetworkTraffics() {
|
|
2973
|
-
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
2974
|
-
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
2975
|
-
}
|
|
2976
|
-
|
|
2977
|
-
const requests = await this.requests;
|
|
2978
|
-
const promises = requests.map(async (request) => request.response.then(
|
|
2979
|
-
async (response) => {
|
|
2980
|
-
let body;
|
|
2981
|
-
try {
|
|
2982
|
-
// There's no 'body' for some requests (redirect etc...)
|
|
2983
|
-
body = JSON.parse((await response.body()).toString());
|
|
2984
|
-
} catch (e) {
|
|
2985
|
-
// only interested in JSON, not HTML responses.
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2988
|
-
request.response = {
|
|
2989
|
-
status: response.status(),
|
|
2990
|
-
statusText: response.statusText(),
|
|
2991
|
-
body,
|
|
2992
|
-
};
|
|
2993
|
-
},
|
|
2994
|
-
));
|
|
2995
|
-
await Promise.all(promises);
|
|
2996
|
-
|
|
2997
|
-
return this.requests;
|
|
2998
|
-
}
|
|
2999
|
-
|
|
3000
3014
|
/**
|
|
3001
3015
|
* Blocks traffic of a given URL or a list of URLs.
|
|
3002
3016
|
*
|
|
@@ -3076,106 +3090,19 @@ class Playwright extends Helper {
|
|
|
3076
3090
|
}
|
|
3077
3091
|
|
|
3078
3092
|
/**
|
|
3079
|
-
*
|
|
3093
|
+
*
|
|
3094
|
+
* {{> flushNetworkTraffics }}
|
|
3080
3095
|
*/
|
|
3081
3096
|
flushNetworkTraffics() {
|
|
3082
|
-
this
|
|
3097
|
+
flushNetworkTraffics.call(this);
|
|
3083
3098
|
}
|
|
3084
3099
|
|
|
3085
3100
|
/**
|
|
3086
|
-
* Stops recording of network traffic. Recorded traffic is not flashed.
|
|
3087
3101
|
*
|
|
3088
|
-
*
|
|
3089
|
-
* I.stopRecordingTraffic();
|
|
3090
|
-
* ```
|
|
3102
|
+
* {{> stopRecordingTraffic }}
|
|
3091
3103
|
*/
|
|
3092
3104
|
stopRecordingTraffic() {
|
|
3093
|
-
|
|
3094
|
-
this.recording = false;
|
|
3095
|
-
}
|
|
3096
|
-
|
|
3097
|
-
/**
|
|
3098
|
-
* Verifies that a certain request is part of network traffic.
|
|
3099
|
-
*
|
|
3100
|
-
* ```js
|
|
3101
|
-
* // checking the request url contains certain query strings
|
|
3102
|
-
* I.amOnPage('https://openai.com/blog/chatgpt');
|
|
3103
|
-
* I.startRecordingTraffic();
|
|
3104
|
-
* await I.seeTraffic({
|
|
3105
|
-
* name: 'sentry event',
|
|
3106
|
-
* url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600',
|
|
3107
|
-
* parameters: {
|
|
3108
|
-
* width: '1919',
|
|
3109
|
-
* height: '1138',
|
|
3110
|
-
* },
|
|
3111
|
-
* });
|
|
3112
|
-
* ```
|
|
3113
|
-
*
|
|
3114
|
-
* ```js
|
|
3115
|
-
* // checking the request url contains certain post data
|
|
3116
|
-
* I.amOnPage('https://openai.com/blog/chatgpt');
|
|
3117
|
-
* I.startRecordingTraffic();
|
|
3118
|
-
* await I.seeTraffic({
|
|
3119
|
-
* name: 'event',
|
|
3120
|
-
* url: 'https://cloudflareinsights.com/cdn-cgi/rum',
|
|
3121
|
-
* requestPostData: {
|
|
3122
|
-
* st: 2,
|
|
3123
|
-
* },
|
|
3124
|
-
* });
|
|
3125
|
-
* ```
|
|
3126
|
-
*
|
|
3127
|
-
* @param {Object} opts - options when checking the traffic network.
|
|
3128
|
-
* @param {string} opts.name A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail.
|
|
3129
|
-
* @param {string} opts.url Expected URL of request in network traffic
|
|
3130
|
-
* @param {Object} [opts.parameters] Expected parameters of that request in network traffic
|
|
3131
|
-
* @param {Object} [opts.requestPostData] Expected that request contains post data in network traffic
|
|
3132
|
-
* @param {number} [opts.timeout] Timeout to wait for request in seconds. Default is 10 seconds.
|
|
3133
|
-
* @return { Promise<*> }
|
|
3134
|
-
*/
|
|
3135
|
-
async seeTraffic({
|
|
3136
|
-
name, url, parameters, requestPostData, timeout = 10,
|
|
3137
|
-
}) {
|
|
3138
|
-
if (!name) {
|
|
3139
|
-
throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
|
|
3140
|
-
}
|
|
3141
|
-
|
|
3142
|
-
if (!url) {
|
|
3143
|
-
throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
|
|
3144
|
-
}
|
|
3145
|
-
|
|
3146
|
-
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
3147
|
-
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
for (let i = 0; i <= timeout * 2; i++) {
|
|
3151
|
-
const found = this._isInTraffic(url, parameters);
|
|
3152
|
-
if (found) {
|
|
3153
|
-
return true;
|
|
3154
|
-
}
|
|
3155
|
-
await new Promise((done) => {
|
|
3156
|
-
setTimeout(done, 1000);
|
|
3157
|
-
});
|
|
3158
|
-
}
|
|
3159
|
-
|
|
3160
|
-
// check request post data
|
|
3161
|
-
if (requestPostData && this._isInTraffic(url)) {
|
|
3162
|
-
const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);
|
|
3163
|
-
|
|
3164
|
-
assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
|
|
3165
|
-
} else if (parameters && this._isInTraffic(url)) {
|
|
3166
|
-
const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);
|
|
3167
|
-
|
|
3168
|
-
assert.fail(
|
|
3169
|
-
`Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
|
|
3170
|
-
+ `${advancedTestResults}`,
|
|
3171
|
-
);
|
|
3172
|
-
} else {
|
|
3173
|
-
assert.fail(
|
|
3174
|
-
`Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
|
|
3175
|
-
+ `Expected url: ${url}.\n`
|
|
3176
|
-
+ `Recorded traffic:\n${this._getTrafficDump()}`,
|
|
3177
|
-
);
|
|
3178
|
-
}
|
|
3105
|
+
stopRecordingTraffic.call(this);
|
|
3179
3106
|
}
|
|
3180
3107
|
|
|
3181
3108
|
/**
|
|
@@ -3212,94 +3139,34 @@ class Playwright extends Helper {
|
|
|
3212
3139
|
}
|
|
3213
3140
|
|
|
3214
3141
|
/**
|
|
3215
|
-
* Verifies that a certain request is not part of network traffic.
|
|
3216
|
-
*
|
|
3217
|
-
* Examples:
|
|
3218
|
-
*
|
|
3219
|
-
* ```js
|
|
3220
|
-
* I.dontSeeTraffic({ name: 'Unexpected API Call', url: 'https://api.example.com' });
|
|
3221
|
-
* I.dontSeeTraffic({ name: 'Unexpected API Call of "user" endpoint', url: /api.example.com.*user/ });
|
|
3222
|
-
* ```
|
|
3223
|
-
*
|
|
3224
|
-
* @param {Object} opts - options when checking the traffic network.
|
|
3225
|
-
* @param {string} opts.name A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail.
|
|
3226
|
-
* @param {string|RegExp} opts.url Expected URL of request in network traffic. Can be a string or a regular expression.
|
|
3227
3142
|
*
|
|
3143
|
+
* {{> grabRecordedNetworkTraffics }}
|
|
3228
3144
|
*/
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
3232
|
-
}
|
|
3233
|
-
|
|
3234
|
-
if (!name) {
|
|
3235
|
-
throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
|
|
3236
|
-
}
|
|
3237
|
-
|
|
3238
|
-
if (!url) {
|
|
3239
|
-
throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
|
|
3240
|
-
}
|
|
3241
|
-
|
|
3242
|
-
if (this._isInTraffic(url)) {
|
|
3243
|
-
assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
|
|
3244
|
-
}
|
|
3145
|
+
async grabRecordedNetworkTraffics() {
|
|
3146
|
+
return grabRecordedNetworkTraffics.call(this);
|
|
3245
3147
|
}
|
|
3246
3148
|
|
|
3247
3149
|
/**
|
|
3248
|
-
* Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
|
|
3249
3150
|
*
|
|
3250
|
-
*
|
|
3251
|
-
* @param [parameters] Parameters that this URL needs to contain
|
|
3252
|
-
* @return {boolean} Whether or not URL with parameters is part of network traffic.
|
|
3253
|
-
* @private
|
|
3151
|
+
* {{> seeTraffic }}
|
|
3254
3152
|
*/
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
return; // We already found traffic. Continue with next request
|
|
3260
|
-
}
|
|
3261
|
-
|
|
3262
|
-
if (!request.url.match(new RegExp(url))) {
|
|
3263
|
-
return; // url not found in this request. continue with next request
|
|
3264
|
-
}
|
|
3265
|
-
|
|
3266
|
-
// URL has matched. Now we check the parameters
|
|
3267
|
-
|
|
3268
|
-
if (parameters) {
|
|
3269
|
-
const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
|
|
3270
|
-
if (advancedReport === true) {
|
|
3271
|
-
isInTraffic = true;
|
|
3272
|
-
}
|
|
3273
|
-
} else {
|
|
3274
|
-
isInTraffic = true;
|
|
3275
|
-
}
|
|
3276
|
-
});
|
|
3277
|
-
|
|
3278
|
-
return isInTraffic;
|
|
3153
|
+
async seeTraffic({
|
|
3154
|
+
name, url, parameters, requestPostData, timeout = 10,
|
|
3155
|
+
}) {
|
|
3156
|
+
await seeTraffic.call(this, ...arguments);
|
|
3279
3157
|
}
|
|
3280
3158
|
|
|
3281
3159
|
/**
|
|
3282
|
-
* Returns all URLs of all network requests recorded so far during execution of test scenario.
|
|
3283
3160
|
*
|
|
3284
|
-
*
|
|
3285
|
-
*
|
|
3161
|
+
* {{> dontSeeTraffic }}
|
|
3162
|
+
*
|
|
3286
3163
|
*/
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
this.requests.forEach((request) => {
|
|
3290
|
-
dumpedTraffic += `${request.method} - ${request.url}\n`;
|
|
3291
|
-
});
|
|
3292
|
-
return dumpedTraffic;
|
|
3164
|
+
dontSeeTraffic({ name, url }) {
|
|
3165
|
+
dontSeeTraffic.call(this, ...arguments);
|
|
3293
3166
|
}
|
|
3294
3167
|
|
|
3295
3168
|
/**
|
|
3296
|
-
*
|
|
3297
|
-
* This also resets recorded websocket messages.
|
|
3298
|
-
*
|
|
3299
|
-
* ```js
|
|
3300
|
-
* await I.startRecordingWebSocketMessages();
|
|
3301
|
-
* ```
|
|
3302
|
-
*
|
|
3169
|
+
* {{> startRecordingWebSocketMessages }}
|
|
3303
3170
|
*/
|
|
3304
3171
|
async startRecordingWebSocketMessages() {
|
|
3305
3172
|
this.flushWebSocketMessages();
|
|
@@ -3333,11 +3200,7 @@ class Playwright extends Helper {
|
|
|
3333
3200
|
}
|
|
3334
3201
|
|
|
3335
3202
|
/**
|
|
3336
|
-
*
|
|
3337
|
-
*
|
|
3338
|
-
* ```js
|
|
3339
|
-
* await I.stopRecordingWebSocketMessages();
|
|
3340
|
-
* ```
|
|
3203
|
+
* {{> stopRecordingWebSocketMessages }}
|
|
3341
3204
|
*/
|
|
3342
3205
|
async stopRecordingWebSocketMessages() {
|
|
3343
3206
|
await this.cdpSession.send('Network.disable');
|
|
@@ -3464,6 +3327,7 @@ function buildLocatorString(locator) {
|
|
|
3464
3327
|
async function findElements(matcher, locator) {
|
|
3465
3328
|
if (locator.react) return findReact(matcher, locator);
|
|
3466
3329
|
if (locator.vue) return findVue(matcher, locator);
|
|
3330
|
+
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
|
|
3467
3331
|
locator = new Locator(locator, 'css');
|
|
3468
3332
|
|
|
3469
3333
|
return matcher.locator(buildLocatorString(locator)).all();
|
|
@@ -3471,6 +3335,8 @@ async function findElements(matcher, locator) {
|
|
|
3471
3335
|
|
|
3472
3336
|
async function findElement(matcher, locator) {
|
|
3473
3337
|
if (locator.react) return findReact(matcher, locator);
|
|
3338
|
+
if (locator.vue) return findVue(matcher, locator);
|
|
3339
|
+
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
|
|
3474
3340
|
locator = new Locator(locator, 'css');
|
|
3475
3341
|
|
|
3476
3342
|
return matcher.locator(buildLocatorString(locator)).first();
|
|
@@ -3526,6 +3392,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
3526
3392
|
async function findClickable(matcher, locator) {
|
|
3527
3393
|
if (locator.react) return findReact(matcher, locator);
|
|
3528
3394
|
if (locator.vue) return findVue(matcher, locator);
|
|
3395
|
+
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
|
|
3529
3396
|
|
|
3530
3397
|
locator = new Locator(locator);
|
|
3531
3398
|
if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
|
|
@@ -3935,134 +3802,3 @@ async function highlightActiveElement(element) {
|
|
|
3935
3802
|
});
|
|
3936
3803
|
}
|
|
3937
3804
|
}
|
|
3938
|
-
|
|
3939
|
-
const createAdvancedTestResults = (url, dataToCheck, requests) => {
|
|
3940
|
-
// Creates advanced test results for a network traffic check.
|
|
3941
|
-
// Advanced test results only applies when expected parameters are set
|
|
3942
|
-
if (!dataToCheck) return '';
|
|
3943
|
-
|
|
3944
|
-
let urlFound = false;
|
|
3945
|
-
let advancedResults;
|
|
3946
|
-
requests.forEach((request) => {
|
|
3947
|
-
// url not found in this request. continue with next request
|
|
3948
|
-
if (urlFound || !request.url.match(new RegExp(url))) return;
|
|
3949
|
-
urlFound = true;
|
|
3950
|
-
|
|
3951
|
-
// Url found. Now we create advanced test report for that URL and show which parameters failed
|
|
3952
|
-
if (!request.requestPostData) {
|
|
3953
|
-
advancedResults = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), dataToCheck);
|
|
3954
|
-
} else if (request.requestPostData) {
|
|
3955
|
-
advancedResults = allRequestPostDataValuePairsMatchExtreme(request.requestPostData, dataToCheck);
|
|
3956
|
-
}
|
|
3957
|
-
});
|
|
3958
|
-
return advancedResults;
|
|
3959
|
-
};
|
|
3960
|
-
|
|
3961
|
-
const extractQueryObjects = (queryString) => {
|
|
3962
|
-
// Converts a string of GET parameters into an array of parameter objects. Each parameter object contains the properties "name" and "value".
|
|
3963
|
-
if (queryString.indexOf('?') === -1) {
|
|
3964
|
-
return [];
|
|
3965
|
-
}
|
|
3966
|
-
const queryObjects = [];
|
|
3967
|
-
|
|
3968
|
-
const queryPart = queryString.split('?')[1];
|
|
3969
|
-
|
|
3970
|
-
const queryParameters = queryPart.split('&');
|
|
3971
|
-
|
|
3972
|
-
queryParameters.forEach((queryParameter) => {
|
|
3973
|
-
const keyValue = queryParameter.split('=');
|
|
3974
|
-
const queryObject = {};
|
|
3975
|
-
// eslint-disable-next-line prefer-destructuring
|
|
3976
|
-
queryObject.name = keyValue[0];
|
|
3977
|
-
queryObject.value = decodeURIComponent(keyValue[1]);
|
|
3978
|
-
queryObjects.push(queryObject);
|
|
3979
|
-
});
|
|
3980
|
-
|
|
3981
|
-
return queryObjects;
|
|
3982
|
-
};
|
|
3983
|
-
|
|
3984
|
-
const allParameterValuePairsMatchExtreme = (queryStringObject, advancedExpectedParameterValuePairs) => {
|
|
3985
|
-
// More advanced check if all request parameters match with the expectations
|
|
3986
|
-
let littleReport = '\nQuery parameters:\n';
|
|
3987
|
-
let success = true;
|
|
3988
|
-
|
|
3989
|
-
for (const expectedKey in advancedExpectedParameterValuePairs) {
|
|
3990
|
-
if (!Object.prototype.hasOwnProperty.call(advancedExpectedParameterValuePairs, expectedKey)) {
|
|
3991
|
-
continue;
|
|
3992
|
-
}
|
|
3993
|
-
let parameterFound = false;
|
|
3994
|
-
const expectedValue = advancedExpectedParameterValuePairs[expectedKey];
|
|
3995
|
-
|
|
3996
|
-
for (const queryParameter of queryStringObject) {
|
|
3997
|
-
if (queryParameter.name === expectedKey) {
|
|
3998
|
-
parameterFound = true;
|
|
3999
|
-
if (expectedValue === undefined) {
|
|
4000
|
-
littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
|
|
4001
|
-
} else if (typeof expectedValue === 'object' && expectedValue.base64) {
|
|
4002
|
-
const decodedActualValue = Buffer.from(queryParameter.value, 'base64').toString('utf8');
|
|
4003
|
-
if (decodedActualValue === expectedValue.base64) {
|
|
4004
|
-
littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
|
|
4005
|
-
} else {
|
|
4006
|
-
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
|
|
4007
|
-
success = false;
|
|
4008
|
-
}
|
|
4009
|
-
} else if (queryParameter.value === expectedValue) {
|
|
4010
|
-
littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
|
|
4011
|
-
} else {
|
|
4012
|
-
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${queryParameter.value}"\n`;
|
|
4013
|
-
success = false;
|
|
4014
|
-
}
|
|
4015
|
-
}
|
|
4016
|
-
}
|
|
4017
|
-
|
|
4018
|
-
if (parameterFound === false) {
|
|
4019
|
-
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> parameter not found in request\n`;
|
|
4020
|
-
success = false;
|
|
4021
|
-
}
|
|
4022
|
-
}
|
|
4023
|
-
|
|
4024
|
-
return success ? true : littleReport;
|
|
4025
|
-
};
|
|
4026
|
-
|
|
4027
|
-
const allRequestPostDataValuePairsMatchExtreme = (RequestPostDataObject, advancedExpectedRequestPostValuePairs) => {
|
|
4028
|
-
// More advanced check if all request post data match with the expectations
|
|
4029
|
-
let littleReport = '\nRequest Post Data:\n';
|
|
4030
|
-
let success = true;
|
|
4031
|
-
|
|
4032
|
-
for (const expectedKey in advancedExpectedRequestPostValuePairs) {
|
|
4033
|
-
if (!Object.prototype.hasOwnProperty.call(advancedExpectedRequestPostValuePairs, expectedKey)) {
|
|
4034
|
-
continue;
|
|
4035
|
-
}
|
|
4036
|
-
let keyFound = false;
|
|
4037
|
-
const expectedValue = advancedExpectedRequestPostValuePairs[expectedKey];
|
|
4038
|
-
|
|
4039
|
-
for (const [key, value] of Object.entries(RequestPostDataObject)) {
|
|
4040
|
-
if (key === expectedKey) {
|
|
4041
|
-
keyFound = true;
|
|
4042
|
-
if (expectedValue === undefined) {
|
|
4043
|
-
littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
|
|
4044
|
-
} else if (typeof expectedValue === 'object' && expectedValue.base64) {
|
|
4045
|
-
const decodedActualValue = Buffer.from(value, 'base64').toString('utf8');
|
|
4046
|
-
if (decodedActualValue === expectedValue.base64) {
|
|
4047
|
-
littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
|
|
4048
|
-
} else {
|
|
4049
|
-
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
|
|
4050
|
-
success = false;
|
|
4051
|
-
}
|
|
4052
|
-
} else if (value === expectedValue) {
|
|
4053
|
-
littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
|
|
4054
|
-
} else {
|
|
4055
|
-
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${value}"\n`;
|
|
4056
|
-
success = false;
|
|
4057
|
-
}
|
|
4058
|
-
}
|
|
4059
|
-
}
|
|
4060
|
-
|
|
4061
|
-
if (keyFound === false) {
|
|
4062
|
-
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> key not found in request\n`;
|
|
4063
|
-
success = false;
|
|
4064
|
-
}
|
|
4065
|
-
}
|
|
4066
|
-
|
|
4067
|
-
return success ? true : littleReport;
|
|
4068
|
-
};
|