storybook-addon-module-mock
在 Storybook@8 上提供类似于 `jest.mock` 的模块模拟功能。
将 “storybook-addon-module-mock” 添加到 Storybook 插件中。
仅在构建器中使用 Webpack 时才有效。
如果您使用 Vite 作为构建器,请使用此包。
https://npmjs.net.cn/package/storybook-addon-vite-mock
截图
用法
关于如何中断模拟
中断方式取决于 Storybook 模式。
- storybook dev
- 使用 Webpack 功能使 `module.exports` 可写
- storybook build
- 插入代码以使用 Babel 功能重写 `module.exports`
插件选项
在使用 Babel 的 `storybook build` 中启用 `include` 和 `exclude`。在 `storybook dev` 中未使用。
如果省略 `include`,则所有模块都将被覆盖。
addons: [
{
name: 'storybook-addon-module-mock',
options: {
include: ["**/action.*"], // glob pattern
exclude: ["**/node_modules/**"],
}
}
],
Storybook@8 & Next.js
- .storybook/main.ts
import type { StorybookConfig } from '@storybook/nextjs';
const config: StorybookConfig = {
framework: {
name: '@storybook/nextjs',
options: {},
},
stories: ['../src/**/*.stories.@(tsx)'],
build: {
test: {
disabledAddons: ['@storybook/addon-docs', '@storybook/addon-essentials/docs'],
},
},
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
{
name: '@storybook/addon-coverage',
options: {
istanbul: {
exclude: ['**/components/**/index.ts'],
},
},
},
{
name: 'storybook-addon-module-mock',
options: {
exclude: ['**/node_modules/@mui/**'],
},
},
],
};
export default config;
示例 1
MockTest.tsx
import React, { FC, useMemo, useState } from 'react';
interface Props {}
/**
* MockTest
*
* @param {Props} { }
*/
export const MockTest: FC<Props> = ({}) => {
const [, reload] = useState({});
const value = useMemo(() => {
return 'Before';
}, []);
return (
<div>
<button onClick={() => reload({})}>{value}</button>
</div>
);
};
MockTest.stories.tsx
`createMock` 将目标模块函数替换为 `jest.fn()` 的返回值。
在故事显示完成后,会自动执行 `mockRestore()`。
import { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, waitFor, within } from '@storybook/test';
import React, { DependencyList } from 'react';
import { createMock, getMock, getOriginal } from 'storybook-addon-module-mock';
import { MockTest } from './MockTest';
const meta: Meta<typeof MockTest> = {
tags: ['autodocs'],
component: MockTest,
};
export default meta;
export const Primary: StoryObj<typeof MockTest> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText('Before')).toBeInTheDocument();
},
};
export const Mock: StoryObj<typeof MockTest> = {
parameters: {
moduleMock: {
mock: () => {
const mock = createMock(React, 'useMemo');
mock.mockImplementation((fn: () => unknown, deps: DependencyList) => {
// Call the original useMemo
const value = getOriginal(mock)(fn, deps);
// Change the return value under certain conditions
return value === 'Before' ? 'After' : value;
});
return [mock];
},
},
},
play: async ({ canvasElement, parameters }) => {
const canvas = within(canvasElement);
expect(canvas.getByText('After')).toBeInTheDocument();
const mock = getMock(parameters, React, 'useMemo');
expect(mock).toBeCalled();
},
};
export const Action: StoryObj<typeof MockTest> = {
parameters: {
moduleMock: {
mock: () => {
const useMemo = React.useMemo;
const mock = createMock(React, 'useMemo');
mock.mockImplementation(useMemo);
return [mock];
},
},
},
play: async ({ canvasElement, parameters }) => {
const canvas = within(canvasElement);
const mock = getMock(parameters, React, 'useMemo');
mock.mockImplementation((fn: () => unknown, deps: DependencyList) => {
const value = getOriginal(mock)(fn, deps);
return value === 'Before' ? 'Action' : value;
});
userEvent.click(await canvas.findByRole('button'));
await waitFor(() => {
expect(canvas.getByText('Action')).toBeInTheDocument();
});
},
};
示例 2
message.ts
export const getMessage = () => {
return 'Before';
};
LibHook.tsx
import React, { FC, useState } from 'react';
import { getMessage } from './message';
interface Props {}
/**
* LibHook
*
* @param {Props} { }
*/
export const LibHook: FC<Props> = ({}) => {
const [, reload] = useState({});
const value = getMessage();
return (
<div>
<button onClick={() => reload({})}>{value}</button>
</div>
);
};
LibHook.stories.tsx
import { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, waitFor, within } from '@storybook/test';
import { createMock, getMock } from 'storybook-addon-module-mock';
import { LibHook } from './LibHook';
import * as message from './message';
const meta: Meta<typeof LibHook> = {
tags: ['autodocs'],
component: LibHook,
};
export default meta;
export const Primary: StoryObj<typeof LibHook> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText('Before')).toBeInTheDocument();
},
};
export const Mock: StoryObj<typeof LibHook> = {
parameters: {
moduleMock: {
mock: () => {
const mock = createMock(message, 'getMessage');
mock.mockReturnValue('After');
return [mock];
},
},
},
play: async ({ canvasElement, parameters }) => {
const canvas = within(canvasElement);
expect(canvas.getByText('After')).toBeInTheDocument();
const mock = getMock(parameters, message, 'getMessage');
console.log(mock);
expect(mock).toBeCalled();
},
};
export const Action: StoryObj<typeof LibHook> = {
parameters: {
moduleMock: {
mock: () => {
const mock = createMock(message, 'getMessage');
return [mock];
},
},
},
play: async ({ canvasElement, parameters }) => {
const canvas = within(canvasElement);
const mock = getMock(parameters, message, 'getMessage');
mock.mockReturnValue('Action');
userEvent.click(await canvas.findByRole('button'));
await waitFor(() => {
expect(canvas.getByText('Action')).toBeInTheDocument();
});
},
};
示例 3
MockTest.tsx
import React, { FC, useMemo, useState } from 'react';
interface Props {}
/**
* MockTest
*
* @param {Props} { }
*/
export const MockTest: FC<Props> = ({}) => {
const [, reload] = useState({});
const value = useMemo(() => {
return 'Before';
}, []);
return (
<div>
<button onClick={() => reload({})}>{value}</button>
</div>
);
};
MockTest.stories.tsx
import { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, waitFor, within } from '@storybook/test';
import React, { DependencyList } from 'react';
import { createMock, getMock, getOriginal } from 'storybook-addon-module-mock';
import { MockTest } from './MockTest';
const meta: Meta<typeof MockTest> = {
tags: ['autodocs'],
component: MockTest,
};
export default meta;
export const Primary: StoryObj<typeof MockTest> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText('Before')).toBeInTheDocument();
},
};
export const Mock: StoryObj<typeof MockTest> = {
parameters: {
moduleMock: {
mock: () => {
const mock = createMock(React, 'useMemo');
mock.mockImplementation((fn: () => unknown, deps: DependencyList) => {
// Call the original useMemo
const value = getOriginal(mock)(fn, deps);
// Change the return value under certain conditions
return value === 'Before' ? 'After' : value;
});
return [mock];
},
},
},
play: async ({ canvasElement, parameters }) => {
const canvas = within(canvasElement);
expect(canvas.getByText('After')).toBeInTheDocument();
const mock = getMock(parameters, React, 'useMemo');
expect(mock).toBeCalled();
},
};
export const Action: StoryObj<typeof MockTest> = {
parameters: {
moduleMock: {
mock: () => {
const useMemo = React.useMemo;
const mock = createMock(React, 'useMemo');
mock.mockImplementation(useMemo);
return [mock];
},
},
},
play: async ({ canvasElement, parameters }) => {
const canvas = within(canvasElement);
const mock = getMock(parameters, React, 'useMemo');
mock.mockImplementation((fn: () => unknown, deps: DependencyList) => {
const value = getOriginal(mock)(fn, deps);
return value === 'Before' ? 'Action' : value;
});
userEvent.click(await canvas.findByRole('button'));
await waitFor(() => {
expect(canvas.getByText('Action')).toBeInTheDocument();
});
},
};
示例 4
ReRenderArgs.tsx
import React, { FC } from 'react';
import styled from './ReRenderArgs.module.scss';
interface Props {
value: string;
}
/**
* ReRenderArgs
*
* @param {Props} { value: string }
*/
export const ReRenderArgs: FC<Props> = ({ value }) => {
return <div className={styled.root}>{value}</div>;
};
ReRenderArgs.stories.tsx
import { Meta, StoryObj } from '@storybook/react';
import { expect, waitFor, within } from '@storybook/test';
import { createMock, getMock, render } from 'storybook-addon-module-mock';
import * as message from './message';
import { ReRender } from './ReRender';
const meta: Meta<typeof ReRender> = {
tags: ['autodocs'],
component: ReRender,
};
export default meta;
export const Primary: StoryObj<typeof ReRender> = {};
export const ReRenderTest: StoryObj<typeof ReRender> = {
parameters: {
moduleMock: {
mock: () => {
const mock = createMock(message, 'getMessage');
return [mock];
},
},
},
play: async ({ canvasElement, parameters }) => {
const canvas = within(canvasElement);
const mock = getMock(parameters, message, 'getMessage');
mock.mockReturnValue('Test1');
render(parameters);
await waitFor(() => {
expect(canvas.getByText('Test1')).toBeInTheDocument();
});
mock.mockReturnValue('Test2');
render(parameters);
await waitFor(() => {
expect(canvas.getByText('Test2')).toBeInTheDocument();
});
},
};