mswjs / msw-storybook-addon

Mock API requests in Storybook with Mock Service Worker.
https://msw-sb.vercel.app
MIT License
407 stars 39 forks source link

Story not waiting for MSW to be ready before running #89

Closed FezVrasta closed 4 months ago

FezVrasta commented 1 year ago

We are experiencing an issue where the story is executed/rendered before MSW is ready. We can see in the console that some requests run before the "MSW is ready" console log.

Is there any solution to this problem? We are running the most recent version of the plugin and Storybook 6.5.10

yannbf commented 1 year ago

Hey @FezVrasta thanks for opening this issue! I wonder if you could experiment with making local changes to your dependency and include the following code changes: https://github.com/mswjs/msw-storybook-addon/compare/main...lyleunderwood:feat/add-experimental-loaders

Then, instead of doing this in your preview.js file:

import { initialize, mswDecorator } from 'msw-storybook-addon'

initialize()
export const decorators = [mswDecorator]

do this:

import { initialize, mswLoader } from 'msw-storybook-addon'

initialize()
export const loaders = [mswLoader]

And let me know if that fixes your problem?

FezVrasta commented 1 year ago

Thanks. We use Storyshots and async loaders are not supported with it ☹️

yannbf commented 1 year ago

Did you try using the Storybook test runner? https://github.com/storybookjs/test-runner

Storyshots will be discontinued at some point in favor of the test runner, which supports every feature of Storybook (including loaders), snapshots etc.

hiuny commented 1 year ago

@yannbf I had the same problem and came across this issue while searching for a solution. The method using loaders works very well. It would be nice if it was released soon https://github.com/mswjs/msw-storybook-addon/pull/72 This pr doesn't seem to have progressed since February 27th.

fdescamps commented 1 year ago

Hello @yannbf,

I am really interested about this work ! Could you please merge it and generate a new version ?

Thanks a lot

Regards,

yannbf commented 1 year ago

Hey peeps, PR is merged, and a scheduled release will soon happen. Thanks for providing feedback! There are quite a few changes I'm planning to make in this addon, including adding support to Storybook 7.0, which might take some time but we'll get there!

fdescamps commented 1 year ago

Thanks for your work !

junkisai commented 10 months ago

I changed to use mswLoader with version 1.10.0 of msw-storybook-addon, but I am experiencing a similar issue as the title of this issue.

It seemed like the following issue also reported a similar phenomenon: https://github.com/storybookjs/test-runner/issues/417

cbovis commented 9 months ago

I changed to use mswLoader with version 1.10.0 of msw-storybook-addon, but I am experiencing a similar issue as the title of this issue.

It seemed like the following issue also reported a similar phenomenon: storybookjs/test-runner#417

I was banging my head against a wall with this one. MSW would say it initialised fine in my local environment but no requests were being intercepted. The set up had been working forever but at some point it mysteriously broke. Even stranger, if I modified something related to the story then requests would be mocked when the story was auto-reloaded. It would also run perfectly fine via our Chromatic visual regression testing.

In desperation I figured I'd try another browser and discovered that the broken behaviour only occurs for me in Chrome. In Firefox, Safari, Edge, Opera, Arc everything is fine. Switched Chrome to incognito mode and it worked fine there which tipped me to extensions. Started removing extensions until eventually all is well in the world again.

Unfortunately I wasn't systematic enough about it to know which extension was causing the problem but I assume it was something that works at the network level. Perhaps this approach is useful to yourself or others in debugging.

jalovatt commented 9 months ago

I'm seeing this behaviour in 2.0.0-beta.1. Not sure how different 2.x is from 1.x, or how MSW's implementation has changed on that side, but a quick skim suggested that the same failure states are possible.

In my case, some extra console logging showed that my API requests were being fired before MSW was finished loading.

Problem

