Cypress are about to deprecate cy.route() and cy.server() in use of cy.intercept()
Here is my OLD Code, that would capture XHR time and output
cy.server()
cy.route('POST', api_URL_Live).as('CONS');
cy.wait('#CONS').then((xhr) => {
CONSTime = Number(JSON.stringify(xhr.duration));
});
This worked perfectly and would out put the duration to a file
New code that no longer captures duration
cy.intercept('POST', api_URL_Live).as('CONS');
cy.wait('#CONS').then((xhr) => {
CONSTime = Number(JSON.stringify(xhr.duration));
});
Does anyone know why this functionality no longer works, any help appreciated?
The APIs of cy.intercept and cy.route have some overlap, but are not the same.
duration is an undocumented property of cy.route, but it does work, although it is not formally supported.
Because it was undocumented and rarely used in cy.route, it was not considered for implementation in cy.intercept.
You can still measure a request's duration using cy.route - cy.route is not going to be deleted, leaving your code broken. It will most likely be moved to a plugin once cy.intercept is stable enough for most common use cases.
If you would like to do the same with cy.intercept, you can measure by using callbacks:
cy.intercept('POST', url, (req) => {
const startTime = Date.now()
req.reply(res => {
// measure the time between request received and response received
totalTime = Date.now() - startTime
})
})
There is an open feature request to add timing data to cy.intercept: https://github.com/cypress-io/cypress/issues/15969
Related
We recently installed datadogRUM in our application and now so many DD events kick off in my cypress test that they cause a timeout and failure
I have tried cy.intercept in multiple ways:
cy.intercept('POST', 'https://rum.browser-intake-datadoghq.com/*', {
statusCode: 202,
body: {
},
}).as('datadogRUM');
cy.intercept('POST', 'https://rum-http-intake.logs.datadoghq.com/*', {});
cy.intercept(/\.*datadog.*$/, (req) => {
console.log('DATADOG INTERCEPTED');
req.reply("console.log('datadog intercept');");
});
cy.intercept({
method: 'POST',
url: '/\.*datadog.*$/'
}, req => {
req.destroy();
});
cy.intercept('POST', 'https://rum-http-intake.logs.datadoghq.com/*', { forceNetworkError: true });
just to start. I feel like I've tried every possible variation. I also created a cypress.json file in my /cypress folder
{
"blockHosts": "*datadoghq.com/*"
}
I get hundreds of calls back in my network tab to https://rum.browser-intake-datadoghq.com/api/v2/rum with the preview of console.log('datadog intercept') as I've intercepted them. They all display the solid blue line as if they are being intercepted and blocked. When I set the intercept to an alias I see the alias in my cypress runner window. But there are no 503s or 404s anywhere. The page still fills up with events, cypress gets overloaded, and my test times out.
I even tried copying the data-dog-rum.ts from the src/utils folder to cypress/utils and either commenting out everything or setting the sampleRate to 0, no dice.
EDIT: I am able to get the test passing by adding
Cypress.on('uncaught:exception', () => {
// returning false here prevents Cypress from
// failing the test
return false;
});
to my support/index.js but now whether I add a cy.intercept in my test makes absolutely no difference. The page still fills up with datadog requests regardless, and whether they come back as 200/pending/cancelled, they still delay a single it block in a spec to where it takes 60 seconds to run instead of approx 10 seconds
You can use javascript to perform the stub inside the routeHandler
cy.intercept('*', (req) => { // look at everything
if (req.url.includes('datadoghq')) { // add more conditions if needed
req.reply({}) // prevent request reaching the server
}
})
blockhosts should work with
Pass only the host
{
"blockHosts": "*datadoghq.com"
}
I am trying to end to end test a file upload page in Cypress, which includes testing if a file upload progress bar works.
Unfortunately, in the local environment the upload happens instantly so the progress bar is not shown.
I am aware that you can throttle the response speed using cy.intercept() to simulate slow networks. However, this isn't slowing the request upload speed:
cy.intercept('post', `/route`, (req) => {
req.on('response', (res) => {
res.setThrottle(1000)
})
}).as('post')
Is there any way that I can apply a throttle to the outgoing request?
I think Cypress itself can only throttle or delay responses using cy.intercept() as mentioned in the question and described here: Throttle or delay response all incoming responses
However, you can probably use the Chrome Debugger Protocol from Chrome DevTools for that. Here you have the function Network.emulateNetworkConditions that allows you to throttle both download and upload throughput. This approach then assumes that your tests run in Chrome.
If you're unsure how to use this in your Cypress tests, here is a blog post about testing an application in offline network mode that uses a similar approach with Network.enable: Testing an Application in Offline Network Mode
In the routeHandler, use setTimeout() to delay the call to req.continue().
To get the command queue to wait for setTimeout, return a Promise wrapper. See Request phase
If the handler returned a Promise, wait for the Promise to resolve.
If you want a delay longer than the default command timeout of 4 seconds, you will need to increase the config since cy.intercept does not take a timeout option.
Cypress.config('defaultCommandTimeout', 6000) // timeout in 6 seconds
cy.intercept('POST', '/route', (req) => {
return new Promise(resolve => {
setTimeout(() => resolve(req.continue()), 5000) // delay by 5 seconds
})
}).as('delayedRequest')
// trigger POST
cy.wait('#delayedRequest')
Cypress.config('defaultCommandTimeout', 4000) // revert to normal timeout
Middleware
If you already have a complex intercept, you can set the delay in a middleware intercept.
Middleware is always executed first, but if does not handle the request the call is passed to the next intercept.
// request is delayed here
cy.intercept('POST', '/route', {
middleware: true // middleware always fires first
},
(req) => new Promise(resolve =>
setTimeout(() => resolve(), 5000) // no handler, just delay
)
)
// then passed to here
cy.intercept('POST', '/route',
(req) => {
req.continue() // handler for request
}
).as('delayedRequest')
// trigger POST
cy.wait('#delayedRequest')
To avoid adding cy.wait, am trying to user the explicit wait, but seems it is broken. What I missed here
const APIAccountPage= {
APIAccount(name) {
//cy.wait(14000);
cy.server();
cy.route('GET', '/api/accounts').as('Accounts');
cy.wait('#Accounts', { timeout: 10000 })
.its('status').should('eq', 200);
cy.get('.vbutton__baseline___3XNit.fonts__letterSpacing___3l5GB.styles__searchIconWrapper___19oVL.styles__iconButton___LLzft')
.should('be.visible')
.should('not.be.disabled');
cy.get('.vbutton__baseline___3XNit.fonts__letterSpacing___3l5GB.styles__searchIconWrapper___19oVL.styles__iconButton___LLzft').click();
cy.wait(4000);
cy.get('.styles__patientSearchClass___2iGOV').type(name);
cy.wait(4000);
cy.get('.styles__patientSearchClass___2iGOV').contains(name).click();
}
}
module.exports = APIAccountPage;
Error Details
Cypress Window
Try this following, if the /api/account is not working, there may be multiple reason, one of which I am thinking is a backend api (which happened in my case) for which you need to handle the authentication.
If it is same then try to provide the complete URL like,
cy.server()
cy.route(
'GET',
'https://<URL>/api/accounts',
'{"errors":[]}',
).as('account');
and call cy.wait('#account') whichever place is appropriate.
Just a sidenote cy.server() and cy.route has been deprecated since 6.0.0, you can read about it from here. Instead, a new method has been introduced called cy.intercept().
cy.intercept('GET', 'https://some-url/api/accounts').as('Accounts')
cy.wait('#Account') //Wait
I have stuck with Cypress fixtures. Can't intercept an XHR request with SSR and navigation routing.
cypress/integration/page.js:
const fetch = require("unfetch")
describe("/about", () => {
beforeEach(() => {
cy.visit("/", { // Visit home page to trigger SSR
onBeforeLoad (win) {
win.fetch = fetch // replace fetch with xhr implementation
},
})
})
it("Has a correct title", () => {
cy.server()
cy.fixture("about").then(about => {
// about object is correct here, like {title: "About+"}
cy.route("GET", "http://localhost:8080/api/documents/url", about) // Not sure where .route should be
cy.get(".main > :nth-child(1) > a").click() // Navigate to the /about page
cy.route("GET", "http://localhost:8080/api/documents/url", about) // Tried both ways
// This hits my server API without stubbing, getting {title: "About"}
cy.title().should("eq", "About+") // About != About+
})
})
})
cypress/fixtures/about.json:
{"title": "About+"}
I see an XHR request (type=xhr) in Dev Tools and it doesn't use the above about stub object but hits real API instead. Why? Double checked URL and method – 100% the same. Can it be that route is coupled to visit and ignores click-based routing?!
Rechecking this once again, I've found a solution. Let me share the details for everyone interested:
1) I use Next.js which is an excellent tool for SSR but it doesn't allow you to disable server-side rendering (yet) according to this and this issues.
2) You can use Cypress with SSR pages but, in this way, you're limited to testing real HTML. Which means you have to either couple tests to real data (not good in most cases) or stub the database itself (slow). In general, you want to stub HTTP requests.
3) Cypress can't stub fetch requests and mocking fetch with XHR-based implementation was trickier than I thought.
First you need to:
// cypress/integration/your-test.js
Cypress.on('window:before:load', (win) => {
delete win.fetch
})
Then:
// pages/your-page.js
Entry.getInitialProps = async function() {
window.fetch = require("unfetch").default
...
}
Other combinations of delete & update code lines I tried didn't yield positive results. For example, when I had window.fetch = line in the test file it didn't work and fetch.toString() gave "native code". Not sure why, no time to explore further.
Axios solves the above but I don't like to bloat my bundle with extra stuff. You can inject XHR-based fetch for tests only.
4) The most important missing piece. You need to wait for route.
it("Has a correct title", () => {
cy.visit("/")
cy.server()
cy.route("GET", "http://localhost:8080/api/documents/url/about", {title: "About+"}).as("about")
cy.get("[href='/about']").click()
cy.wait("#about") // !!!
cy.get("h1").contains("About+")
})
The following example times out in most cases (outputs timed out):
Promise = require('bluebird');
new Promise(resolve => {
setTimeout(resolve, 1000);
})
.timeout(1001)
.then(() => {
console.log('finished');
})
.catch(error => {
if (error instanceof Promise.TimeoutError) {
console.log('timed out');
} else {
console.log('other error');
}
});
Does this mean that the Bluebird's promise overhead takes longer than 1ms?
I see it often time out even if I use .timeout(1002).
The main reason for asking - I'm trying to figure what the safe threshold is, which gets more important with smaller timeouts.
Using Bluebird 3.5.0, under Node.js 8.1.2
I have traced your bug in Bluebird's code. Consider this:
const p = new Promise(resolve => setTimeout(resolve, 1000));
const q = p.timeout(1001); // Bluebird spawns setTimeout(fn, 1001) deep inside
That looks rather innocent, yeah? Though, not in this case. Internally, Bluebird implemented it something like (not actually valid JS; timeout clearing logic is omitted):
Promise.prototype.timeout = function(ms) {
const original = this;
let result = original.then(); // Looks like noop
setTimeout(() => {
if result.isPending() {
make result rejected with TimeoutError; // Pseudocode
}
}, ms);
return result;
}
Bug was presence of line ret.isPending(). It resulted in brief time when original.isPending() === false and ret.isPending() === true because "resolved" status didn't propagate yet from original to children. Your code hit that extremely short period and BOOM, you had race condition.
I think what's going on here is that there's a race between the time the rest of the promise chain takes and the timer from the .timeout(). Since they are both so close in timing, sometimes one wins and sometimes the other wins - they are racy. When I run this code that logs the sequence of events, I get different ordering on different runs. The exact output order is unpredictable (e.g. racy).
const Promise = require('bluebird');
let buffer = [];
function log(x) {
buffer.push(x);
}
new Promise(resolve => {
setTimeout(() => {
log("hit my timeout");
resolve();
}, 1000);
}).timeout(1001).then(() => {
log('finished');
}).catch(error => {
if (error instanceof Promise.TimeoutError) {
log('timed out');
} else {
log('other error');
}
});
setTimeout(() => {
console.log(buffer.join("\n"));
}, 2000);
Sometimes this outputs:
hit my timeout
finished
And, sometimes it outputs:
hit my timeout
timed out
As has been mentioned in the comments, if .then() was always executed via microtask (which should precede any macrotasks), then one would think that the .then() would precede the setTimeout() from the .timeout(), but things are apparently not that simple.
Since the details of promise .then() scheduling is not mandated by specification (only that the stack is clear of application code) a code design should not assume a specific scheduling algorithm. Thus, a timeout this close to the execution of the async operation it is following can be racy and thus unpredictable.
If you could explain exactly what problem you're trying to solve, we could probably offer more concrete advice about what to do. No timers in Javascript are precise to the ms because JS is single threaded and all timer events have to go through the event queue and they only call their callbacks when their event gets serviced (not exactly when the timer fired). That said, timer events will always be served in order so a setTimeout(..., 1000) will always come before setTimeout(..., 1001), even though there may not be exactly 1ms delta between the executing of the two callbacks.