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

如何用Vitest和React Testing Library测试复选框交互后的UI变化

问题:点击复选框后父组件未添加Tailwind蓝色边框类名

我正在使用vitest和react testing library测试用户与复选框交互后的UI更新,具体需求是确保用户点击复选框时,父组件能添加上Tailwind的border-p-purplish-blue类,但测试发现点击后目标类名并未添加。

待测试组件代码

export const AddOn: FC<props> = ({
  title,
  desc,
  price,
  type,
  handleAdd,
  handleRemove,
  checked,
}) => {
  const [isChecked, toggleCheck] = useState(checked);

  useEffect(() => {
    isChecked
      ? handleAdd(title.toLowerCase(), price)
      : handleRemove(title.toLowerCase(), price);
  }, [isChecked]); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <div
      className={
        "relative w-full border border-n-light-gray rounded-md p-3 lg:px-6 lg:py-3 flex gap-4 items-center hover:opacity-70 " +
        (checked ? " border-p-purplish-blue bg-n-alabaster" : "")
      }
      data-testid={`addon-${title}-container`}
    >
      <div className="flex gap-4">
        <input
          autoComplete="off"
          className="w-6 h-6 self-center cursor-pointer"
          data-testid={`addon-${title}`}
          defaultChecked={checked}
          type="checkbox"
          onClick={() => toggleCheck(!checked)}
          onKeyPress={(e) => {
            e.preventDefault();
            toggleCheck(!checked);
          }}
        />
        <div className="w-8/12 sm:w-full text-left">
          <h3 className="text-base text-p-marine-blue font-bold">{title}</h3>
          <p className="text-n-cool-gray justify-self-start">{desc}</p>
        </div>
      </div>
      <p className="text-p-purplish-blue text-base font-medium absolute right-2 lg:right-6">
        {type === "monthly" ? `+$${price}/mo` : `+$${price}/yr`}
      </p>
    </div>
  );
};

我的测试代码

test("UI TEST: should show the container with a blue border after user clicks on", async () => {
  render(
    <AddOn
      checked={false}
      desc="test"
      handleAdd={() => {}}
      handleRemove={() => {}}
      price={0}
      title="title-test"
      type="test"
    />
  );

  const addOnOnlineService: HTMLInputElement = await screen.findByTestId(
    "addon-title-test"
  );

  await userEvent.click(addOnOnlineService);

  const testContainer: HTMLDivElement = await screen.findByTestId(
    "addon-title-test-container"
  );

  await waitFor(() => {
    expect(Array.from(testContainer.classList)).toContain(
      "border-p-purplish-blue"
    );
  });
});

问题根源与解决方案

1. UI渲染依赖错误状态

组件父容器的className判断逻辑用的是props传入的checked,而非组件内部维护的isChecked状态。点击复选框只会更新内部状态,但UI渲染逻辑没关联这个状态,所以不会触发样式变化。

2. 状态切换逻辑错误

复选框的点击/按键事件中,调用toggleCheck(!checked)是基于props的初始值切换,而非当前内部状态isChecked,这会导致状态更新逻辑混乱,无法正确反映用户的交互操作。

修复后的组件代码

export const AddOn: FC<props> = ({
  title,
  desc,
  price,
  type,
  handleAdd,
  handleRemove,
  checked,
}) => {
  const [isChecked, toggleCheck] = useState(checked);

  // 可选:当外部props的checked变化时,同步内部状态
  useEffect(() => {
    toggleCheck(checked);
  }, [checked]);

  useEffect(() => {
    isChecked
      ? handleAdd(title.toLowerCase(), price)
      : handleRemove(title.toLowerCase(), price);
  }, [isChecked, handleAdd, handleRemove, title, price]);

  return (
    <div
      className={
        "relative w-full border border-n-light-gray rounded-md p-3 lg:px-6 lg:py-3 flex gap-4 items-center hover:opacity-70 " +
        // 改为使用内部状态isChecked判断样式
        (isChecked ? " border-p-purplish-blue bg-n-alabaster" : "")
      }
      data-testid={`addon-${title}-container`}
    >
      <div className="flex gap-4">
        <input
          autoComplete="off"
          className="w-6 h-6 self-center cursor-pointer"
          data-testid={`addon-${title}`}
          // 用内部状态控制复选框选中状态,替代默认值
          checked={isChecked}
          type="checkbox"
          // 基于当前内部状态切换
          onClick={() => toggleCheck(!isChecked)}
          onKeyPress={(e) => {
            e.preventDefault();
            toggleCheck(!isChecked);
          }}
        />
        <div className="w-8/12 sm:w-full text-left">
          <h3 className="text-base text-p-marine-blue font-bold">{title}</h3>
          <p className="text-n-cool-gray justify-self-start">{desc}</p>
        </div>
      </div>
      <p className="text-p-purplish-blue text-base font-medium absolute right-2 lg:right-6">
        {type === "monthly" ? `+$${price}/mo` : `+$${price}/yr`}
      </p>
    </div>
  );
};

优化后的测试代码

修复后状态更新是同步的,无需waitFor,可直接断言:

test("UI TEST: should show the container with a blue border after user clicks on", async () => {
  render(
    <AddOn
      checked={false}
      desc="test"
      handleAdd={() => {}}
      handleRemove={() => {}}
      price={0}
      title="title-test"
      type="test"
    />
  );

  const addOnCheckbox = screen.getByTestId("addon-title-test");
  const testContainer = screen.getByTestId("addon-title-test-container");

  // 初始状态断言
  expect(testContainer).not.toHaveClass("border-p-purplish-blue");

  await userEvent.click(addOnCheckbox);

  // 点击后状态断言
  expect(testContainer).toHaveClass("border-p-purplish-blue");
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 13:35:19