// @vitest-environment jsdom import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useState } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { ThemedModal } from '../src/components/modal/ThemedModal'; function ModalHarness({ noFocusableContent = false }) { const [open, setOpen] = useState(false); return ( <> setOpen(false)} ariaLabel="测试弹窗" > {noFocusableContent ? (

没有可聚焦控件

) : ( <> )}
); } describe('ThemedModal', () => { beforeEach(() => { vi.spyOn(HTMLElement.prototype, 'getClientRects').mockImplementation( () => [ { width: 1, height: 1, top: 0, right: 1, bottom: 1, left: 0, x: 0, y: 0, toJSON: () => ({}), }, ] as unknown as DOMRectList, ); }); afterEach(() => { vi.restoreAllMocks(); }); it('moves focus into the dialog, traps Tab, and restores the opener after close', async () => { const user = userEvent.setup(); render(); const opener = screen.getByRole('button', { name: '打开弹窗' }); await user.click(opener); const cancel = await screen.findByRole('button', { name: '取消' }); const confirm = screen.getByRole('button', { name: '确认' }); await waitFor(() => expect(document.activeElement).toBe(cancel)); await user.tab(); expect(document.activeElement).toBe(confirm); await user.tab(); expect(document.activeElement).toBe(cancel); await user.tab({ shift: true }); expect(document.activeElement).toBe(confirm); await user.keyboard('{Escape}'); await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()); expect(document.activeElement).toBe(opener); }); it('focuses the dialog itself when it has no focusable content', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: '打开弹窗' })); const dialog = await screen.findByRole('dialog', { name: '测试弹窗' }); await waitFor(() => expect(document.activeElement).toBe(dialog)); }); it('restores focus after a backdrop close', async () => { const user = userEvent.setup(); render(); const opener = screen.getByRole('button', { name: '打开弹窗' }); await user.click(opener); const dialog = await screen.findByRole('dialog', { name: '测试弹窗' }); fireEvent.click(dialog.parentElement!); await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()); expect(document.activeElement).toBe(opener); }); });