Closed TeemuKoivisto closed 8 months ago
Hey, @TeemuKoivisto. That's a good suggestion.
What we should have done long time ago is showcased how to correctly use MSW with Cypress. Currently, we have a minimal usage example which will be fine for some percentage of use cases but it is not fully correct in terms of awaiting MSW's worker while running tests in Cypress.
I recall someone from the community posting a fantastic example of how they use a custom Cypress plugin (or a different terminology) that allows them to import and await MSW as a part of their Cypress run vs exposing MSW on window
globally. I believe that is the right direction to take.
That example is somewhat similar to yours above but it didn't rely on
window
, to my best recollection.
@kettanaito great and thanks! Yeah it's a little complicated so I wager I'd be nice to have a solid example to make people less hesitant to use msw
. It'd cool to see their setup and how they managed to set msw as part of their Cypress run. Hmm yeah there are setup hooks that are run before the tests themselves. Probably one of those.
@TeemuKoivisto This is a blog post I wrote awhile back but its a little outdated at this point. I could potentially re-write it to deal with Cypress v10 and above if you need it. Hope it helps.
The blog post from @Capocaccia is one of the best integration examples I've seen. Posting here for clarity:
//cypress/support/index.js
import { worker } from '../../src/mocks/browser';
Cypress.on('test:before:run:async', async () => {
await worker.start();
});
The idea they propose here is to start the worker in Cypress' internal hook. I think this is marvelous. Give it a try.
The original article also mentions worker deduplication but MSW ships with that built-in. Subsequent calls to
worker.start()
while the worker is registering/registered have no effect.
Just as an update, I had to remove msw
and switch to cy.intercept
. I am not sure why but for me, starting msw
in 'test:before:run:async'
didn't seem to execute properly. I have an interesting edge-case where I am using web workers to execute fetch
in parallel, which fails miserably in Cypress as my workers will be starved and unresponsive while Cypress' workers load all the assets et cetera.
So I added a hacky if-else like this:
let worker: DriveWorker;
// @ts-ignore
if (window.Cypress) {
// Don't use workers inside Cypress since Cypress uses workers itself heavily which deprioritizes
// our workers, taking forever to run
const postWorkerMsg = (ev: WorkerEvent) => {
worker.onmessage({ data: ev });
};
const workerSelf = {
onmessage: onmessage({ postMessage: postWorkerMsg }),
postMessage: postWorkerMsg,
};
worker = {
onmessage: (ev: { data: WorkerEvent }) => {},
postMessage(msg: ClientEvent) {
workerSelf.onmessage({ data: msg });
},
terminate() {},
};
} else {
worker = new Worker(
// @ts-ignore
new URL("../../workers/driveWorker", import.meta.url),
{ type: "module" },
) as DriveWorker;
}
to switch back to single-threaded execution while in Cypress. Something similar probably happens with msw
where the service worker is somehow starved by Cypress.
Now that I added this if-else hack, I perhaps could await msw
properly to intercept the fetch requests. But since this got quite convoluted already, I'll probably skip using workers altogether in Cypress - seems just too flaky. I'll definitely keep my eye on this if the situation improves.
I stand by that the simplest and most straightforward integration is just following the Browser integration for your entire app, enable it conditionally during the Cypress run (env variables are your friends) and get client-side request interception out of the box.
Since you await responses anyway, you are unlikely to deal with race conditions as long as you write correct assertions, which you should be doing regardless of the API mocking library of choice.
I will try to add a Cypress usage example to the new examples repo to have a single point of reference and close this issue afterward.
@kettanaito I concur that using similar conditional env I was able to launch the app and use msw in Cypress. It was only my other workers which then failed. Probably with that new hack & reverting to using env for msw it could work but it just got too much at that point.
I think the easiest way to implement MSW within Cypress without exposing worker to the window
is the following:
I just extracted what I need from cypress-msw-interceptor and you can adjust as you need.
Also it works with OOB Intercept by Cypress but this way you can reuse you handlers if you already implement them for your app.
Note: Just make sure to expose the file mockServiceWorker.js
generated by msw
in you public
folder where you serve your app.
in support
folder add a msw.ts
file:
import { setupWorker, type SetupWorker, type RequestHandler } from 'msw'
declare global {
namespace Cypress {
interface Chainable {
interceptRequest(...handlers: RequestHandler[]): void
}
}
}
let worker: SetupWorker
before(() => {
worker = setupWorker()
cy.wrap(worker.start({ onUnhandledRequest: 'bypass' }), { log: false })
})
Cypress.on('test:before:run', () => {
if (!worker) return
worker.resetHandlers()
})
Cypress.Commands.add('interceptRequest', (...handlers: RequestHandler[]) => {
worker.use(...handlers)
})
Import into e2e.ts
file:
import './msw'
Now you can use worker to mock requests:
import { rest } from 'msw'
it('visit app', () => {
const handler = rest.post('/api/items', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{ name: 'apple' },
{ name: 'orange' },
]))
})
cy.interceptRequest(handler)
// your app inside will fetch '/api/items'
cy.visit('/')
})
I hope it helps!
great
I think the easiest way to implement MSW within Cypress without exposing worker to the
window
is the following: I just extracted what I need from cypress-msw-interceptor and you can adjust as you need.Also it works with OOB Intercept by Cypress but this way you can reuse you handlers if you already implement them for your app.
Note: Just make sure to expose the file
mockServiceWorker.js
generated bymsw
in youpublic
folder where you serve your app.in
support
folder add amsw.ts
file:import { setupWorker, type SetupWorker, type RequestHandler } from 'msw' declare global { namespace Cypress { interface Chainable { interceptRequest(...handlers: RequestHandler[]): void } } } let worker: SetupWorker before(() => { worker = setupWorker() cy.wrap(worker.start({ onUnhandledRequest: 'bypass' }), { log: false }) }) Cypress.on('test:before:run', () => { if (!worker) return worker.resetHandlers() }) Cypress.Commands.add('interceptRequest', (...handlers: RequestHandler[]) => { worker.use(...handlers) })
Import into
e2e.ts
file:import './msw'
Now you can use worker to mock requests:
import { rest } from 'msw' it('visit app', () => { const handler = rest.post('/api/items', (req, res, ctx) => { return res( ctx.status(200), ctx.json([ { name: 'apple' }, { name: 'orange' }, ])) }) cy.interceptRequest(handler) // your app inside will fetch '/api/items' cy.visit('/') })
I hope it helps!
Great example! can you show an repo?
Great example! can you show an repo?
There you have msw-cypress it's just simple.
Thanks all for sharing! I just integrated MSW into Cypress with a small tweak since cypress/support/index.js
is now cypress/support/e2e.{js,jsx,ts,tsx}
as of cypress 10.0.0
// cypress/support/e2e.{js,jsx,ts,tsx}
import { worker } from '../../src/mocks/browser';
Cypress.on('test:before:run:async', async () => {
await worker.start();
});
Can someone please open a pull request to mswjs/examples
repo and migrate the existing Cypress setups to use Cypress.on('test:before:run:async')
? I think that's the way to go and I'd like to recommend that to our users.
One question: does this approach allow one to reference the worker instance in tests to append runtime handlers with worker.use()
? If yes, how?
Can someone please open a pull request to
mswjs/examples
repo and migrate the existing Cypress setups to useCypress.on('test:before:run:async')
? I think that's the way to go and I'd like to recommend that to our users.One question: does this approach allow one to reference the worker instance in tests to append runtime handlers with
worker.use()
? If yes, how?Examples using Cypress
I'll take a look
@kettanaito I ran up against this the other day and went ahead and put together a small, easily reproducible example using the latest Cypress and MSW that shows the constant restart issue very clearly.
Grab that zip, run npm i
and then npm start
. Load up http://localhost:3000/ and if you click the buttons, you will see the responses populating correctly in the UI from each.
Now, stop that process and run npm run cy:open
and select Component Testing. Load it up in Chrome and select App.cy.tsx
. You'll see that its just in a crazy loop and keeps trying to restart.
Open up cypress/support/component.ts
and you will see line 42 where I added the test:before:run:async
event where I'm trying to start the worker. I also had to change the serviceWorker url to what you see there otherwise it would error on that.
What is even crazier is that i had this same setup at a prior employer with msw 1.3.2 and I had it all working correctly. I even tried to duplicate that in this example repo (separately) with that version of msw and now I can't even get that working as well, so not sure how it ever worked previously :\ That project had a much more elaborate setup but I tried to boil it down to what I would think would be sufficient and still could not get it to work in the older version of msw.
I’m also seeing this issue with the Cypress test runner getting in a crazy infinite loop after trying to move from a webpack config to vite.
@reintroducing small tip, you can use ${import.meta.env.BASE_URL}/mockServiceWorker.js
to get the URL
I feel like because this worked before in Webpack this is very likely a vite-related Cypress regression. Have you raised there?
@MattyBalaam i previously had this working in Vite so not exactly sure where the problem is, unfortunately.
@reintroducing Was that vite 4, rather than vite 5?
@MattyBalaam good call, it was Vite 4.4.4 and msw 1.3.2
OK, I think I got it. There is a badly documented devServerPublicPathRoute
config setting for Cypress which has helped for your minimal example at least. It will show as an error for not being a valid config property but seems to fix the issue.
https://github.com/cypress-io/cypress/issues/28347#issuecomment-2111054407
export default defineConfig({
component: {
devServer: {
framework: "react",
bundler: "vite",
},
devServerPublicPathRoute: "",
},
retries: 0,
});
@MattyBalaam ooooohhhh, thats promising. so this isn't an msw issue after all? at this point i don't even know where i'd log a bug for this :\
Yeah, it’s more a documentation and error handling issue really I think. Possibly there could be something MSW could do here if it is retiggering a reload as I see that in some of the code… but could also be more in Cypress’s court to handle that better. @kettanaito any thoughts here?
OK, acho que entendi. Há uma
devServerPublicPathRoute
configuração mal documentada para o Cypress que ajudou pelo menos no seu exemplo mínimo. Ela será exibida como um erro por não ser uma propriedade de configuração válida, mas parece corrigir o problema.cypress-io/cypress#28347 (comentário)
export default defineConfig({ component: { devServer: { framework: "react", bundler: "vite", }, devServerPublicPathRoute: "", }, retries: 0, });
Is there something like this for nextjs?
If you also know something about how to intercept requests made by the nextjs middleware (server side request) it would help a lot, I'm having a lot of problems with this
Scope
Improves an existing behavior
Compatibility
Feature description
Hi, I just recently faced a really bizarre bug where my
window.location.pathname
would not be updated to correct path when I hadmsw
enabled.Turns out, since there was a home page outside the main Vue app that loaded without
msw
and inner app withmsw
there was no time formsw
to completely load.I had to add following checks to ensure it had loaded:
So my small request is, could this be added to some Cypress example in how to use msw? I'm pretty sure I won't be the only one stumbling across this and scratching their head for a long while since to cause is not so obvious.
Showing that you should load
msw
first with eg:And then say add a command:
With your
cypress/support/index.d.ts
looking like:And use it at the start of your test:
Thanks for the library though!
EDIT: Okay, it broke again. I thought I had it working but apparently when
msw
is enabled something goes wrong withvue-router
. If I callhistory.pushState
manually it works again.EDIT EDIT: Cypress really doesn't go along well with
msw
. Here's an example I got working https://github.com/TeemuKoivisto/sveltekit-monorepo-template