Skip to content

Commit

Permalink
feat(puppeteer): network traffics manipulation (#4263)
Browse files Browse the repository at this point in the history
  • Loading branch information
kobenguyent authored Mar 23, 2024
1 parent a867294 commit c44ce74
Show file tree
Hide file tree
Showing 10 changed files with 622 additions and 706 deletions.
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

0 comments on commit c44ce74

Please sign in to comment.