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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 13:29:50