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

React Testing Library测试点击按钮无法关闭React Bootstrap Modal问题

React Testing Library测试React Bootstrap Modal点击关闭不生效问题

问题现象

使用React Testing Library测试React Bootstrap提供的Modal组件时,期望验证点击按钮后弹窗正常关闭,但测试始终无法通过,执行点击操作后Modal元素仍存在于文档中。

相关代码

被测组件代码

import { HashLink as Link } from "react-router-hash-link";
import { Modal } from "react-bootstrap";
import styles from "./PledgeSubmittedModalCard.module.scss";
import iconCheck from "../../icons/icon-check.svg";

export const PledgeSubmittedModalCard = ({
  onCloseClick,
  confirmationPledgeText,
}) => {
  return (
    <Modal
      show
      backdrop="static"
      keyboard={false}
      centered
      dialogClassName={styles.pledgesModal}
      data-testid="pledge-submitted-modal"
      onHide={onCloseClick}
    >
      <div className={styles.modalContainer}>
        <img
          className={styles.iconCheck}
          src={iconCheck}
          alt="completed action icon"
        />
        <h5 className={styles.sayThanks}>Thanks for your support!</h5>
        <p role="paragraph" className={styles.confirmationText}>
          {confirmationPledgeText}
        </p>
        <Link to="#top">
          <button onClick={onCloseClick} className={styles.confirmationBtn}>
            Got it
          </button>
        </Link>
      </div>
    </Modal>
  );
};

初始测试用例代码

import {
  prettyDOM,
  render,
  screen,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BrowserRouter } from "react-router-dom";
import { PledgeSubmittedModalCard } from "../index";

const onClickMock = jest.fn();

const MockPledgeSubmittedModalCard = () => {
  return (
    <BrowserRouter>
      <PledgeSubmittedModalCard onCloseClick={onClickMock} />
    </BrowserRouter>
  );
};

describe("PledgeSubmittedModalCard", () => {
  it("Should close modal when button clicked", () => {
    render(<MockPledgeSubmittedModalCard />);
    const modalElement = screen.queryByTestId("pledge-submitted-modal");
    const buttonElement = screen.getByRole("button", {
      name: "Got it",
    });
    console.log(prettyDOM(modalElement));
    userEvent.click(buttonElement);
    // 该断言可通过
    expect(onClickMock).toBeCalledTimes(1);
    // 该断言失败
    expect(modalElement).not.toBeInTheDocument();
  });
});

报错信息

● PledgeSubmittedModalCard › Should close modal when button clicked

    expect(element).not.toBeInTheDocument()

    expected document not to contain element, found <div class="modal-dialog pledgesModal modal-dialog-centered" data-testid="pledge-submitted-modal"><div class="modal-content"><div class="modalContainer"><img alt="completed action icon" class="iconCheck" src="icon-check.svg" /><h5 class="sayThanks">Thanks for your support!</h5><p class="confirmationText" role="paragraph" /><a href="/#top"><button class="confirmationBtn">Got it</button></a></div></div></div> instead

      38 |     expect(onClickMock).toBeCalledTimes(1);
      39 |     // expect(modalElement).toBeNull();
    > 40 |     expect(modalElement).not.toBeInTheDocument();
         |                              ^
      41 |   });
      42 | });
      43 |


Test Suites: 1 failed, 7 skipped, 1 of 8 total
Tests:       1 failed, 26 skipped, 1 passed, 28 total
Snapshots:  0, 0 total
Time:        3.544 s, estimated 4 s
Ran all test suites with tests matching "PledgeSubmittedModalCard".

根因分析

测试失败由三个核心问题导致:

  1. 组件显隐逻辑硬编码:Modal组件的show属性被直接写死为true,没有绑定可变的状态值。React Bootstrap的Modal是受控组件,是否渲染完全由show属性决定,即使点击按钮触发了onCloseClick回调,没有任何逻辑修改show的值,Modal会始终保持显示状态。
  2. 测试逻辑不符合真实使用场景:测试中传入的onClickMock仅做调用次数统计,没有模拟父组件更新显隐状态的逻辑,和实际业务中点击关闭按钮后修改状态隐藏弹窗的逻辑不一致。
  3. 断言逻辑存在缺陷:在点击操作前就缓存了modalElement的DOM引用,点击后DOM更新时该变量不会同步最新状态;同时React Bootstrap Modal默认带关闭动画,节点不会在点击后立刻从DOM移除,同步断言无法拿到最终的DOM状态。

修复方案

1. 调整组件为标准受控模式

给组件新增显隐控制入参,将Modal的show属性绑定到外部传入的状态值,不要硬编码固定值;测试环境可关闭动画减少不必要的异步等待:

export const PledgeSubmittedModalCard = ({
  onCloseClick,
  confirmationPledgeText,
  isOpen // 新增显隐控制参数
}) => {
  return (
    <Modal
      show={isOpen} // 绑定可变的显隐状态
      backdrop="static"
      keyboard={false}
      centered
      dialogClassName={styles.pledgesModal}
      data-testid="pledge-submitted-modal"
      onHide={onCloseClick}
      animation={process.env.NODE_ENV !== 'test'} // 测试环境关闭动画
    >
      {/* 原有内部逻辑不变 */}
    </Modal>
  );
};

2. 重构测试用例,模拟真实状态逻辑

测试中的Mock组件需要实现和真实父组件一致的状态更新逻辑,点击按钮时修改显隐状态;点击后不要复用之前缓存的元素引用,配合异步等待方法断言元素移除:

import React from 'react';
import {
  render,
  screen,
  waitForElementToBeRemoved
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BrowserRouter } from "react-router-dom";
import { PledgeSubmittedModalCard } from "../index";

// 模拟真实业务中的父组件状态控制逻辑
const MockPledgeSubmittedModalCard = () => {
  const [isOpen, setIsOpen] = React.useState(true);
  return (
    <BrowserRouter>
      <PledgeSubmittedModalCard 
        onCloseClick={() => setIsOpen(false)}
        isOpen={isOpen}
      />
    </BrowserRouter>
  );
};

describe("PledgeSubmittedModalCard", () => {
  it("Should close modal when button clicked", async () => {
    const user = userEvent.setup();
    render(<MockPledgeSubmittedModalCard />);
    
    // 验证初始状态弹窗存在
    expect(screen.getByTestId("pledge-submitted-modal")).toBeInTheDocument();
    const buttonElement = screen.getByRole("button", { name: "Got it" });
    
    await user.click(buttonElement);
    // 实时查询DOM,等待弹窗完全移除
    await waitForElementToBeRemoved(() => 
      screen.queryByTestId("pledge-submitted-modal")
    );
  });
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 14:48:19