I have a question.
I'm wondering if I can stub sub-components when showing a component in storybook.
For example, assume a component defined like:
import SubComponent from './sub-component.jsx'
class Component extends React.Component {
render() {
return (
<div>
...
<SubComponent />
...
</div>
)
}
}
The case is that the SubComponent is very complex and is difficult to browse in the same story,
or just show a story of the Component without showing SubComponent.
I want skip rendering the SubComponent.
Is there any idea?
@MasahiroMorita Have you considered using mock functions, e.g. https://github.com/testdouble/testdouble.js/blob/master/docs/7-replacing-dependencies.md#testdoublereplace-api
I haven't tried this and don't think this code is exactly right, but it would look something like:
stories.add('mocked', () => {
td.replace('path/to/SubComponent').when().thenReturn(<Stub/>);
const Component = require('../path/to/Component');
return <Component foo="bar" />;
});
The important part is that I'm using require
to dynamically load Component
, and I stub out SubCmoponent
before I load component.
Let me know if that works. Could make an interesting add-on.
You can also do this with proxyquire
and jest
/sinon
:
proxyquire('path/to/SubComponent', jest.fn(() => </Stub>));
There's probably a way to do it in pure jest
also.
Great! I'll try proxyquire.
https://github.com/tomitrescak/proxyrequire
Thank you.
@MasahiroMorita great. please report back if you get it working! i think this could be a very interesting pattern for Storybook.
I'm going to close this for now, but please do let us know how it goes, and feel free to reopen if you
need more help. 馃憤
@shilman
I found this: https://github.com/Wildhoney/Mocktail
It is very easy to mock components.
You can define a Component like:
import React from 'react'
import { mock } from 'mocktail'
function SomeComponent(props) {
return (<div>This is a component</div>)
}
export default mock(SomeComponent)
then in stories:
import { env, ENV, inject } from 'mocktail'
env(ENV.TESTING)
inject('SomeComponent', (props)=> <div>This is mock component</div>)
import SomeComponent from 'some-component'
storiesOf('SomeComponent', module)
.add('some-component', ()=> <SomeComponent />)
this will substitute SomeComponent to the mock, and it will display 'This is mock component', rather than the original one.
It is very simple solution. I hope you will add some descriptions or usage how to mock components using mocktail in storybook manual.
I tried proxyquire, but it doesn't work with webpack.
I know this is an old issue but i spend some time trying to find a solution to this problems and i found one in this comment.
So my .storybook/webpack.config.js look like this
...
test: /\.(ts|tsx|js|jsx)$/,
use: [
{
loader: require.resolve("babel-loader"),
options: {
presets: [["react-app", { flow: false, typescript: true }]]
}
},
{
loader: path.resolve("loaders/mockLoader.js")
}
]
...
and my mockLoader.js look like this
var path = require("path");
var fs = require("fs");
var loaderUtils = require("loader-utils");
module.exports = function(source) {
var result = source;
if (this.resourcePath.indexOf("node_modules") === -1) {
var mockFileName = loaderUtils.interpolateName(
this,
"[path][name].mock.[ext]",
{}
);
if (fs.existsSync(mockFileName)) {
//add this file to watch mode
this.addDependency(mockFileName);
result = fs.readFileSync(mockFileName);
}
}
return result;
};
this way i just have to put a Component.mock.[ext] just right next to my Component.[ext] and in storybook webpack will load this implementation instead of original one
There is another option, robust, well mantained and works very well
I wrote an article how to stub components in Storybook https://medium.com/p/how-to-stub-vuejs-react-container-components-in-storybook-eb4193c48f46
TLDR:
The approach is similar to @rcadel's one, but uses a NormalModuleReplaccementPlugin plugin instead of a loader:
plugins: [
new webpack.NormalModuleReplacementPlugin(
/\.vue$/,
resource => {
const resPath = resource.request;
const mockPath = resPath.slice(0, -3) + 'mock.vue';
const absMockPath = path.resolve(resource.context, mockPath);
const absRootMockPath = path.resolve(
__dirname,
'./component-stub.vue'
);
if (fs.existsSync(absMockPath)) {
resource.request = mockPath;
} else if (resPath.endsWith('container.vue')) {
resource.request = absRootMockPath;
}
}),
],
It checks all the .vue
components and
.mock.vue
fake compoent if there is one.storybook/component-stub.vue
fake component if the filename ends with container.vue
.The same logic can be applied to React components
I just used the approach outlined by @aantipov to mock an NPM dependency:
{
plugins: [
new webpack.NormalModuleReplacementPlugin(
/webextension-polyfill-ts/,
resource => {
// Gets absolute path to mock `webextension-polyfill-ts` package
// NOTE: this is required beacuse the `webextension-polyfill-ts`
// package can't be used outside the environment provided by web extensions
const absRootMockPath = path.resolve(
__dirname,
"../src/__mocks__/webextension-polyfill-ts.ts",
);
// Gets relative path from requesting module to our mocked module
const relativePath = path.relative(
resource.context,
absRootMockPath,
);
// Updates the `resource.request` to reference our mocked module instead of the real one
resource.request = relativePath;
},
),
],
},
I just hit a snag where the webextension-polyfill-ts
package was crashing Storybook because it can't be used outside of Web Extension environments - this fix allowed me to mock that module with my own copy. Works great :+1:
Most helpful comment
I wrote an article how to stub components in Storybook https://medium.com/p/how-to-stub-vuejs-react-container-components-in-storybook-eb4193c48f46
TLDR:
The approach is similar to @rcadel's one, but uses a NormalModuleReplaccementPlugin plugin instead of a loader:
It checks all the
.vue
components and.mock.vue
fake compoent if there is one.storybook/component-stub.vue
fake component if the filename ends withcontainer.vue
.The same logic can be applied to React components