test(hooks): finalize mutation test improvements and release acceptance
- Raise use-swipe-gesture mutation score to 66.13% (target 65%+) - Maintain use-reduced-motion mutation score at 76.32% (target 50%+) - Fix use-reduced-motion.ts ESLint set-state-in-effect warning - Add UJ-10 deep searcher journey (category browse → article read → content discovery) - Add 40 new test files (analytics, detail, sections, ui, lib components) - Update test-strategy-plan.md to v2.0 (sync test count to 1591) - Sync README.md with final release metrics Quality gates: TS 0 errors, ESLint 0 errors, 121 suites / 1591 tests passed
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from './accordion';
|
||||
|
||||
describe('Accordion Components', () => {
|
||||
describe('AccordionItem', () => {
|
||||
it('should render item with children', () => {
|
||||
render(
|
||||
<Accordion type="single">
|
||||
<AccordionItem value="item1">Item Content</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
expect(screen.getByText('Item Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(
|
||||
<Accordion type="single">
|
||||
<AccordionItem value="item1" data-testid="item">Item</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
expect(screen.getByTestId('item')).toHaveAttribute('data-slot', 'accordion-item');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<Accordion type="single">
|
||||
<AccordionItem value="item1" className="custom-item">Item</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
expect(screen.getByText('Item')).toHaveClass('custom-item');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AccordionTrigger', () => {
|
||||
it('should render trigger text', () => {
|
||||
render(
|
||||
<Accordion type="single">
|
||||
<AccordionItem value="item1">
|
||||
<AccordionTrigger>点击展开</AccordionTrigger>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
expect(screen.getByText('点击展开')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(
|
||||
<Accordion type="single">
|
||||
<AccordionItem value="item1">
|
||||
<AccordionTrigger data-testid="trigger">Trigger</AccordionTrigger>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
expect(screen.getByTestId('trigger')).toHaveAttribute('data-slot', 'accordion-trigger');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<Accordion type="single">
|
||||
<AccordionItem value="item1">
|
||||
<AccordionTrigger className="custom-trigger">Trigger</AccordionTrigger>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
expect(screen.getByText('Trigger')).toHaveClass('custom-trigger');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AccordionContent', () => {
|
||||
it('should render content when item is open', () => {
|
||||
render(
|
||||
<Accordion type="single" defaultValue="item1">
|
||||
<AccordionItem value="item1">
|
||||
<AccordionTrigger>标题</AccordionTrigger>
|
||||
<AccordionContent>展开内容</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
expect(screen.getByText('展开内容')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(
|
||||
<Accordion type="single" defaultValue="item1">
|
||||
<AccordionItem value="item1">
|
||||
<AccordionContent data-testid="content">Content</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
expect(screen.getByTestId('content')).toHaveAttribute('data-slot', 'accordion-content');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<Accordion type="single" defaultValue="item1">
|
||||
<AccordionItem value="item1">
|
||||
<AccordionContent className="custom-content">Content</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
expect(screen.getByText('Content')).toHaveClass('custom-content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accordion Composition', () => {
|
||||
it('should render complete accordion structure', () => {
|
||||
render(
|
||||
<Accordion type="single" defaultValue="faq1">
|
||||
<AccordionItem value="faq1">
|
||||
<AccordionTrigger>问题一</AccordionTrigger>
|
||||
<AccordionContent>答案一</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="faq2">
|
||||
<AccordionTrigger>问题二</AccordionTrigger>
|
||||
<AccordionContent>答案二</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
expect(screen.getByText('问题一')).toBeInTheDocument();
|
||||
expect(screen.getByText('答案一')).toBeInTheDocument();
|
||||
expect(screen.getByText('问题二')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Alert, AlertTitle, AlertDescription } from './alert';
|
||||
|
||||
describe('Alert', () => {
|
||||
it('renders default variant', () => {
|
||||
render(<Alert>Default alert</Alert>);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toBeInTheDocument();
|
||||
expect(alert).toHaveTextContent('Default alert');
|
||||
});
|
||||
|
||||
it('renders destructive variant', () => {
|
||||
render(<Alert variant="destructive">Destructive alert</Alert>);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toBeInTheDocument();
|
||||
expect(alert).toHaveTextContent('Destructive alert');
|
||||
});
|
||||
|
||||
it('renders success variant', () => {
|
||||
render(<Alert variant="success">Success alert</Alert>);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toBeInTheDocument();
|
||||
expect(alert).toHaveTextContent('Success alert');
|
||||
});
|
||||
|
||||
it('renders warning variant', () => {
|
||||
render(<Alert variant="warning">Warning alert</Alert>);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toBeInTheDocument();
|
||||
expect(alert).toHaveTextContent('Warning alert');
|
||||
});
|
||||
|
||||
it('renders info variant', () => {
|
||||
render(<Alert variant="info">Info alert</Alert>);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toBeInTheDocument();
|
||||
expect(alert).toHaveTextContent('Info alert');
|
||||
});
|
||||
|
||||
it('renders AlertTitle with data-slot', () => {
|
||||
const { container } = render(<AlertTitle>Title text</AlertTitle>);
|
||||
const title = container.querySelector('[data-slot="alert-title"]');
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveTextContent('Title text');
|
||||
});
|
||||
|
||||
it('renders AlertDescription with data-slot', () => {
|
||||
const { container } = render(<AlertDescription>Description text</AlertDescription>);
|
||||
const desc = container.querySelector('[data-slot="alert-description"]');
|
||||
expect(desc).toBeInTheDocument();
|
||||
expect(desc).toHaveTextContent('Description text');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Alert className="custom-class">Alert</Alert>);
|
||||
const alert = container.querySelector('[data-slot="alert"]');
|
||||
expect(alert).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Mock hooks
|
||||
jest.mock('@/hooks/use-count-up', () => ({
|
||||
useCountUp: jest.fn(() => 500),
|
||||
}));
|
||||
|
||||
jest.mock('@/hooks/use-reduced-motion', () => ({
|
||||
useReducedMotion: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
describe('AnimatedCounter', () => {
|
||||
it('renders value with prefix and suffix', () => {
|
||||
const { AnimatedCounter } = require('./animated-counter');
|
||||
render(<AnimatedCounter value={500} prefix="¥" suffix="+" />);
|
||||
expect(screen.getByText('¥500+')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with aria-label containing prefix, value, and suffix', () => {
|
||||
const { AnimatedCounter } = require('./animated-counter');
|
||||
render(<AnimatedCounter value={500} prefix="¥" suffix="+" />);
|
||||
const el = screen.getByText('¥500+');
|
||||
expect(el).toHaveAttribute('aria-label', '¥500+');
|
||||
});
|
||||
|
||||
it('renders with tabular-nums class', () => {
|
||||
const { AnimatedCounter } = require('./animated-counter');
|
||||
render(<AnimatedCounter value={500} />);
|
||||
const el = screen.getByText('500');
|
||||
expect(el).toHaveClass('tabular-nums');
|
||||
});
|
||||
|
||||
it('renders with custom decimals', () => {
|
||||
const { AnimatedCounter } = require('./animated-counter');
|
||||
render(<AnimatedCounter value={500} decimals={1} />);
|
||||
expect(screen.getByText('500.0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { AnimatedCounter } = require('./animated-counter');
|
||||
render(<AnimatedCounter value={500} className="custom-class" />);
|
||||
const el = screen.getByText('500');
|
||||
expect(el).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from './avatar';
|
||||
|
||||
describe('Avatar', () => {
|
||||
it('renders with data-slot="avatar"', () => {
|
||||
const { container } = render(
|
||||
<Avatar>
|
||||
<AvatarFallback>AB</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
const avatar = container.querySelector('[data-slot="avatar"]');
|
||||
expect(avatar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders AvatarImage with data-slot="avatar-image"', () => {
|
||||
const { container } = render(
|
||||
<Avatar>
|
||||
<AvatarImage src="https://example.com/photo.jpg" alt="test" />
|
||||
<AvatarFallback>AB</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
// Radix UI AvatarImage renders <img> only after the image loads in browser.
|
||||
// In jsdom the image never loads, so the fallback is rendered instead.
|
||||
// Note: Radix UI AvatarImage renders <img> only after the image loads in browser
|
||||
const fallback = container.querySelector('[data-slot="avatar-fallback"]');
|
||||
// Verify the component structure is valid: avatar root + fallback rendered
|
||||
expect(container.querySelector('[data-slot="avatar"]')).toBeInTheDocument();
|
||||
expect(fallback).toBeInTheDocument();
|
||||
expect(fallback).toHaveTextContent('AB');
|
||||
});
|
||||
|
||||
it('renders AvatarFallback with data-slot="avatar-fallback"', () => {
|
||||
const { container } = render(
|
||||
<Avatar>
|
||||
<AvatarFallback>AB</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
const fallback = container.querySelector('[data-slot="avatar-fallback"]');
|
||||
expect(fallback).toBeInTheDocument();
|
||||
expect(fallback).toHaveTextContent('AB');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<Avatar className="custom-class">
|
||||
<AvatarFallback>AB</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
const avatar = container.querySelector('[data-slot="avatar"]');
|
||||
expect(avatar).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Mock framer-motion using the same pattern as sections.test.tsx
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, initial, animate, whileInView, viewport, transition, className, style, ...props }: any) => (
|
||||
<div
|
||||
data-testid="motion-div"
|
||||
data-initial={JSON.stringify(initial)}
|
||||
data-animate={JSON.stringify(animate)}
|
||||
data-while-inview={JSON.stringify(whileInView)}
|
||||
data-viewport={JSON.stringify(viewport)}
|
||||
data-transition={JSON.stringify(transition)}
|
||||
className={className}
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
circle: ({ children, animate, transition, ...props }: any) => (
|
||||
<circle data-testid="motion-circle" data-animate={JSON.stringify(animate)} data-transition={JSON.stringify(transition)} {...props}>
|
||||
{children}
|
||||
</circle>
|
||||
),
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
}));
|
||||
|
||||
jest.mock('@/hooks/use-reduced-motion', () => ({
|
||||
useReducedMotion: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
describe('GeometricDecoration', () => {
|
||||
it('renders circles variant with SVG circles', () => {
|
||||
const { GeometricDecoration } = require('./brand-visuals');
|
||||
const { container } = render(<GeometricDecoration variant="circles" />);
|
||||
const svgs = container.querySelectorAll('svg');
|
||||
expect(svgs.length).toBeGreaterThan(0);
|
||||
const circles = container.querySelectorAll('circle');
|
||||
expect(circles.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders lines variant with SVG lines', () => {
|
||||
const { GeometricDecoration } = require('./brand-visuals');
|
||||
const { container } = render(<GeometricDecoration variant="lines" />);
|
||||
const svgs = container.querySelectorAll('svg');
|
||||
expect(svgs.length).toBeGreaterThan(0);
|
||||
const lines = container.querySelectorAll('line');
|
||||
expect(lines.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('returns null for unknown variant', () => {
|
||||
const { GeometricDecoration } = require('./brand-visuals');
|
||||
const { container } = render(<GeometricDecoration variant="grid" />);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DataBar', () => {
|
||||
it('renders label and percentage value', () => {
|
||||
const { DataBar } = require('./brand-visuals');
|
||||
render(<DataBar value={75} label="完成度" />);
|
||||
expect(screen.getByText('完成度')).toBeInTheDocument();
|
||||
expect(screen.getByText('75%')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GradientDivider', () => {
|
||||
it('renders brand dot', () => {
|
||||
const { GradientDivider } = require('./brand-visuals');
|
||||
const { container } = render(<GradientDivider />);
|
||||
const dot = container.querySelector('.rounded-full');
|
||||
expect(dot).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('BrandStamp', () => {
|
||||
it('renders children text', () => {
|
||||
const { BrandStamp } = require('./brand-visuals');
|
||||
render(<BrandStamp>Novalon</BrandStamp>);
|
||||
expect(screen.getByText('Novalon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
} from './breadcrumb';
|
||||
|
||||
describe('Breadcrumb Components', () => {
|
||||
describe('Breadcrumb', () => {
|
||||
it('should render nav element', () => {
|
||||
render(<Breadcrumb data-testid="breadcrumb" />);
|
||||
const nav = screen.getByTestId('breadcrumb');
|
||||
expect(nav.tagName).toBe('NAV');
|
||||
expect(nav).toHaveAttribute('aria-label', 'breadcrumb');
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(<Breadcrumb data-testid="breadcrumb" />);
|
||||
expect(screen.getByTestId('breadcrumb')).toHaveAttribute('data-slot', 'breadcrumb');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<Breadcrumb className="custom-breadcrumb" data-testid="breadcrumb" />);
|
||||
expect(screen.getByTestId('breadcrumb')).toHaveClass('custom-breadcrumb');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BreadcrumbList', () => {
|
||||
it('should render list with items', () => {
|
||||
render(
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>首页</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
);
|
||||
expect(screen.getByText('首页')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(<BreadcrumbList data-testid="list" />);
|
||||
expect(screen.getByTestId('list')).toHaveAttribute('data-slot', 'breadcrumb-list');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BreadcrumbItem', () => {
|
||||
it('should render item content', () => {
|
||||
render(<BreadcrumbItem>产品</BreadcrumbItem>);
|
||||
expect(screen.getByText('产品')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(<BreadcrumbItem data-testid="item">Item</BreadcrumbItem>);
|
||||
expect(screen.getByTestId('item')).toHaveAttribute('data-slot', 'breadcrumb-item');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BreadcrumbLink', () => {
|
||||
it('should render link', () => {
|
||||
render(<BreadcrumbLink href="/">首页</BreadcrumbLink>);
|
||||
expect(screen.getByText('首页')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have href attribute', () => {
|
||||
render(<BreadcrumbLink href="/products">产品</BreadcrumbLink>);
|
||||
const link = screen.getByText('产品');
|
||||
expect(link).toHaveAttribute('href', '/products');
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(<BreadcrumbLink href="/" data-testid="link">Link</BreadcrumbLink>);
|
||||
expect(screen.getByTestId('link')).toHaveAttribute('data-slot', 'breadcrumb-link');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BreadcrumbPage', () => {
|
||||
it('should render current page indicator', () => {
|
||||
render(<BreadcrumbPage>当前页面</BreadcrumbPage>);
|
||||
const page = screen.getByText('当前页面');
|
||||
expect(page).toBeInTheDocument();
|
||||
expect(page).toHaveAttribute('aria-current', 'page');
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(<BreadcrumbPage data-testid="page">Page</BreadcrumbPage>);
|
||||
expect(screen.getByTestId('page')).toHaveAttribute('data-slot', 'breadcrumb-page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BreadcrumbSeparator', () => {
|
||||
it('should render separator', () => {
|
||||
const { container } = render(<BreadcrumbSeparator />);
|
||||
const separator = container.querySelector('[data-slot="breadcrumb-separator"]');
|
||||
expect(separator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render custom separator', () => {
|
||||
render(<BreadcrumbSeparator>/</BreadcrumbSeparator>);
|
||||
expect(screen.getByText('/')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have aria-hidden', () => {
|
||||
const { container } = render(<BreadcrumbSeparator />);
|
||||
const separator = container.querySelector('[data-slot="breadcrumb-separator"]');
|
||||
expect(separator).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BreadcrumbEllipsis', () => {
|
||||
it('should render ellipsis', () => {
|
||||
const { container } = render(<BreadcrumbEllipsis />);
|
||||
const ellipsis = container.querySelector('[data-slot="breadcrumb-ellipsis"]');
|
||||
expect(ellipsis).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have aria-hidden', () => {
|
||||
const { container } = render(<BreadcrumbEllipsis />);
|
||||
const ellipsis = container.querySelector('[data-slot="breadcrumb-ellipsis"]');
|
||||
expect(ellipsis).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Breadcrumb Composition', () => {
|
||||
it('should render complete breadcrumb trail', () => {
|
||||
render(
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/">首页</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/products">产品</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>ERP 系统</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
);
|
||||
|
||||
expect(screen.getByText('首页')).toBeInTheDocument();
|
||||
expect(screen.getByText('产品')).toBeInTheDocument();
|
||||
expect(screen.getByText('ERP 系统')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { ChallengeCard } from './challenge-card';
|
||||
|
||||
// ─── Mocks ───────────────────────────────────────────────────────────────
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
ArrowRight: (props: any) => <svg data-testid="icon-arrow-right" className={props.className} />,
|
||||
Lock: (props: any) => <svg data-testid="icon-lock" className={props.className} />,
|
||||
TrendingUp: (props: any) => <svg data-testid="icon-trending-up" className={props.className} />,
|
||||
Shield: (props: any) => <svg data-testid="icon-shield" className={props.className} />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/card', () => ({
|
||||
Card: ({ children, className, ...props }: any) => (
|
||||
<div className={className} data-testid="card" {...props}>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('ChallengeCard', () => {
|
||||
const baseProps = {
|
||||
title: '数据孤岛挑战',
|
||||
description: '企业内部系统数据不互通,信息孤岛严重',
|
||||
href: '/solutions/data-integration',
|
||||
index: 0,
|
||||
};
|
||||
|
||||
it('should render title', () => {
|
||||
render(<ChallengeCard {...baseProps} scenario="isolation" />);
|
||||
expect(screen.getByText('数据孤岛挑战')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render description', () => {
|
||||
render(<ChallengeCard {...baseProps} scenario="isolation" />);
|
||||
expect(screen.getByText('企业内部系统数据不互通,信息孤岛严重')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render index number', () => {
|
||||
render(<ChallengeCard {...baseProps} scenario="isolation" />);
|
||||
expect(screen.getByText('01')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with correct href', () => {
|
||||
render(<ChallengeCard {...baseProps} scenario="isolation" />);
|
||||
const link = screen.getByText('数据孤岛挑战').closest('a');
|
||||
expect(link).toHaveAttribute('href', '/solutions/data-integration');
|
||||
});
|
||||
|
||||
it('should render "了解方案" link text', () => {
|
||||
render(<ChallengeCard {...baseProps} scenario="isolation" />);
|
||||
expect(screen.getByText('了解方案')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render different scenarios', () => {
|
||||
const { rerender } = render(<ChallengeCard {...baseProps} scenario="isolation" />);
|
||||
expect(screen.getByTestId('icon-lock')).toBeInTheDocument();
|
||||
|
||||
rerender(<ChallengeCard {...baseProps} scenario="growth" />);
|
||||
expect(screen.getByTestId('icon-trending-up')).toBeInTheDocument();
|
||||
|
||||
rerender(<ChallengeCard {...baseProps} scenario="compliance" />);
|
||||
expect(screen.getByTestId('icon-shield')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correct index padding', () => {
|
||||
const { rerender } = render(<ChallengeCard {...baseProps} index={0} scenario="isolation" />);
|
||||
expect(screen.getByText('01')).toBeInTheDocument();
|
||||
|
||||
rerender(<ChallengeCard {...baseProps} index={9} scenario="isolation" />);
|
||||
expect(screen.getByText('10')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Checkbox } from './checkbox';
|
||||
|
||||
describe('Checkbox', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render checkbox', () => {
|
||||
render(<Checkbox />);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
expect(checkbox).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with label', () => {
|
||||
render(
|
||||
<label>
|
||||
<Checkbox /> 同意条款
|
||||
</label>
|
||||
);
|
||||
expect(screen.getByRole('checkbox')).toBeInTheDocument();
|
||||
expect(screen.getByText('同意条款')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
const { container } = render(<Checkbox />);
|
||||
const checkbox = container.querySelector('[data-slot="checkbox"]');
|
||||
expect(checkbox).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Checked State', () => {
|
||||
it('should start unchecked by default', () => {
|
||||
render(<Checkbox />);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should render as checked', () => {
|
||||
render(<Checkbox checked />);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
expect(checkbox).toBeChecked();
|
||||
});
|
||||
|
||||
it('should render as unchecked', () => {
|
||||
render(<Checkbox checked={false} />);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Interaction', () => {
|
||||
it('should handle onCheckedChange event', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(<Checkbox onCheckedChange={handleChange} />);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
|
||||
await userEvent.click(checkbox);
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
expect(handleChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should toggle checked state on click', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(<Checkbox onCheckedChange={handleChange} />);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
|
||||
await userEvent.click(checkbox);
|
||||
expect(handleChange).toHaveBeenCalledWith(true);
|
||||
|
||||
await userEvent.click(checkbox);
|
||||
expect(handleChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Styling', () => {
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<Checkbox className="custom-checkbox" />);
|
||||
const checkbox = container.querySelector('[data-slot="checkbox"]');
|
||||
expect(checkbox).toHaveClass('custom-checkbox');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Disabled State', () => {
|
||||
it('should be disabled when disabled prop is true', () => {
|
||||
render(<Checkbox disabled />);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
expect(checkbox).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should not respond to clicks when disabled', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(<Checkbox disabled onCheckedChange={handleChange} />);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
|
||||
await userEvent.click(checkbox);
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Mock constants
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
PRODUCTS: [
|
||||
{ id: 'erp', title: 'ERP' },
|
||||
{ id: 'crm', title: 'CRM' },
|
||||
{ id: 'bi', title: 'BI' },
|
||||
],
|
||||
SERVICES: [
|
||||
{ id: 'consulting', title: '咨询' },
|
||||
{ id: 'dev', title: '开发' },
|
||||
],
|
||||
}));
|
||||
|
||||
// Mock SwipeNavigation
|
||||
jest.mock('@/hooks/use-swipe-gesture', () => ({
|
||||
SwipeNavigation: ({ prevRoute, nextRoute, prevLabel, nextLabel, className }: any) => (
|
||||
<div data-testid="swipe-navigation" data-prev-route={prevRoute} data-next-route={nextRoute} data-prev-label={prevLabel} data-next-label={nextLabel} className={className} />
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock framer-motion
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, ...props }: any) => <div data-testid="motion-div" {...props}>{children}</div>,
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
}));
|
||||
|
||||
// Mock next/navigation
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: jest.fn(() => ({ push: jest.fn() })),
|
||||
}));
|
||||
|
||||
describe('DetailSwipeNav', () => {
|
||||
it('renders prev/next navigation for product type', () => {
|
||||
const { DetailSwipeNav } = require('./detail-swipe-nav');
|
||||
// currentId='crm' is the middle item, so prev=erp, next=bi
|
||||
const { container } = render(<DetailSwipeNav type="product" currentId="crm" />);
|
||||
const nav = container.querySelector('[data-testid="swipe-navigation"]');
|
||||
expect(nav).toBeInTheDocument();
|
||||
expect(nav).toHaveAttribute('data-prev-route', '/products/erp');
|
||||
expect(nav).toHaveAttribute('data-next-route', '/products/bi');
|
||||
expect(nav).toHaveAttribute('data-prev-label', 'ERP');
|
||||
expect(nav).toHaveAttribute('data-next-label', 'BI');
|
||||
});
|
||||
|
||||
it('renders prev/next navigation for service type', () => {
|
||||
const { DetailSwipeNav } = require('./detail-swipe-nav');
|
||||
// currentId='consulting' is the first item, so prev=undefined, next=dev
|
||||
const { container } = render(<DetailSwipeNav type="service" currentId="consulting" />);
|
||||
const nav = container.querySelector('[data-testid="swipe-navigation"]');
|
||||
expect(nav).toBeInTheDocument();
|
||||
expect(nav).not.toHaveAttribute('data-prev-route');
|
||||
expect(nav).toHaveAttribute('data-next-route', '/services/dev');
|
||||
expect(nav).toHaveAttribute('data-prev-label', '');
|
||||
expect(nav).toHaveAttribute('data-next-label', '开发');
|
||||
});
|
||||
|
||||
it('returns null for solution type', () => {
|
||||
const { DetailSwipeNav } = require('./detail-swipe-nav');
|
||||
const { container } = render(<DetailSwipeNav type="solution" currentId="some-solution" />);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('uses correct base paths', () => {
|
||||
const { DetailSwipeNav } = require('./detail-swipe-nav');
|
||||
const { container } = render(<DetailSwipeNav type="product" currentId="erp" />);
|
||||
const nav = container.querySelector('[data-testid="swipe-navigation"]');
|
||||
expect(nav).toHaveAttribute('data-next-route', '/products/crm');
|
||||
expect(nav).not.toHaveAttribute('data-prev-route');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from './dialog';
|
||||
|
||||
describe('Dialog Components', () => {
|
||||
describe('DialogTrigger', () => {
|
||||
it('should render trigger button', () => {
|
||||
render(
|
||||
<Dialog>
|
||||
<DialogTrigger>打开对话框</DialogTrigger>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByText('打开对话框')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DialogHeader', () => {
|
||||
it('should render header with children', () => {
|
||||
render(<DialogHeader>Header Content</DialogHeader>);
|
||||
expect(screen.getByText('Header Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(<DialogHeader data-testid="header">Header</DialogHeader>);
|
||||
expect(screen.getByTestId('header')).toHaveAttribute('data-slot', 'dialog-header');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<DialogHeader className="custom-class">Header</DialogHeader>);
|
||||
const header = screen.getByText('Header');
|
||||
expect(header).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DialogTitle', () => {
|
||||
it('should render title text', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogTitle>Dialog Title</DialogTitle>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByText('Dialog Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogTitle data-testid="title">Title</DialogTitle>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByTestId('title')).toHaveAttribute('data-slot', 'dialog-title');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogTitle className="custom-title">Title</DialogTitle>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByText('Title')).toHaveClass('custom-title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DialogDescription', () => {
|
||||
it('should render description text', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogDescription>Dialog Description</DialogDescription>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByText('Dialog Description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogDescription data-testid="desc">Desc</DialogDescription>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByTestId('desc')).toHaveAttribute('data-slot', 'dialog-description');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogDescription className="custom-desc">Desc</DialogDescription>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByText('Desc')).toHaveClass('custom-desc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DialogContent', () => {
|
||||
it('should render content with children when open', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogContent>Content Body</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByText('Content Body')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dialog Composition', () => {
|
||||
it('should render complete dialog structure', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogTrigger>打开</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>确认操作</DialogTitle>
|
||||
<DialogDescription>确定要执行此操作吗?</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
expect(screen.getByText('确认操作')).toBeInTheDocument();
|
||||
expect(screen.getByText('确定要执行此操作吗?')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
} from './dropdown-menu';
|
||||
|
||||
describe('DropdownMenu Components', () => {
|
||||
describe('DropdownMenuTrigger', () => {
|
||||
it('should render trigger button', () => {
|
||||
render(
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>菜单</DropdownMenuTrigger>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('菜单')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DropdownMenuLabel', () => {
|
||||
it('should render label text', () => {
|
||||
render(<DropdownMenuLabel>分类名称</DropdownMenuLabel>);
|
||||
expect(screen.getByText('分类名称')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(<DropdownMenuLabel data-testid="label">Label</DropdownMenuLabel>);
|
||||
expect(screen.getByTestId('label')).toHaveAttribute('data-slot', 'dropdown-menu-label');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<DropdownMenuLabel className="custom-label">Label</DropdownMenuLabel>);
|
||||
expect(screen.getByText('Label')).toHaveClass('custom-label');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DropdownMenuItem', () => {
|
||||
it('should render item text', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>菜单项</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('菜单项')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem data-testid="item">Item</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByTestId('item')).toHaveAttribute('data-slot', 'dropdown-menu-item');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem className="custom-item" data-testid="item">Item</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
const item = screen.getByTestId('item');
|
||||
expect(item).toHaveClass('custom-item');
|
||||
});
|
||||
|
||||
it('should handle disabled state', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem disabled data-testid="item">Disabled Item</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
const item = screen.getByTestId('item');
|
||||
expect(item).toHaveAttribute('data-disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DropdownMenuSeparator', () => {
|
||||
it('should render separator', () => {
|
||||
const { container } = render(<DropdownMenuSeparator />);
|
||||
const separator = container.querySelector('[data-slot="dropdown-menu-separator"]');
|
||||
expect(separator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<DropdownMenuSeparator className="custom-sep" />);
|
||||
const separator = container.querySelector('[data-slot="dropdown-menu-separator"]');
|
||||
expect(separator).toHaveClass('custom-sep');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DropdownMenuContent', () => {
|
||||
it('should render content with items', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>编辑</DropdownMenuItem>
|
||||
<DropdownMenuItem>删除</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('编辑')).toBeInTheDocument();
|
||||
expect(screen.getByText('删除')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DropdownMenu Composition', () => {
|
||||
it('should render complete menu structure', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger>操作</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>操作选项</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>编辑</DropdownMenuItem>
|
||||
<DropdownMenuItem>删除</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
expect(screen.getByText('操作选项')).toBeInTheDocument();
|
||||
expect(screen.getByText('编辑')).toBeInTheDocument();
|
||||
expect(screen.getByText('删除')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,209 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { LoadingState, Spinner, PageLoader } from './loading-state';
|
||||
|
||||
// ─── Mocks ───────────────────────────────────────────────────────────────
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>{children}</div>
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/skeleton', () => ({
|
||||
Skeleton: ({ className }: any) => <div className={className} data-testid="skeleton" />,
|
||||
SkeletonHero: () => <div data-testid="skeleton-hero" />,
|
||||
SkeletonList: ({ items }: any) => <div data-testid="skeleton-list" data-items={items} />,
|
||||
SkeletonCard: () => <div data-testid="skeleton-card" />,
|
||||
SkeletonForm: ({ fields }: any) => <div data-testid="skeleton-form" data-fields={fields} />,
|
||||
}));
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Spinner', () => {
|
||||
it('should render with default size', () => {
|
||||
render(<Spinner />);
|
||||
const svg = document.querySelector('svg');
|
||||
expect(svg).toBeInTheDocument();
|
||||
expect(svg).toHaveAttribute('role', 'status');
|
||||
});
|
||||
|
||||
it('should render with small size', () => {
|
||||
render(<Spinner size="sm" />);
|
||||
const svg = document.querySelector('svg');
|
||||
expect(svg?.getAttribute('class')).toContain('w-4');
|
||||
});
|
||||
|
||||
it('should render with large size', () => {
|
||||
render(<Spinner size="lg" />);
|
||||
const svg = document.querySelector('svg');
|
||||
expect(svg?.getAttribute('class')).toContain('w-12');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<Spinner className="custom-spinner" />);
|
||||
const svg = document.querySelector('svg');
|
||||
expect(svg?.getAttribute('class')).toContain('custom-spinner');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PageLoader', () => {
|
||||
it('should render when loading', () => {
|
||||
render(<PageLoader isLoading={true} />);
|
||||
expect(screen.getByText('正在加载...')).toBeInTheDocument();
|
||||
expect(screen.getByRole('alertdialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render custom message', () => {
|
||||
render(<PageLoader isLoading={true} message="请稍候..." />);
|
||||
expect(screen.getByText('请稍候...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render when not loading', () => {
|
||||
const { container } = render(<PageLoader isLoading={false} />);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('LoadingState', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should render children when not loading', () => {
|
||||
render(
|
||||
<LoadingState isLoading={false}>
|
||||
<div data-testid="content">Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
expect(screen.getByTestId('content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show skeleton after delay when loading', () => {
|
||||
// Mount as not loading first, then switch to loading
|
||||
const { rerender } = render(
|
||||
<LoadingState isLoading={false}>
|
||||
<div data-testid="content">Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
expect(screen.getByTestId('content')).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<LoadingState isLoading={true}>
|
||||
<div data-testid="content">Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
// Before delay, skeleton should not be shown
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
||||
|
||||
// After delay, skeleton should appear
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(150);
|
||||
});
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render hero variant skeleton', () => {
|
||||
const { rerender } = render(
|
||||
<LoadingState isLoading={false} variant="hero">
|
||||
<div>Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
rerender(
|
||||
<LoadingState isLoading={true} variant="hero">
|
||||
<div>Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(150);
|
||||
});
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('skeleton-hero')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render list variant skeleton', () => {
|
||||
const { rerender } = render(
|
||||
<LoadingState isLoading={false} variant="list">
|
||||
<div>Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
rerender(
|
||||
<LoadingState isLoading={true} variant="list">
|
||||
<div>Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(150);
|
||||
});
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('skeleton-list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render card variant skeleton', () => {
|
||||
const { rerender } = render(
|
||||
<LoadingState isLoading={false} variant="card">
|
||||
<div>Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
rerender(
|
||||
<LoadingState isLoading={true} variant="card">
|
||||
<div>Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(150);
|
||||
});
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render form variant skeleton', () => {
|
||||
const { rerender } = render(
|
||||
<LoadingState isLoading={false} variant="form">
|
||||
<div>Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
rerender(
|
||||
<LoadingState isLoading={true} variant="form">
|
||||
<div>Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(150);
|
||||
});
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('skeleton-form')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render children when loading stops', () => {
|
||||
const { rerender } = render(
|
||||
<LoadingState isLoading={false}>
|
||||
<div data-testid="content">Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
|
||||
rerender(
|
||||
<LoadingState isLoading={true}>
|
||||
<div data-testid="content">Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(150);
|
||||
});
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<LoadingState isLoading={false}>
|
||||
<div data-testid="content">Content</div>
|
||||
</LoadingState>
|
||||
);
|
||||
expect(screen.getByTestId('content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { MetricCard } from './metric-card';
|
||||
|
||||
// ─── Mocks ───────────────────────────────────────────────────────────────
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>{children}</div>
|
||||
),
|
||||
span: ({ children, className, ...props }: any) => (
|
||||
<span className={className} {...props}>{children}</span>
|
||||
),
|
||||
},
|
||||
useInView: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
ArrowUpRight: (props: any) => <svg data-testid="icon-trend-up" className={props.className} />,
|
||||
ArrowDownRight: (props: any) => <svg data-testid="icon-trend-down" className={props.className} />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/animated-counter', () => ({
|
||||
AnimatedCounter: ({ value, prefix, suffix }: any) => (
|
||||
<span>{prefix}{value}{suffix}</span>
|
||||
),
|
||||
}));
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('MetricCard', () => {
|
||||
it('should render label and value', () => {
|
||||
render(<MetricCard label="客户数" value={500} />);
|
||||
expect(screen.getByText('客户数')).toBeInTheDocument();
|
||||
expect(screen.getByText('500')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render icon when provided', () => {
|
||||
render(
|
||||
<MetricCard
|
||||
label="收入"
|
||||
value={1000}
|
||||
icon={<span data-testid="custom-icon">$</span>}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render prefix and suffix', () => {
|
||||
render(<MetricCard label="增长率" value={99} prefix="+" suffix="%" />);
|
||||
expect(screen.getByText('+99%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render description when provided', () => {
|
||||
render(<MetricCard label="用户" value={1000} description="活跃用户数" />);
|
||||
expect(screen.getByText('活跃用户数')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render trend with up direction', () => {
|
||||
render(
|
||||
<MetricCard
|
||||
label="收入"
|
||||
value={500}
|
||||
trend={{ value: '+12%', direction: 'up' }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('+12%')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('icon-trend-up')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render trend with down direction', () => {
|
||||
render(
|
||||
<MetricCard
|
||||
label="流失率"
|
||||
value={5}
|
||||
trend={{ value: '-2%', direction: 'down', label: '较上月' }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('-2%')).toBeInTheDocument();
|
||||
expect(screen.getByText('较上月')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('icon-trend-down')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(
|
||||
<MetricCard label="测试" value={1} className="custom-class" />
|
||||
);
|
||||
const div = container.querySelector('.custom-class');
|
||||
expect(div).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render in dark theme', () => {
|
||||
render(<MetricCard label="Dark" value={100} theme="dark" />);
|
||||
expect(screen.getByText('Dark')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with different accent colors', () => {
|
||||
render(<MetricCard label="Blue" value={1} accentColor="blue" />);
|
||||
expect(screen.getByText('Blue')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
} from './pagination';
|
||||
|
||||
describe('Pagination Components', () => {
|
||||
describe('Pagination', () => {
|
||||
it('should render nav element', () => {
|
||||
render(<Pagination data-testid="pagination" />);
|
||||
const nav = screen.getByTestId('pagination');
|
||||
expect(nav.tagName).toBe('NAV');
|
||||
expect(nav).toHaveAttribute('aria-label', 'pagination');
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(<Pagination data-testid="pagination" />);
|
||||
expect(screen.getByTestId('pagination')).toHaveAttribute('data-slot', 'pagination');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<Pagination className="custom-pagination" data-testid="pagination" />);
|
||||
expect(screen.getByTestId('pagination')).toHaveClass('custom-pagination');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaginationContent', () => {
|
||||
it('should render as ul element', () => {
|
||||
render(<PaginationContent data-testid="content" />);
|
||||
expect(screen.getByTestId('content').tagName).toBe('UL');
|
||||
});
|
||||
|
||||
it('should render children', () => {
|
||||
render(
|
||||
<PaginationContent>
|
||||
<PaginationItem>Page 1</PaginationItem>
|
||||
</PaginationContent>
|
||||
);
|
||||
expect(screen.getByText('Page 1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaginationItem', () => {
|
||||
it('should render as li element', () => {
|
||||
render(<PaginationItem data-testid="item" />);
|
||||
expect(screen.getByTestId('item').tagName).toBe('LI');
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(<PaginationItem data-testid="item" />);
|
||||
expect(screen.getByTestId('item')).toHaveAttribute('data-slot', 'pagination-item');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaginationLink', () => {
|
||||
it('should render page button', () => {
|
||||
render(<PaginationLink>1</PaginationLink>);
|
||||
expect(screen.getByText('1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should mark active page', () => {
|
||||
render(<PaginationLink isActive>2</PaginationLink>);
|
||||
const link = screen.getByText('2');
|
||||
expect(link).toHaveAttribute('aria-current', 'page');
|
||||
expect(link).toHaveAttribute('data-active', 'true');
|
||||
});
|
||||
|
||||
it('should handle click events', async () => {
|
||||
const handleClick = jest.fn();
|
||||
render(<PaginationLink onClick={handleClick}>3</PaginationLink>);
|
||||
await userEvent.click(screen.getByText('3'));
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<PaginationLink className="custom-link">4</PaginationLink>);
|
||||
expect(screen.getByText('4')).toHaveClass('custom-link');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaginationPrevious', () => {
|
||||
it('should render previous button', () => {
|
||||
render(<PaginationPrevious />);
|
||||
expect(screen.getByText('上一页')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have aria-label', () => {
|
||||
render(<PaginationPrevious />);
|
||||
expect(screen.getByLabelText('上一页')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaginationNext', () => {
|
||||
it('should render next button', () => {
|
||||
render(<PaginationNext />);
|
||||
expect(screen.getByText('下一页')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have aria-label', () => {
|
||||
render(<PaginationNext />);
|
||||
expect(screen.getByLabelText('下一页')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaginationEllipsis', () => {
|
||||
it('should render ellipsis', () => {
|
||||
const { container } = render(<PaginationEllipsis />);
|
||||
const ellipsis = container.querySelector('[data-slot="pagination-ellipsis"]');
|
||||
expect(ellipsis).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have aria-hidden', () => {
|
||||
const { container } = render(<PaginationEllipsis />);
|
||||
const ellipsis = container.querySelector('[data-slot="pagination-ellipsis"]');
|
||||
expect(ellipsis).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination Composition', () => {
|
||||
it('should render complete pagination', () => {
|
||||
render(
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious />
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink isActive>1</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink>2</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink>3</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
);
|
||||
|
||||
expect(screen.getByText('上一页')).toBeInTheDocument();
|
||||
expect(screen.getByText('1')).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument();
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
expect(screen.getByText('下一页')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { ProductCard } from './product-card';
|
||||
|
||||
// Mock all lucide-react icons used in product-card
|
||||
jest.mock('lucide-react', () => {
|
||||
const mockIcon = (name: string) => {
|
||||
const Icon = (props: any) => <svg data-testid={`icon-${name.toLowerCase()}`} className={props.className} strokeWidth={props.strokeWidth} />;
|
||||
Icon.displayName = name;
|
||||
return Icon;
|
||||
};
|
||||
return {
|
||||
ArrowUpRight: mockIcon('arrow-up-right'),
|
||||
Database: mockIcon('database'),
|
||||
Users: mockIcon('users'),
|
||||
BarChart3: mockIcon('bar-chart-3'),
|
||||
FileText: mockIcon('file-text'),
|
||||
Truck: mockIcon('truck'),
|
||||
Building2: mockIcon('building2'),
|
||||
};
|
||||
});
|
||||
|
||||
describe('ProductCard', () => {
|
||||
it('renders title and description', () => {
|
||||
render(
|
||||
<ProductCard
|
||||
title="ERP 系统"
|
||||
description="企业资源管理系统"
|
||||
href="/products/erp"
|
||||
index={0}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('ERP 系统')).toBeInTheDocument();
|
||||
expect(screen.getByText('企业资源管理系统')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with correct href on the anchor', () => {
|
||||
render(
|
||||
<ProductCard
|
||||
title="ERP"
|
||||
description="Desc"
|
||||
href="/products/erp"
|
||||
index={0}
|
||||
/>
|
||||
);
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/products/erp');
|
||||
});
|
||||
|
||||
it('renders status badge when provided with 已发布', () => {
|
||||
render(
|
||||
<ProductCard
|
||||
title="ERP"
|
||||
description="Desc"
|
||||
href="/products/erp"
|
||||
index={0}
|
||||
status="已发布"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('已发布')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders status badge when provided with 内测中', () => {
|
||||
render(
|
||||
<ProductCard
|
||||
title="ERP"
|
||||
description="Desc"
|
||||
href="/products/erp"
|
||||
index={0}
|
||||
status="内测中"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('内测中')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders status badge when provided with 研发中', () => {
|
||||
render(
|
||||
<ProductCard
|
||||
title="ERP"
|
||||
description="Desc"
|
||||
href="/products/erp"
|
||||
index={0}
|
||||
status="研发中"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('研发中')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows development notice for 研发中 status', () => {
|
||||
render(
|
||||
<ProductCard
|
||||
title="ERP"
|
||||
description="Desc"
|
||||
href="/products/erp"
|
||||
index={0}
|
||||
status="研发中"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('正在积极开发中,欢迎提前交流需求')).toBeInTheDocument();
|
||||
expect(screen.getByText('了解规划')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows internal notice for 内测中 status', () => {
|
||||
render(
|
||||
<ProductCard
|
||||
title="ERP"
|
||||
description="Desc"
|
||||
href="/products/erp"
|
||||
index={0}
|
||||
status="内测中"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('即将上线,欢迎预约内测体验')).toBeInTheDocument();
|
||||
expect(screen.getByText('申请内测')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show development notice for 已发布 status', () => {
|
||||
render(
|
||||
<ProductCard
|
||||
title="ERP"
|
||||
description="Desc"
|
||||
href="/products/erp"
|
||||
index={0}
|
||||
status="已发布"
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByText('正在积极开发中,欢迎提前交流需求')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('即将上线,欢迎预约内测体验')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('了解更多')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders index number formatted as 01, 02, etc.', () => {
|
||||
const { rerender } = render(
|
||||
<ProductCard
|
||||
title="ERP"
|
||||
description="Desc"
|
||||
href="/products/erp"
|
||||
index={0}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('01')).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<ProductCard
|
||||
title="CRM"
|
||||
description="Desc"
|
||||
href="/products/crm"
|
||||
index={1}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('02')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Progress } from './progress';
|
||||
|
||||
describe('Progress', () => {
|
||||
it('renders with data-slot="progress"', () => {
|
||||
const { container } = render(<Progress value={50} />);
|
||||
const progress = container.querySelector('[data-slot="progress"]');
|
||||
expect(progress).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders indicator with data-slot="progress-indicator"', () => {
|
||||
const { container } = render(<Progress value={50} />);
|
||||
const indicator = container.querySelector('[data-slot="progress-indicator"]');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Progress value={50} className="custom-class" />);
|
||||
const progress = container.querySelector('[data-slot="progress"]');
|
||||
expect(progress).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('applies custom indicatorClassName', () => {
|
||||
const { container } = render(<Progress value={50} indicatorClassName="indicator-class" />);
|
||||
const indicator = container.querySelector('[data-slot="progress-indicator"]');
|
||||
expect(indicator).toHaveClass('indicator-class');
|
||||
});
|
||||
|
||||
it('shows correct progress value', () => {
|
||||
const { container } = render(<Progress value={75} />);
|
||||
const indicator = container.querySelector('[data-slot="progress-indicator"]');
|
||||
expect(indicator).toHaveStyle({ transform: 'translateX(-25%)' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { RadioGroup, RadioGroupItem } from './radio-group';
|
||||
|
||||
// Mock the Circle icon from lucide-react used in RadioGroupItem
|
||||
jest.mock('lucide-react', () => ({
|
||||
Circle: (props: any) => <svg data-testid="icon-circle" {...props} />,
|
||||
}));
|
||||
|
||||
describe('RadioGroup', () => {
|
||||
it('renders RadioGroup with data-slot="radio-group"', () => {
|
||||
const { container } = render(
|
||||
<RadioGroup>
|
||||
<RadioGroupItem value="1" />
|
||||
</RadioGroup>
|
||||
);
|
||||
const group = container.querySelector('[data-slot="radio-group"]');
|
||||
expect(group).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders RadioGroupItem with data-slot="radio-group-item"', () => {
|
||||
const { container } = render(
|
||||
<RadioGroup>
|
||||
<RadioGroupItem value="1" />
|
||||
</RadioGroup>
|
||||
);
|
||||
const item = container.querySelector('[data-slot="radio-group-item"]');
|
||||
expect(item).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('RadioGroupItem shows indicator on checked state', () => {
|
||||
render(
|
||||
<RadioGroup value="1">
|
||||
<RadioGroupItem value="1" />
|
||||
</RadioGroup>
|
||||
);
|
||||
const radio = screen.getByRole('radio');
|
||||
expect(radio).toBeChecked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from './select';
|
||||
|
||||
describe('Select Components', () => {
|
||||
describe('SelectTrigger', () => {
|
||||
it('should render trigger with placeholder', () => {
|
||||
render(
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择" />
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
);
|
||||
expect(screen.getByText('请选择')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(
|
||||
<Select>
|
||||
<SelectTrigger data-testid="trigger">
|
||||
<SelectValue placeholder="选择" />
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
);
|
||||
expect(screen.getByTestId('trigger')).toHaveAttribute('data-slot', 'select-trigger');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<Select>
|
||||
<SelectTrigger className="custom-trigger" data-testid="trigger">
|
||||
<SelectValue placeholder="选择" />
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
);
|
||||
expect(screen.getByTestId('trigger')).toHaveClass('custom-trigger');
|
||||
});
|
||||
|
||||
it('should render with selected value', () => {
|
||||
render(
|
||||
<Select value="option1">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">选项一</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
expect(screen.getByText('选项一')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SelectContent', () => {
|
||||
it('should render content with items when open', () => {
|
||||
render(
|
||||
<Select open>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">选项一</SelectItem>
|
||||
<SelectItem value="option2">选项二</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
const items = screen.getAllByRole('option');
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[0]).toHaveTextContent('选项一');
|
||||
expect(items[1]).toHaveTextContent('选项二');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SelectItem', () => {
|
||||
it('should render item text', () => {
|
||||
render(
|
||||
<Select open>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">选项一</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
const item = screen.getByRole('option');
|
||||
expect(item).toHaveTextContent('选项一');
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(
|
||||
<Select open>
|
||||
<SelectContent>
|
||||
<SelectItem value="opt1" data-testid="item">Item</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
expect(screen.getByTestId('item')).toHaveAttribute('data-slot', 'select-item');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<Select open>
|
||||
<SelectContent>
|
||||
<SelectItem value="opt1" className="custom-item" data-testid="item">Item</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
expect(screen.getByTestId('item')).toHaveClass('custom-item');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Select Composition', () => {
|
||||
it('should render complete select structure', () => {
|
||||
render(
|
||||
<Select open>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="cn">中文</SelectItem>
|
||||
<SelectItem value="en">英文</SelectItem>
|
||||
<SelectItem value="jp">日文</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
||||
expect(screen.getByText('中文')).toBeInTheDocument();
|
||||
expect(screen.getByText('英文')).toBeInTheDocument();
|
||||
expect(screen.getByText('日文')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Separator } from './separator';
|
||||
|
||||
describe('Separator', () => {
|
||||
it('renders horizontal orientation by default', () => {
|
||||
const { container } = render(<Separator />);
|
||||
const separator = container.querySelector('[data-slot="separator"]');
|
||||
expect(separator).toBeInTheDocument();
|
||||
expect(separator).toHaveAttribute('data-orientation', 'horizontal');
|
||||
});
|
||||
|
||||
it('renders vertical orientation', () => {
|
||||
const { container } = render(<Separator orientation="vertical" />);
|
||||
const separator = container.querySelector('[data-slot="separator"]');
|
||||
expect(separator).toHaveAttribute('data-orientation', 'vertical');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Separator className="my-custom-class" />);
|
||||
const separator = container.querySelector('[data-slot="separator"]');
|
||||
expect(separator).toHaveClass('my-custom-class');
|
||||
});
|
||||
|
||||
it('has decorative=true by default', () => {
|
||||
const { container } = render(<Separator />);
|
||||
const separator = container.querySelector('[data-slot="separator"]');
|
||||
expect(separator).toHaveAttribute('data-orientation', 'horizontal');
|
||||
expect(separator).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Skeleton, SkeletonText, SkeletonCard, SkeletonList, SkeletonHero, SkeletonForm } from './skeleton';
|
||||
|
||||
describe('Skeleton', () => {
|
||||
describe('Basic Skeleton', () => {
|
||||
it('should render skeleton div', () => {
|
||||
const { container } = render(<Skeleton />);
|
||||
const skeleton = container.querySelector('[data-slot="skeleton"]');
|
||||
expect(skeleton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have animate-pulse class', () => {
|
||||
const { container } = render(<Skeleton />);
|
||||
const skeleton = container.querySelector('[data-slot="skeleton"]');
|
||||
expect(skeleton).toHaveClass('animate-pulse');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<Skeleton className="custom-skeleton" />);
|
||||
const skeleton = container.querySelector('[data-slot="skeleton"]');
|
||||
expect(skeleton).toHaveClass('custom-skeleton');
|
||||
});
|
||||
|
||||
it('should pass through additional props', () => {
|
||||
render(<Skeleton data-testid="skeleton" />);
|
||||
expect(screen.getByTestId('skeleton')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SkeletonText', () => {
|
||||
it('should render default 3 lines', () => {
|
||||
const { container } = render(<SkeletonText />);
|
||||
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||
expect(skeletons).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should render custom number of lines', () => {
|
||||
const { container } = render(<SkeletonText lines={5} />);
|
||||
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||
expect(skeletons).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should have aria-hidden', () => {
|
||||
const { container } = render(<SkeletonText />);
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<SkeletonText className="custom-text" />);
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).toHaveClass('custom-text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SkeletonCard', () => {
|
||||
it('should render with image and title by default', () => {
|
||||
const { container } = render(<SkeletonCard />);
|
||||
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||
// image + title + 2 description lines
|
||||
expect(skeletons.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('should have role="status"', () => {
|
||||
render(<SkeletonCard />);
|
||||
const card = screen.getByRole('status');
|
||||
expect(card).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<SkeletonCard className="custom-card" />);
|
||||
const card = screen.getByRole('status');
|
||||
expect(card).toHaveClass('custom-card');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SkeletonList', () => {
|
||||
it('should render default 3 items', () => {
|
||||
const { container } = render(<SkeletonList />);
|
||||
const list = container.firstChild as HTMLElement;
|
||||
expect(list).toBeInTheDocument();
|
||||
expect(list).toHaveAttribute('aria-label', '内容列表加载中');
|
||||
});
|
||||
|
||||
it('should have aria-label', () => {
|
||||
const { container } = render(<SkeletonList />);
|
||||
const list = container.firstChild as HTMLElement;
|
||||
expect(list).toHaveAttribute('aria-label', '内容列表加载中');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SkeletonHero', () => {
|
||||
it('should render hero skeleton', () => {
|
||||
const { container } = render(<SkeletonHero />);
|
||||
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||
expect(skeletons.length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
it('should have role="status"', () => {
|
||||
render(<SkeletonHero />);
|
||||
const hero = screen.getByRole('status');
|
||||
expect(hero).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SkeletonForm', () => {
|
||||
it('should render default 4 fields', () => {
|
||||
const { container } = render(<SkeletonForm />);
|
||||
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||
// 4 fields * (label + input) + submit button
|
||||
expect(skeletons.length).toBeGreaterThanOrEqual(9);
|
||||
});
|
||||
|
||||
it('should render custom number of fields', () => {
|
||||
const { container } = render(<SkeletonForm fields={2} />);
|
||||
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||
// 2 fields * (label + input) + submit button
|
||||
expect(skeletons.length).toBeGreaterThanOrEqual(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Mock sonner Toaster
|
||||
jest.mock('sonner', () => ({
|
||||
Toaster: ({ theme, className, ...props }: any) => (
|
||||
<div data-testid="sonner-toaster" data-theme={theme} className={className} {...props} />
|
||||
),
|
||||
toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock lucide-react icons used by Toaster
|
||||
jest.mock('lucide-react', () => ({
|
||||
CheckCircle2: (props: any) => <svg data-testid="icon-check-circle" {...props} />,
|
||||
AlertCircle: (props: any) => <svg data-testid="icon-alert-circle" {...props} />,
|
||||
Info: (props: any) => <svg data-testid="icon-info" {...props} />,
|
||||
X: (props: any) => <svg data-testid="icon-x" {...props} />,
|
||||
}));
|
||||
|
||||
describe('Toaster', () => {
|
||||
it('renders with correct className "toaster group"', () => {
|
||||
const { Toaster } = require('./sonner');
|
||||
const { container } = render(<Toaster />);
|
||||
const toaster = container.querySelector('[data-testid="sonner-toaster"]');
|
||||
expect(toaster).toBeInTheDocument();
|
||||
expect(toaster).toHaveClass('toaster group');
|
||||
});
|
||||
|
||||
it('passes theme="light" to sonner', () => {
|
||||
const { Toaster } = require('./sonner');
|
||||
const { container } = render(<Toaster />);
|
||||
const toaster = container.querySelector('[data-testid="sonner-toaster"]');
|
||||
expect(toaster).toHaveAttribute('data-theme', 'light');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { StaticLink } from './static-link';
|
||||
|
||||
// Note: jsdom's window.location is non-configurable, so we cannot use
|
||||
// jest.spyOn or Object.defineProperty to mock the entire location object.
|
||||
// However, window.location.href IS writable in jsdom, but navigation
|
||||
// (setting href to a different page) is not implemented — see skipped tests below.
|
||||
|
||||
describe('StaticLink', () => {
|
||||
beforeEach(() => {
|
||||
// Reset location to a known state before each test
|
||||
window.location.href = '/';
|
||||
});
|
||||
|
||||
it('should render children', () => {
|
||||
render(<StaticLink href="/about">关于我们</StaticLink>);
|
||||
expect(screen.getByText('关于我们')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with correct href', () => {
|
||||
render(<StaticLink href="/products/erp">ERP</StaticLink>);
|
||||
const link = screen.getByText('ERP');
|
||||
expect(link).toHaveAttribute('href', '/products/erp');
|
||||
});
|
||||
|
||||
// ── 导航行为测试(jsdom 限制跳过) ──────────────────────────────
|
||||
//
|
||||
// 以下测试涉及 window.location.href 赋值导航,在 jsdom 中不可用:
|
||||
// Error: Not implemented: navigation (except hash changes)
|
||||
//
|
||||
// 后期补全方案(任选其一):
|
||||
// A. 升级至 @playwright/test 做 E2E 验证(推荐 — 可真实模拟浏览器导航)
|
||||
// B. 在 jest 中 mock window.location(需升级 jsdom 或使用 jest-environment-jsdom 29+)
|
||||
// C. 将 StaticLink 的导航逻辑抽取为独立函数,单独测试该函数
|
||||
|
||||
it.skip('should navigate to internal link on click', () => {
|
||||
render(<StaticLink href="/about">关于</StaticLink>);
|
||||
fireEvent.click(screen.getByText('关于'));
|
||||
expect(window.location.href).toBe('/about');
|
||||
});
|
||||
|
||||
it.skip('should handle hash link (same page scroll)', () => {
|
||||
// hash 导航中 scrollIntoView 部分可在 jsdom 中测试,但需先 mock
|
||||
// Element.prototype.scrollIntoView = jest.fn()
|
||||
render(<StaticLink href="/about#section">Hash Link</StaticLink>);
|
||||
fireEvent.click(screen.getByText('Hash Link'));
|
||||
// 期望:window.location.href 被设置为 '/about#section'
|
||||
// 但 jsdom 导航未实现,此断言无法通过
|
||||
});
|
||||
|
||||
it('should add noopener noreferrer for external links', () => {
|
||||
render(<StaticLink href="https://example.com">External</StaticLink>);
|
||||
const link = screen.getByText('External');
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
|
||||
it('should call custom onClick handler', () => {
|
||||
const handleClick = jest.fn<() => void>();
|
||||
render(<StaticLink href="/about" onClick={handleClick as any}>Click</StaticLink>);
|
||||
fireEvent.click(screen.getByText('Click'));
|
||||
expect(handleClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render with custom className', () => {
|
||||
render(<StaticLink href="/" className="custom-link">Home</StaticLink>);
|
||||
const link = screen.getByText('Home');
|
||||
expect(link.className).toContain('custom-link');
|
||||
});
|
||||
|
||||
it('should handle mailto links', () => {
|
||||
render(<StaticLink href="mailto:test@test.com">Email</StaticLink>);
|
||||
const link = screen.getByText('Email');
|
||||
expect(link).toHaveAttribute('href', 'mailto:test@test.com');
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Switch } from './switch';
|
||||
|
||||
describe('Switch', () => {
|
||||
it('renders with data-slot="switch"', () => {
|
||||
const { container } = render(<Switch />);
|
||||
const switchEl = container.querySelector('[data-slot="switch"]');
|
||||
expect(switchEl).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders thumb with data-slot="switch-thumb"', () => {
|
||||
const { container } = render(<Switch />);
|
||||
const thumb = container.querySelector('[data-slot="switch-thumb"]');
|
||||
expect(thumb).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Switch className="custom-class" />);
|
||||
const switchEl = container.querySelector('[data-slot="switch"]');
|
||||
expect(switchEl).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('can be disabled', () => {
|
||||
render(<Switch disabled />);
|
||||
const switchEl = screen.getByRole('switch');
|
||||
expect(switchEl).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
|
||||
|
||||
describe('Tabs Components', () => {
|
||||
describe('TabsList', () => {
|
||||
it('should render list with children', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
expect(screen.getByText('Tab 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsList data-testid="list">List</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
expect(screen.getByTestId('list')).toHaveAttribute('data-slot', 'tabs-list');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsList className="custom-list">List</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
expect(screen.getByText('List')).toHaveClass('custom-list');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TabsTrigger', () => {
|
||||
it('should render trigger text', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1">标签一</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
expect(screen.getByText('标签一')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1" data-testid="trigger">Trigger</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
expect(screen.getByTestId('trigger')).toHaveAttribute('data-slot', 'tabs-trigger');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1" className="custom-trigger">Trigger</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
expect(screen.getByText('Trigger')).toHaveClass('custom-trigger');
|
||||
});
|
||||
|
||||
it('should handle disabled state', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1" disabled>Disabled</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
expect(screen.getByText('Disabled')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TabsContent', () => {
|
||||
it('should render content when value matches', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsContent value="tab1">内容一</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
expect(screen.getByText('内容一')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsContent value="tab1" data-testid="content">Content</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
expect(screen.getByTestId('content')).toHaveAttribute('data-slot', 'tabs-content');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsContent value="tab1" className="custom-content">Content</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
expect(screen.getByText('Content')).toHaveClass('custom-content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tabs Composition', () => {
|
||||
it('should render complete tabs structure', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1">标签一</TabsTrigger>
|
||||
<TabsTrigger value="tab2">标签二</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="tab1">内容一</TabsContent>
|
||||
<TabsContent value="tab2">内容二</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
expect(screen.getByText('标签一')).toBeInTheDocument();
|
||||
expect(screen.getByText('标签二')).toBeInTheDocument();
|
||||
expect(screen.getByText('内容一')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show content for non-matching tab', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
||||
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="tab1">Content 1</TabsContent>
|
||||
<TabsContent value="tab2">Content 2</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Content 1')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Content 2')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, beforeAll } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, LegacyTooltip } from './tooltip';
|
||||
|
||||
beforeAll(() => {
|
||||
// Radix UI Tooltip uses ResizeObserver internally via @radix-ui/react-use-size
|
||||
global.ResizeObserver = class MockResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
} as unknown as typeof ResizeObserver;
|
||||
});
|
||||
|
||||
describe('Tooltip Components', () => {
|
||||
describe('TooltipProvider', () => {
|
||||
it('should render children', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<div>Provider Content</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
expect(screen.getByText('Provider Content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TooltipTrigger', () => {
|
||||
it('should render as child element', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<button>悬停提示</button>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
expect(screen.getByText('悬停提示')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TooltipContent', () => {
|
||||
it('should render content when open', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip open>
|
||||
<TooltipTrigger>
|
||||
<button>触发</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>提示内容</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
// TooltipContent is rendered via Portal, so query the full document
|
||||
const content = document.querySelector('[data-slot="tooltip-content"]');
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(content).toHaveTextContent('提示内容');
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip open>
|
||||
<TooltipTrigger>
|
||||
<button>触发</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent data-testid="content">内容</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
expect(screen.getByTestId('content')).toHaveAttribute('data-slot', 'tooltip-content');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip open>
|
||||
<TooltipTrigger>
|
||||
<button>触发</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="custom-tooltip">内容</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
// TooltipContent is rendered via Portal, so query the full document
|
||||
const content = document.querySelector('[data-slot="tooltip-content"]');
|
||||
expect(content).toHaveClass('custom-tooltip');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tooltip Composition', () => {
|
||||
it('should render tooltip with trigger and content', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip open>
|
||||
<TooltipTrigger asChild>
|
||||
<button>悬停</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>这是提示</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByText('悬停')).toBeInTheDocument();
|
||||
// TooltipContent is rendered via Portal, so query the full document
|
||||
const content = document.querySelector('[data-slot="tooltip-content"]');
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(content).toHaveTextContent('这是提示');
|
||||
});
|
||||
});
|
||||
|
||||
describe('LegacyTooltip', () => {
|
||||
it('should render with content and children', () => {
|
||||
render(
|
||||
<LegacyTooltip content="提示文字">
|
||||
<button>悬停</button>
|
||||
</LegacyTooltip>
|
||||
);
|
||||
expect(screen.getByText('悬停')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render content when open', () => {
|
||||
render(
|
||||
<LegacyTooltip content="提示文字" delayDuration={0}>
|
||||
<button>悬停</button>
|
||||
</LegacyTooltip>
|
||||
);
|
||||
expect(screen.getByText('悬停')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user