๐บ๏ธ Storybook + Testing Helper #2481
Replies: 26 comments 45 replies
-
I have nothing to add to this. It's well thought through and I think it solves all cases of which I'm aware. I agree that duck punching The trickiest bit of all of this will be helping people know when to use E2E and when to use I'm a big fan of this proposal ๐ Would love to hear what others have to say. |
Beta Was this translation helpful? Give feedback.
-
This stub proposal is intended to simulate multiple routes and sub-routes to simulate integration tests along with navigations between pages? Or Remix should explicitly opinionate on e2e tests for this use case? |
Beta Was this translation helpful? Give feedback.
-
A bit off topic but related, i'm curious about how you would distribute your effort between Storybook/Cypress Testing Library and E2E tests to ensure high quality UI. |
Beta Was this translation helpful? Give feedback.
-
Any updates on this proposal, it would allow a Remix Stack with preconfigured Storybook's... |
Beta Was this translation helpful? Give feedback.
-
Hiya, friends! I had the privilege of a chatting with @ryanflorence @shilman and @yannbf today on how we might integrate Remix components and Storybook. I've mentioned them here to keep me honest โ as I'm not super familiar with all the working parts. GoalRemix and Storybook have very similar goals:
Storybook takes a component-driven approach to visual integration testing. Cypress is great at testing e2e feature flows. But we want to get as much visual integration testing juice out of components as we can. The discussion we had today was about connecting Remix components โ in Storybook โ in a way that excludes the server. PrerequisitesRemix has a project in flight to migrate some functionality back to React Router. In order to properly mock requests, Ryan estimates that this work might be done around August/September. ErgonomicsRyan whipped up a quick demonstration using Storybook parameters to to demonstrate a dynamic way to set up the component. Whatever.paramaters = {
remix: (setup) => {
// let scopedJunk = {}
return setup({
Routes: [{
Actions: () => {
scopedJunk.foo = โlolโ
}
}]โจ
})
}
} SerializationWhile dynamic parameters are allowable (in this way), the channel architecture between Storybook canvas and manager works best when all parameters are serializable. TypeScriptThere was talk of ways to reduce boilerplate while keeping code statically analyzable. DecoratorsAn eventual integration addon will likely use Storybook Decorators to provide ergonomics around repeated code.
|
Beta Was this translation helpful? Give feedback.
-
I really love this proposal and wanted to add that E2E tests - even with modern tools such as Cypress - are very flaky. With enough code and enough UI complexity, eventually, all functional tests suites approach 100% false positive failure rate and must be automatically retriggered to prevent false positives from interfering with development process. The stability improvements through more unit tests that this proposal would bring would be tremendously helpful. Also I don't like that the current example has shared state, for reasons that @kentcdodds has written about a lot. Maybe there is a way to create this API without mutating |
Beta Was this translation helpful? Give feedback.
-
When it comes time to implementing this, please consider tools other than Storybook as well, such as Preview.js, Ladle and so on (disclaimer I'm the author of Preview.js). Luckily the current proposal looks like it'd be general enough to work in this context as well. Looking forward to seeing it come about! |
Beta Was this translation helpful? Give feedback.
-
Yeah to me the main use-case is to develop components and pages. Don't care about the testing aspect. But it would allow me to develop my react components with hot reloading (which remix does not offer). |
Beta Was this translation helpful? Give feedback.
-
Hey, just a thing that has not been mentioned about testing: helpers you design for Storybook will indeed work in other context or even other competing tools (Preview, Ladle as mentioned above). However the best mix between SB and testing happens when you are able to load Stories within your unit test, thanks to @storybook/testing-react. My workflow is usually like this:
It would be even better if Testing Playground (https://chrome.google.com/webstore/detail/testing-playground/hejbmebodbijjdhflfknehhcgaklhano) worked with Storybook, because you would be able to compute accessible selectors from Storybook. The iframe prevents that currently but maybe in the future. So to sum it up, for testing, you might want to focus in importing your mocked story within Vitest and see if it works. Other random insights:
|
Beta Was this translation helpful? Give feedback.
-
What about providing a context with all the utilities that can/should be mocked? I might be missing something but that is what I do as a workaround for the moment. I'll take the // fallback implementation of remix's <Link />
function Anchor({ to, children, ...rest }: LinkProps) {
return (
<a href={`#${to}`} {...rest}>
{children}
</a>
);
}
const RemixContext = createContext<RemixContextValue>({
Link: Anchor,
});
export function useRemix() {
return useContext(RemixContext)
} Now within my components I can do the following: function ComponentWithLink() {
const { Link } = useRemix()
return <Link to="/hi">Hi</Link>
} And by default a simple anchor will be used. Now, since I need to make use of the remix's implementation, I create a provider: export function RemixContextProvider({ children }: PropsWithChildren<{}>) {
return (
<RemixContext.Provider value={{ Link: remix.Link }}>
{children}
</RemixContext.Provider>
);
} that I use to wrap the Yet, I do not know how good of a practice is to have a hook returning components, but it makes mocking way easier. |
Beta Was this translation helpful? Give feedback.
-
Any updates on this? Good temporary workarounds? I've gotten very used to using storybook for component dev and would hate to sacrifice that with Remix. |
Beta Was this translation helpful? Give feedback.
-
Nuxt.js is apparently facing a similar issue. It might be worth to have a peek at a potential solution that the team over there came up with. Not sure it would fit requirement of |
Beta Was this translation helpful? Give feedback.
-
Curious how the new (awesome) release of React Router affects this discussion? |
Beta Was this translation helpful? Give feedback.
-
I really needed this for a design system library with basic Form and Input components with validation that would fail to render if not rendered in a Remix route. If you have similar basic needs then this temporary workaround might work for you also. It uses a storybook decorator to wrap all stories in a RemixBrowser component that is initialized with a single splat route that matches all urls and renders the story. // .storybook/preview.tsx
import { RemixStub } from "./RemixStub";
export const decorators = [
(Story) => (
<RemixStub>
<Story />
</RemixStub>
),
]; // RemixStub.tsx
import { RemixBrowser } from "@remix-run/react";
export function RemixStub({ children }) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const win = window as any;
win.__remixManifest = {
routes: {
"routes/$": {
id: "routes/$",
path: "*",
},
},
};
win.__remixRouteModules = {
"routes/$": {
default: () => children,
},
};
win.__remixContext = {
appState: {},
matches: [],
routeData: {},
};
return <RemixBrowser />;
} |
Beta Was this translation helpful? Give feedback.
-
What is the official solution until this proposal lands? I've started creating stories with Ladle and the first component that uses Form/Link started throwing errors that can't be fixed trivially. I'm looking at ways to use Remix internals to create a context that provide the bare necessities but I'm not sure if it's even possible without ugly hacks or at all. |
Beta Was this translation helpful? Give feedback.
-
Has anyone been able to get the RemixStub working with an |
Beta Was this translation helpful? Give feedback.
-
Pro tip: win.__remixContext = {
routeData: {
"routes/$": { locale: "en-US" },
},
// the restโฆ
} will let you use test components that use |
Beta Was this translation helpful? Give feedback.
-
Sadly the RemixStub workaround is failing in 7.0alpha38 using vite-builder :-( [1] 12:00:56 PM [vite] Internal server error: Failed to parse source for import analysis because the content contains invalid JS syntax. If you are using JSX, make sure to name the file with the .jsx or .tsx extension.
[1] Plugin: vite:import-analysis
[1] File: /Users/fergus/proj/test/.storybook/preview.js
[1] 17 | <RemixStub>
[1] 18 | <Story />
[1] 19 | </RemixStub>
[1] | ^
[1] 20 | ),
[1] 21 | ]; |
Beta Was this translation helpful? Give feedback.
-
I've created an example Storybook + Testing Helper implementation based on Ryan's https://github.com/jrestall/remix-stubs Please raise issues and/or contribute fixes and improvements. |
Beta Was this translation helpful? Give feedback.
-
@jrestall I encounter following error when using with following story: let story: Meta<typeof TopProductsColumn> = {
title: 'Top Products Column',
component: TopProductsColumn,
decorators: [
(Story) => {
const [args, updateArgs] = useArgs();
const RemixStub = createRemixStub([
{
element: (
<RemixMockSetup>
<Story />
</RemixMockSetup>
),
path: 'product/1234',
loader: () => { return null },
action: async ({ request }) => {
return { top_products: [], super_deal_products: [] };
},
}
]);
return (<RemixStub initialEntries={["/post/1234"]} />);
}
],
}; Where |
Beta Was this translation helpful? Give feedback.
-
Since this is in active development right now I just wanted to say how much I want to be able to develop components in Storybook and also how I use it. So I use storybook exclusively for building and testing UIs. That means I want any component at any level to potentially be openable to work on in Storybook and I also want the entire UX of that component to be disabled. In other words as cool and useful as mocking like createRemixStub is I don't as a user want to mock anything! I don't want a link to work, or a form to submit or a page to navigate or anything else like that. If I'm working on that side of things I'll do it in Remix. So in an ideal world from my POV I'd like the Storybook env to mock every single Remix function to be like Like this is an example of an error I get when trying to work on a component that uses Remix-Validated-Form: invariant@http://localhost:6006/node_modules/.cache/.vite-storybook/deps/chunk-U2MEYXUE.js?v=c891f1a6:169:11
useRemixRouteContext@http://localhost:6006/node_modules/.cache/.vite-storybook/deps/chunk-U2MEYXUE.js?v=c891f1a6:1649:12
useActionData@http://localhost:6006/node_modules/.cache/.vite-storybook/deps/chunk-U2MEYXUE.js?v=c891f1a6:2333:7
useErrorResponseForForm@http://localhost:6006/node_modules/.cache/.vite-storybook/deps/chunk-YOKLCYJ4.js?v=c891f1a6:1387:22
ValidatedForm@http://localhost:6006/node_modules/.cache/.vite-storybook/deps/chunk-YOKLCYJ4.js?v=c891f1a6:1834:47
renderWithHooks@http://localhost:6006/node_modules/.cache/.vite-storybook/deps/chunk-HAKVT6QR.js?v=c891f1a6:11763:35
... etc From which I guess that is expecting some Remix APIs to be available and when they are not it throws. That's cool I totally understand but my point is that I don't want to use |
Beta Was this translation helpful? Give feedback.
-
We've just started using this and it's super exciting to be able to render Remix routes within Storybook. After digging into I worry that without using generic typing to bridge the gap between the real routes and the stubbed routes, that the Storybook stories will get out of sync very quickly. These errors would only be caught at PR time, whereas the error could be caught by the type checker. |
Beta Was this translation helpful? Give feedback.
-
Is anyone still watching this discussions? I have submitted a question but have not reached a resolution. All I want to achieve is to display a UI component in Storybook that uses the Link component in Thanks. |
Beta Was this translation helpful? Give feedback.
-
Hi @ryanflorence , I read your proposal and I have a perhaps stupid question regarding your Tests example (in your initial post). |
Beta Was this translation helpful? Give feedback.
-
I've managed to set up I have two big questions so far:
|
Beta Was this translation helpful? Give feedback.
-
We need a way to stub the context of Remix for both unit tests and storybook.
Problem
It's common for a reusable component to render a
<Link>
inside of it or access something likeuseLoaderData
. AlsouseFetcher
is so flexible it will likely make it into a lot of shared components as well.All of these components need the Remix context which includes a lot of things, but the two biggest problems are:
The manifest is generated at build time, and requests need a running server. Developers don't want either of those things for story book or component unit tests.
That simple code relies on a lot of Remix internals. We need to a way to send it down a bunch of render paths for tests or storybook without the tests or the developer knowing a bunch of internals or having a server running.
Proposal
We need a way to easily stub out the context of a fake Remix app that doesn't require app developers (and their tests) to know the shape of Remix internals, or require a running server. It also needs to go through as many actual Remix code paths as possible. In React Router it's easy because you can just render a
<MemoryRouter>
around your component and be done. We need something like that for Remix. I think we can do it with a function like this:The only configuration I can think of right now is
routes
but I'm leaving the API open to needing more.Storybook
The story could look something like this:
Of course, today you could refactor your like button to just take a bunch of non-remix props to render in different states. That's a bit cumbersome to do, requires more development effort to design components that way (not to mention the component above that access Remix hooks still can't be tested well), but most importantly for a UI designer--it won't let you "feel" the way the button moves through loading states. That, to me, is the big benefit of using story book in the first place.
Additionally, it's easy to imagine that for a "component library" team they might just make a single
RemixStub
for an entire "Storybook version of the app". None of the stories even need to create routes or anything. Very easily abstract-able to keep the stories clean and feeling very "component" oriented.Tests
I don't think tests really need this API, it might just be more convenient. You can use jest/vitest mocks today to send your code down different rendering paths in unit tests:
This works out great, and works today just fine. There are some risks though:
<button name="likeed">
the tests would pass but the component is broken.useFetcher
incorrectly. TypeScript helps here but it's still a risk and most tests w/ mocks have a lot of type casting that will potentially lie as well.With
createRemixStub
you can have some unit tests on this button without those risks and without needing to do a full blown e2e test with your server and database:The async portions of these tests could be implemented with deferreds, where actions and loaders could be manually triggered to resolve, but that doesn't change anything about this proposal, but is up to the preference of the developer writing the test.
In some cases this might be better, though the test needs to know the order Remix is going to call action and then loaders on the routes, a bit of implementation detail there, but not too much I don't think.
Some tests might be better served with mocking, others with creating a remix stub.
Implementation Notes
Implementing
createRemixStub
might require some changes to Remix React to make thefetch
implementation pluggable (so it doesn't actually try to fetch stuff from the server), or we could duck punchwindow.fetch
to fake out responses to URLs we know are trying to access to the Remix server and letting everything else go through to the realwindow.fetch
. I think I'd got with duck punching window.fetch.It also does dynamic imports of route modules, so we probably need to plug in a different
routeModulesCache
. I think that's already pluggable (since server/client modules are resolved differently) so it should be as simple as making a route modules cache that's a no-op for testing.Then we'd need to just make up some assets based on the routes for the asset manifest and stub out everything else on context.
Off the top of my head, that should be it. I don't think it'd be too difficult since the multiple server/client render targets of Remix already made a lot of these pieces pluggable. Testing is just another target.
Beta Was this translation helpful? Give feedback.
All reactions