同步工作区剩余变更,主要包括: - 营销页面组件与布局持续优化(about/news/services/solutions/team 等) - 详情页四层叙事组件、布局组件、UI 组件调整 - CMS 数据模型、API 路由、权限、工作流、站内通知、媒体管理扩展 - 新增/补充单元测试与 E2E 测试(cms-workflow.spec.ts 等) - ESLint 9 迁移、jest/tsconfig 配置更新、依赖调整 - 新增 ADR、CMS 评估文档、Release Review / Acceptance 报告 - 移除水墨装饰组件与大体积未使用字体文件
163 lines
5.3 KiB
TypeScript
163 lines
5.3 KiB
TypeScript
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
import { renderHook, render, screen, fireEvent } from '@testing-library/react';
|
|
import { useFocusTrap } from './use-focus-trap';
|
|
|
|
|
|
describe('useFocusTrap', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('Initial State', () => {
|
|
it('should return a ref', () => {
|
|
const { result } = renderHook(() => useFocusTrap<HTMLDivElement>(false));
|
|
expect(result.current).toBeDefined();
|
|
expect(result.current.current).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('When Active', () => {
|
|
it('should store previous active element', () => {
|
|
const { result } = renderHook(() => useFocusTrap<HTMLDivElement>(true));
|
|
expect(result.current).toBeDefined();
|
|
});
|
|
|
|
it('should add keydown event listener', () => {
|
|
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
|
|
renderHook(() => useFocusTrap<HTMLDivElement>(true));
|
|
expect(addEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));
|
|
});
|
|
|
|
it('should set body overflow to hidden', () => {
|
|
renderHook(() => useFocusTrap<HTMLDivElement>(true));
|
|
expect(document.body.style.overflow).toBe('hidden');
|
|
});
|
|
});
|
|
|
|
describe('When Inactive', () => {
|
|
it('should not add keydown event listener', () => {
|
|
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
|
|
renderHook(() => useFocusTrap<HTMLDivElement>(false));
|
|
expect(addEventListenerSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not set body overflow', () => {
|
|
renderHook(() => useFocusTrap<HTMLDivElement>(false));
|
|
expect(document.body.style.overflow).toBe('unset');
|
|
});
|
|
});
|
|
|
|
describe('Cleanup', () => {
|
|
it('should remove keydown event listener on unmount', () => {
|
|
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');
|
|
const { unmount } = renderHook(() => useFocusTrap<HTMLDivElement>(true));
|
|
unmount();
|
|
expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));
|
|
});
|
|
|
|
it('should restore body overflow on unmount', () => {
|
|
const { unmount } = renderHook(() => useFocusTrap<HTMLDivElement>(true));
|
|
unmount();
|
|
expect(document.body.style.overflow).toBe('unset');
|
|
});
|
|
});
|
|
|
|
describe('Tab Navigation', () => {
|
|
it('should handle Tab key press', () => {
|
|
const { result } = renderHook(() => useFocusTrap<HTMLDivElement>(true));
|
|
|
|
const mockEvent = new KeyboardEvent('keydown', { key: 'Tab' });
|
|
document.dispatchEvent(mockEvent);
|
|
|
|
expect(result.current).toBeDefined();
|
|
});
|
|
|
|
it('should handle Shift+Tab key press', () => {
|
|
const { result } = renderHook(() => useFocusTrap<HTMLDivElement>(true));
|
|
|
|
const mockEvent = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true });
|
|
document.dispatchEvent(mockEvent);
|
|
|
|
expect(result.current).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Escape Key', () => {
|
|
it('should handle Escape key press', () => {
|
|
const { result } = renderHook(() => useFocusTrap<HTMLDivElement>(true));
|
|
|
|
const mockEvent = new KeyboardEvent('keydown', { key: 'Escape' });
|
|
document.dispatchEvent(mockEvent);
|
|
|
|
expect(result.current).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('State Changes', () => {
|
|
it('should handle activation change', () => {
|
|
const { result, rerender } = renderHook(
|
|
({ isActive }) => useFocusTrap<HTMLDivElement>(isActive),
|
|
{ initialProps: { isActive: false } }
|
|
);
|
|
|
|
expect(result.current).toBeDefined();
|
|
|
|
rerender({ isActive: true });
|
|
expect(document.body.style.overflow).toBe('hidden');
|
|
|
|
rerender({ isActive: false });
|
|
expect(document.body.style.overflow).toBe('unset');
|
|
});
|
|
});
|
|
|
|
describe('Focus Cycling', () => {
|
|
function TrapComponent({ isActive }: { isActive: boolean }) {
|
|
const ref = useFocusTrap<HTMLDivElement>(isActive);
|
|
return (
|
|
<div>
|
|
<button>outside</button>
|
|
<div ref={ref} data-testid="trap">
|
|
<button>first</button>
|
|
<button>last</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
it('cycles focus from last element to first on Tab', () => {
|
|
render(<TrapComponent isActive />);
|
|
const buttons = screen.getAllByRole('button');
|
|
buttons[buttons.length - 1]!.focus();
|
|
|
|
fireEvent.keyDown(buttons[buttons.length - 1]!, { key: 'Tab' });
|
|
|
|
expect(buttons[buttons.length - 1]).toHaveFocus();
|
|
});
|
|
|
|
it('cycles focus from first element to last on Shift+Tab', () => {
|
|
render(<TrapComponent isActive />);
|
|
const buttons = screen.getAllByRole('button');
|
|
buttons[1]!.focus();
|
|
|
|
fireEvent.keyDown(buttons[1]!, { key: 'Tab', shiftKey: true });
|
|
|
|
expect(buttons[1]).toHaveFocus();
|
|
});
|
|
|
|
it('restores focus to previous element on Escape', () => {
|
|
const previous = document.createElement('button');
|
|
previous.textContent = 'previous';
|
|
document.body.appendChild(previous);
|
|
previous.focus();
|
|
|
|
render(<TrapComponent isActive />);
|
|
const buttons = screen.getAllByRole('button');
|
|
|
|
fireEvent.keyDown(buttons[1]!, { key: 'Escape' });
|
|
|
|
expect(previous).toHaveFocus();
|
|
document.body.removeChild(previous);
|
|
});
|
|
});
|
|
});
|