chore: sync marketing pages, CMS extensions, tests and project docs
同步工作区剩余变更,主要包括: - 营销页面组件与布局持续优化(about/news/services/solutions/team 等) - 详情页四层叙事组件、布局组件、UI 组件调整 - CMS 数据模型、API 路由、权限、工作流、站内通知、媒体管理扩展 - 新增/补充单元测试与 E2E 测试(cms-workflow.spec.ts 等) - ESLint 9 迁移、jest/tsconfig 配置更新、依赖调整 - 新增 ADR、CMS 评估文档、Release Review / Acceptance 报告 - 移除水墨装饰组件与大体积未使用字体文件
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user