Skip to content
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

add fetch collector #126

Merged
merged 2 commits into from
Nov 3, 2021
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
65 changes: 65 additions & 0 deletions src/collector/LogCollectCypressFetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const LOG_TYPE = require('../constants').LOG_TYPES;
const CONSTANTS = require('../constants');
const LogFormat = require("./LogFormat");

module.exports = class LogCollectCypressFetch {

constructor(collectorState, config) {
this.config = config;
this.collectorState = collectorState;

this.format = new LogFormat(config);
}

register() {
const formatFetch = (options) => (options.alias !== undefined ? '(' + options.alias + ') ' : '') +
(options.consoleProps["Request went to origin?"] !== 'yes' ? 'STUBBED ' : '') +
options.consoleProps.Method + ' ' + options.consoleProps.URL;

const formatDuration = (durationInMs) =>
durationInMs < 1000 ? `${durationInMs} ms` : `${durationInMs / 1000} s`;

Cypress.on('log:added', (options) => {
if (options.instrument === 'command' && options.name === 'request' && options.displayName === 'fetch') {
const log = formatFetch(options);
const severity = options.state === 'failed' ? CONSTANTS.SEVERITY.WARNING : '';
this.collectorState.addLog([LOG_TYPE.CYPRESS_FETCH, log, severity], options.id);
}
});

Cypress.on('log:changed', async (options) => {
if (
options.instrument === 'command' && options.name === 'request' && options.displayName === 'fetch' &&
options.state !== 'pending'
) {
let statusCode;

statusCode = options.consoleProps["Response Status Code"];

const isSuccess = statusCode && (statusCode + '')[0] === '2';
const severity = isSuccess ? CONSTANTS.SEVERITY.SUCCESS : CONSTANTS.SEVERITY.WARNING;
let log = formatFetch(options);

if (options.consoleProps.Duration) {
log += ` (${formatDuration(options.consoleProps.Duration)})`;
}
if (statusCode) {
log += `\nStatus: ${statusCode}`;
}
if (options.err && options.err.message) {
log += ' - ' + options.err.message;
}

if (
!isSuccess &&
options.consoleProps["Response Body"]
) {
log += `\nResponse body: ${await this.format.formatXhrBody(options.consoleProps["Response Body"])}`;
}

this.collectorState.updateLog(log, severity, options.id);
}
});
}

}
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {

CYPRESS_LOG: 'cy:log',
CYPRESS_XHR: 'cy:xhr',
CYPRESS_FETCH: 'cy:fetch',
CYPRESS_REQUEST: 'cy:request',
CYPRESS_ROUTE: 'cy:route',
CYPRESS_INTERCEPT: 'cy:intercept',
Expand Down
2 changes: 1 addition & 1 deletion src/installLogsCollector.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface SupportOptions {
* What types of logs to collect and print.
* By default all types are enabled.
* The 'cy:command' is the general type that contain all types of commands that are not specially treated.
* @default ['cons:log','cons:info', 'cons:warn', 'cons:error', 'cy:log', 'cy:xhr', 'cy:request', 'cy:route', 'cy:command']
* @default ['cons:log','cons:info', 'cons:warn', 'cons:error', 'cy:log', 'cy:xhr', 'cy:fetch', 'cy:request', 'cy:route', 'cy:command']
*/
collectTypes?: readonly string[];

Expand Down
4 changes: 4 additions & 0 deletions src/installLogsCollector.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const LogCollectCypressRequest = require("./collector/LogCollectCypressRequest")
const LogCollectCypressRoute = require("./collector/LogCollectCypressRoute");
const LogCollectCypressIntercept = require("./collector/LogCollectCypressIntercept");
const LogCollectCypressXhr = require("./collector/LogCollectCypressXhr");
const LogCollectCypressFetch = require("./collector/LogCollectCypressFetch");
const LogCollectCypressLog = require("./collector/LogCollectCypressLog");

const LogCollectorState = require("./collector/LogCollectorState");
Expand Down Expand Up @@ -50,6 +51,9 @@ function registerLogCollectorTypes(logCollectorState, config) {
if (config.collectTypes.includes(LOG_TYPE.CYPRESS_XHR)) {
(new LogCollectCypressXhr(logCollectorState, config)).register();
}
if (config.collectTypes.includes(LOG_TYPE.CYPRESS_FETCH)) {
(new LogCollectCypressFetch(logCollectorState, config)).register();
}
if (config.collectTypes.includes(LOG_TYPE.CYPRESS_REQUEST)) {
(new LogCollectCypressRequest(logCollectorState, config)).register();
}
Expand Down
1 change: 1 addition & 0 deletions src/installLogsCollector.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"cons:error",
"cy:log",
"cy:xhr",
"cy:fetch",
"cy:request",
"cy:intercept",
"cy:route",
Expand Down
4 changes: 4 additions & 0 deletions src/installLogsPrinter.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@ function logToTerminal(messages, options, data) {
color = 'green';
icon = LOG_SYMBOLS.route;
trim = options.routeTrimLength || 5000;
} else if (type === LOG_TYPES.CYPRESS_FETCH) {
color = 'green';
icon = LOG_SYMBOLS.route;
trim = options.routeTrimLength || 5000;
} else if (type === LOG_TYPES.CYPRESS_ROUTE) {
color = 'green';
icon = LOG_SYMBOLS.route;
Expand Down
145 changes: 145 additions & 0 deletions test/cypress/integration/fetchApi.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
describe('Fetch Api', () => {
it('Stubbed failed fetch API', () => {
cy.visit('/commands/network-requests');

cy.intercept(
{
method: 'PUT',
url: 'comments/*',
},
{
statusCode: 404,
body: {error: 'Test message.'},
delay: 500,
}
).as('putComment');

cy.window().then((w) => {
fetch('/comments/10', {
method: 'PUT',
body: 'test',
});
});

cy.wait('@putComment');

cy.get('.breaking-get', {timeout: 1});
})

context('Timeout', () => {

it('forceNetworkError ', () => {
cy.visit('/commands/network-requests');

cy.intercept(
{
method: 'PUT',
url: 'comments/*',

},
{
forceNetworkError: true,
}
).as('putComment');

cy.window().then((w) => {
fetch('/comments/10', {
method: 'PUT',
body: 'test',
});
});

cy.wait('@putComment');

cy.get('.breaking-get', {timeout: 1});
});

// Currently Cypress can't handle fetch Abort properly. It produces an unhandled entry, while the request remains "pending":
// cy:command ✘ uncaught exception AbortError: The user aborted a request.
it('timeout using AbortController', () => {
cy.visit('/commands/network-requests');

cy.intercept(
{
method: 'PUT',
url: 'comments/*',

},
{
delay: 500,
}
).as('putComment');

cy.window().then((w) => {

const controller = new AbortController();
setTimeout(() => controller.abort(), 100);

fetch('/comments/10', {
method: 'PUT',
body: 'test',
signal: controller.signal
});
});

cy.wait('@putComment');

cy.get('.breaking-get', {timeout: 1});
});
});


context('Real Fetch Requests', () => {
const testRealFetchRequest = (options) => {
cy.visit('/commands/network-requests');

if (options.interceptPath) {
// only intercepted fetch requests logs a response body. Read more at https://github.com/cypress-io/cypress/issues/17656
cy.intercept(options.interceptPath);
}

cy.window().then((window) => {
const document = window.document;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of the button? Wouldn't it have been more easier to directly trigger window.fetch(..) here? And using and intercept await its response?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

window.fetch would result in a xhr query because of fetch-polyfill. This is also testing the case of not-intercepted requests.


// Create a div to put a message to after receiving the fetch response
const containerDiv = document.createElement('div');
containerDiv.className = 'network-request-message';
const networkComment = document.querySelector('.network-comment');
networkComment.after(containerDiv);

// Crate a button that triggers the fetch
const button = document.createElement('button');
button.className = 'network-request btn btn-primary';
button.innerHTML = 'Fetch Request ';
button.addEventListener('click', () =>
fetch(options.url).then(() => {
containerDiv.innerHTML = 'received response';
})
);
containerDiv.before(button);
});

cy.get('.network-request-message').should('not.contain', 'received response');
cy.get('.network-request').click();
cy.get('.network-request-message').should('contain', 'received response');

cy.get('.breaking-get', {timeout: 100}); // longer timeout to ensure fetch log update is included
};

it('Fetch successful without interceptor', () =>
testRealFetchRequest({
url: 'https://jsonplaceholder.cypress.io/comments/1',
}));

it('Fetch failed without interceptors', () =>
testRealFetchRequest({
url: 'https://www.mocky.io/v2/5ec993803000009700a6ce1f',
}));

it('Fetch failed with interceptors', () =>
testRealFetchRequest({
url: 'https://www.mocky.io/v2/5ec993803000009700a6ce1f',
interceptPath: 'https://www.mocky.io/**/*',
}));
});
});
34 changes: 34 additions & 0 deletions test/specs/commandsLogging.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,40 @@ describe('Commands logging.', () => {
});
}).timeout(60000);

it('Should log fetch requests.', async () => {
await runTest(commandBase([], [`fetchApi.spec.js`]), (error, stdout, stderr) => {
const cleanStdout = clean(stdout, true);
// cy.intercept stubbed aliased commands are logged
expect(stdout).to.contain(`(putComment) STUBBED PUT https://example.cypress.io/comments/10\n`);
expect(stdout).to.contain(`cy:fetch ${ICONS.warning}`);
expect(stdout).to.contain(`Status: 404\n`);
expect(stdout).to.contain(`Response body: {\n${PADDING} "error": "Test message."\n${PADDING}}\n`);

// timeouts / abort
expect(cleanStdout).to.contain(
`(putComment) STUBBED PUT https://example.cypress.io/comments/10 - forceNetworkError called`,
'network failed request contains failure message'
);

// test real fetch requests
expect(cleanStdout).to.contain(
`cy:fetch ${ICONS.route} GET https://jsonplaceholder.cypress.io/comments/1\n${PADDING} Status: 200\n`,
'non-intercepted success fetch contains url and status'
);

expect(cleanStdout).to.contain(
`cy:fetch ${ICONS.warning} GET https://www.mocky.io/v2/5ec993803000009700a6ce1f\n${PADDING} Status: 400\n`,
'non-intercepted non-success fetch contains url and status'
);

expect(cleanStdout).to.contain(
`cy:fetch ${ICONS.warning} GET https://www.mocky.io/v2/5ec993803000009700a6ce1f\n${PADDING} Status: 400\n${PADDING} Response body: {\n${PADDING} "status": "Wrong!",\n${PADDING} "data": {\n${PADDING} "corpo": "corpo da resposta",\n${PADDING} "titulo": "titulo da resposta"\n${PADDING} }\n${PADDING} }\n`,
'intercepted non-success fetch contains url, status and a response body'
);

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have a case for aborted / timeout as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a way to force timeout but cypress itself doesn't handle that well (an error entry is added and the query command remains in a pending state). I can probably add now a test using forceNetworkError which seems to provide a better output.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that doesn't affect other tests I say go for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test case added

});
}).timeout(60000);

it('Should only log XHR response body for non-successful requests not handled by cy.route.', async () => {
await runTest(commandBase([], ['xhrTypes.spec.js']), (error, stdout, stderr) => {
const cleanStdout = clean(stdout, true);
Expand Down