Notifications组件测试失败:应用中已卸载但测试中仍存在
问题:Notifications组件测试中通知无法被检测到卸载
我编写的Notifications组件测试用例里,验证通知在指定时间后消失的测试始终失败。执行到expect(screen.queryByTestId('notification')).not.toBeInTheDocument()断言时,发现元素仍存在于DOM中,但实际应用里组件能正常卸载。我尝试过调整计时器时长(甚至设为极大值),但测试结果没有变化,推测不是计时器的问题。
测试代码
import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { act } from 'react' import Notifications, { defaultNotificationTime, defaultOpacity, queuedNotificationTime, fadeInTime } from '../Components/Notifications'; import { NotificationContext } from '../Context/NotificationContext'; const renderNotification = (notifications: string[], setNotifications: jest.Mock) => { return render( <NotificationContext.Provider value={{ notifications, setNotifications }}> <Notifications /> </NotificationContext.Provider> ); }; beforeEach(() => jest.useFakeTimers()); afterEach(() => jest.useRealTimers()); const setNotifications = jest.fn(); describe('Notifications Component', () => { test('renders with no notifications', () => { renderNotification([], setNotifications); expect(screen.queryByTestId('notification')).toBeNull(); }); test('displays a notification and hides it after the default time', async () => { const notifications = ['Test notification']; // Initial render with notification renderNotification(notifications, setNotifications); // Verify the notification element is in the document expect(screen.getByTestId('notification')).toBeInTheDocument(); act(() => { jest.advanceTimersByTime(fadeInTime); }); expect(screen.getByTestId('notification')).toHaveStyle(`opacity: ${defaultOpacity}`); expect(screen.getByTestId('notification')).toHaveTextContent(notifications[0]); act(() => { jest.advanceTimersByTime(defaultNotificationTime); }); expect(screen.queryByTestId('notification')).toHaveStyle(`opacity: 0`); act(() => { jest.advanceTimersByTime(fadeInTime * 2); }); expect(screen.queryByTestId('notification')).not.toBeInTheDocument(); }); });
失败提示
Notifications Component › displays a notification and hides it after the default time expect(element).not.toBeInTheDocument() expected document not to contain element, found <div data-testid="notification" id="notification" style="opacity: 0; transition: opacity 0.75s ease-in-out;"><p>Test notification</p></div> instead 41 | jest.advanceTimersByTime(fadeInTime * 2); 42 | }); > 43 | expect(screen.queryByTestId('notification')).not.toBeInTheDocument();
对应组件代码
import { useState, useEffect, useMemo, memo, useContext } from 'react'; import { NotificationContext } from '../Context/NotificationContext'; import './Style/index.css'; export const defaultOpacity = 0.85; export const defaultNotificationTime = 2500; export const queuedNotificationTime = 2250; export const fadeInTime = 750; const Notifications: React.FC = () => { const { notifications, setNotifications } = useContext(NotificationContext); const [fadeIn, setFadeIn] = useState<boolean>(false); const [currentNotification, setCurrentNotification] = useState<string | null>(null); // Manage CSS styles for the notification const notificationCSS = useMemo(() => ({ opacity: fadeIn ? defaultOpacity : 0, transition: `opacity ${fadeInTime / 1000}s ease-in-out` // Transition duration and easing }), [fadeIn]); useEffect(() => { if (notifications.length === 0) { setCurrentNotification(null); return; } setCurrentNotification(notifications[0]); const fadeInTimer = setTimeout(() => { setFadeIn(true); }, 0); // trigger shortly after setting the notification to ensure fade in effect const notificationShowTime = notifications.length > 1 ? queuedNotificationTime : defaultNotificationTime; const timer = setTimeout(() => { setFadeIn(false); const exitTimer = setTimeout(() => { setNotifications((prev: string[]) => prev.slice(1)); }, fadeInTime); // Ensure this timeout matches the CSS transition duration return () => clearTimeout(exitTimer); }, notificationShowTime); return () => { clearTimeout(fadeInTimer); clearTimeout(timer); }; }, [notifications, setNotifications]); return ( <> {currentNotification && ( <div id='notification' data-testid='notification' style={notificationCSS}> <p>{currentNotification}</p> </div> )} </> ); }; export default memo(Notifications);
解决方案
问题核心:测试中使用的setNotifications是一个空的jest mock函数,没有实际更新上下文里的notifications数组。组件调用setNotifications((prev) => prev.slice(1))时,mock不会修改测试传入的notifications值,导致notifications始终为['Test notification'],currentNotification不会被设为null,元素也就不会从DOM中移除。
修改后的测试代码
让setNotifications mock实际更新测试状态,并跟踪notifications的变化:
import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { act } from 'react' import Notifications, { defaultNotificationTime, defaultOpacity, queuedNotificationTime, fadeInTime } from '../Components/Notifications'; import { NotificationContext } from '../Context/NotificationContext'; beforeEach(() => jest.useFakeTimers()); afterEach(() => jest.useRealTimers()); describe('Notifications Component', () => { test('renders with no notifications', () => { const setNotifications = jest.fn(); render( <NotificationContext.Provider value={{ notifications: [], setNotifications }}> <Notifications /> </NotificationContext.Provider> ); expect(screen.queryByTestId('notification')).toBeNull(); }); test('displays a notification and hides it after the default time', () => { // 用变量跟踪测试用的notifications数组 let notifications = ['Test notification']; const setNotifications = jest.fn((updateFn) => { // 执行组件传入的更新函数,更新测试状态 notifications = updateFn(notifications); }); // 保存rerender方法,用于后续更新上下文后重新渲染 const { rerender } = render( <NotificationContext.Provider value={{ notifications, setNotifications }}> <Notifications /> </NotificationContext.Provider> ); // 验证初始渲染 expect(screen.getByTestId('notification')).toBeInTheDocument(); // 快进淡入动画时间,验证淡入完成 act(() => { jest.advanceTimersByTime(fadeInTime); }); expect(screen.getByTestId('notification')).toHaveStyle(`opacity: ${defaultOpacity}`); expect(screen.getByTestId('notification')).toHaveTextContent('Test notification'); // 快进通知显示时间,验证开始淡出 act(() => { jest.advanceTimersByTime(defaultNotificationTime); }); expect(screen.queryByTestId('notification')).toHaveStyle(`opacity: 0`); // 快进淡出动画时间,触发移除通知的逻辑 act(() => { jest.advanceTimersByTime(fadeInTime); }); // 验证setNotifications被调用,且数组已清空 expect(setNotifications).toHaveBeenCalled(); expect(notifications).toEqual([]); // 用更新后的notifications重新渲染组件 rerender( <NotificationContext.Provider value={{ notifications, setNotifications }}> <Notifications /> </NotificationContext.Provider> ); // 验证通知元素已从DOM移除 expect(screen.queryByTestId('notification')).not.toBeInTheDocument(); }); });
可选优化
可以把重新渲染的逻辑整合到mock函数里,减少重复代码:
test('displays a notification and hides it after the default time', () => { let notifications = ['Test notification']; let rerender: (ui: React.ReactElement) => void; const setNotifications = jest.fn((updateFn) => { notifications = updateFn(notifications); // 自动重新渲染 rerender( <NotificationContext.Provider value={{ notifications, setNotifications }}> <Notifications /> </NotificationContext.Provider> ); }); ({ rerender } = render( <NotificationContext.Provider value={{ notifications, setNotifications }}> <Notifications /> </NotificationContext.Provider> )); // ... 之前的断言步骤 act(() => { jest.advanceTimersByTime(fadeInTime); }); expect(setNotifications).toHaveBeenCalled(); expect(notifications).toEqual([]); // 无需手动调用rerender,mock里已处理 expect(screen.queryByTestId('notification')).not.toBeInTheDocument(); });
内容的提问来源于stack exchange,提问作者Brian Lee
相关产品推荐
相关产品推荐