I see two cases in mswLoader where the loader could incorrectly assume things are good to go.

  1. If the service worker hasn't loaded enough for if (... && navigator.serviceWorker.controller) to pass, the loader returns too early.

    I was able to directly identify this as the source of failure in my case:

    loaders: [
    async (context) => {
      console.log('calling mswLoader');
    
      await mswLoader(context);
    
      console.log('mswLoader finished');
    
      if (!navigator.serviceWorker.controller) {
        console.log('worker not found');
      }
    },
    ],
    
    // Console output:
    calling mswLoader
    mswLoader finished
    worker not found
    API request firing
    API request finished
    [MSW] Mocking enabled.
    Failed to load resource: the server responded with a status of 405 ()
  2. serviceWorker.ready resolves when the ServiceWorkerRegistration becomes active (MDN), but that happens when the inner ServiceWorker has a state of either activating and activated. (MDN)

    MSW doesn't actually enable itself until the worker is activated (MSW source), so again mswLoader is able to return too early.

    I haven't spotted a case where this was causing my queries to fire too early, but it is at least possible.

Fix

I added a function to explicitly wait for both of the conditions above, preventing my stories from rendering until MSW is actually in place. With it, I've been unable to reproduce this issue at all.

loaders: [
  async (context) => {
    await mswLoader(context);
    await waitForActivatedServiceWorker();
  },
],

const waitForActivatedServiceWorker = async () => {
  // Wait for the worker to be loaded
  const serviceWorker = navigator.serviceWorker.controller
    || await new Promise((resolve, reject) => {
      let triesLeft = 10;

      const fn = () => {
        if (navigator.serviceWorker.controller) {
          resolve(navigator.serviceWorker.controller);
        } else {
          triesLeft -= 1;

          if (triesLeft === 0) {
            reject(new Error('Timed out waiting for service worker'));
          } else {
            setTimeout(fn, 100);
          }
        }
      };

      setTimeout(fn, 100);
    });

  if (!serviceWorker) {
    throw new Error('No service worker found');
  }

  // Make sure the worker is actually ready to go
  if (serviceWorker.state !== 'activated') {
    await new Promise<void>((resolve) => {
      const fn = (e: Event) => {
        if (e.target && 'state' in e.target && e.target.state === 'activated') {
          serviceWorker.removeEventListener('statechange', fn);
          resolve();
        }
      };

      serviceWorker.addEventListener('statechange', fn);
    });
  }
};
0xR commented 7 months ago

The solution from @jalovatt didn't work for me

this worked for me:

-  loaders: [mswLoader],
+  loaders: [mswLoader, () => getWorker().start()],
newtriks commented 5 months ago

The solution from @jalovatt didn't work for me

this worked for me:

-  loaders: [mswLoader],
+  loaders: [mswLoader, () => getWorker().start()],

As a heads up @yannbf, upgrading to the latest version of this library I found unreliable results with 404's and various requests attempted before mocking enabled. @0xR message seems so far to be the most reliable working solution.

thehig commented 5 months ago

Took me a minute to find getWorker function.

It's here: import { initialize, mswLoader, getWorker } from 'msw-storybook-addon';

timkolotov commented 5 months ago

I'm using msw-storybook-addon==2.0.2 with msw==2.3.1 and have this issue which mostly happens in Firefox.

I think the only reason why adding the second call to worker.start() works is because it adds extra time needed for MSW to complete initialization, therefore, I don't think it is reliable. Besides, it floods the console with warnings about the redundant call to worker.start() and removes options' augmentations added by the addon here

https://github.com/mswjs/msw-storybook-addon/blob/8f27913940c887c513a19f01f1410ded6825fedb/packages/msw-addon/src/initialize.browser.ts#L14-L19

I also think that the problem is indeed in the fact that the addon only waits for navigator.serviceWorker.ready to be resolved, but it may resolve before the mocking is enabled and enableMocking is called here https://github.com/mswjs/msw/blob/bed402cdd9b79ef3084b3195cdcce0d83c7e2cfc/src/browser/setupWorker/start/createStartHandler.ts#L115

