codeceptjs 3.5.2 → 3.5.4-beta.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 +4 -5
- package/docs/basics.md +1 -2
- package/docs/build/Playwright.js +461 -0
- package/docs/changelog.md +0 -7
- package/docs/custom-helpers.md +2 -2
- package/docs/data.md +6 -2
- package/docs/docker.md +2 -3
- package/docs/helpers/Playwright.md +305 -153
- package/docs/installation.md +1 -1
- package/docs/mobile.md +0 -2
- package/docs/plugins.md +0 -2
- package/docs/quickstart.md +0 -1
- package/docs/testcafe.md +1 -1
- package/docs/webdriver.md +0 -2
- package/lib/command/init.js +1 -1
- package/lib/helper/Playwright.js +562 -136
- package/lib/helper/extras/PlaywrightReact.js +9 -0
- package/lib/plugin/standardActingHelpers.js +0 -2
- package/lib/plugin/wdio.js +0 -1
- package/lib/recorder.js +2 -1
- package/package.json +5 -7
- package/typings/index.d.ts +0 -1
- package/typings/promiseBasedTypes.d.ts +127 -0
- package/typings/types.d.ts +127 -25
- package/docs/helpers/Nightmare.md +0 -1258
- package/docs/nightmare.md +0 -223
- package/lib/helper/Nightmare.js +0 -1410
- package/lib/helper/Protractor.js +0 -1832
- package/lib/helper/clientscripts/nightmare.js +0 -213
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ CodeceptJS tests are:
|
|
|
36
36
|
|
|
37
37
|
* **Synchronous**. You don't need to care about callbacks or promises or test scenarios which are linear. But, your tests should be linear.
|
|
38
38
|
* Written from **user's perspective**. Every action is a method of `I`. That makes test easy to read, write and maintain even for non-tech persons.
|
|
39
|
-
* Backend **API agnostic**. We don't know which WebDriver implementation is running this test.
|
|
39
|
+
* Backend **API agnostic**. We don't know which WebDriver implementation is running this test.
|
|
40
40
|
|
|
41
41
|
CodeceptJS uses **Helper** modules to provide actions to `I` object. Currently, CodeceptJS has these helpers:
|
|
42
42
|
|
|
@@ -44,7 +44,6 @@ CodeceptJS uses **Helper** modules to provide actions to `I` object. Currently,
|
|
|
44
44
|
* [**Puppeteer**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Puppeteer.md) - uses Google Chrome's Puppeteer for fast headless testing.
|
|
45
45
|
* [**WebDriver**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/WebDriver.md) - uses [webdriverio](http://webdriver.io/) to run tests via WebDriver protocol.
|
|
46
46
|
* [**TestCafe**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/TestCafe.md) - cheap and fast cross-browser test automation.
|
|
47
|
-
* [**Nightmare**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Nightmare.md) - uses Electron and NightmareJS to run tests.
|
|
48
47
|
* [**Appium**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Appium.md) - for **mobile testing** with Appium
|
|
49
48
|
* [**Detox**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Detox.md) - This is a wrapper on top of Detox library, aimed to unify testing experience for CodeceptJS framework. Detox provides a grey box testing for mobile applications, playing especially well for React Native apps.
|
|
50
49
|
|
|
@@ -54,7 +53,7 @@ And more to come...
|
|
|
54
53
|
|
|
55
54
|
CodeceptJS is a successor of [Codeception](http://codeception.com), a popular full-stack testing framework for PHP.
|
|
56
55
|
With CodeceptJS your scenario-driven functional and acceptance tests will be as simple and clean as they can be.
|
|
57
|
-
You don't need to worry about asynchronous nature of NodeJS or about various APIs of Selenium, Puppeteer,
|
|
56
|
+
You don't need to worry about asynchronous nature of NodeJS or about various APIs of Selenium, Puppeteer, TestCafe, etc. as CodeceptJS unifies them and makes them work as they are synchronous.
|
|
58
57
|
|
|
59
58
|
|
|
60
59
|
## Features
|
|
@@ -306,10 +305,10 @@ Thanks all to those who are and will have contributing to this awesome project!
|
|
|
306
305
|
<a href="https://github.com/Georgegriff"><img src="https://avatars.githubusercontent.com/u/9056958?v=4" title="Georgegriff" width="80" height="80"></a>
|
|
307
306
|
<a href="https://github.com/mirao"><img src="https://avatars.githubusercontent.com/u/12584138?v=4" title="mirao" width="80" height="80"></a>
|
|
308
307
|
<a href="https://github.com/KMKoushik"><img src="https://avatars.githubusercontent.com/u/24666922?v=4" title="KMKoushik" width="80" height="80"></a>
|
|
309
|
-
<a href="https://github.com/nikocanvacom"><img src="https://avatars.githubusercontent.com/u/83254493?v=4" title="nikocanvacom" width="80" height="80"></a>
|
|
310
|
-
<a href="https://github.com/elukoyanov"><img src="https://avatars.githubusercontent.com/u/11647141?v=4" title="elukoyanov" width="80" height="80"></a>
|
|
311
308
|
<a href="https://github.com/actions-user"><img src="https://avatars.githubusercontent.com/u/65916846?v=4" title="actions-user" width="80" height="80"></a>
|
|
312
309
|
<a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4" title="dependabot[bot]" width="80" height="80"></a>
|
|
310
|
+
<a href="https://github.com/nikocanvacom"><img src="https://avatars.githubusercontent.com/u/83254493?v=4" title="nikocanvacom" width="80" height="80"></a>
|
|
311
|
+
<a href="https://github.com/elukoyanov"><img src="https://avatars.githubusercontent.com/u/11647141?v=4" title="elukoyanov" width="80" height="80"></a>
|
|
313
312
|
<a href="https://github.com/gkushang"><img src="https://avatars.githubusercontent.com/u/3663389?v=4" title="gkushang" width="80" height="80"></a>
|
|
314
313
|
<a href="https://github.com/tsuemura"><img src="https://avatars.githubusercontent.com/u/17092259?v=4" title="tsuemura" width="80" height="80"></a>
|
|
315
314
|
<a href="https://github.com/EgorBodnar"><img src="https://avatars.githubusercontent.com/u/63167966?v=4" title="EgorBodnar" width="80" height="80"></a>
|
package/docs/basics.md
CHANGED
|
@@ -41,7 +41,6 @@ Refer to following guides to more information on:
|
|
|
41
41
|
* [▶ Playwright](/playwright)
|
|
42
42
|
* [▶ WebDriver](/webdriver)
|
|
43
43
|
* [▶ Puppeteer](/puppeteer)
|
|
44
|
-
* [▶ Nightmare](/nightmare)
|
|
45
44
|
* [▶ TestCafe](/testcafe)
|
|
46
45
|
|
|
47
46
|
> ℹ Depending on a helper selected a list of available actions may change.
|
|
@@ -783,7 +782,7 @@ within({frame: "#editor"}, () => {
|
|
|
783
782
|
|
|
784
783
|
> ℹ IFrames can also be accessed via `I.switchTo` command of a corresponding helper.
|
|
785
784
|
|
|
786
|
-
Nested IFrames can be set by passing an array *(WebDriver
|
|
785
|
+
Nested IFrames can be set by passing an array *(WebDriver & Puppeteer only)*:
|
|
787
786
|
|
|
788
787
|
```js
|
|
789
788
|
within({frame: [".content", "#editor"]}, () => {
|
package/docs/build/Playwright.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
|
|
4
4
|
const Helper = require('@codeceptjs/helper');
|
|
5
5
|
const { v4: uuidv4 } = require('uuid');
|
|
6
|
+
const assert = require('assert');
|
|
6
7
|
const Locator = require('../locator');
|
|
7
8
|
const store = require('../store');
|
|
8
9
|
const recorder = require('../recorder');
|
|
@@ -311,6 +312,11 @@ class Playwright extends Helper {
|
|
|
311
312
|
this.electronSessions = [];
|
|
312
313
|
this.storageState = null;
|
|
313
314
|
|
|
315
|
+
// for network stuff
|
|
316
|
+
this.requests = [];
|
|
317
|
+
this.recording = false;
|
|
318
|
+
this.recordedAtLeastOnce = false;
|
|
319
|
+
|
|
314
320
|
// override defaults with config
|
|
315
321
|
this._setConfig(config);
|
|
316
322
|
}
|
|
@@ -3667,6 +3673,330 @@ class Playwright extends Helper {
|
|
|
3667
3673
|
async stopMockingRoute(url, handler) {
|
|
3668
3674
|
return this.browserContext.unroute(...arguments);
|
|
3669
3675
|
}
|
|
3676
|
+
|
|
3677
|
+
/**
|
|
3678
|
+
* Starts recording of network traffic.
|
|
3679
|
+
* This also resets recorded network requests.
|
|
3680
|
+
*
|
|
3681
|
+
* ```js
|
|
3682
|
+
* I.startRecordingTraffic();
|
|
3683
|
+
* ```
|
|
3684
|
+
*
|
|
3685
|
+
* @return {Promise<void>}
|
|
3686
|
+
*/
|
|
3687
|
+
async startRecordingTraffic() {
|
|
3688
|
+
this.flushNetworkTraffics();
|
|
3689
|
+
this.recording = true;
|
|
3690
|
+
this.recordedAtLeastOnce = true;
|
|
3691
|
+
|
|
3692
|
+
this.page.on('requestfinished', async (request) => {
|
|
3693
|
+
const information = {
|
|
3694
|
+
url: request.url(),
|
|
3695
|
+
method: request.method(),
|
|
3696
|
+
requestHeaders: request.headers(),
|
|
3697
|
+
requestPostData: request.postData(),
|
|
3698
|
+
};
|
|
3699
|
+
|
|
3700
|
+
this.debugSection('REQUEST: ', JSON.stringify(information));
|
|
3701
|
+
|
|
3702
|
+
information.requestPostData = JSON.parse(information.requestPostData);
|
|
3703
|
+
this.requests.push(information);
|
|
3704
|
+
return this._waitForAction();
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
/**
|
|
3709
|
+
* Grab the recording network traffics
|
|
3710
|
+
*
|
|
3711
|
+
* @return { Array<any> }
|
|
3712
|
+
*
|
|
3713
|
+
*/
|
|
3714
|
+
grabRecordedNetworkTraffics() {
|
|
3715
|
+
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
3716
|
+
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
3717
|
+
}
|
|
3718
|
+
return this.requests;
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
/**
|
|
3722
|
+
* Blocks traffic for URL.
|
|
3723
|
+
*
|
|
3724
|
+
* Examples:
|
|
3725
|
+
*
|
|
3726
|
+
* ```js
|
|
3727
|
+
* I.blockTraffic('http://example.com/css/style.css');
|
|
3728
|
+
* I.blockTraffic('http://example.com/css/*.css');
|
|
3729
|
+
* I.blockTraffic('http://example.com/**');
|
|
3730
|
+
* I.blockTraffic(/\.css$/);
|
|
3731
|
+
* ```
|
|
3732
|
+
*
|
|
3733
|
+
* @param url URL to block . URL can contain * for wildcards. Example: https://www.example.com** to block all traffic for that domain. Regexp are also supported.
|
|
3734
|
+
*/
|
|
3735
|
+
async blockTraffic(url) {
|
|
3736
|
+
this.page.route(url, (route) => {
|
|
3737
|
+
route
|
|
3738
|
+
.abort()
|
|
3739
|
+
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
3740
|
+
.catch((e) => {});
|
|
3741
|
+
});
|
|
3742
|
+
return this._waitForAction();
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
/**
|
|
3746
|
+
* Mocks traffic for URL(s).
|
|
3747
|
+
* This is a powerful feature to manipulate network traffic. Can be used e.g. to stabilize your tests, speed up your tests or as a last resort to make some test scenarios even possible.
|
|
3748
|
+
*
|
|
3749
|
+
* Examples:
|
|
3750
|
+
*
|
|
3751
|
+
* ```js
|
|
3752
|
+
* I.mockTraffic('/api/users/1', '{ id: 1, name: 'John Doe' }');
|
|
3753
|
+
* I.mockTraffic('/api/users/*', JSON.stringify({ id: 1, name: 'John Doe' }));
|
|
3754
|
+
* I.mockTraffic([/^https://api.example.com/v1/, 'https://api.example.com/v2/**'], 'Internal Server Error', 'text/html');
|
|
3755
|
+
* ```
|
|
3756
|
+
*
|
|
3757
|
+
* @param urls string|Array These are the URL(s) to mock, e.g. "/fooapi/*" or "['/fooapi_1/*', '/barapi_2/*']". Regular expressions are also supported.
|
|
3758
|
+
* @param responseString string The string to return in fake response's body.
|
|
3759
|
+
* @param contentType Content type of fake response. If not specified default value 'application/json' is used.
|
|
3760
|
+
*/
|
|
3761
|
+
async mockTraffic(urls, responseString, contentType = 'application/json') {
|
|
3762
|
+
// Required to mock cross-domain requests
|
|
3763
|
+
const headers = { 'access-control-allow-origin': '*' };
|
|
3764
|
+
|
|
3765
|
+
if (typeof urls === 'string') {
|
|
3766
|
+
urls = [urls];
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
urls.forEach((url) => {
|
|
3770
|
+
this.page.route(url, (route) => {
|
|
3771
|
+
if (this.page.isClosed()) {
|
|
3772
|
+
// Sometimes it happens that browser has been closed in the meantime.
|
|
3773
|
+
// In this case we just don't fulfill to prevent error in test scenario.
|
|
3774
|
+
return;
|
|
3775
|
+
}
|
|
3776
|
+
route.fulfill({
|
|
3777
|
+
contentType,
|
|
3778
|
+
headers,
|
|
3779
|
+
body: responseString,
|
|
3780
|
+
});
|
|
3781
|
+
});
|
|
3782
|
+
});
|
|
3783
|
+
return this._waitForAction();
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
/**
|
|
3787
|
+
* Resets all recorded network requests.
|
|
3788
|
+
*/
|
|
3789
|
+
flushNetworkTraffics() {
|
|
3790
|
+
this.requests = [];
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
/**
|
|
3794
|
+
* Stops recording of network traffic. Recorded traffic is not flashed.
|
|
3795
|
+
*
|
|
3796
|
+
* ```js
|
|
3797
|
+
* I.stopRecordingTraffic();
|
|
3798
|
+
* ```
|
|
3799
|
+
*/
|
|
3800
|
+
stopRecordingTraffic() {
|
|
3801
|
+
this.page.removeAllListeners('request');
|
|
3802
|
+
this.recording = false;
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
/**
|
|
3806
|
+
* Verifies that a certain request is part of network traffic.
|
|
3807
|
+
*
|
|
3808
|
+
* ```js
|
|
3809
|
+
* // checking the request url contains certain query strings
|
|
3810
|
+
* I.amOnPage('https://openai.com/blog/chatgpt');
|
|
3811
|
+
* I.startRecordingTraffic();
|
|
3812
|
+
* await I.seeTraffic({
|
|
3813
|
+
* name: 'sentry event',
|
|
3814
|
+
* url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600',
|
|
3815
|
+
* parameters: {
|
|
3816
|
+
* width: '1919',
|
|
3817
|
+
* height: '1138',
|
|
3818
|
+
* },
|
|
3819
|
+
* });
|
|
3820
|
+
* ```
|
|
3821
|
+
*
|
|
3822
|
+
* ```js
|
|
3823
|
+
* // checking the request url contains certain post data
|
|
3824
|
+
* I.amOnPage('https://openai.com/blog/chatgpt');
|
|
3825
|
+
* I.startRecordingTraffic();
|
|
3826
|
+
* await I.seeTraffic({
|
|
3827
|
+
* name: 'event',
|
|
3828
|
+
* url: 'https://cloudflareinsights.com/cdn-cgi/rum',
|
|
3829
|
+
* requestPostData: {
|
|
3830
|
+
* st: 2,
|
|
3831
|
+
* },
|
|
3832
|
+
* });
|
|
3833
|
+
* ```
|
|
3834
|
+
*
|
|
3835
|
+
* @param {Object} opts - options when checking the traffic network.
|
|
3836
|
+
* @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.
|
|
3837
|
+
* @param {string} opts.url Expected URL of request in network traffic
|
|
3838
|
+
* @param {Object} [opts.parameters] Expected parameters of that request in network traffic
|
|
3839
|
+
* @param {Object} [opts.requestPostData] Expected that request contains post data in network traffic
|
|
3840
|
+
* @param {number} [opts.timeout] Timeout to wait for request in seconds. Default is 10 seconds.
|
|
3841
|
+
* @return { Promise<*> }
|
|
3842
|
+
*/
|
|
3843
|
+
async seeTraffic({
|
|
3844
|
+
name, url, parameters, requestPostData, timeout = 10,
|
|
3845
|
+
}) {
|
|
3846
|
+
if (!name) {
|
|
3847
|
+
throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
if (!url) {
|
|
3851
|
+
throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
3855
|
+
throw new Error('Failure in test automation. You use "I.seeInTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
for (let i = 0; i <= timeout * 2; i++) {
|
|
3859
|
+
const found = this._isInTraffic(url, parameters);
|
|
3860
|
+
if (found) {
|
|
3861
|
+
return true;
|
|
3862
|
+
}
|
|
3863
|
+
await new Promise((done) => setTimeout(done, 1000));
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
// check request post data
|
|
3867
|
+
if (requestPostData && this._isInTraffic(url)) {
|
|
3868
|
+
const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);
|
|
3869
|
+
|
|
3870
|
+
assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
|
|
3871
|
+
} else if (parameters && this._isInTraffic(url)) {
|
|
3872
|
+
const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);
|
|
3873
|
+
|
|
3874
|
+
assert.fail(
|
|
3875
|
+
`Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
|
|
3876
|
+
+ `${advancedTestResults}`,
|
|
3877
|
+
);
|
|
3878
|
+
} else {
|
|
3879
|
+
assert.fail(
|
|
3880
|
+
`Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
|
|
3881
|
+
+ `Expected url: ${url}.\n`
|
|
3882
|
+
+ `Recorded traffic:\n${this._getTrafficDump()}`,
|
|
3883
|
+
);
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
/**
|
|
3888
|
+
* Returns full URL of request matching parameter "urlMatch".
|
|
3889
|
+
*
|
|
3890
|
+
* @param {string|RegExp} urlMatch Expected URL of request in network traffic. Can be a string or a regular expression.
|
|
3891
|
+
*
|
|
3892
|
+
* Examples:
|
|
3893
|
+
*
|
|
3894
|
+
* ```js
|
|
3895
|
+
* I.grabTrafficUrl('https://api.example.com/session');
|
|
3896
|
+
* I.grabTrafficUrl(/session.*start/);
|
|
3897
|
+
* ```
|
|
3898
|
+
*
|
|
3899
|
+
* @return {Promise<*>}
|
|
3900
|
+
*/
|
|
3901
|
+
grabTrafficUrl(urlMatch) {
|
|
3902
|
+
if (!this.recordedAtLeastOnce) {
|
|
3903
|
+
throw new Error('Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.');
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
for (const i in this.requests) {
|
|
3907
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
3908
|
+
if (this.requests.hasOwnProperty(i)) {
|
|
3909
|
+
const request = this.requests[i];
|
|
3910
|
+
|
|
3911
|
+
if (request.url && request.url.match(new RegExp(urlMatch))) {
|
|
3912
|
+
return request.url;
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
assert.fail(`Method "getTrafficUrl" failed: No request found in traffic that matches ${urlMatch}`);
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
/**
|
|
3921
|
+
* Verifies that a certain request is not part of network traffic.
|
|
3922
|
+
*
|
|
3923
|
+
* Examples:
|
|
3924
|
+
*
|
|
3925
|
+
* ```js
|
|
3926
|
+
* I.dontSeeTraffic({ name: 'Unexpected API Call', url: 'https://api.example.com' });
|
|
3927
|
+
* I.dontSeeTraffic({ name: 'Unexpected API Call of "user" endpoint', url: /api.example.com.*user/ });
|
|
3928
|
+
* ```
|
|
3929
|
+
*
|
|
3930
|
+
* @param {Object} opts - options when checking the traffic network.
|
|
3931
|
+
* @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.
|
|
3932
|
+
* @param {string|RegExp} opts.url Expected URL of request in network traffic. Can be a string or a regular expression.
|
|
3933
|
+
*
|
|
3934
|
+
*/
|
|
3935
|
+
dontSeeTraffic({ name, url }) {
|
|
3936
|
+
if (!this.recordedAtLeastOnce) {
|
|
3937
|
+
throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
if (!name) {
|
|
3941
|
+
throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
if (!url) {
|
|
3945
|
+
throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
if (this._isInTraffic(url)) {
|
|
3949
|
+
assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
/**
|
|
3954
|
+
* Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
|
|
3955
|
+
*
|
|
3956
|
+
* @param url URL to look for.
|
|
3957
|
+
* @param [parameters] Parameters that this URL needs to contain
|
|
3958
|
+
* @return {boolean} Whether or not URL with parameters is part of network traffic.
|
|
3959
|
+
* @private
|
|
3960
|
+
*/
|
|
3961
|
+
_isInTraffic(url, parameters) {
|
|
3962
|
+
let isInTraffic = false;
|
|
3963
|
+
this.requests.forEach((request) => {
|
|
3964
|
+
if (isInTraffic) {
|
|
3965
|
+
return; // We already found traffic. Continue with next request
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
if (!request.url.match(new RegExp(url))) {
|
|
3969
|
+
return; // url not found in this request. continue with next request
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
// URL has matched. Now we check the parameters
|
|
3973
|
+
|
|
3974
|
+
if (parameters) {
|
|
3975
|
+
const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
|
|
3976
|
+
if (advancedReport === true) {
|
|
3977
|
+
isInTraffic = true;
|
|
3978
|
+
}
|
|
3979
|
+
} else {
|
|
3980
|
+
isInTraffic = true;
|
|
3981
|
+
}
|
|
3982
|
+
});
|
|
3983
|
+
|
|
3984
|
+
return isInTraffic;
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
/**
|
|
3988
|
+
* Returns all URLs of all network requests recorded so far during execution of test scenario.
|
|
3989
|
+
*
|
|
3990
|
+
* @return {string} List of URLs recorded as a string, seperaeted by new lines after each URL
|
|
3991
|
+
* @private
|
|
3992
|
+
*/
|
|
3993
|
+
_getTrafficDump() {
|
|
3994
|
+
let dumpedTraffic = '';
|
|
3995
|
+
this.requests.forEach((request) => {
|
|
3996
|
+
dumpedTraffic += `${request.method} - ${request.url}\n`;
|
|
3997
|
+
});
|
|
3998
|
+
return dumpedTraffic;
|
|
3999
|
+
}
|
|
3670
4000
|
}
|
|
3671
4001
|
|
|
3672
4002
|
module.exports = Playwright;
|
|
@@ -4164,3 +4494,134 @@ function highlightActiveElement(element, context) {
|
|
|
4164
4494
|
|
|
4165
4495
|
highlightElement(element, context);
|
|
4166
4496
|
}
|
|
4497
|
+
|
|
4498
|
+
const createAdvancedTestResults = (url, dataToCheck, requests) => {
|
|
4499
|
+
// Creates advanced test results for a network traffic check.
|
|
4500
|
+
// Advanced test results only applies when expected parameters are set
|
|
4501
|
+
if (!dataToCheck) return '';
|
|
4502
|
+
|
|
4503
|
+
let urlFound = false;
|
|
4504
|
+
let advancedResults;
|
|
4505
|
+
requests.forEach((request) => {
|
|
4506
|
+
// url not found in this request. continue with next request
|
|
4507
|
+
if (urlFound || !request.url.match(new RegExp(url))) return;
|
|
4508
|
+
urlFound = true;
|
|
4509
|
+
|
|
4510
|
+
// Url found. Now we create advanced test report for that URL and show which parameters failed
|
|
4511
|
+
if (!request.requestPostData) {
|
|
4512
|
+
advancedResults = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), dataToCheck);
|
|
4513
|
+
} else if (request.requestPostData) {
|
|
4514
|
+
advancedResults = allRequestPostDataValuePairsMatchExtreme(request.requestPostData, dataToCheck);
|
|
4515
|
+
}
|
|
4516
|
+
});
|
|
4517
|
+
return advancedResults;
|
|
4518
|
+
};
|
|
4519
|
+
|
|
4520
|
+
const extractQueryObjects = (queryString) => {
|
|
4521
|
+
// Converts a string of GET parameters into an array of parameter objects. Each parameter object contains the properties "name" and "value".
|
|
4522
|
+
if (queryString.indexOf('?') === -1) {
|
|
4523
|
+
return [];
|
|
4524
|
+
}
|
|
4525
|
+
const queryObjects = [];
|
|
4526
|
+
|
|
4527
|
+
const queryPart = queryString.split('?')[1];
|
|
4528
|
+
|
|
4529
|
+
const queryParameters = queryPart.split('&');
|
|
4530
|
+
|
|
4531
|
+
queryParameters.forEach((queryParameter) => {
|
|
4532
|
+
const keyValue = queryParameter.split('=');
|
|
4533
|
+
const queryObject = {};
|
|
4534
|
+
// eslint-disable-next-line prefer-destructuring
|
|
4535
|
+
queryObject.name = keyValue[0];
|
|
4536
|
+
queryObject.value = decodeURIComponent(keyValue[1]);
|
|
4537
|
+
queryObjects.push(queryObject);
|
|
4538
|
+
});
|
|
4539
|
+
|
|
4540
|
+
return queryObjects;
|
|
4541
|
+
};
|
|
4542
|
+
|
|
4543
|
+
const allParameterValuePairsMatchExtreme = (queryStringObject, advancedExpectedParameterValuePairs) => {
|
|
4544
|
+
// More advanced check if all request parameters match with the expectations
|
|
4545
|
+
let littleReport = '\nQuery parameters:\n';
|
|
4546
|
+
let success = true;
|
|
4547
|
+
|
|
4548
|
+
for (const expectedKey in advancedExpectedParameterValuePairs) {
|
|
4549
|
+
if (!Object.prototype.hasOwnProperty.call(advancedExpectedParameterValuePairs, expectedKey)) {
|
|
4550
|
+
continue;
|
|
4551
|
+
}
|
|
4552
|
+
let parameterFound = false;
|
|
4553
|
+
const expectedValue = advancedExpectedParameterValuePairs[expectedKey];
|
|
4554
|
+
|
|
4555
|
+
for (const queryParameter of queryStringObject) {
|
|
4556
|
+
if (queryParameter.name === expectedKey) {
|
|
4557
|
+
parameterFound = true;
|
|
4558
|
+
if (expectedValue === undefined) {
|
|
4559
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
|
|
4560
|
+
} else if (typeof expectedValue === 'object' && expectedValue.base64) {
|
|
4561
|
+
const decodedActualValue = Buffer.from(queryParameter.value, 'base64').toString('utf8');
|
|
4562
|
+
if (decodedActualValue === expectedValue.base64) {
|
|
4563
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
|
|
4564
|
+
} else {
|
|
4565
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
|
|
4566
|
+
success = false;
|
|
4567
|
+
}
|
|
4568
|
+
} else if (queryParameter.value === expectedValue) {
|
|
4569
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
|
|
4570
|
+
} else {
|
|
4571
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${queryParameter.value}"\n`;
|
|
4572
|
+
success = false;
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4577
|
+
if (parameterFound === false) {
|
|
4578
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> parameter not found in request\n`;
|
|
4579
|
+
success = false;
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
|
|
4583
|
+
return success ? true : littleReport;
|
|
4584
|
+
};
|
|
4585
|
+
|
|
4586
|
+
const allRequestPostDataValuePairsMatchExtreme = (RequestPostDataObject, advancedExpectedRequestPostValuePairs) => {
|
|
4587
|
+
// More advanced check if all request post data match with the expectations
|
|
4588
|
+
let littleReport = '\nRequest Post Data:\n';
|
|
4589
|
+
let success = true;
|
|
4590
|
+
|
|
4591
|
+
for (const expectedKey in advancedExpectedRequestPostValuePairs) {
|
|
4592
|
+
if (!Object.prototype.hasOwnProperty.call(advancedExpectedRequestPostValuePairs, expectedKey)) {
|
|
4593
|
+
continue;
|
|
4594
|
+
}
|
|
4595
|
+
let keyFound = false;
|
|
4596
|
+
const expectedValue = advancedExpectedRequestPostValuePairs[expectedKey];
|
|
4597
|
+
|
|
4598
|
+
for (const [key, value] of Object.entries(RequestPostDataObject)) {
|
|
4599
|
+
if (key === expectedKey) {
|
|
4600
|
+
keyFound = true;
|
|
4601
|
+
if (expectedValue === undefined) {
|
|
4602
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
|
|
4603
|
+
} else if (typeof expectedValue === 'object' && expectedValue.base64) {
|
|
4604
|
+
const decodedActualValue = Buffer.from(value, 'base64').toString('utf8');
|
|
4605
|
+
if (decodedActualValue === expectedValue.base64) {
|
|
4606
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
|
|
4607
|
+
} else {
|
|
4608
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
|
|
4609
|
+
success = false;
|
|
4610
|
+
}
|
|
4611
|
+
} else if (value === expectedValue) {
|
|
4612
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
|
|
4613
|
+
} else {
|
|
4614
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${value}"\n`;
|
|
4615
|
+
success = false;
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
if (keyFound === false) {
|
|
4621
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> key not found in request\n`;
|
|
4622
|
+
success = false;
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4626
|
+
return success ? true : littleReport;
|
|
4627
|
+
};
|
package/docs/changelog.md
CHANGED
|
@@ -7,13 +7,6 @@ layout: Section
|
|
|
7
7
|
|
|
8
8
|
# Releases
|
|
9
9
|
|
|
10
|
-
## 3.5.2
|
|
11
|
-
|
|
12
|
-
🐛 Bug Fixes
|
|
13
|
-
|
|
14
|
-
* **[Playwright]** reverted `clearField` to previous implementation
|
|
15
|
-
* **[OpenAI]** fixed running helper in pause mode. [#3755](https://github.com/codeceptjs/CodeceptJS/issues/3755) by **[KobeNguyenT](https://github.com/KobeNguyenT)**
|
|
16
|
-
|
|
17
10
|
## 3.5.1
|
|
18
11
|
|
|
19
12
|
🛩️ Features
|
package/docs/custom-helpers.md
CHANGED
|
@@ -102,7 +102,7 @@ This way, if your tests are written with TypeScript, your IDE will be able to le
|
|
|
102
102
|
|
|
103
103
|
## Accessing Elements
|
|
104
104
|
|
|
105
|
-
WebDriver, Puppeteer
|
|
105
|
+
WebDriver, Puppeteer and Playwright drivers provide API for web elements.
|
|
106
106
|
However, CodeceptJS do not expose them to tests by design, keeping test to be action focused.
|
|
107
107
|
If you need to get access to web elements, it is recommended to implement operations for web elements in a custom helper.
|
|
108
108
|
|
|
@@ -154,7 +154,7 @@ You can also pass additional config options to your helper from a config - **(pl
|
|
|
154
154
|
```js
|
|
155
155
|
helpers: {
|
|
156
156
|
// here goes standard helpers:
|
|
157
|
-
// WebDriver,
|
|
157
|
+
// WebDriver, Playwright, etc...
|
|
158
158
|
// and their configuration
|
|
159
159
|
MyHelper: {
|
|
160
160
|
require: "./my_helper.js", // path to module
|
package/docs/data.md
CHANGED
|
@@ -23,7 +23,7 @@ API is supposed to be a stable interface and it can be used by acceptance tests.
|
|
|
23
23
|
## REST
|
|
24
24
|
|
|
25
25
|
[REST helper](https://codecept.io/helpers/REST/) allows sending raw HTTP requests to application.
|
|
26
|
-
This is a tool to make shortcuts and create your data pragmatically via API. However, it doesn't provide tools for testing APIs, so it should be paired with
|
|
26
|
+
This is a tool to make shortcuts and create your data pragmatically via API. However, it doesn't provide tools for testing APIs, so it should be paired with Playwright or WebDriver helper for browser testing.
|
|
27
27
|
|
|
28
28
|
Enable REST helper in the config. It is recommended to set `endpoint`, a base URL for all API requests. If you need some authorization you can optionally set default headers too.
|
|
29
29
|
|
|
@@ -92,7 +92,11 @@ I.sendPostRequest('/update-status', {}, { http_x_requested_with: 'xmlhttprequest
|
|
|
92
92
|
## GraphQL
|
|
93
93
|
|
|
94
94
|
[GraphQL helper](https://codecept.io/helpers/GraphQL/) allows sending GraphQL queries and mutations to application, over Http.
|
|
95
|
-
|
|
95
|
+
<<<<<<< HEAD
|
|
96
|
+
This is a tool to make shortcuts and create your data pragmatically via GraphQL endpoint. However, it doesn't provide tools for testing the endpoint, so it should be paired with WebDriver helper for browser testing.
|
|
97
|
+
=======
|
|
98
|
+
This is a tool to make shortcuts and create your data pragmatically via GraphQL endpoint. However, it doesn't provide tools for testing the endpoint, so it should be paired with WebDriver helpers for browser testing.
|
|
99
|
+
>>>>>>> 3.x
|
|
96
100
|
|
|
97
101
|
Enable GraphQL helper in the config. It is recommended to set `endpoint`, the URL to which the requests go to. If you need some authorization you can optionally set default headers too.
|
|
98
102
|
|
package/docs/docker.md
CHANGED
|
@@ -13,7 +13,7 @@ CodeceptJS runner is available inside container as `codeceptjs`.
|
|
|
13
13
|
|
|
14
14
|
### Locally
|
|
15
15
|
|
|
16
|
-
You can execute CodeceptJS with Puppeteer
|
|
16
|
+
You can execute CodeceptJS with Puppeteer locally with no extra configuration.
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
19
|
docker run --net=host -v $PWD:/tests codeceptjs/codeceptjs
|
|
@@ -56,7 +56,7 @@ services:
|
|
|
56
56
|
|
|
57
57
|
### Linking Containers
|
|
58
58
|
|
|
59
|
-
If using the
|
|
59
|
+
If using the WebDriver driver, link the container with a Selenium Standalone docker container with an alias of `selenium`. Additionally, make sure your `codeceptjs.conf.js` contains the following to allow CodeceptJS to identify where Selenium is running.
|
|
60
60
|
|
|
61
61
|
```javascript
|
|
62
62
|
...
|
|
@@ -80,7 +80,6 @@ $ docker run -it --rm -v /<path_to_codeceptjs_test_dir>/:/tests/ --link selenium
|
|
|
80
80
|
|
|
81
81
|
You may run use `-v $(pwd)/:tests/` if running this from the root of your CodeceptJS tests directory.
|
|
82
82
|
_Note: The output of your test run will appear in your local directory if your output path is `./output` in the CodeceptJS config_
|
|
83
|
-
_Note: If running with the Nightmare driver, it is not necessary to run a selenium docker container and link it. So `--link selenium-chrome:selenium` may be omitted_
|
|
84
83
|
|
|
85
84
|
### Build
|
|
86
85
|
|