[RFC] Allow play to mount the component of the story #27389
Replies: 1 comment
-
The
A third option could be passing a function that receives the original Story’s UI, similar to how decorator functions work. Here’s an example: // ...
play: async ({ mount }) => {
const onChangeSpy = fn();
// we pass a function here that receives the original Story's UI.
await mount((Story) => (
<Combobox.Root onChange={onChangeSpy}>
<Story />
</Combobox.Root>
));
},
// ... As you can see in the example above this would make it possible to easily reuse the UI without having to repeat ourselves and assert on values defined in our test scenario. This is particularly useful for stories of components that are composed out of multiple components, like headless components that are structured like so: <Combobox.Root>
<Combobox.Input />
<Combobox.Options>
<Combobox.Option {...}>
<Combobox.Option {...}>
</Combobox.Options>
</Combobox.Root The example Curious to hear what you think of this idea. If I should add this suggestion somewhere else, please let me know. |
Beta Was this translation helpful? Give feedback.
-
Mount in play itself
In our effort to make Storybook as suitable as possible for UI unit testing, we started to realise that the current
play
semantics might be too constraining.At the moment you might write a play function like this:
Now, let’s imagine that a user could instead write it like this:
This could be seen as strictly better for several reasons:
const canvas = await mount()
is actually also a bit shorter thanconst canvas = within(canvasElement)
How to avoid making this a breaking change?
Introducing this new semantic to the play function (being able to
mount
itself), while not breaking existing users, is not easy, but we think possible.We need to detect if the user wants to mount, so that storybook doesn’t mount before the
play
function starts. For that, we can use a similar trick that playwright and Vitest use in their fixture API:https://playwright.dev/docs/test-fixtures
Both testing frameworks analyse the dependencies of a fixture or a test function by inspecting which arguments are destructured by inspecting
fn.toString()
.For example, in the above example,
fn.toString()
prints the followingThis is a bit magical, but this allows us to introduce this change, with the following caveat:
In the future we might introduce fixtures ourselves as well, which would make this caveat possibly less controversial.
One downside of this is that it makes it harder to reuse play functions. We want to solve this by making the whole context itself also available on the context (self-referencing).
Comparison with a testing-library based unit test
One benefit of this change would be that it is super straight-forward to move a testing-library+ JSDOM based unit test to storybook.
In general, the semantics of the
play
function now gives you full control of the whole test execution.We note that the refactoring in the third example would give user many of the benefits of using Storybook:
But it is optional. User can also get lots of benefits from Storybook if they don’t do the refactor:
Allows for fewer hooks to be used for a single test
Some people might prefer doing everything in
play
in favor of using a separate CSF construct such asargs
,loaders
,beforeEach
,decorators
, andrender
.I think the strongest point of this proposal is that users don’t necessarily need any of those. You can still use them and they are powerful abstraction mechanisms that are great, but the user is not forced.
For example, instead of using
beforeEach
, you can just do:If you have a story that needs async data, you don’t need to learn about the loaders API:
Not using any abstraction here, is shorter, more typesafe and easier to read:
Reuse the play function for different stories
That being said, we think that there are still many use cases, where the inheritance model of CSF makes a lot of sense. For example, you might want to set up a
play
function in your meta, and use that with a bunch of different UI.The API of mount
So what should the exact API of this
mount
function look like?Arguments of mount
To make the API as familiar as possible and help people porting testing-library stories to storybook, we will make our
mount
API compatible with testing-libraryrender
function:For some renderers, testing-library has some extra features that we don’t plan to directly support. However, we can support a subset of those features of testing-library that easily map to properties that are already available, and can be returned from our existing
render
function.Whatever is passed to
mount
will still be wrapped by storybook decorators. In some sense it is providing (or overriding) what you can now return fromrender
in CSF with a slightly different syntax for some renderers.Return type of mount
The return type of
await mount()
in testing-library iswithin(canvasElement)
+ some extra utils such asunmount
andrerender
.But since
testing-library/dom
is heavy-dependency, we don’t want to always include it in storybook by default.Therefore if
@storybook/test
is installed, the return type ofmount
will bewithin(canvasElement)
and otherwise just bevoid
. We have to investigate if this is possible with typescript.We don’t plan to also directly support the extra utilities that testing-library returns (such as
unmount
andrerender
).How does this work with portable stories
Supporting a subset of the testing-library API in storybook, also unlocks an interesting opportunity for portable stories.
In storybook, you can use the
composeStories
API to reuse stories to run as a unit test:However, this doesn’t run
beforeEach
and theplay
function that can be defined in a story. So we want to add the following API:If the
mount
API supports a subset of the testing-library features, this assignment just works, without any mapping. Of course, the user could also use another rendering library, which would mean it needs to do some manual mapping.The user can set the
mount
definition once in portable stories usingsetPreviewAnnotations
:Configure mount
We could also allow people to configure
mount
themself in CSF, even outside of a portable story context.Configure it with testing-library
For example we could allow someone to configure it using testing-library. This would allow users to use all the features of testing-library, and not only the subset that we can easily support ourself.
Especially with community renderers, but even for a renderer as angular, it would make it a lot easier to get feature parity with testing-library.
It is not completely straight-forward how to setup
mount
in storybook, while maintaining all storybook features, especially if it comes to reactivity. But it would probably look something like this:We could make this a recipe, or export some utilities to easily configure storybook to use
testing-library
for mounting.Instead of this, we could also make a more general purpose API for users to add extra properties on the story context. And allow people to add or override existing context properties based on other context properties.
The playwright fixture API is doing this exactly:
https://playwright.dev/docs/test-fixtures
It has a couple of interesting features:
beforeEach
/afterEach
)Configure it with another renderer
This feature was also requested before by Justin Fagnani:
https://x.com/justinfagnani/status/1542558568737452032
#20357
As there are many web-component flavours, and we now only support the LIT flavour. This would allow our web component framework be independent of Lit.
Another reason to support this feature, is that you may have multiple renderers in one storybook. Especially if you use web-components, it could be that you use Next/Sveltekit as a metaframework, but still use web components for your design system.
Beta Was this translation helpful? Give feedback.
All reactions