msw icon indicating copy to clipboard operation
msw copied to clipboard

MSW leaves cypress component test runner in a loop

Open yannbf opened this issue 3 years ago โ€ข 59 comments

Describe the bug

Cypress 7 recently released an interesting feature called component test runner. It allows to mount a component and run it in isolation, rather than having to run the entire application. I was trying to use it with MSW (which would be awesome!), but for some reason, starting the service worker makes the test behave weirdly, becoming flaky, sometimes looping and causing trouble.

Environment

  • msw: 0.28.2
  • nodejs: 14.14.0
  • npm: 6.4.8
  • browser: Chromium opened by cypress

To Reproduce

Steps to reproduce the behavior:

before, check this repro: https://github.com/yannbf/msw-cypress-component-issue

  1. yarn
  2. yarn cy
  3. Select Login.test.jsx
  4. Check the comments in https://github.com/yannbf/msw-cypress-component-issue/blob/main/src/components/Login.test.jsx#L9. As soon as you remove the start of the service worker, the tests flow naturally. When you add them, there's something weird that results in cypress runner looping.

Expected behavior

MSW should not impact cypress

Screenshots

2021-05-22 at 23 00 45 - Amber Beaver

yannbf avatar May 22 '21 21:05 yannbf

The effect is related to the following code: https://github.com/mswjs/msw/blob/86c23ec16c10698eb329798e883980a9b9159f05/src/setupWorker/start/utils/getWorkerInstance.ts#L25-L33

commenting it out removes the loop and flaky behavior, but of course msw doesn't work as expected

yannbf avatar May 25 '21 20:05 yannbf

Hey, @yannbf. Thanks for reporting this.

The question is why navigator.serviceWorker.controller becomes undefined?

It's highly recommended to have the MSW set up on the application's side. That way your app registers the worker, and Cypress loads your app that already has the mocking enabled. If you absolutely must have MSW integration on the Cypress side, I'd imagine it being set up in some global hook (i.e. before all test suites) as opposed to a single test's before.

kettanaito avatar May 27 '21 12:05 kettanaito

The question is why navigator.serviceWorker.controller becomes undefined?

It's highly recommended to have the MSW set up on the application's side. That way your app registers the worker, and Cypress loads your app that already has the mocking enabled. If you absolutely must have MSW integration on the Cypress side, I'd imagine it being set up in some global hook (i.e. before all test suites) as opposed to a single test's before.

Great one. Cypress component test runner does not rely on having an application anymore, as it bootstraps components in isolation. So indeed msw should be set somewhere at a higher level once. I tried setting it up in a support file (and updated the repro repo), and the result is that cypress fails (the test is just asserting if the element exists), then if I retry, it gets in an infinite loop.

image

yannbf avatar May 30 '21 12:05 yannbf

@lmiller1990 I heard you were looking into msw integration with CT. Do you think it's currently possible?

yannbf avatar May 30 '21 12:05 yannbf

My first instinct was to do exactly what you are doing, and set it up inside of beforeEach (or before). I am also curious as to why navigator.serviceWorker.controller is undefined. I've used msw many times before (but not with Cypress) and not seen this before.

I don't see any reason why this isn't possible - but I also don't see an obvious reason why this isn't working in the first place. I am not sure I can prioritize this dive right now, but I would like to use msw in Cypress, so I'll try to explore this a bit more at some point in the near future. I'll follow this thread and post anything that comes to mind.

According to MDN:

The controller read-only property of the ServiceWorkerContainer interface returns a ServiceWorker object if its state is activating or activated (the same object returned by ServiceWorkerRegistration.active). This property returns null if the request is a force refresh (Shift + refresh) or if there is no active worker.

I wonder if we are doing a refresh somewhere on the Cypress end that would be causing the last part of this to occur.

Edit: created this issue for more visibility.

lmiller1990 avatar May 31 '21 00:05 lmiller1990

Hey @lmiller1990 thanks a lot for helping out with this! I noticed that the service worker is only registered after running the test. I believe that ideally the service worker registration should happen as soon as cypress is bootstrapped.

Is there any way of running a "global setup" code that executes when cypress is open, without having to running any test suite? If we had a way to do that, I believe msw could work! As an example, in Storybook there is a preview.js file which executes once Storybook is bootstrapped. By setting msw in that file, it works across every story. The service worker is registered outside of the context of a story, and is optionally configured when running stories.

yannbf avatar May 31 '21 20:05 yannbf

Normally we recommend using before or beforeEach for this - seems like a race condition.

edit: I see what you are doing now: https://github.com/yannbf/msw-cypress-component-issue/blob/74115f93f9cd7d5239b01d56501d6fbd248294dc/cypress/support/index.js. You can actually put a before or beforeEach hook in support/index.js. As long as it returns a Promise, Cypress will wait. So something like:

before(() => {
  return new Promise(res => {
    /* need some way to know when MSW has started, then resolve the promise */
    setupWorker(...).start()
  })
})

We need some way to know when MSW has started. Is there something like that? if start() returns a Promise, that would be perfect.

For future reference, Cypress does expose some event. You don't see them often, since you generally should just use before or beforeEach. The events are documented here. test:before:run and test:before:run might be useful. They work like Cypress.on('test:before:run', (attributes , test) => { /* stuff */ }).

lmiller1990 avatar Jun 01 '21 22:06 lmiller1990

@lmiller1990 calling worker.start() does return a Promise that indicates when the worker is activated and ready to process requests. You can await that promise in your before hook.

The navigator.serviceWorker.controller may be undefined because the worker is not registered in the origin where the code tries to resolve its controller. Cypress runs in its own context (and host), while the tested app has its own context (and that's where the worker is registered). We have a custom logic on the worker's side to look up the proper registration and respect it when working with cypress (support for registering the worker in a nested iframe to still affect the requests of the parent frame). It just may happen that accessing the controller in the parent frame doesn't resolve to the child frame's controller. This needs to be investigated. We have iframe integration tests where you can inspect how the controller behaves in the case of nested iframes:

  • https://github.com/mswjs/msw/tree/master/test/msw-api/setup-worker/scenarios/iframe

I don't have the capacity to look into this at the moment but will keep an eye on this issue to help in whichever way I can. Thank you all for the discussion on this topic.

kettanaito avatar Jun 03 '21 13:06 kettanaito

I not sure i run into exact some problems

I try integrate MSW with cypress components test


import { useAsyncFn } from "react-use"

export function TestApp({ children }) {
  const [
    {
      value: startedMockServer,
      loading: startMockServerLoading,
      error: startMockServerError,
    },
    startWorkerFunction,
  ] = useAsyncFn(async () => {
    await mockWorker.start({
      serviceWorker: {
        url: 'https://cdn.jsdelivr.net/npm/msw/lib/esm/mockServiceWorker.js',
      },
    });
    window.msw = {
      worker: mockWorker,
    };
    console.log('MockServiceWorker Init');
    return true;
  }, []);

  useEffect(() => {
    if (!startedMockServer && !startMockServerLoading) {
      startWorkerFunction();
    }
  }, [startedMockServer, startWorkerFunction, startMockServerLoading]);
  if (startMockServerLoading) return null;
  if (startMockServerError)
    throw new Error('Init TestApp error');

  return (
        <>{children}</>
  );
}

I attempt create TestApp for configure test environment first.

but it suck in mockWorker.start(), the promise just never return

I have attempt add some log to debugging

https://github.com/mswjs/msw/blob/9a43bc9cba49f3c18b1ab1ed700d3f41f9fe86f5/src/setupWorker/start/createStartHandler.ts#L70

Turn out i find i suck here, the worker just never response the event

neviaumi avatar Jun 17 '21 14:06 neviaumi

I can confirm that the issue exists but have not enough insights to share as to its cause. Feel free to share your findings, it's a general network/javascript debugging that should take place to find out what goes wrong. Thank you for your patience.

kettanaito avatar Jun 17 '21 17:06 kettanaito

I've had some luck with the following approach:

cypress/support/msw.js:

import { setupWorker } from 'msw'

export const serviceWorker = setupWorker()

export const waitForServiceWorker = serviceWorker.start({
  serviceWorker: {
    url: '/mockServiceWorker.js',
  },
})

cypress/support/index.js:

import './msw'

src/components/example.spec.js:

import { serviceWorker, waitForServiceWorker } from '../../cypress/support/msw'
import { rest } from 'msw'

describe('example', function () {
  before(() => waitForServiceWorker)
  afterEach(() => serviceWorker.resetHandlers())

  it('makes a request', function () {
    serviceWorker.use(
      rest.post('/foo', (req, res, ctx) => {
        return res(
          ctx.status(200),
          ctx.json({ foo: 'bar' })
        )
      })
    )

    mount(<ComponentThatMakesARequest/>)
  })
})

I'm now hitting https://github.com/cypress-io/cypress/issues/14745 but that seems to be a separate issue - I can at least see the response being served by the msw ServiceWorker.

hubgit avatar Jun 23 '21 21:06 hubgit

I'm trying to get this working as well but I can't even get past the fact that the mockServiceWorker.js file cannot be served during component testing. My app is running on localhost:3000 and the component tests run on localhost:8080. If I try to point it to localhost:3000/ i get the following:

serviceWorker: {
    url: 'http://localhost:3000/mockServiceWorker.js',
},

[MSW] Failed to register the Service Worker:

Failed to register a ServiceWorker: The origin of the provided scriptURL ('http://localhost:3000') does not match the current origin ('http://localhost:8080').

If I try to path it relatively (serviceWorker: {url: 'dist/public/mockServiceWorker.js'},) I get the following that The script has an unsupported MIME type ('text/html').

If I try to go from a base path I get the following:

serviceWorker: {url: '/dist/public/mockServiceWorker.js'},

[MSW] Failed to register a Service Worker for scope ('http://localhost:8080/') with script ('http://localhost:8080/dist/public/mockServiceWorker.js'): Service Worker script does not exist at the given path.

Did you forget to run "npx msw init <PUBLIC_DIR>"?

How can I get past this? I know that I need to have the service worker file alongside the server running on 8080 but I don't know where I can even copy that file to since Cypress is just kind of sandboxing this whole thing.

Any help would be appreciated.

reintroducing avatar Mar 14 '22 21:03 reintroducing

Hey, @reintroducing. The worker script must always be served from the same hostname. You cannot have a worker at :3000 and handle requests that happen at :8000โ€”that's against the security design of service workers and any browser will forbid this.

Your second approach is correct. Only you're pointing at a non-existing resource: there's nothing at dist/public/mockServiceWorker.js. You get the Mime type error because accessing that URL likely produces a 404 page (HTML page). You can try following that URL yourself and see where it leads.

My suspicion is that you need to omit the /dist part of your worker script URL. I assume that /dist is the public folder, which is the root of your application. I think even /public looks suspicious in the URL but you're the best to verify this.

  1. Find how your application serves the worker script. I suspect it's at http://localhost:8080/mockServiceWorker.js but I may be wrong.
  2. Use that (relative) URL as the custom worker URL in serviceWorker.url option to worker.start().

kettanaito avatar Mar 15 '22 11:03 kettanaito

@kettanaito Thanks for your reply. My apologies, I left out the fact that I had tried all the possibly URLs I could think of at :8080 as well but I basically just get the ole Cannot GET /mockServiceWorker.js with the path changing based on what I was trying. The reason for this is because I don't know where in the filesystem Cypress is serving the files from and therefore I don't know where I should copy the mSW.js file into for it to be served accordingly alongside the component testing application. Here is an example URL of my App.spec (http://localhost:8080/__/#/tests/component/app/App.spec.js) but again, I can't figure out where its being served from as it pertains to the filesystem itself.

reintroducing avatar Mar 15 '22 14:03 reintroducing

I was able to resolve my issue when I realized that webpack for the cypress component tests was being served on localhost:8080/public (whereas my app serves on localhost:3000/dist/public, long story, dont ask). Once i realized that I was able to copy the worker file into the /public directory and didn't need the file path in the worker.start call since its served from / by default.

reintroducing avatar Mar 16 '22 16:03 reintroducing

Any updates on this?

vospascal avatar May 04 '22 20:05 vospascal

I'm having the same problem, and none of the above workarounds have worked for me. I'd really love to be able to use MSW in my Cypress tests, is there any progress on this issue?

roryschadler avatar Aug 04 '22 20:08 roryschadler

Any updates on this? I'm having the same situation and even Cypress is updated a lot with new version. It's really hard to know cypress new versions problem or msw problem. Below is my procject's version. Please share some informations who has same situation with this issue.

"cypress": "^10.11.0",
"msw": "^0.47.4",

sanghunlee-711 avatar Nov 07 '22 08:11 sanghunlee-711

There is a minimal reproduction in the OP, we should update it to the latest version of everything and see if it's still happening. I suspect it's a config issue.

Also we have an issue in Cypress to track and work needed on our end: https://github.com/cypress-io/cypress/issues/16742

lmiller1990 avatar Nov 07 '22 23:11 lmiller1990

I'm running in the same problem as well. And tried a lot of different (suggested) solutions. But none seem to work.

As soon as MSW is loaded, Cypress gets in some sort of reload loop.

Screenshot 2022-12-14 at 13 38 54

And I'm getting the following errors:

[MSW] Cannot intercept requests on this page because it's outside of the worker's scope ("http://localhost:8080/__cypress/src/"). If you wish to mock API requests on this page, you must resolve this scope issue.

- (Recommended) Register the worker at the root level ("/") of your application.
- Set the "Service-Worker-Allowed" response header to allow out-of-scope workers.

Which is correct. And setting the scope to '/' does not help.

Sometimes it correctly says: MSW Enabled, but the loop still happens.

Screenshot 2022-12-14 at 15 11 37


I'm trying to get MSW working in combination with Cypress Component tests in a Nx workspace (so I can write a blog about it). Running the application, with 'normal' Cypress tests and Jest tests with MSWjs all work perfectly ๐Ÿ‘๐Ÿผ

Example repo: https://github.com/the-ult/angular-nx-playground/. (Work in progress ๐Ÿ˜‡ )

And the tests are in:

  • https://github.com/the-ult/angular-nx-playground/blob/main/libs/movie/feature-movies/src/lib/movies.page.cy.ts
  • https://github.com/the-ult/angular-nx-playground/blob/main/libs/movie/feature-movies/src/lib/media-card/media-card.component.cy.ts

Cypress Component Test setup for movie/feature-movies:

  • https://github.com/the-ult/angular-nx-playground/blob/main/libs/movie/feature-movies/cypress/support/commands.ts

The mockServiceWorker.js is located in: https://github.com/the-ult/angular-nx-playground/tree/main/libs/shared/test/msw/src/assets

package.json

  "msw": {
    "workerDirectory": "libs/shared/test/msw/src/assets"
  },

And (automatically) served on: https://localhost:8080/__cypress/src/mockServiceWorker.js


Questions

  • Where should mockServiceWorker.js be placed so it is served on http://localhost:8080/mockServiceWorker.js ?? Or is it correct it is served now? But how do we get the correct scope?

  • What would be the proper way to setup MSW with Nx (separate libs)? What I would like to try to setup is a more global setup method in libs/shared/test/msw/browser which can be loaded in the specific (integration) Component Tests in the before();


Versions

Node : 18.12.1
OS   : darwin x64
pnpm : 7.18.2

nx : 15.3.3
@nrwl/angular : 15.3.3
@nrwl/cypress : 15.3.3
@nrwl/devkit : 15.3.3
@nrwl/eslint-plugin-nx : 15.3.3
@nrwl/jest : 15.3.3
@nrwl/js : 15.3.3
@nrwl/linter : 15.3.3
@nrwl/nx-cloud : 15.0.2
@nrwl/webpack : 15.3.3
@nrwl/workspace : 15.3.3
typescript : 4.8.4

cypress: 12.1.0
msw: 0.49.2 // Tried 0.0.0-fetch.rc-3 as well

the-ult avatar Dec 14 '22 14:12 the-ult

@the-ult that is really interesting, I was working on something similar to this earlier: https://github.com/cypress-io/cypress/pull/25120

You say it's only happening in CT and not E2E - I guess what might be happen is that, instead of the request going to the main network layer, it heads to the dev server route instead, and this messes things up.

I'll try your repo. Do I just clone and run something? yarn cypress open maybe?

Also stupid question, but what does MSW do that cy.intercept() doesn't (this should be vastly more powerful, understandable if you just want to use MSW since you are migrating from Jest or something and don't want to touch your code too much).

lmiller1990 avatar Dec 14 '22 23:12 lmiller1990

Also stupid question, but what does MSW do that cy.intercept() doesn't (this should be vastly more powerful, understandable if you just want to use MSW since you are migrating from Jest or something and don't want to touch your code too much).

