Open mmirus opened 2 years ago
I'm having this exact same issue - and it was related to when I started specifically passing in multiple mocks to the msw handler. I still don't know what's causing the issue, but thanks for your workaround.
Example of what caused it:
// Works
Primary.parameters = {
msw: {
handlers: [someMock],
},
};
Secondary.parameters = {
msw: {
handlers: [someMock],
},
}
//doesn't work
Primary.parameters = {
msw: {
handlers: [someMock, someMock2],
},
};
Secondary.parameters = {
msw: {
handlers: [someMock, someMock2],
},
}
This seems to be an issue with msw 0.4x, as we don't see the problem with 0.3x.
@yannbf I've recreated this problem in a branch of the mealdrop app here: https://github.com/yannbf/mealdrop/compare/main...aaronmcadam:mealdrop:am-persisting-mocks
Any ideas?
Here's a screencap of the issue:
I also experienced this when mocking different graphql query responses within the same storybook file. @mmirus's solution does work. Please let us know if this issue is addressed. Thanks!
I think if we had some way of resetting handlers like we can in unit tests (with server.resetHandlers()
), that might help us make sure the story uses the correct handler.
Also experiencing this with rest handlers. Can't seem to override global handlers I defined in my preview file either.
Noticing this aswell using 1.6.3. Thankfully doesn't affect chromatic tests. For now i need to refresh the page to get the correct handlers to load for that story. Not the solution of course.
I kinda work around this by setting RTK's refetchOnMountOrArgChange
to true
. That seems to workaround the issue and swapped to named handlers not arrays.
I've run into this issue and noticing that the way initialize
works introduces a memory leak in the test environment if you reuse your stories there.
MSW documentation covers reseting handlers between tests and closing the server after each test file has completed. Storybook documentation also covers how to reuse stories in test.
By then introducing msw-storybook-handler:
For all cases, I think resetHandlers should be called between stories.
For reusing a server, perhaps initialize
could accept it as another option to set as the internal "api".
For test case, esp. where you're not reusing an existing server, documenting how to close it in test.
Ex:
// .storybook/preview.js
import {server} from '../mock/server';
export const api = initialize({api: server});
// setupFileAfterEnv.js
import {api} from './.storybook/preview';
beforeAll(() => api.listen());
afterEach(() => api.resetHandlers());
afterAll(() => api.close());
It's a little clunky, though. Would appreciate other's thoughts. Currently, I've resorted to checking the test environment in .storybook/preview.js and rewriting the mswDecorator to ensure things are cleaned up.
Currently, I've resorted to checking the test environment in .storybook/preview.js and rewriting the mswDecorator to ensure things are cleaned up.
How are you doing that?
Copy/pasting the internals of mswDecorator
, replacing the api
reference with our server.
import {server} from '../src/mock/server';
const isTest = process.env.NODE_ENV === 'test';
if (!isTest) {
initialize()
}
export const decorators = [
isTest
? (storyFn, context) => {
const {
parameters: {msw},
} = context;
if (msw) {
if (Array.isArray(msw) && msw.length > 0) {
// Support an Array of request handlers (backwards compatability).
server.use(...msw);
} else if ('handlers' in msw && msw.handlers) {
// Support an Array named request handlers
// or an Object of named request handlers with named arrays of handlers
const handlers = Object.values(msw.handlers)
.filter(Boolean)
.reduce(
(handlers, handlersList) => handlers.concat(handlersList),
[]
);
if (handlers.length > 0) {
server.use(...handlers);
}
}
}
return storyFn();
}
: mswDecorator
];
Here's something a little better I have working:
export const mswDecoratorFactory = (api) => {
const onUnmount = () => {
api.close?.() || api.stop();
api.resetHandlers();
};
return (Story, context) => {
const initialRender = useRef(true);
const {
parameters: {msw},
} = context;
if (msw && initialRender.current) {
let handlers = [];
if (Array.isArray(msw)) {
handlers = msw;
} else if ('handlers' in msw && msw.handlers) {
handlers = Object.values(msw.handlers)
.filter(Boolean)
.reduce(
(handlers, handlersList) => handlers.concat(handlersList),
[]
);
}
if (handlers.length > 0) {
api.listen?.() || api.start();
api.use(...handlers);
}
}
useEffect(() => {
initialRender.current = false;
return onUnmount;
}, []);
return <Story />;
};
};
Usage:
export const decorators = [
mswDecoratorFactory(!!process?.versions?.node ? require('path/to/server').server : require('path/to/browser').worker)
];
In my case, I'm also using @mswjs/data
for mocks, which can be used independently of msw
itself. In my decorator, I've added a drop(db)
to onUnmount
to ensure it's clean between Stories. YMMV.
I'm having the same issue as this. I tried to import all my handlers into preview.js
and then only add the handler required for each story, but that didn't work either. Like the others above, my current solution has just been to refresh.
export const marketHandler = rest.get(
`some/coin/route`,
(req, res, ctx) => {
return res(ctx.json(mockMarketPrices));
}
);
export const loadingHandler = rest.get(
`some/coin/route`,
(req, res, ctx) => {
return res(ctx.delay('infinite'));
}
);
export const allNamedHandlers = {
marketHandler,
loadingHandler
}
// preview.js
export const parameters = {
msw: {
handlers: allNamedHandlers,
}
}
export const MockedSuccess = Template.bind({});
MockedSuccess.parameters = {
msw: {
handlers: { marketHandler },
},
};
export const MockedLoading = Template.bind({});
MockedLoading.parameters = {
msw: {
handlers: { loadingHandler },
},
};
I thought I had the same issue, but it turned out that my problem was actually this one : https://github.com/mswjs/msw/issues/251#issuecomment-650200531 (I am using Apollo and the requests were cached and never re-executed)
@JohnValJohn Did you find a way to clear the cache when switching stories?
@JohnValJohn Did you find a way to clear the cache when switching stories?
edit: I just found out that the cache object has a reset method. So I ended up importing the cache object used by Apollo and just calling the reset method.
Creation of apollo client:
export const cache = new InMemoryCache();
export const client = new ApolloClient({cache});
And then in a Decorator:
cache.reset()
I had the same issue
Also suffering from this. Added the window.location.reload()
decorator as suggested which solves the problem but makes the UX of clicking around the Storybook feel glitchy and slow.
I've been watching this issue since I also implemented the workaround of reload(). It would be nice to have MSW clean up on story switches.
This really causes issues when you have autodocs enabled. The reload hack makes the Default and Loading stories work correctly but then where they load together in the docs page everything is showing the loading skeleton. If I move my non loading handler to the meta section then all stories including the loading story are using the non-loading handler. parameters in my stories don't seem to override parameters in the meta section.
Is this an issue with the addon, msw, or storybook?
By the way, this is storybook 7 / vite/ react 18 I'm using.
I replaced the addon by removing it and directly calling setupWorker in my stories file and get the same results.
Also I added a couple of fake handlers with console.log and added console.log to my handlers and one at the top of the actual component. Here is what I have and the result.
//...imports
const defaultWorker = setupWorker(
rest.get(BASE_URL, (_req, res, ctx) => {
console.log("from the default worker");
return res(ctx.json(restaurants));
})
);
const loadingWorker = setupWorker(
rest.get(BASE_URL, (_req, res, ctx) => {
console.log("from the loading worker");
return res(ctx.delay("infinite"));
})
);
const foo = () => {
console.log("foo");
};
const bar = () => {
console.log("bar");
};
const getRestaurants = () => {
defaultWorker.start();
};
const getNoResponse = () => {
loadingWorker.start();
};
const meta: Meta<typeof RestaurantsSection> = {
title: "Pages/HomePage/Components/RestaurantsSection",
component: RestaurantsSection,
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/3Q1HTCalD0lJnNvcMoEw1x/Mealdrop?type=design&node-id=135-311&t=QGU1YHR0aYc88VDn-4",
},
},
args: {
title: "Our Favorite Picks",
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
parameters: {
handlers: [getRestaurants(), foo()],
},
};
export const Loading: Story = {
parameters: {
handlers: [getNoResponse(), bar()],
},
};
On initial page load of the docs section I don't see the [MSW] Mocking enabled.
that I expect to see twice because docs loads both stories but if I go to docs and then each story and refresh the page I get the following results in devtools console.
docs result
foo
bar
[MSW] Mocking enabled.
[MSW] Mocking enabled.
from the component
from the component
from the component
from the default worker
from the loading worker
from the default worker
from the loading worker
from the component
from the default worker
from the loading worker
from the component
default story result
foo
bar
from the component
from the component
[MSW] Mocking enabled.
[MSW] Mocking enabled.
from the default worker
from the loading worker
loading story result
foo
bar
from the component
from the component
[MSW] Mocking enabled.
[MSW] Mocking enabled.
from the default worker
from the loading worker
I don't understand why both stories call both handlers. I would expect each one to overwrite the handler instead of calling both. The foo and bar handlers show it's a storybook issue don't they?
Since I'm just learning on the meal drop project, I could afford to scrap things and start over. After creating a new vite app with latest storybook 7.0.20 and the same version msw-storybook-addon and now the two stories with different handlers works with the exception of autodocs. So I can only guess something updated in storybook fixed the problem for me.
No page refresh needed
export const Default: Story = {
name: "Default Story",
parameters: {
msw: {
handlers: [
rest.get(BASE_URL, (_req, res, ctx) => res(ctx.json(restaurants))),
],
},
},
};
export const Loading: Story = {
name: "Loading Story",
parameters: {
msw: {
handlers: [
rest.get(BASE_URL, (_req, res, ctx) => res(ctx.delay("infinite"))),
],
},
},
};
The autodocs issue is that it chooses to display by the last handler by default I guess? So it shows the loading story for the components in the docs page.
Hey everyone! Thanks for all the discussions, and @aaronmcadam thanks for the reproduction.
That one reproduction specifically seems to be a bug in MSW version 0.42.3
which crashes after setting the first mock with the following error:
[MSW] Uncaught exception in the request handler for "GET https://mealdrop.netlify.app/.netlify/functions/restaurants?id=1":
TypeError: response2.headers.all is not a function
at Object.transformResponse (http://localhost:6006/node_modules/.cache/sb-vite/deps/chunk-XODS3P5D.js?v=f2eee344:23901:36)
at handleRequest (http://localhost:6006/node_modules/.cache/sb-vite/deps/chunk-XODS3P5D.js?v=f2eee344:23840:144)
at async http://localhost:6006/node_modules/.cache/sb-vite/deps/chunk-XODS3P5D.js?v=f2eee344:23851:11
That's why the mocks seem to leak, but it turns out that MSW is then broken and can't reset/set up mocks after.
I tried with later versions of MSW, including the most recent one 1.3.2
, and things seem to be working correctly.
To everyone here, can you first try a few things for me:
mswLoader
instead of mswDecorator
. Using MSW in a decorator works for most scenarios, but there's a slight chance the service worker will not get registered in time. Timing issues could potentially cause leaking. A potential solution to that is to use Storybook loaders. They get executed before a story renders, differently than decorators, which execute as the story renders. You can find instructions here, and make sure to remove mswDecorator if you are using mswLoader. This unfortunately affects users who use composeStories
, because loaders are not handled in composeStories
, contrary to decorators.Thank you so much, and sorry this has been affecting you. Whenever I tried to reproduce this issue reliably, I couldn't. If there is a proper reproduction, it will go a long way to fixing this for everyone!
@yannbf Hey, just adding to this bug - I think one of the cases where the issue most definitely occurs is when you use await delay('infinite')
from the msw
package. If you have a story with a handler that uses delay('infinite')
and then switch to a story without any delay in the handler, the new handler doesn't resolve.
I'm experiencing this issue with msw v2.2.13, so "upgrade MSW" isn't an option. Also already using mswLoader
.
@yannbf any updates here? still experiencing the same issue in the latest version of storybook + addon
if you're using react-query
you can do something like this:
const Providers = ({ children }: { children: React.ReactNode }) => (
<ReactQueryProvider queryClient={queryClient}>
{children}
</ReactQueryProvider>
);
const ResetQueries = () => {
const queryClient = useQueryClient();
useEffect(() => {
return () => {
queryClient.resetQueries();
};
}, []);
return null;
};
const decorators: Preview['decorators'] = [
(Story) => (
<Providers>
<Story />
<ResetQueries />
</Providers>
),
];
I was having this issue and just got it fixed — in my case it was a misconfiguration on my part. I was doing something like this:
export const loaders = [
async (ctx) => {
initialize();
return mswLoader(ctx);
},
];
After quite a bit of debugging I found out that because loaders run once per story, that was reinitiliazing the MSW worker without cleaning up the previous one, which caused both the old and the new requests handlers to run, and the older one to "win". The fix was to do just like the docs recommend:
initialize();
export const loaders = [mswLoader];
I thought I had the same issue, but it turned out that my problem was actually this one : mswjs/msw#251 (comment) (I am using Apollo and the requests were cached and never re-executed)
Same for me, but I am using SWR. To solve the issue I used the following decorator:
import React from 'react';
import { SWRConfig, mutate } from 'swr';
export const invalidateSWRCache = (Story, { parameters }) => {
if (parameters.invalidateSWRCache) {
mutate(() => true, undefined, { revalidate: false });
return (
<SWRConfig
value={{
dedupingInterval: 0,
revalidateOnFocus: false,
revalidateOnMount: true,
revalidateIfStale: false,
}}
>
<Story />
</SWRConfig>
);
}
return <Story />;
};
then in your story add the invalidateSWRCache: true
parameter. It will make SWR fetch fresh results every time.
I also faced the delay('infinite')
issue, when opening the story for the first time, the request is fired normally, but after you switch to another story and back, the request is not fired again. This was important for testing my component.
To solve this, in association with the solution above, and also with using different ids for the different requests (to create different URLs), I implemented this arguably hacky solution:
import type { Meta, StoryObj } from '@storybook/react';
import { http, HttpResponse } from 'msw';
import { useEffect } from 'react';
import { MyComponent } from './MyComponent';
const InfinitePromise = (returnVal: HttpResponse) => {
let resolve = () => {};
const promise = new Promise<HttpResponse>(res => {
resolve = () => {
res(returnVal);
};
});
return {
promise,
resolve,
};
};
let infinitePromise = InfinitePromise(HttpResponse.json({}));
const meta: Meta<typeof MyComponent> = {
parameters: {
invalidateSWRCache: true,
},
component: MyComponent,
};
export default meta;
type Story = StoryObj<typeof meta>;
export const RendersTheComponent: Story = {
args: {
thingyId: 'some_id',
},
parameters: {
msw: {
handlers: {
getThingy: http.get(`https://example.com/some_id`, () => {
return HttpResponse.json({ thingy_id: 'some_id' });
}),
},
},
},
};
const ResolveOnUnmount = Story => {
useEffect(() => {
return () => {
infinitePromise.resolve();
};
}, []);
return <Story />;
};
export const ShowsLoadingState: Story = {
args: {
thingyId: 'some_other_id',
},
parameters: {
msw: {
handlers: {
getThingy: http.get(`https://example.com/some_other_id`, async () => {
infinitePromise = InfinitePromise(
HttpResponse.json({ thingy_id: 'some_other_id' })
);
return await infinitePromise.promise;
}),
},
},
},
decorators: [ResolveOnUnmount],
};
It won't work if the story you are switching to fires the exact same request to the exact same URL. I assume it's because SWR won't issue another request to the same URL if the previous one is still ongoing, which is of course what you'd expect in this situation.
Hi there! Thanks for the add-on!
I'm running into a problem with a story that demos a loading state:
ctx.delay('infinite')
. This story works great!Any help debugging / solving this would be appreciated! Please let me know if additional info would be helpful.
It's a bit hard to debug this / pinpoint what part of the stack may be responsible for not ending the pending request and initiating a new one when switching from the Loading story to the next one. Apologies if I'm barking up the wrong tree.
In the meanwhile, since refreshing the browser tab on the second (non-loading) story allows that story to load correctly, I've worked around this by using a decorator that reloads the iframe when leaving the Loading story. This is the only workaround I've found that helps.