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".
根因分析
测试失败由三个核心问题导致:
- 组件显隐逻辑硬编码:Modal组件的
show属性被直接写死为true,没有绑定可变的状态值。React Bootstrap的Modal是受控组件,是否渲染完全由show属性决定,即使点击按钮触发了onCloseClick回调,没有任何逻辑修改show的值,Modal会始终保持显示状态。 - 测试逻辑不符合真实使用场景:测试中传入的
onClickMock仅做调用次数统计,没有模拟父组件更新显隐状态的逻辑,和实际业务中点击关闭按钮后修改状态隐藏弹窗的逻辑不一致。 - 断言逻辑存在缺陷:在点击操作前就缓存了
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
相关产品推荐
相关产品推荐