@lmiller1990 im not the poster but I can tell you why we use msw over cy.intercept in our app. We use msw extensively to mock just about every api during development so we donโ€™t have to depend on a BE server to run alongside our FE app. Then, during tests, msw is utilized to mock those same calls so we never have to write a cy.intercept in our tests and the api calls โ€œjust workโ€. It serves double duty!

reintroducing avatar Dec 14 '22 23:12 reintroducing

@reintroducing thanks, I figured it was something like this - makes sense.

lmiller1990 avatar Dec 15 '22 03:12 lmiller1990

what does MSW do that cy.intercept() doesn't

@lmiller1990, cy.intercept() is bound to Cypress. MSW is framework-, tool-, and environment-agnostic. It's simply a better time investment that allows to reuse API mocks across the entire stack.

MSW also works completely differently compared to cy.intercept(), since MSW doesn't patch request clients or meddle with internal browser flags. It uses standard web APIs to allow requests to fully execute, intercepting them at the network level.

kettanaito avatar Dec 15 '22 10:12 kettanaito

Also stupid question, but what does MSW do that cy.intercept() doesn't (this should be vastly more powerful, understandable if you just want to use MSW since you are migrating from Jest or something and don't want to touch your code too much).

There are no stupid questions ๐Ÿ˜‰