enableMocking sends a message to the service worker to start mocking, and only after the worker receives it it can be used because that's when the client is added, and if there are no clients, all requests are bypassed https://github.com/mswjs/msw/blob/bed402cdd9b79ef3084b3195cdcce0d83c7e2cfc/src/mockServiceWorker.js#L106-L111

The workaround I decided to use is a simple promise with is resolved upon receiving a message from the worker confirming activating and just use it instead of the loader provided by the addon:

const mockWatcher = new Promise<void>(resolve => {
    navigator.serviceWorker.addEventListener('message', event => {
        if (event.data.type === 'MOCKING_ENABLED') resolve();
    });
});

@yannbf, is it something that can be used by the addon itself? I can create PR if it makes sense.

WesselKroos commented 4 months ago

I was getting the error: msw storybook Cannot read properties of undefined (reading 'url') which was thrown by the initialize() function. So I had to move that method to a loader as well.

And while making changes in the preview.ts, hotreloading was double-loading the serviceworker, causing the same error to be thrown. So I had to prevent re-execution while hotreloading.

Which resulted in this patch:

import {
  Context,
  initialize as originalInitialize,
  mswLoader as originalMswLoader,
} from 'msw-storybook-addon';
import { StartOptions } from 'msw/browser';

let initializeOptions: StartOptions | undefined;
export const initialize = (options: StartOptions | undefined) => {
  initializeOptions = options;
};

let loaded = false;
export const mswLoader = async (context: Context) => {
  if (loaded) return;

  originalInitialize(initializeOptions);

  await originalMswLoader(context);
  loaded = true;
};
jalovatt commented 4 months ago

I've started running into this again, even with the fix I described previously. The suggestion of waiting for MOCKING_ENABLED also didn't help, and honestly at this point I'm not sure if it's msw-storybook-addon or msw itself at fault.

Adding some console logs shows:

loaders running
mswLoader resolved
got service worker, state = activated
loaders finished
[MSW] Mocking enabled.
TypeError: Cannot read properties of undefined (reading 'id')

^
An API request we depend on, received the Storybook HTML because the mock route isn't in place yet.

I've removed the previous fix in favor of just waiting for a successful test request to one of the mocked routes, just to be absolutely sure.

github-actions[bot] commented 4 months ago

:rocket: Issue was released in v2.0.3 :rocket:

yannbf commented 4 months ago

Hey everyone, this issue should be finally fully fixed by in v2.0.3, please try it out and report back if you have any issues. Thank you so much for all of your input and ideas!

frle10 commented 3 months ago

Hello @yannbf ,

my team and I are still experiencing this issue, however it happens every so often. About every 20 refreshes a request will return a 404 as if it wasn't intercepted by msw.

The console print of [MSW] Mocking enabled happens first normally every time, and then it's followed by failed requests with a 404 status code every 20 or so refreshes. There is no reliable way to recreate this other than trying to refresh the Storybook page until it happens.

We are using the latest version of msw-storybook-plugin, so 2.0.3, and the latest current version of msw, which is 2.3.4. Our storybook version is 7.6.20.

I tested this in the latest version of Chrome and on OSes MacOS and Windows.

We even tried fixes from this discussion but nothing changes. It almost seems like there could be more timing issues at hand here that might be a bit harder to detect. I'm happy to provide whatever is necessary to see if this is our issue or a plugin issue.

In preview.tsx we have the following stuff for msw:

initialize({
  onUnhandledRequest: 'bypass',
});
const preview: ProjectAnnotations<Renderer> = {
  ...
  loaders: [mswLoader],
};
daniele-roncaglioni commented 4 weeks ago

Hello all,

seems like I have the same issue as @frle10 while on v2.0.3. Some requests get intercepted by msw and some don't, it seems to be really random.

This happens in the case where I have a global msw handler trying to intercept requests coming from a global storybook decorator.

If I use the same decorator and handler but on the story level this issue doesn't happen.