-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature Request]: Create pre play functionality #21252
Comments
It sounds like something that can possibly be achieved by mocking imports? Assuming that you are importing |
We're planning to revisit both of these areas (adding more test hooks + import mocking) in 7.x, so this is a timely request. cc @yannbf @kasperpeulen @tmeasday |
I'd assume this is not a solution, because the problem (or one of the problems) is that I can't use React hooks inside the
Sounds great, thanks! Maybe to give this a different spin: Testing Library recommends to use a custom Maybe the API would look more like this? export const Example: Story = {
// customRender could be globally shared across all stories
customRender: (story) => {
const axiosInstance = createAxiosInstance();
// add some Jest spies here on axiosInstance
// maybe storybook exposes a "renderStory" function that is already a custom render function, but can be further wrapped
const result = renderStory(<AxiosInstance value={axiosInstance}>{story}</AxiosInstance>);
return {
canvas: within(canvasElement), // maybe we can also safe us some time to do this in every `play` function
axiosInstance,
...result
}
},
play: async ({ canvas, axiosInstance, ...rest }) => {
await expect(
await canvas.findByText('Lorem')
).toBeInTheDocument();
// run expects here on axiosInstance spies
},
}; |
@donaldpipowitch I have had similar problems with testing in Storybook. I don't think we have anything recommended, but there is this hack around it. const meta = {
title: 'Example/Page',
component: Page,
decorators: [
(Story, context) => {
const location = useLocation();
context.args.onLocationChanged(location);
return <Story />;
},
(Story) => {
return (
<MemoryRouter initialEntries={['/page1']}>
<Routes>
<Route path={'page1'} element={<Story />}></Route>
</Routes>
</MemoryRouter>
);
},
],
argTypes: {
onLocationChanged: { action: true },
},
} satisfies Meta<ComponentProps<typeof Page> & { onLocationChanged(location: Location): void }>;
export default meta;
type Story = StoryObj<typeof meta>;
export const LoggedIn: Story = {
play: async (context) => {
const canvas = within(context.canvasElement);
const args = context.args as jest.Mocked<typeof context.args>;
const [location] = args.onLocationChanged.mock.lastCall!;
console.log(location);
const link = await canvas.findByRole('link', { name: /some link/i });
await userEvent.click(link);
await waitFor(() => args.onLocationChanged.mock.calls.length > 1);
const [location2] = args.onLocationChanged.mock.lastCall!;
console.log(location2);
},
}; I thought we could also inject context and custom args in decorators, but for some reason I can not get it to work. decorators: [
(Story, context) => {
const location = useLocation();
return <Story location={location} args={{location}} />;
}, |
Thank you @kasperpeulen ! |
@kasperpeulen I would have expected what you did (or something like it) to work. There are certain context fields you aren't allowed to update from decorators, but storybook/code/lib/preview-api/src/modules/store/decorators.ts Lines 23 to 43 in 7aecf57
|
I think yes @kasperpeulen. Thinking about it, it might be tricky to fix this however. We could look at it together sometime. |
@tmeasday, @kasperpeulen did you do something about discrepancy of context in |
Not yet @sjwilczynski! We talked around it but I think this issue escaped our attention. @kasperpeulen let's find some time to go over it when you are back! |
I don't even know if it's possible @tmeasday, given that we would need to somehow "prerender" the story in order for decorators to run, and only then get the "final" context into the play function. This is where we define the play function: |
@yannbf it is a bit tricky. Right above that code we wrap the I think what we want is the One option would be to make the Another is to make the Also it might make the portable stories use case more difficult as they can no longer just call |
Interestingly I just realized that even tough assigning property to a context doesn't make it available in //decorator code
makeDecorator({
name: "withNovaEnvironment",
parameterName: "novaEnvironment",
wrapper: (getStory, context, settings) => {
const environment = createNovaEnvironment();
// code omitted for brevity
context.parameters.environment = environment;
return (
<NovaMockEnvironmentProvider environment={environment}>
{getStory(context)}
</NovaMockEnvironmentProvider>
);
},
}); // inside play function play: async (context) => {
// this is properly defined
console.log(context.parameters.environment);
}, @yannbf, @tmeasday do you think we could safely depend on this behavior? |
Oh, I don't think I would count on that sticking around forever, we could definitely break it without considering it a breaking change. Having said that, it's probably not something that is likely to stop working any time soon. |
This is where the context is shallow-copied before being passed into decorator, while the I wonder why the context was shallow-copied in the first place. Is it because the very first decorator's context object is a React prop thus it's not extensible? If that is the case, can we actually shallow copy at the beginning and stick to the copied context object in both decorators and play function? |
@nandin-borjigin we'll have to think about that. Generally speaking the SB codebase assumes objects are immutable and thus generally cloned rather than having things added to them as they are passed around. |
Hey everyone! It's been some time, but I'm here with some news. In Storybook 8, we introduced a new way of writing the play function (check the RFC) which should give you full flexibility on what you want to do. export const Example: Story = {
play: async ({ canvas, mount }) => {
// Do anything before mounting
const someAsyncData = await fetchData()
// Mount your story however you want
await mount(<YourComponent data={someAsyncData} />)
// write your test
const canvas = within(canvasElement);
await expect(await canvas.findByText('Lorem')).toBeInTheDocument();
expect(setup.location.pathname).toBe('/example/users');
},
}; The other thing is that loaders can now mutate the story context, which can be useful for some use cases mentioned here. export const Example: Story = {
loaders: async (context) => { // loaders are executed before the component renders
context.location = useLocation();
},
play: async ({ canvas, location }) => { // location is now available in the context of play function (as well as other places like decorators)
await expect(
await canvas.findByText('Lorem')
).toBeInTheDocument();
await expect(setup.location.pathname).toBe('/example/users');
},
}; |
Is your feature request related to a problem? Please describe
I'd like to pass data to my play functions so I can run
expect
's on it. For example all my Stories use independent Axios instances I could get via a customuseAxiosInstance
hook or I'd like to checklocation
from React RoutersuseLocation
.Describe the solution you'd like
It would be amazing to use hooks in
play
functions. As I don't think this will be possible/useful/expected I could imagine something similar could be done with a "pre play functionality" where React hooks are allowed and where the returned values are passed toplay
.Describe alternatives you've considered
Allow hooks in
play
directly or somehow access decorators insideplay
where I could maybe grab values from.I'm just wondering... what if
MemoryRouter
is not defined inmeta.decorators
but as part ofmeta.component
? Maybe the control should be "inversed" and my component could pass data toplay
. Something like this:The only problem would be:
play
can't really know ifusePlaySetup
will be called. This would have to be synchronous or that would need to be a "flag" that indicatesplay
should only be run whenusePlaySetup
was called. Some "deferred" play option...Are you able to assist to bring the feature to reality?
yes, I can
Additional context
No response
The text was updated successfully, but these errors were encountered: