In theory, yes. Cypress documentation recommends adding spies in the onBeforeLoad
phase.
cy.visit('/', {
onBeforeLoad(win) {
cy.spy(win, 'snowplow');
},
});
However, at this point the spied property may not exist yet. To make sure snowplow
exists on window
, do:
cy.visit('/', {
onLoad(window$) {
cy.spy(window$, 'snowplow');
},
});
or
cy.visit('/');
cy.window().should('have.property', 'snowplow');
cy.window().then(window$ => cy.spy(window$, 'snowplow');
See What's the best way to stub functions and methods below for possible workarounds.
Yes, if it's a built-in method like window.addEventListener
. Cypress recommends using the onBeforeLoad
hook for that.
However, if the method is added by your application, then it will not exist when onBeforeLoad
is called. In that scenario, you must use the onLoad
hook, which may be already too late to spy on your method since it could have already been called.
You can alias your spy by using the as
method.
cy.spy(win, 'snowplow').as('snowplow');
If you prefix your alias with the @
symbol, you can access it with cy.get
.
cy.get('@snowplow');
You can still reference your original function without an alias. These two are equivalent.
expect(window$.snowplow).to.exist;
expect('@snowplow').to.exist;
Notice that the first variant uses a reference to your original method. Cypress proxies original methods, but does not change their behavior. You can still call them the way you did.
Note: when chaining calls, only the former will work.
cy.window()
.then(window$ => {
window$.snowplow?.('trackPageView', null);
return window$;
})
.then(window$ => {
// Bad: expect('@snowplow').to.have.been.called;
expect(window$.snowplow).to.have.been.called;
});
Note: function calls are counted correctly only when you call your method from within a Cypress test. If the method is called by your application, any calls to it will not be counted.
Note:
cy.spy()
creates spies in a sandbox, so all spies created are automatically reset/restored between tests without you having to explicitly reset/restore them source.
Two. One for your application, and one for the Cypress UI.
Currently, the type definition for Window
is reused for both. If you have augmented the basic Window
interface (for example, you added global variables your application depends on), this may lead to bugs.
See this for more context, and remember to use cy.window().then(window$ => { /* ... */ })
to access your application window.
Note: the dollar sign is used as a suffix when the referenced window is the application window (sometimes called remote window by Cypress).
Cypress recommends yielding the Window
using the cy.window()
method, and then wrapping the yielded window with cy.wrap
.
cy.window().then(window => {
cy.wrap(window).should(window$ => {
const context = getSnowplowSearchPhotosContext(window$);
expect(validate.context.search(context)).to.be.true;
expect(context.data.isDefault).to.be.false;
});
});
However, using assertion without the should
callback also works:
cy.window().then(window$ => {
const context = getSnowplowSearchPhotosContext(window$);
expect(validate.context.search(context)).to.be.true;
expect(context.data.isDefault).to.be.false;
});
Just before the test suite is ran. Use the beforeEach
callback.
You can also do it right in the test case.
cy.window().then(window$ => cy.spy(window$, 'snowplow').as('snowplow'));
cy.visit('/');
cy.get('@snowplow').should('exist');
In theory, yes.
cy.server();
cy.route('GET', /foo/).as('endpoint');
Key points:
route
is for listening to requests, and stubbing requests (if you provide theresponse
option)request
is for making requests- Remember to call
server
before callingroute
. See documentation - Server can be started before you call
cy.visit()
source
Note: Cypress only currently supports intercepting
XMLHttpRequests
. Requests using the Fetch API and other types of network requests like page loads and<script>
tags will not be intercepted or visible in the Command Log. See this for more details and temporary workarounds.
Note: by default, Cypress is configured to ignore requests that are used to fetch static content like
.js
or.html
files. This keeps the Command Log less noisy. This option can be changed by overriding the default whitelisting in thecy.server()
options. (We should be able to test code splitting this way)
Note: this does seem to work only for requests made to our server, not any server. See this spec for details.
To listen to outgoing requests:
cy.route({
url: 'https://logger.unsplash.com/i?*',
onRequest: req => {
cy.log('A tracking request has been made. Now we can use `expect`');
},
});
Note: network requests made to analytics are not XHR requests (they are image requests, which is made to bypass origin policies). If the requested resource is an image, and it lives in a different origin then your server,
cy.route
will not be able to see it.
Known workardounds are either to associate the image with a DOM element, or to collect or network requests by calling window$.performance.getEntries()
.
window.performance.getEntries().filter(entry => entry.name.includes(SNOWPLOW_TRACKING_ENDPOINT));
Another solution is to stub the service.
If you spy on a method that gets overriden during the lifecycle of your app, the reference to the spied method will change.
For example, when a third-party <script>
such as Snowplow bootstraps itself, it will mutate the Window
object and add a property snowplow
. If you started spying on (or stubbed) window.snowplow
before it happened, you won't be able to spy on it before the reference is mutated.
If that is the case, use global event handlers to stub/spy on your global methods.
There are two ways: global and local. They have different use cases.
Put it outside your top-level describe
block. It will affect all tests in that file.
Cypress.on('window:before:load', win => {
win.snowplow = cy.stub().as('snowplow');
});
Use this method to stub global methods.
Note: once you stub a property this way, it cannot be “unstubbed” or overriden. Every test case in tha file will always refer to the global spy or stub. If you're using this method to stub a property added asynchronously, the real code will have no effect on your tests.
See this for more details.
Put it in the beginning of your describe
block, inside a before
or beforeEach
callback.
beforeEach(() => {
cy.visit('/', {
onBeforeLoad: window$ => {
window$.snowplow = cy.stub().as('snowplow');
},
});
});
Because before
and beforeEach
blocks work for specific describe
blocks, you can use this method to mock globals in certain scenarios but not in others.
Note: if your spy/stub is assigned this way, it can be mutated by your application. In that scenario, your alias will no longer work.
It's normal. Cypress re-creates the same stub each time the page is loaded. If you're calling cy.visit
a lot in your code, multiple stubs (with the same alias, if you're using one) will be generated.
On the contrary, client-side navigation reuses the same stub.
When asserting the number of calls made to the stub, only the last instance of the stub is considered (as expected).
Yes. You can, for example, have it disabled for all tests in cypress.json
and enable it only for a single test with Cypress.env
.
Yes. When a test case throws, it simply fails, and the test runner continues to run the remaining test cases in your suite.
If throwing is actually the desired behavior of your app, uses expect(foo).to.throw
.
Do this:
if (window.Cypress) {
window.appReady = true;
}
Await appReady
in your test.
beforeEach(() => {
cy.visit('/');
cy.window().should('have.property', 'appReady', true);
});
You may want to set appReady
when your root component mounts. Keep in mind componentDidMount
on classes works differently than its equivalent achieved with React Hooks — componentDidMount
is called once all its children have mounted (recursively, bottom-up) but components using Hooks can falsely report their readiness.
See documentation.
You should use the methods that have the auto-awaiting mechanism built into them. This includes, but is not limited to methods like should
, get
, find
, and contains
.
Key takeaways:
- Avoid setting timeouts
- Watch out for
.then
— it will not be retried - To chain multiple assertions in a row, use a Should callback
- Don't return inside the
should
callback, it will be ignored anyway - If you need to run side effects, use
then
and notshould
Whenever you can. There are some limitations:
-
Methods like
findByPlaceholderText
must yield a single element. To find the input element you're interested in, you must first target its parent.cy.get('header').findByPlaceholderText('Search free high-resolution photos');
-
Labels and placeholders must consist of plain text only. If, say, your
<label>
has a<span>
inside,findByLabelText
will not find your input field.
Use sinon.match
. The helpers it provides are essentially like propTypes
.