// @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 ? (
没有可聚焦控件
) : (
<>
>
)}
>
);
}
/**
* 标题栏在模态之外,但它是窗口边框:弹窗打开时最小化 / 最大化 / 关闭必须照常可点。
* 工作区内容反过来仍要被模态挡住,不能因为放行标题栏就一起漏过去。
*/
function WindowChromeHarness({
onMinimize,
onWorkspaceClick,
}: {
onMinimize: () => void;
onWorkspaceClick: () => void;
}) {
return (
<>
undefined} ariaLabel="测试弹窗">
>
);
}
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);
});
it('lets window title bar clicks through while workspace clicks stay trapped', async () => {
const user = userEvent.setup();
const onMinimize = vi.fn();
const onWorkspaceClick = vi.fn();
render(
,
);
await screen.findByRole('dialog', { name: '测试弹窗' });
await user.click(screen.getByRole('button', { name: '最小化' }));
expect(onMinimize).toHaveBeenCalledTimes(1);
await user.click(screen.getByRole('button', { name: '工作区按钮' }));
expect(onWorkspaceClick).not.toHaveBeenCalled();
});
});