found
found copied to clipboard
Support react-storybook?
not sure what this issue is about...react storybook should work with any react component.
the Link component doesn't work in react storybook.
unless you have some information that would help someone solve a bug here there isn't much we can do. I don't think anyone has any interest in making a storybook plugin, and without any details it's hard to think there might be a bug in the library
Is the idea that you want to be able to render a (non-functional) link outside of a full fledged router?
Yea, sorry, just trying to ask if there any quick solutions before jumping into creating another layer for storybook like storybook-router
Ideally we’d just want that here. It’s sort of the same thing as https://github.com/4Catalyzer/found/issues/136
BTW if you actually want navigation, you can use MemoryProtocol in Farce v0.2.4.
I'm going to close this out in favor of #136 for the "stub router object" case, as the solution ought to be the same – and in the event you want some sort of mock integration, MemoryProtocol or ServerProtocol should both be good.
I'm still not 100% clear on how to solve the following 2 challenges when it comes to use within storybook:
found/lib/Linkis not available- Rendering of an HOC using
withRouter
Surely this is simple enough to mock when inside a Jest test, as discussed in #136, but when implementing a storybook a decorator would be required.
The idea is that it's the same as in #136. Either use some sort of mock/stub, or render everything under a server router or memory router.
Thanks @taion. For the those devs like me looking for a snippet to make this work here is what a simple storybook implementation would look like
import React from 'react'
import { storiesOf } from '@storybook/react'
import MyComponent from ‘.’
import createFarceRouter from 'found/lib/createFarceRouter'
import createRender from 'found/lib/createRender'
import MemoryProtocol from 'farce/lib/MemoryProtocol'
import resolver from 'found/lib/resolver'
const StoryRouter = createFarceRouter({
historyProtocol: new MemoryProtocol('/'),
routeConfig: [{ path: '/', Component: MyComponent }],
render: createRender({})
})
storiesOf('Header', module).add('default', () => <StoryRouter resolver={resolver} />)
This doesn't give much customisation in terms of locations, routing and history. For this reason I've been working on a simple Storybook decorator to allow matching and navigation. I will be publishing it soon, so please don't close this ticket for now so that I can post it here for the reference of anyone looking into this in the future.
I've just published storybook-found-router a decorator for React applications using Found routing.
It can simply be installed using npm install storybook-found-router -D
@taion it would be great if you could take a look at it since obviously you have the best knowledge on Found and can advice if you see any issue with the implementation of the found instances I'm using.
If you find this helpful it might be useful to mention about this in the your readme file (possibly in the extensions paragraph?)
Apologies – I've been quite busy lately. I'll try to look at this on Monday.
Sorry again, I keep forgetting about this. I'm going to re-open this to keep track of it.
@santino I've just installed your storybook-found-router and can confirm it works out-of-the-box.
I'm migrating an app from React Router v3 to Found Relay and my stories of components rendering Buttons based on Link got broken because of Connect(BaseLink) requiring a store.
Luckily I found your lib, installed it and so far it:
- [x] works in stories
- [x] works in Storyshots
Thx a lot
Just for the record, one drawback of using found-relay I just discovered is that I can't use relay-mock-ntework-layer directly because my QueryRenderer is now in the router.
I need to figure out how to address this.
Wait, why can't you? You still set up your own network layer with Found Relay.
See e.g. https://github.com/taion/relay-todomvc/pull/28.
Wait, why can't you? You still set up your own network layer with Found Relay.
I mean I can't use it directly like passing the environment directly to the component I'm writing the story for. I need to setup a router in the story.
Hello again.
After 2 hours 34 minutes and 34 seconds I came up with a solution for my found-relay Storybook setup.
// file: PersonForm.story.js
/* global module */
import React from "react";
import { Resolver } from "found-relay";
import createFarceRouter from "found/lib/createFarceRouter";
import createRender from "found/lib/createRender";
import RedirectException from "found/lib/RedirectException";
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from "@storybook/react";
import MemoryProtocol from "farce/lib/MemoryProtocol";
/* eslint-enable */
import routeConfig from "app/routes";
import getEnvironment from "../../test/support/relay-environment-mock";
// import { person /*, personMutationErrors */ } from "../../test/support/data";
// import { person } from "../../test/support/data";
const environment = getEnvironment({});
const StoryRouter = createFarceRouter({
routeConfig,
historyProtocol: new MemoryProtocol("/"),
render: createRender({})
});
const relayResolver = new Resolver(environment);
// Redirects to path when rendering the Router
const storyResolver = path => ({
resolveElements(match) {
const { location } = match;
if (location.pathname !== path) {
throw new RedirectException(path);
}
return relayResolver.resolveElements(match);
}
});
storiesOf("people/PersonForm", module)
.add("creating new Person", () => (
<StoryRouter resolver={storyResolver("/people/new")} />
));
The most difficult part was to find out how to render an already created StoryRouter on the path of the corresponding Component. I tried some story components withRouter and other stuff but none of them worked so I decided to investigate the resolver prop. Once I got there I was trying to yield router.replace(path) with errors but after further investigating on generators I discovered that you can stop the iteration by throwing an exception and that's how I can render the Router in an specific route.
After this experience, now that I have to test my Relay based components through the router, with this setup I can say I expanded my test coverage. Now I'm also testing my routeConfig with my Storyshots tests. Before this experience, I didn't have any test for my UI routing.
The only caveat is that you have to install babel-plugin-dynamic-import-node to run your Storyshots tests if you're using import(...) - I'm using Jest by the way.
I'm sorry you had to spend so much time on this.
Why do you need to trigger the redirect rather than just creating the MemoryProtocol with the path you want?
Yeah, but so far I think it wasn't a waste of time 😅
I trigger the redirect to avoid creating a new Router each time I need to render a story, also avoiding creating a higher-order component inside a render method.
Otherwise, if I'm not wrong, I would have to do something like the following for each story:
const StoryRouter = createFarceRouter({
routeConfig,
historyProtocol: new MemoryProtocol(path),
render: createRender({})
});
By the way, thx for the Found router @taion . It works great so far. The only thing a little bit awkward is when googling something about it ... you know Found is not a typical keyword for searches 😆
Again just for the record, using the same resolver technique I easily and finally added a StoryShot for error handling at the Router.
// file: stories/routes/HttpError.story.jsx
/* global module */
import React from "react";
import createFarceRouter from "found/lib/createFarceRouter";
import createRender from "found/lib/createRender";
import HttpError from "found/lib/HttpError";
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from "@storybook/react";
import MemoryProtocol from "farce/lib/MemoryProtocol";
/* eslint-enable */
import routeConfig, { renderError } from "app/routes";
const StoryRouter = createFarceRouter({
routeConfig,
historyProtocol: new MemoryProtocol("/"),
render: createRender({ renderError })
});
storiesOf("routes/HttpError", module).add("401", () => (
<StoryRouter
resolver={{
resolveElements() {
throw new HttpError(401);
}
}}
/>
));
This is actually getting better 😃
I've just stumbled with an essential detail. In order to achieve reproducible snapshots, using the MemoryProtocol, we need to mock Math.random before creating the router. For example:
// NOTICE:
// We need to mock Math because MemoryProtocol uses a random _keyPrefix for the routes
const realMath = global.Math;
const mockMath = Object.create(global.Math);
mockMath.random = () => 0.5;
global.Math = mockMath;
const StoryRouter = createFarceRouter({
routeConfig,
historyProtocol: new MemoryProtocol("/"),
render: createRender({})
});
// Put back the realMath (don't know if this is necessary though)
global.Math = realMath;
@hisapy Do you want to PR to Farce and make keyPrefix specifiable as an option?
https://github.com/4Catalyzer/farce/blob/d3e6e0cdd6a6ee43b7acc8d8b67ed00cefc76ce1/src/MemoryProtocol.js#L9-L27
Hello again guys,
I didn't have for PR to make keyPrefix as an specifiable option but anyway, I want to share my conclusions after more than 1 month using the StoryRouter (see previous comments) approach, but first, let's recap that this approach was taken to address the creation of stories of components rendering Link from found/lib/Link.
The following statements assume the use jest of enzyme's mount to render the snapshots generated by StoryShots.
1. Using the StoryRouter is only useful for stories and StoryShots where you want to test that the router match a given path:
This is because the only Component rendered in the snapshot is the Route config. For example:
<div>
<FarceRouter
resolver={
Object {
"resolveElements": [Function],
}
}
>
<Provider
store={
Object {
"dispatch": [Function],
"farce": Object {
"matcher": Matcher {
"routeConfig": Array [
// routeConfig rendered here
// other stuff of store rendered here
}>
<Connect(BaseRouter) ... >
// lots of router stuff here
</div>
From Storybook, everything renders properly, but from testing point of view a route just render as
Route {
"Component": [Function],
"path": "new",
"prepareVariables": [Function],
"query": [Function] // this is here for found-relay
}
It doesn't go deeper than this. I discovered because one time, the StoryShots tests was all green even though a couple of stories were broken in the Storybook.
The only interesting part a snapshot generated rendering a StoryRouter is
resolvedMatch={
Object {
"location": Object {
"action": "POP",
"delta": 0,
"hash": "",
"index": 0,
"key": "i:1",
"pathname": "/admin/users/VXNlcjo5/edit",
"query": Object {},
"search": "",
"state": undefined,
},
....
}
It is basically an assertion that the path used for the MemoryProtocol actually exists in the routeConfig, in this case /admin/users/:id/edit.
2. Mock found/lib/Link to write stories for components rendering Link instead of using the StoryRouter approach
In order to render your component tree instead of just Function in the generated snapshots we need to render the component without the router and to avoid Link errors because the router is not in context and because there is no Provider wrapping the Connected component we need to mock the Link implementation.
To mock Link at the Storybook level, add the following alias in the resolve section of your Storybook webpack.config.js
resolve: {
alias: {
// the expected mock module is in the same dir but can another if needed
"found/lib/Link": require.resolve("./found-link-mock.jsx"),
}
}
and add the corresponding mock implementation
// file: found-link-mock.jsx
import React from "react";
import { action } from "@storybook/addon-actions";
// Set the custom link component.
export default class Link extends React.Component {
handleClick(e) {
e.preventDefault();
const { to } = this.props; // eslint-disable-line react/prop-types
action("Link")(to);
}
render() {
// eslint-disable-next-line react/prop-types
const { children, style, className } = this.props;
return (
<a
className={className}
style={style}
href="#"
onClick={e => this.handleClick(e)}
>
{children}
</a>
);
}
}
Now you should be able to see stories of components rendering Link that log event using the ActionLogger addon.
And for StoryShots you can use the same mock, in your StoryShots.test.js file. For example
// file: test/StoryShots.test.js
import initStoryshots from "@storybook/addon-storyshots";
import { mount } from "enzyme";
import toJson from "enzyme-to-json";
// Use the same mock used for Storybook
jest.mock('found/lib/Link', () => require('../.storybook/found-link-mock'))
initStoryshots({/* options */})
you can use a similar approach to mock withRouter if needed
3. Found Relay
If you have Relay components, then you'd like to export the graphql queries from the module where your routeConfig is defined and render the component using the QueryRenderer with an environment from relay-mock-network-layer and with manually set variables prop, for example if you have a path post/:id.
Conclusion
In general, you should avoid the StoryRouter approach for StoryShots. As soon as I have time I'd like to write a post about this subject, specially the Relay environment mock part because it let's you write Stories with the same source of truth as the original app.
For (2) above, I think it should be possible to define a dummy context provider that just gives <Link> the bare minimum needed to render.
Yeah I was thinking something like that but haven't found how to define the provider based on the environment (dev, storybook, test)
For number (3), I noticed that in order to take StoryShots of our Relay components and not just the <QueryRenderer />, we need to wait until the render prop is done getting the data for the component.
In this sense, I submitted this pull request to add pass the Jest done callback to the testMethod of the StoryShots addon. Once merged, the same technique might be used to write StoryShots directly from the application routeConfig using the StoryRouter approach.
Well, in most cases, if you're not actually exercising the navigation logic, you really just need to provide the URL, plus createHref/createLocation methods for links.