In my previous project we were working with GraphQL. And intercepting GraphQL requests in earlier version of Cypress (pre-intercept) was a bit cumbersome. With MSWjs this was easy.

But the greatest 'win' of MSWjs is one-easy-way for API mocking for everything.

  • Starting the app(s) with mock data (easy development) -> MSW
  • E2E testing -> MSW
  • Jest (integration tests) -> MSW
  • Cypress Component Tests -> MSW
  • Playwright -> MSW

So for everything you can use the same API and easily re-use mock-data and Handlers. Without having to learn cy.intercept, jest Spy/Mock, etc.

(That's why I was trying to create this sample repo ๐Ÿ˜‡ )

I'll try your repo. Do I just clone and run something? yarn cypress open maybe?

Good question. I'll update the README.md and add install/run instructions.

I'm using pnpm

install

pnpm i

Run

App

pnpm start:msw movie-db

Jest Tests

pnpm test
// OR
nx test --runInBand

E2E

pnpm e2e:msw
// OR
nx e2e movie-db-e2e --watch --browser=chrome

Component Test ๐Ÿ”ฅ FAILS ๐Ÿ”ฅ

pnpm exec nx run movie-feature-media-items:component-test --skip-nx-cache=true --watch=true --browser=chrome
// OR
nx run movies-feature-movies:component-test --watch=true --browser=chrome
  • media-card works since it does not load msw
  • movies-page loads and gets in the loop

the-ult avatar Dec 15 '22 10:12 the-ult

Ok thanks @kettanaito and @the-ult. I'll poke around Cypress a bit, it probably won't be until next week though. I'm fairly sure something is getting muddled up in the routing for CT.

The CT specific routes are here https://github.com/cypress-io/cypress/blob/develop/packages/server/lib/routes-ct.ts The shared routes are https://github.com/cypress-io/cypress/blob/develop/packages/server/lib/routes.ts

Small files, so this should not be too hard to debug.

lmiller1990 avatar Dec 16 '22 00:12 lmiller1990

Awesome. In de meantime, I'll try and fix the E2E and improve the documentation ๐Ÿ‘๐Ÿผ

And see if I can debug some myself as well

[edit] E2E is working again. Monday I'll improve the documentation

the-ult avatar Dec 16 '22 08:12 the-ult

Thanks for all the involvement on this, folks! Let me know if I can be of any help. I should be available sometime at the beginning of the next year.

kettanaito avatar Dec 16 '22 11:12 kettanaito

Any news on this? This is a game changer once working.

lydemann avatar Feb 09 '23 14:02 lydemann

I updated my sample project a little bit. But did not get the component tests working. Haven't had time to try and debug the files @lmiller1990 mentioned.

Will try to give it another look next week

the-ult avatar Feb 09 '23 14:02 the-ult