feat(ui): 重构核心 UI 组件库,新增 shadcn/ui 组件
- 重构 Button、Card、Badge、Input、Textarea 等基础组件 - 新增 Accordion、Alert、Dialog、Dropdown、Form 等 shadcn/ui 组件 - 新增 AnimatedCounter、StatsShowcase、MetricCard 等数据展示组件 - 新增 ScrollReveal 滚动动画组件 - 重构 Toast 通知系统与 Tooltip 提示组件 - 更新设计令牌系统,对齐新品牌视觉
This commit is contained in:
@@ -1,54 +1,88 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { ScrollProgress } from './scroll-progress';
|
||||
|
||||
// Mock framer-motion
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, ...props }: React.ComponentProps<'div'>) => <div {...props}>{children}</div>,
|
||||
},
|
||||
useScroll: () => ({ scrollYProgress: { get: () => 0, onChange: jest.fn() } }),
|
||||
useSpring: () => ({ get: () => 0 }),
|
||||
}));
|
||||
|
||||
// Mock useReducedMotion
|
||||
jest.mock('@/hooks/use-reduced-motion', () => ({
|
||||
useReducedMotion: () => false,
|
||||
}));
|
||||
|
||||
describe('ScrollProgress', () => {
|
||||
let scrollYValue = 0;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
scrollYValue = 0;
|
||||
Object.defineProperty(window, 'scrollY', {
|
||||
get: () => scrollYValue,
|
||||
// Mock document height for predictable progress calculation
|
||||
Object.defineProperty(document.documentElement, 'scrollHeight', {
|
||||
value: 1000,
|
||||
configurable: true,
|
||||
});
|
||||
window.addEventListener = jest.fn((event, handler) => {
|
||||
if (event === 'scroll') {
|
||||
(handler as EventListener)(new Event('scroll'));
|
||||
}
|
||||
Object.defineProperty(window, 'innerHeight', {
|
||||
value: 500,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render when scroll position is less than 100px', () => {
|
||||
scrollYValue = 0;
|
||||
it('should always render the progress container', () => {
|
||||
const { container } = render(<ScrollProgress />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
// ScrollProgress 始终渲染(带 aria-hidden="true")
|
||||
expect(container.firstChild).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should render progressbar with correct ARIA attributes when visible', async () => {
|
||||
scrollYValue = 150;
|
||||
it('should have aria-hidden="true" for decorative purposes', () => {
|
||||
const { container } = render(<ScrollProgress />);
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
render(<ScrollProgress />);
|
||||
it('should apply fixed positioning class', () => {
|
||||
const { container } = render(<ScrollProgress />);
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).toHaveClass('fixed');
|
||||
expect(wrapper).toHaveClass('pointer-events-none');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const progressbar = screen.getByRole('progressbar');
|
||||
expect(progressbar).toBeInTheDocument();
|
||||
expect(progressbar).toHaveAttribute('aria-label', '页面滚动进度');
|
||||
expect(progressbar).toHaveAttribute('aria-valuemin', '0');
|
||||
expect(progressbar).toHaveAttribute('aria-valuemax', '100');
|
||||
});
|
||||
it('should render inner progress bar div', () => {
|
||||
const { container } = render(<ScrollProgress />);
|
||||
const innerBar = (container.firstChild as HTMLElement).firstChild as HTMLElement;
|
||||
expect(innerBar).toBeInTheDocument();
|
||||
// 默认 progress 为 0,宽度为 0%
|
||||
expect(innerBar.style.width).toBe('0%');
|
||||
});
|
||||
|
||||
it('should apply default height of 2px', () => {
|
||||
const { container } = render(<ScrollProgress />);
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper.style.height).toBe('2px');
|
||||
});
|
||||
|
||||
it('should support custom height', () => {
|
||||
const { container } = render(<ScrollProgress height={4} />);
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper.style.height).toBe('4px');
|
||||
});
|
||||
|
||||
it('should apply default top position', () => {
|
||||
const { container } = render(<ScrollProgress />);
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper.style.top).toBe('0px');
|
||||
});
|
||||
|
||||
it('should support bottom position', () => {
|
||||
const { container } = render(<ScrollProgress position="bottom" />);
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper.style.bottom).toBe('0px');
|
||||
});
|
||||
|
||||
it('should use brand color by default', () => {
|
||||
const { container } = render(<ScrollProgress />);
|
||||
const innerBar = (container.firstChild as HTMLElement).firstChild as HTMLElement;
|
||||
expect(innerBar.style.background).toBe('var(--color-brand)');
|
||||
});
|
||||
|
||||
it('should support custom color', () => {
|
||||
const { container } = render(<ScrollProgress color="#ff0000" />);
|
||||
const innerBar = (container.firstChild as HTMLElement).firstChild as HTMLElement;
|
||||
// jsdom 将 #ff0000 转换为 rgb(255, 0, 0)
|
||||
expect(innerBar.style.background).toContain('255');
|
||||
expect(innerBar.style.background).toContain('0');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user