codeceptjs 3.5.13-beta.2 → 3.5.13-beta.3
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/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/stopRecordingTraffic.mustache +5 -0
- package/lib/helper/Playwright.js +8 -204
- package/lib/helper/WebDriver.js +264 -3
- package/lib/helper/networkTraffics/utils.js +137 -0
- package/package.json +2 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Verifies that a certain request is not part of network traffic.
|
|
2
|
+
|
|
3
|
+
Examples:
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
I.dontSeeTraffic({ name: 'Unexpected API Call', url: 'https://api.example.com' });
|
|
7
|
+
I.dontSeeTraffic({ name: 'Unexpected API Call of "user" endpoint', url: /api.example.com.*user/ });
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
@param {Object} opts - options when checking the traffic network.
|
|
11
|
+
@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.
|
|
12
|
+
@param {string|RegExp} opts.url Expected URL of request in network traffic. Can be a string or a regular expression.
|
|
13
|
+
@return {void} automatically synchronized promise through #recorder
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Grab the recording network traffics
|
|
2
|
+
|
|
3
|
+
```js
|
|
4
|
+
const traffics = await I.grabRecordedNetworkTraffics();
|
|
5
|
+
expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1');
|
|
6
|
+
expect(traffics[0].response.status).to.equal(200);
|
|
7
|
+
expect(traffics[0].response.body).to.contain({ name: 'this was mocked' });
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
@return { Array } recorded network traffics
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Verifies that a certain request is part of network traffic.
|
|
2
|
+
|
|
3
|
+
```js
|
|
4
|
+
// checking the request url contains certain query strings
|
|
5
|
+
I.amOnPage('https://openai.com/blog/chatgpt');
|
|
6
|
+
I.startRecordingTraffic();
|
|
7
|
+
await I.seeTraffic({
|
|
8
|
+
name: 'sentry event',
|
|
9
|
+
url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600',
|
|
10
|
+
parameters: {
|
|
11
|
+
width: '1919',
|
|
12
|
+
height: '1138',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// checking the request url contains certain post data
|
|
19
|
+
I.amOnPage('https://openai.com/blog/chatgpt');
|
|
20
|
+
I.startRecordingTraffic();
|
|
21
|
+
await I.seeTraffic({
|
|
22
|
+
name: 'event',
|
|
23
|
+
url: 'https://cloudflareinsights.com/cdn-cgi/rum',
|
|
24
|
+
requestPostData: {
|
|
25
|
+
st: 2,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
@param {Object} opts - options when checking the traffic network.
|
|
31
|
+
@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.
|
|
32
|
+
@param {string} opts.url Expected URL of request in network traffic
|
|
33
|
+
@param {Object} [opts.parameters] Expected parameters of that request in network traffic
|
|
34
|
+
@param {Object} [opts.requestPostData] Expected that request contains post data in network traffic
|
|
35
|
+
@param {number} [opts.timeout] Timeout to wait for request in seconds. Default is 10 seconds.
|
|
36
|
+
@return {void} automatically synchronized promise through #recorder
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -51,6 +51,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
|
|
|
51
51
|
const {
|
|
52
52
|
seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError,
|
|
53
53
|
} = require('./errors/ElementAssertion');
|
|
54
|
+
const { createAdvancedTestResults, allParameterValuePairsMatchExtreme, extractQueryObjects } = require('./networkTraffics/utils');
|
|
54
55
|
const { log } = require('../output');
|
|
55
56
|
|
|
56
57
|
const pathSeparator = path.sep;
|
|
@@ -2976,14 +2977,8 @@ class Playwright extends Helper {
|
|
|
2976
2977
|
}
|
|
2977
2978
|
|
|
2978
2979
|
/**
|
|
2979
|
-
*
|
|
2980
|
-
* This also resets recorded network requests.
|
|
2980
|
+
* {{> flushNetworkTraffics }}
|
|
2981
2981
|
*
|
|
2982
|
-
* ```js
|
|
2983
|
-
* I.startRecordingTraffic();
|
|
2984
|
-
* ```
|
|
2985
|
-
*
|
|
2986
|
-
* @return {void}
|
|
2987
2982
|
*/
|
|
2988
2983
|
startRecordingTraffic() {
|
|
2989
2984
|
this.flushNetworkTraffics();
|
|
@@ -3010,18 +3005,8 @@ class Playwright extends Helper {
|
|
|
3010
3005
|
}
|
|
3011
3006
|
|
|
3012
3007
|
/**
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
* ```js
|
|
3016
|
-
* const traffics = await I.grabRecordedNetworkTraffics();
|
|
3017
|
-
* expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1');
|
|
3018
|
-
* expect(traffics[0].response.status).to.equal(200);
|
|
3019
|
-
* expect(traffics[0].response.body).to.contain({ name: 'this was mocked' });
|
|
3020
|
-
* ```
|
|
3021
|
-
*
|
|
3022
|
-
* @return { Promise<Array<any>> }
|
|
3023
|
-
*
|
|
3024
|
-
*/
|
|
3008
|
+
* {{> grabRecordedNetworkTraffics }}
|
|
3009
|
+
*/
|
|
3025
3010
|
async grabRecordedNetworkTraffics() {
|
|
3026
3011
|
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
3027
3012
|
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
@@ -3129,18 +3114,14 @@ class Playwright extends Helper {
|
|
|
3129
3114
|
}
|
|
3130
3115
|
|
|
3131
3116
|
/**
|
|
3132
|
-
*
|
|
3117
|
+
* {{> flushNetworkTraffics }}
|
|
3133
3118
|
*/
|
|
3134
3119
|
flushNetworkTraffics() {
|
|
3135
3120
|
this.requests = [];
|
|
3136
3121
|
}
|
|
3137
3122
|
|
|
3138
3123
|
/**
|
|
3139
|
-
*
|
|
3140
|
-
*
|
|
3141
|
-
* ```js
|
|
3142
|
-
* I.stopRecordingTraffic();
|
|
3143
|
-
* ```
|
|
3124
|
+
* {{> stopRecordingTraffic }}
|
|
3144
3125
|
*/
|
|
3145
3126
|
stopRecordingTraffic() {
|
|
3146
3127
|
this.page.removeAllListeners('request');
|
|
@@ -3148,42 +3129,7 @@ class Playwright extends Helper {
|
|
|
3148
3129
|
}
|
|
3149
3130
|
|
|
3150
3131
|
/**
|
|
3151
|
-
*
|
|
3152
|
-
*
|
|
3153
|
-
* ```js
|
|
3154
|
-
* // checking the request url contains certain query strings
|
|
3155
|
-
* I.amOnPage('https://openai.com/blog/chatgpt');
|
|
3156
|
-
* I.startRecordingTraffic();
|
|
3157
|
-
* await I.seeTraffic({
|
|
3158
|
-
* name: 'sentry event',
|
|
3159
|
-
* url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600',
|
|
3160
|
-
* parameters: {
|
|
3161
|
-
* width: '1919',
|
|
3162
|
-
* height: '1138',
|
|
3163
|
-
* },
|
|
3164
|
-
* });
|
|
3165
|
-
* ```
|
|
3166
|
-
*
|
|
3167
|
-
* ```js
|
|
3168
|
-
* // checking the request url contains certain post data
|
|
3169
|
-
* I.amOnPage('https://openai.com/blog/chatgpt');
|
|
3170
|
-
* I.startRecordingTraffic();
|
|
3171
|
-
* await I.seeTraffic({
|
|
3172
|
-
* name: 'event',
|
|
3173
|
-
* url: 'https://cloudflareinsights.com/cdn-cgi/rum',
|
|
3174
|
-
* requestPostData: {
|
|
3175
|
-
* st: 2,
|
|
3176
|
-
* },
|
|
3177
|
-
* });
|
|
3178
|
-
* ```
|
|
3179
|
-
*
|
|
3180
|
-
* @param {Object} opts - options when checking the traffic network.
|
|
3181
|
-
* @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.
|
|
3182
|
-
* @param {string} opts.url Expected URL of request in network traffic
|
|
3183
|
-
* @param {Object} [opts.parameters] Expected parameters of that request in network traffic
|
|
3184
|
-
* @param {Object} [opts.requestPostData] Expected that request contains post data in network traffic
|
|
3185
|
-
* @param {number} [opts.timeout] Timeout to wait for request in seconds. Default is 10 seconds.
|
|
3186
|
-
* @return { Promise<*> }
|
|
3132
|
+
* {{> seeTraffic }}
|
|
3187
3133
|
*/
|
|
3188
3134
|
async seeTraffic({
|
|
3189
3135
|
name, url, parameters, requestPostData, timeout = 10,
|
|
@@ -3265,18 +3211,7 @@ class Playwright extends Helper {
|
|
|
3265
3211
|
}
|
|
3266
3212
|
|
|
3267
3213
|
/**
|
|
3268
|
-
*
|
|
3269
|
-
*
|
|
3270
|
-
* Examples:
|
|
3271
|
-
*
|
|
3272
|
-
* ```js
|
|
3273
|
-
* I.dontSeeTraffic({ name: 'Unexpected API Call', url: 'https://api.example.com' });
|
|
3274
|
-
* I.dontSeeTraffic({ name: 'Unexpected API Call of "user" endpoint', url: /api.example.com.*user/ });
|
|
3275
|
-
* ```
|
|
3276
|
-
*
|
|
3277
|
-
* @param {Object} opts - options when checking the traffic network.
|
|
3278
|
-
* @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.
|
|
3279
|
-
* @param {string|RegExp} opts.url Expected URL of request in network traffic. Can be a string or a regular expression.
|
|
3214
|
+
* {{> dontSeeTraffic }}
|
|
3280
3215
|
*
|
|
3281
3216
|
*/
|
|
3282
3217
|
dontSeeTraffic({ name, url }) {
|
|
@@ -3988,134 +3923,3 @@ async function highlightActiveElement(element) {
|
|
|
3988
3923
|
});
|
|
3989
3924
|
}
|
|
3990
3925
|
}
|
|
3991
|
-
|
|
3992
|
-
const createAdvancedTestResults = (url, dataToCheck, requests) => {
|
|
3993
|
-
// Creates advanced test results for a network traffic check.
|
|
3994
|
-
// Advanced test results only applies when expected parameters are set
|
|
3995
|
-
if (!dataToCheck) return '';
|
|
3996
|
-
|
|
3997
|
-
let urlFound = false;
|
|
3998
|
-
let advancedResults;
|
|
3999
|
-
requests.forEach((request) => {
|
|
4000
|
-
// url not found in this request. continue with next request
|
|
4001
|
-
if (urlFound || !request.url.match(new RegExp(url))) return;
|
|
4002
|
-
urlFound = true;
|
|
4003
|
-
|
|
4004
|
-
// Url found. Now we create advanced test report for that URL and show which parameters failed
|
|
4005
|
-
if (!request.requestPostData) {
|
|
4006
|
-
advancedResults = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), dataToCheck);
|
|
4007
|
-
} else if (request.requestPostData) {
|
|
4008
|
-
advancedResults = allRequestPostDataValuePairsMatchExtreme(request.requestPostData, dataToCheck);
|
|
4009
|
-
}
|
|
4010
|
-
});
|
|
4011
|
-
return advancedResults;
|
|
4012
|
-
};
|
|
4013
|
-
|
|
4014
|
-
const extractQueryObjects = (queryString) => {
|
|
4015
|
-
// Converts a string of GET parameters into an array of parameter objects. Each parameter object contains the properties "name" and "value".
|
|
4016
|
-
if (queryString.indexOf('?') === -1) {
|
|
4017
|
-
return [];
|
|
4018
|
-
}
|
|
4019
|
-
const queryObjects = [];
|
|
4020
|
-
|
|
4021
|
-
const queryPart = queryString.split('?')[1];
|
|
4022
|
-
|
|
4023
|
-
const queryParameters = queryPart.split('&');
|
|
4024
|
-
|
|
4025
|
-
queryParameters.forEach((queryParameter) => {
|
|
4026
|
-
const keyValue = queryParameter.split('=');
|
|
4027
|
-
const queryObject = {};
|
|
4028
|
-
// eslint-disable-next-line prefer-destructuring
|
|
4029
|
-
queryObject.name = keyValue[0];
|
|
4030
|
-
queryObject.value = decodeURIComponent(keyValue[1]);
|
|
4031
|
-
queryObjects.push(queryObject);
|
|
4032
|
-
});
|
|
4033
|
-
|
|
4034
|
-
return queryObjects;
|
|
4035
|
-
};
|
|
4036
|
-
|
|
4037
|
-
const allParameterValuePairsMatchExtreme = (queryStringObject, advancedExpectedParameterValuePairs) => {
|
|
4038
|
-
// More advanced check if all request parameters match with the expectations
|
|
4039
|
-
let littleReport = '\nQuery parameters:\n';
|
|
4040
|
-
let success = true;
|
|
4041
|
-
|
|
4042
|
-
for (const expectedKey in advancedExpectedParameterValuePairs) {
|
|
4043
|
-
if (!Object.prototype.hasOwnProperty.call(advancedExpectedParameterValuePairs, expectedKey)) {
|
|
4044
|
-
continue;
|
|
4045
|
-
}
|
|
4046
|
-
let parameterFound = false;
|
|
4047
|
-
const expectedValue = advancedExpectedParameterValuePairs[expectedKey];
|
|
4048
|
-
|
|
4049
|
-
for (const queryParameter of queryStringObject) {
|
|
4050
|
-
if (queryParameter.name === expectedKey) {
|
|
4051
|
-
parameterFound = true;
|
|
4052
|
-
if (expectedValue === undefined) {
|
|
4053
|
-
littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
|
|
4054
|
-
} else if (typeof expectedValue === 'object' && expectedValue.base64) {
|
|
4055
|
-
const decodedActualValue = Buffer.from(queryParameter.value, 'base64').toString('utf8');
|
|
4056
|
-
if (decodedActualValue === expectedValue.base64) {
|
|
4057
|
-
littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
|
|
4058
|
-
} else {
|
|
4059
|
-
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
|
|
4060
|
-
success = false;
|
|
4061
|
-
}
|
|
4062
|
-
} else if (queryParameter.value === expectedValue) {
|
|
4063
|
-
littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
|
|
4064
|
-
} else {
|
|
4065
|
-
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${queryParameter.value}"\n`;
|
|
4066
|
-
success = false;
|
|
4067
|
-
}
|
|
4068
|
-
}
|
|
4069
|
-
}
|
|
4070
|
-
|
|
4071
|
-
if (parameterFound === false) {
|
|
4072
|
-
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> parameter not found in request\n`;
|
|
4073
|
-
success = false;
|
|
4074
|
-
}
|
|
4075
|
-
}
|
|
4076
|
-
|
|
4077
|
-
return success ? true : littleReport;
|
|
4078
|
-
};
|
|
4079
|
-
|
|
4080
|
-
const allRequestPostDataValuePairsMatchExtreme = (RequestPostDataObject, advancedExpectedRequestPostValuePairs) => {
|
|
4081
|
-
// More advanced check if all request post data match with the expectations
|
|
4082
|
-
let littleReport = '\nRequest Post Data:\n';
|
|
4083
|
-
let success = true;
|
|
4084
|
-
|
|
4085
|
-
for (const expectedKey in advancedExpectedRequestPostValuePairs) {
|
|
4086
|
-
if (!Object.prototype.hasOwnProperty.call(advancedExpectedRequestPostValuePairs, expectedKey)) {
|
|
4087
|
-
continue;
|
|
4088
|
-
}
|
|
4089
|
-
let keyFound = false;
|
|
4090
|
-
const expectedValue = advancedExpectedRequestPostValuePairs[expectedKey];
|
|
4091
|
-
|
|
4092
|
-
for (const [key, value] of Object.entries(RequestPostDataObject)) {
|
|
4093
|
-
if (key === expectedKey) {
|
|
4094
|
-
keyFound = true;
|
|
4095
|
-
if (expectedValue === undefined) {
|
|
4096
|
-
littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
|
|
4097
|
-
} else if (typeof expectedValue === 'object' && expectedValue.base64) {
|
|
4098
|
-
const decodedActualValue = Buffer.from(value, 'base64').toString('utf8');
|
|
4099
|
-
if (decodedActualValue === expectedValue.base64) {
|
|
4100
|
-
littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
|
|
4101
|
-
} else {
|
|
4102
|
-
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
|
|
4103
|
-
success = false;
|
|
4104
|
-
}
|
|
4105
|
-
} else if (value === expectedValue) {
|
|
4106
|
-
littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
|
|
4107
|
-
} else {
|
|
4108
|
-
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${value}"\n`;
|
|
4109
|
-
success = false;
|
|
4110
|
-
}
|
|
4111
|
-
}
|
|
4112
|
-
}
|
|
4113
|
-
|
|
4114
|
-
if (keyFound === false) {
|
|
4115
|
-
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> key not found in request\n`;
|
|
4116
|
-
success = false;
|
|
4117
|
-
}
|
|
4118
|
-
}
|
|
4119
|
-
|
|
4120
|
-
return success ? true : littleReport;
|
|
4121
|
-
};
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -36,6 +36,7 @@ const { blurElement } = require('./scripts/blurElement');
|
|
|
36
36
|
const {
|
|
37
37
|
dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError,
|
|
38
38
|
} = require('./errors/ElementAssertion');
|
|
39
|
+
const { allParameterValuePairsMatchExtreme, extractQueryObjects, createAdvancedTestResults } = require('./networkTraffics/utils');
|
|
39
40
|
|
|
40
41
|
const SHADOW = 'shadow';
|
|
41
42
|
const webRoot = 'body';
|
|
@@ -452,6 +453,11 @@ class WebDriver extends Helper {
|
|
|
452
453
|
this.activeSessionName = '';
|
|
453
454
|
this.customLocatorStrategies = config.customLocatorStrategies;
|
|
454
455
|
|
|
456
|
+
// for network stuff
|
|
457
|
+
this.requests = [];
|
|
458
|
+
this.recording = false;
|
|
459
|
+
this.recordedAtLeastOnce = false;
|
|
460
|
+
|
|
455
461
|
this._setConfig(config);
|
|
456
462
|
|
|
457
463
|
Locator.addFilter((locator, result) => {
|
|
@@ -491,8 +497,9 @@ class WebDriver extends Helper {
|
|
|
491
497
|
if (config.host) {
|
|
492
498
|
// webdriverio spec
|
|
493
499
|
config.hostname = config.host;
|
|
494
|
-
config.path = '/wd/hub';
|
|
500
|
+
config.path = config.path ? config.path : '/wd/hub';
|
|
495
501
|
}
|
|
502
|
+
|
|
496
503
|
config.baseUrl = config.url || config.baseUrl;
|
|
497
504
|
if (config.desiredCapabilities && Object.keys(config.desiredCapabilities).length) {
|
|
498
505
|
config.capabilities = config.desiredCapabilities;
|
|
@@ -633,6 +640,11 @@ class WebDriver extends Helper {
|
|
|
633
640
|
if (this.browser.capabilities && this.browser.capabilities.platformName) {
|
|
634
641
|
this.browser.capabilities.platformName = this.browser.capabilities.platformName.toLowerCase();
|
|
635
642
|
}
|
|
643
|
+
|
|
644
|
+
if (this.options.automationProtocol) {
|
|
645
|
+
this.puppeteerBrowser = await this.browser.getPuppeteer();
|
|
646
|
+
}
|
|
647
|
+
|
|
636
648
|
return this.browser;
|
|
637
649
|
}
|
|
638
650
|
|
|
@@ -2603,9 +2615,9 @@ class WebDriver extends Helper {
|
|
|
2603
2615
|
return;
|
|
2604
2616
|
}
|
|
2605
2617
|
this.geoLocation = { latitude, longitude };
|
|
2606
|
-
|
|
2618
|
+
|
|
2607
2619
|
await this.browser.call(async () => {
|
|
2608
|
-
const pages = await puppeteerBrowser.pages();
|
|
2620
|
+
const pages = await this.puppeteerBrowser.pages();
|
|
2609
2621
|
await pages[0].setGeolocation({ latitude, longitude });
|
|
2610
2622
|
});
|
|
2611
2623
|
}
|
|
@@ -2667,6 +2679,255 @@ class WebDriver extends Helper {
|
|
|
2667
2679
|
runInWeb(fn) {
|
|
2668
2680
|
return fn();
|
|
2669
2681
|
}
|
|
2682
|
+
|
|
2683
|
+
/**
|
|
2684
|
+
*
|
|
2685
|
+
* _Note:_ Only works when devtoolsProtocol is enabled.
|
|
2686
|
+
*
|
|
2687
|
+
* {{> flushNetworkTraffics }}
|
|
2688
|
+
*/
|
|
2689
|
+
flushNetworkTraffics() {
|
|
2690
|
+
if (!this.options.automationProtocol) {
|
|
2691
|
+
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
this.requests = [];
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/**
|
|
2698
|
+
*
|
|
2699
|
+
* _Note:_ Only works when devtoolsProtocol is enabled.
|
|
2700
|
+
*
|
|
2701
|
+
* {{> stopRecordingTraffic }}
|
|
2702
|
+
*/
|
|
2703
|
+
stopRecordingTraffic() {
|
|
2704
|
+
if (!this.options.automationProtocol) {
|
|
2705
|
+
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
this.page.removeAllListeners('request');
|
|
2709
|
+
this.recording = false;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
/**
|
|
2713
|
+
*
|
|
2714
|
+
* _Note:_ Only works when devtoolsProtocol is enabled.
|
|
2715
|
+
*
|
|
2716
|
+
* {{> startRecordingTraffic }}
|
|
2717
|
+
*
|
|
2718
|
+
*/
|
|
2719
|
+
async startRecordingTraffic() {
|
|
2720
|
+
if (!this.options.automationProtocol) {
|
|
2721
|
+
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
this.flushNetworkTraffics();
|
|
2725
|
+
this.recording = true;
|
|
2726
|
+
this.recordedAtLeastOnce = true;
|
|
2727
|
+
|
|
2728
|
+
this.page = (await this.puppeteerBrowser.pages())[0];
|
|
2729
|
+
await this.page.setRequestInterception(true);
|
|
2730
|
+
|
|
2731
|
+
this.page.on('request', (request) => {
|
|
2732
|
+
const information = {
|
|
2733
|
+
url: request.url(),
|
|
2734
|
+
method: request.method(),
|
|
2735
|
+
requestHeaders: request.headers(),
|
|
2736
|
+
requestPostData: request.postData(),
|
|
2737
|
+
response: request.response(),
|
|
2738
|
+
};
|
|
2739
|
+
|
|
2740
|
+
this.debugSection('REQUEST: ', JSON.stringify(information));
|
|
2741
|
+
|
|
2742
|
+
if (typeof information.requestPostData === 'object') {
|
|
2743
|
+
information.requestPostData = JSON.parse(information.requestPostData);
|
|
2744
|
+
}
|
|
2745
|
+
request.continue();
|
|
2746
|
+
this.requests.push(information);
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
/**
|
|
2751
|
+
*
|
|
2752
|
+
* _Note:_ Only works when devtoolsProtocol is enabled.
|
|
2753
|
+
*
|
|
2754
|
+
* {{> grabRecordedNetworkTraffics }}
|
|
2755
|
+
*/
|
|
2756
|
+
async grabRecordedNetworkTraffics() {
|
|
2757
|
+
if (!this.options.automationProtocol) {
|
|
2758
|
+
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
2762
|
+
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
const promises = this.requests.map(async (request) => {
|
|
2766
|
+
const resp = await request.response;
|
|
2767
|
+
|
|
2768
|
+
if (!resp) {
|
|
2769
|
+
return {
|
|
2770
|
+
url: '',
|
|
2771
|
+
response: {
|
|
2772
|
+
status: '',
|
|
2773
|
+
statusText: '',
|
|
2774
|
+
body: '',
|
|
2775
|
+
},
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
let body;
|
|
2780
|
+
try {
|
|
2781
|
+
// There's no 'body' for some requests (redirect etc...)
|
|
2782
|
+
body = JSON.parse((await resp.body()).toString());
|
|
2783
|
+
} catch (e) {
|
|
2784
|
+
// only interested in JSON, not HTML responses.
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
return {
|
|
2788
|
+
url: resp.url(),
|
|
2789
|
+
response: {
|
|
2790
|
+
status: resp.status(),
|
|
2791
|
+
statusText: resp.statusText(),
|
|
2792
|
+
body,
|
|
2793
|
+
},
|
|
2794
|
+
};
|
|
2795
|
+
});
|
|
2796
|
+
return Promise.all(promises);
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
/**
|
|
2800
|
+
*
|
|
2801
|
+
* _Note:_ Only works when devtoolsProtocol is enabled.
|
|
2802
|
+
*
|
|
2803
|
+
* {{> seeTraffic }}
|
|
2804
|
+
*/
|
|
2805
|
+
async seeTraffic({
|
|
2806
|
+
name, url, parameters, requestPostData, timeout = 10,
|
|
2807
|
+
}) {
|
|
2808
|
+
if (!this.options.automationProtocol) {
|
|
2809
|
+
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
if (!name) {
|
|
2813
|
+
throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
if (!url) {
|
|
2817
|
+
throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
2821
|
+
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
for (let i = 0; i <= timeout * 2; i++) {
|
|
2825
|
+
const found = this._isInTraffic(url, parameters);
|
|
2826
|
+
if (found) {
|
|
2827
|
+
return true;
|
|
2828
|
+
}
|
|
2829
|
+
await new Promise((done) => {
|
|
2830
|
+
setTimeout(done, 1000);
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// check request post data
|
|
2835
|
+
if (requestPostData && this._isInTraffic(url)) {
|
|
2836
|
+
const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);
|
|
2837
|
+
|
|
2838
|
+
assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
|
|
2839
|
+
} else if (parameters && this._isInTraffic(url)) {
|
|
2840
|
+
const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);
|
|
2841
|
+
|
|
2842
|
+
assert.fail(
|
|
2843
|
+
`Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
|
|
2844
|
+
+ `${advancedTestResults}`,
|
|
2845
|
+
);
|
|
2846
|
+
} else {
|
|
2847
|
+
assert.fail(
|
|
2848
|
+
`Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
|
|
2849
|
+
+ `Expected url: ${url}.\n`
|
|
2850
|
+
+ `Recorded traffic:\n${this._getTrafficDump()}`,
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
/**
|
|
2856
|
+
*
|
|
2857
|
+
* _Note:_ Only works when devtoolsProtocol is enabled.
|
|
2858
|
+
*
|
|
2859
|
+
* {{> dontSeeTraffic }}
|
|
2860
|
+
*
|
|
2861
|
+
*/
|
|
2862
|
+
dontSeeTraffic({ name, url }) {
|
|
2863
|
+
if (!this.options.automationProtocol) {
|
|
2864
|
+
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
if (!this.recordedAtLeastOnce) {
|
|
2868
|
+
throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
if (!name) {
|
|
2872
|
+
throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
if (!url) {
|
|
2876
|
+
throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
if (this._isInTraffic(url)) {
|
|
2880
|
+
assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
/**
|
|
2885
|
+
* Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
|
|
2886
|
+
*
|
|
2887
|
+
* @param url URL to look for.
|
|
2888
|
+
* @param [parameters] Parameters that this URL needs to contain
|
|
2889
|
+
* @return {boolean} Whether or not URL with parameters is part of network traffic.
|
|
2890
|
+
* @private
|
|
2891
|
+
*/
|
|
2892
|
+
_isInTraffic(url, parameters) {
|
|
2893
|
+
let isInTraffic = false;
|
|
2894
|
+
this.requests.forEach((request) => {
|
|
2895
|
+
if (isInTraffic) {
|
|
2896
|
+
return; // We already found traffic. Continue with next request
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
if (!request.url.match(new RegExp(url))) {
|
|
2900
|
+
return; // url not found in this request. continue with next request
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
// URL has matched. Now we check the parameters
|
|
2904
|
+
|
|
2905
|
+
if (parameters) {
|
|
2906
|
+
const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
|
|
2907
|
+
if (advancedReport === true) {
|
|
2908
|
+
isInTraffic = true;
|
|
2909
|
+
}
|
|
2910
|
+
} else {
|
|
2911
|
+
isInTraffic = true;
|
|
2912
|
+
}
|
|
2913
|
+
});
|
|
2914
|
+
|
|
2915
|
+
return isInTraffic;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
/**
|
|
2919
|
+
* Returns all URLs of all network requests recorded so far during execution of test scenario.
|
|
2920
|
+
*
|
|
2921
|
+
* @return {string} List of URLs recorded as a string, separated by new lines after each URL
|
|
2922
|
+
* @private
|
|
2923
|
+
*/
|
|
2924
|
+
_getTrafficDump() {
|
|
2925
|
+
let dumpedTraffic = '';
|
|
2926
|
+
this.requests.forEach((request) => {
|
|
2927
|
+
dumpedTraffic += `${request.method} - ${request.url}\n`;
|
|
2928
|
+
});
|
|
2929
|
+
return dumpedTraffic;
|
|
2930
|
+
}
|
|
2670
2931
|
}
|
|
2671
2932
|
|
|
2672
2933
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
module.exports = {
|
|
133
|
+
createAdvancedTestResults,
|
|
134
|
+
extractQueryObjects,
|
|
135
|
+
allParameterValuePairsMatchExtreme,
|
|
136
|
+
allRequestPostDataValuePairsMatchExtreme,
|
|
137
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeceptjs",
|
|
3
|
-
"version": "3.5.13-beta.
|
|
3
|
+
"version": "3.5.13-beta.3",
|
|
4
4
|
"description": "Supercharged End 2 End Testing Framework for NodeJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"acceptance",
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
"contributor-faces": "1.1.0",
|
|
135
135
|
"documentation": "12.3.0",
|
|
136
136
|
"dtslint": "4.2.1",
|
|
137
|
-
"electron": "28.1
|
|
137
|
+
"electron": "28.2.1",
|
|
138
138
|
"eslint": "8.56.0",
|
|
139
139
|
"eslint-config-airbnb-base": "15.0.0",
|
|
140
140
|
"eslint-plugin-import": "2.29.1",
|