Skip to content

feat(puppeteer): network traffics manipulation #4263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
337 changes: 220 additions & 117 deletions docs/helpers/Puppeteer.md

Large diffs are not rendered by default.

159 changes: 19 additions & 140 deletions lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
const {
seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError,
} = require('./errors/ElementAssertion');
const { createAdvancedTestResults, allParameterValuePairsMatchExtreme, extractQueryObjects } = require('./networkTraffics/utils');
const { log } = require('../output');
const {
dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics,
} = require('./network/actions');

const pathSeparator = path.sep;

Expand Down Expand Up @@ -3010,37 +3011,6 @@ class Playwright extends Helper {
});
}

/**
* {{> grabRecordedNetworkTraffics }}
*/
async grabRecordedNetworkTraffics() {
if (!this.recording || !this.recordedAtLeastOnce) {
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
}

const promises = this.requests.map(async (request) => {
const resp = await request.response;
let body;
try {
// There's no 'body' for some requests (redirect etc...)
body = JSON.parse((await resp.body()).toString());
} catch (e) {
// only interested in JSON, not HTML responses.
}

return {
url: resp.url(),
response: {
status: resp.status(),
statusText: resp.statusText(),
body,
},
};
});

return Promise.all(promises);
}

/**
* Blocks traffic of a given URL or a list of URLs.
*
Expand Down Expand Up @@ -3120,67 +3090,19 @@ class Playwright extends Helper {
}

/**
*
* {{> flushNetworkTraffics }}
*/
flushNetworkTraffics() {
this.requests = [];
flushNetworkTraffics.call(this);
}

/**
*
* {{> stopRecordingTraffic }}
*/
stopRecordingTraffic() {
this.page.removeAllListeners('request');
this.recording = false;
}

/**
* {{> seeTraffic }}
*/
async seeTraffic({
name, url, parameters, requestPostData, timeout = 10,
}) {
if (!name) {
throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
}

if (!url) {
throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
}

if (!this.recording || !this.recordedAtLeastOnce) {
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
}

for (let i = 0; i <= timeout * 2; i++) {
const found = this._isInTraffic(url, parameters);
if (found) {
return true;
}
await new Promise((done) => {
setTimeout(done, 1000);
});
}

// check request post data
if (requestPostData && this._isInTraffic(url)) {
const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);

assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
} else if (parameters && this._isInTraffic(url)) {
const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);

assert.fail(
`Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
+ `${advancedTestResults}`,
);
} else {
assert.fail(
`Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
+ `Expected url: ${url}.\n`
+ `Recorded traffic:\n${this._getTrafficDump()}`,
);
}
stopRecordingTraffic.call(this);
}

/**
Expand Down Expand Up @@ -3217,73 +3139,30 @@ class Playwright extends Helper {
}

/**
* {{> dontSeeTraffic }}
*
* {{> grabRecordedNetworkTraffics }}
*/
dontSeeTraffic({ name, url }) {
if (!this.recordedAtLeastOnce) {
throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
}

if (!name) {
throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
}

if (!url) {
throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
}

if (this._isInTraffic(url)) {
assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
}
async grabRecordedNetworkTraffics() {
return grabRecordedNetworkTraffics.call(this);
}

/**
* Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
*
* @param url URL to look for.
* @param [parameters] Parameters that this URL needs to contain
* @return {boolean} Whether or not URL with parameters is part of network traffic.
* @private
* {{> seeTraffic }}
*/
_isInTraffic(url, parameters) {
let isInTraffic = false;
this.requests.forEach((request) => {
if (isInTraffic) {
return; // We already found traffic. Continue with next request
}

if (!request.url.match(new RegExp(url))) {
return; // url not found in this request. continue with next request
}

// URL has matched. Now we check the parameters

if (parameters) {
const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
if (advancedReport === true) {
isInTraffic = true;
}
} else {
isInTraffic = true;
}
});

return isInTraffic;
async seeTraffic({
name, url, parameters, requestPostData, timeout = 10,
}) {
await seeTraffic.call(this, ...arguments);
}

/**
* Returns all URLs of all network requests recorded so far during execution of test scenario.
*
* @return {string} List of URLs recorded as a string, separated by new lines after each URL
* @private
* {{> dontSeeTraffic }}
*
*/
_getTrafficDump() {
let dumpedTraffic = '';
this.requests.forEach((request) => {
dumpedTraffic += `${request.method} - ${request.url}\n`;
});
return dumpedTraffic;
dontSeeTraffic({ name, url }) {
dontSeeTraffic.call(this, ...arguments);
}

/**
Expand Down
85 changes: 82 additions & 3 deletions lib/helper/Puppeteer.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ const ElementNotFound = require('./errors/ElementNotFound');
const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused');
const Popup = require('./extras/Popup');
const Console = require('./extras/Console');
const findReact = require('./extras/React');
const { highlightElement } = require('./scripts/highlightElement');
const { blurElement } = require('./scripts/blurElement');
const { focusElement } = require('./scripts/focusElement');
const {
dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError,
} = require('./errors/ElementAssertion');
const {
dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics,
} = require('./network/actions');

let puppeteer;
let perfTiming;
Expand Down Expand Up @@ -226,6 +227,11 @@ class Puppeteer extends Helper {
this.sessionPages = {};
this.activeSessionName = '';

// for network stuff
this.requests = [];
this.recording = false;
this.recordedAtLeastOnce = false;

// for websocket messages
this.webSocketMessages = [];
this.recordingWebSocketMessages = false;
Expand Down Expand Up @@ -2514,6 +2520,79 @@ class Puppeteer extends Helper {
});
}

/**
*
* {{> flushNetworkTraffics }}
*/
flushNetworkTraffics() {
flushNetworkTraffics.call(this);
}

/**
*
* {{> stopRecordingTraffic }}
*/
stopRecordingTraffic() {
stopRecordingTraffic.call(this);
}

/**
* {{> startRecordingTraffic }}
*
*/
async startRecordingTraffic() {
this.flushNetworkTraffics();
this.recording = true;
this.recordedAtLeastOnce = true;

await this.page.setRequestInterception(true);

this.page.on('request', (request) => {
const information = {
url: request.url(),
method: request.method(),
requestHeaders: request.headers(),
requestPostData: request.postData(),
response: request.response(),
};

this.debugSection('REQUEST: ', JSON.stringify(information));

if (typeof information.requestPostData === 'object') {
information.requestPostData = JSON.parse(information.requestPostData);
}
request.continue();
this.requests.push(information);
});
}

/**
*
* {{> grabRecordedNetworkTraffics }}
*/
async grabRecordedNetworkTraffics() {
return grabRecordedNetworkTraffics.call(this);
}

/**
*
* {{> seeTraffic }}
*/
async seeTraffic({
name, url, parameters, requestPostData, timeout = 10,
}) {
await seeTraffic.call(this, ...arguments);
}

/**
*
* {{> dontSeeTraffic }}
*
*/
dontSeeTraffic({ name, url }) {
dontSeeTraffic.call(this, ...arguments);
}

async getNewCDPSession() {
const client = await this.page.target().createCDPSession();
return client;
Expand Down Expand Up @@ -2566,7 +2645,7 @@ class Puppeteer extends Helper {
/**
* Grab the recording WS messages
*
* @return { Array<any> }
* @return { Array<any>|undefined }
*
*/
grabWebSocketMessages() {
Expand Down
Loading