You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Storybook+Jest play方法调用shouldHaveBeenCalled断言失败

Pagination组件Storybook交互测试失败问题

组件代码

import {
  FC,
  ReactElement,
  useState,
  MouseEvent as ReactMouseEvent,
  ChangeEvent as ReactChangeEvent,
} from 'react';

import { Stack, TablePagination } from '@mui/material';

export const Pagination: FC<IPaginationProps> = ({
  total,
  page = 1,
  rowsPerPage = 25,
  position = 'start',
  rowsPerPageOptions = [10, 25, 50, 100],
  onPageChange,
  onRowsPerPageChange,
}): ReactElement => {
  const [localPage, setPage] = useState(page);
  const [localRowsPerPage, setRowsPerPage] = useState(rowsPerPage);

  const handleChangePage = (event: ReactMouseEvent<HTMLButtonElement> | null, newPage: number) => {
    setPage(newPage);
    onPageChange({ page: newPage, total, rowsPerPage: localRowsPerPage });
  };

  const handleChangeRowsPerPage = (
    event: ReactChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
  ) => {
    const perPage = parseInt(event.target.value, 10);
    setRowsPerPage(perPage);
    setPage(0);
    onRowsPerPageChange({ page: 0, total, rowsPerPage: perPage });
  };

  return (
    <Stack direction="row" justifyContent={`flex-${position}`}>
      <TablePagination
        component="div"
        showFirstButton
        showLastButton
        count={total}
        page={localPage}
        onPageChange={handleChangePage}
        rowsPerPage={localRowsPerPage}
        onRowsPerPageChange={handleChangeRowsPerPage}
        rowsPerPageOptions={rowsPerPageOptions}
      />
    </Stack>
  );
};

Storybook故事代码

export default {
  title: 'Components/Pagination',
  component: Pagination,
  parameters: {
    actions: {
      handles: ['click'],
    },
  },
  args: {
    total: 100,
    page: 1,
    onPageChange: ({ page }) => {
      // eslint-disable-next-line no-console
      console.log({ page });
    },
    onRowsPerPageChange: ({ rowsPerPage }) => {
      // eslint-disable-next-line no-console
      console.log({ rowsPerPage });
    },
  },
  decorators: [
    (Story) => (
      <div style={{ margin: '3em', boxShadow: '7px 5px 14px -8px rgba(0,0,0,0.55)' }}>
        {Story()}
      </div>
    ),
  ],
} as ComponentMeta<typeof Pagination>;


const Template: ComponentStory<typeof Pagination> = ({ ...args }) => <Pagination {...args} />;

export const Default = Template.bind({});
Default.play = async ({ canvasElement, args }) => {
  const onPageChangeSpy = jest.spyOn(args, 'onPageChange');
  const canvas = await within(canvasElement);

  const prevBtn = canvas.getByLabelText('Go to previous page');
  await userEvent.click(prevBtn);
  await expect(onPageChangeSpy).toHaveBeenCalled();
};

问题描述

按钮点击后分页状态正常变化,说明onPageChange已被调用,但测试报错:

Expected number of calls: >= 1
Received number of calls: 0

奇怪的是,在Storybook交互标签页点击重新运行后,测试又能正常通过。

问题原因

测试失败的核心原因是组件渲染时机与spy创建时机不匹配:

  • Storybook会先渲染组件,再执行play函数里的代码
  • 当你在play里用jest.spyOn给args.onPageChange创建spy时,组件已经使用原始的onPageChange函数完成渲染了
  • 后续组件内部调用的还是原始函数,spy无法捕获到调用
  • 重新运行时,组件会在spy创建之后重新渲染,因此能正确捕获调用

解决方案

方案1:修改play函数,先替换spy再重新渲染

Default.play = async ({ canvasElement, args, rerender }) => {
  // 创建spy并替换args中的原始函数
  const onPageChangeSpy = jest.fn();
  args.onPageChange = onPageChangeSpy;
  // 强制重新渲染组件,让组件使用新的spy函数
  await rerender();

  const canvas = within(canvasElement);
  const prevBtn = canvas.getByLabelText('Go to previous page');
  await userEvent.click(prevBtn);
  await expect(onPageChangeSpy).toHaveBeenCalled();
};

方案2:直接在args中定义spy(推荐)

在Story的args里直接使用jest.fn(),让组件从一开始就使用spy函数:

args: {
  total: 100,
  page: 1,
  onPageChange: jest.fn(({ page }) => {
    console.log({ page });
  }),
  onRowsPerPageChange: jest.fn(({ rowsPerPage }) => {
    console.log({ rowsPerPage });
  }),
},

然后修改play函数直接引用args中的spy:

Default.play = async ({ canvasElement, args }) => {
  const canvas = within(canvasElement);
  const prevBtn = canvas.getByLabelText('Go to previous page');
  await userEvent.click(prevBtn);
  await expect(args.onPageChange).toHaveBeenCalled();
};

两种方案都能确保组件使用的是spy函数,测试即可正常捕获调用。

内容的提问来源于stack exchange,提问作者Tomer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 22:35:36