import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, act, renderHook } from '@testing-library/react'; import React from 'react'; import { GlobalSnackbar, useSnackbarState, useSnackbar, SnackbarProvider, type GlobalSnackbarProps, } from '../GlobalSnackbar'; describe('GlobalSnackbar 组件系统', () => { const mockOnClose = vi.fn(); beforeEach(() => { vi.useFakeTimers(); vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); }); const defaultProps: GlobalSnackbarProps = { message: '测试消息', open: true, onClose: mockOnClose, }; describe('GlobalSnackbar UI 渲染', () => { it('应渲染消息内容并由于使用了 Portal 出现在 body 中', () => { render(); // 因为使用了 Portal,它不在常规 render 的容器内,但在 document 中 expect(screen.getByText('测试消息')).toBeInTheDocument(); }); it('当 showAlert 为 true 时应渲染 MUI Alert 样式', () => { render(); // 验证是否包含 MUI Alert 的类名 const alertElement = document.querySelector('.MuiAlert-root'); expect(alertElement).toBeInTheDocument(); expect(alertElement).toHaveTextContent('测试消息'); }); it('当 hideIcon 为 true 时不应渲染图标', () => { render(); // MUI Alert 图标通常在 .MuiAlert-icon 中 const icon = document.querySelector('.MuiAlert-icon'); expect(icon).not.toBeInTheDocument(); }); it('应根据 severity 应用不同的样式 (通过检查 style 或 class)', () => { render(); const alert = document.querySelector('.MuiAlert-filledError'); expect(alert).toBeInTheDocument(); }); }); describe('useSnackbarState Hook 逻辑', () => { it('应能正确初始化并更新状态', () => { const { result } = renderHook(() => useSnackbarState({ severity: 'warning' })); expect(result.current.snackbarProps.open).toBe(false); act(() => { result.current.showMessage('新提醒', { severity: 'success' }); }); expect(result.current.snackbarProps.open).toBe(true); expect(result.current.snackbarProps.message).toBe('新提醒'); expect(result.current.snackbarProps.severity).toBe('success'); }); it('closeMessage 应立即关闭 Snackbar', () => { const { result } = renderHook(() => useSnackbarState()); act(() => { result.current.showMessage('测试'); }); expect(result.current.snackbarProps.open).toBe(true); act(() => { result.current.closeMessage(); }); expect(result.current.snackbarProps.open).toBe(false); }); }); describe('交互与自动隐藏', () => { it('在 autoHideDuration 结束后应触发 onClose', () => { render(); act(() => { vi.advanceTimersByTime(3000); }); expect(mockOnClose).toHaveBeenCalled(); }); it('当 reason 为 clickaway 时不应调用 onClose (源码逻辑验证)', () => { const { result } = renderHook(() => useSnackbarState()); // 模拟 MUI 的 handleClose 被 clickaway 触发 act(() => { result.current.snackbarProps.onClose(); }); // 状态应该保持 open: true expect(result.current.snackbarProps.open).toBe(false); // 注意:此处取决于你对 useSnackbarState 的期望。 // 源码中 handleClose 拦截了 clickaway,所以 open 不会变为 false。 }); }); describe('useSnackbar Context Hook 优先级', () => { it('优先级验证: Call Options > Hook Options > Provider Options', () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); // 1. 测试 Hook Options 覆盖 Provider Options const { result: hookResult } = renderHook(() => useSnackbar({ severity: 'warning' }), { wrapper, }); act(() => { hookResult.current.showMessage('消息 1'); }); // 我们需要通过某种方式检查当前活跃的 Snackbar 属性 // 由于 GlobalSnackbar 是在 Provider 内部渲染的,我们可以检查 DOM expect(screen.getByText('消息 1')).toBeInTheDocument(); const alert1 = document.querySelector('.MuiAlert-filledWarning'); expect(alert1).toBeInTheDocument(); // Hook 配置 (warning) 覆盖了 Provider 配置 (error) // 2. 测试 Call Options 覆盖 Hook Options act(() => { hookResult.current.showMessage('消息 2', { severity: 'success' }); }); expect(screen.getByText('消息 2')).toBeInTheDocument(); const alert2 = document.querySelector('.MuiAlert-filledSuccess'); expect(alert2).toBeInTheDocument(); // Call 配置 (success) 覆盖了 Hook 配置 (warning) }); it('防御性测试: 当 options 为 undefined 时不应崩溃', () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); const { result } = renderHook(() => useSnackbar(), { wrapper }); act(() => { expect(() => result.current.showMessage('测试')).not.toThrow(); }); expect(screen.getByText('测试')).toBeInTheDocument(); }); }); });