安全/鉴权:登出清服务端 httpOnly 令牌、登录 IP 限流前置 bcrypt、停用账号 令牌在 6 个内联 role handler 失效、admin/items 存在性 oracle 认证前置、 同意偏好 sanitize fail-closed、api-crypto 按 HTTP 方法判体。 CMS:workflow 状态更新+审计收进交互式 $transaction(失败即回滚)、编辑器 richtext 走 textarea 防换行损毁、array/object 子字段按作用域读写防污染、 about/contact/erp-upgrade 补 ISR revalidate 与路由映射。 UI/a11y:AnimatedCounter 入视口前显起点、CLS 累计上报、MetricCard 动画入 effect 并卸载取消 rAF、后台状态四态映射/搜索防抖+序号守卫/媒体单 input/ 吞错改横幅/表单 label-aria 关联。 新增对应判别性单测与 pagination/rate-limit/client-ip/sanitize/validate-content-data 等模块及 tests-integration 真库集成层。
642 lines
24 KiB
TypeScript
642 lines
24 KiB
TypeScript
import { describe, it, expect, jest } from '@jest/globals';
|
||
import { render, screen, act } from '@testing-library/react';
|
||
import '@testing-library/jest-dom';
|
||
|
||
// ─── Mocks ───────────────────────────────────────────────────────────────
|
||
|
||
jest.mock('framer-motion', () => ({
|
||
motion: {
|
||
div: ({ children, initial, animate, variants, whileInView, whileHover, whileTap, viewport, transition, className, onMouseMove, onMouseEnter, onMouseLeave, onMouseUp, style, ref, href, ...props }: any) => (
|
||
href ? (
|
||
<a
|
||
data-testid="motion-a"
|
||
href={href}
|
||
className={className}
|
||
style={style}
|
||
data-variants={JSON.stringify(variants)}
|
||
data-while-hover={JSON.stringify(whileHover)}
|
||
data-while-tap={JSON.stringify(whileTap)}
|
||
{...props}
|
||
>
|
||
{children}
|
||
</a>
|
||
) : (
|
||
<div
|
||
data-testid="motion-div"
|
||
data-initial={JSON.stringify(initial)}
|
||
data-animate={JSON.stringify(animate)}
|
||
data-variants={JSON.stringify(variants)}
|
||
data-while-hover={JSON.stringify(whileHover)}
|
||
data-while-tap={JSON.stringify(whileTap)}
|
||
data-while-inview={JSON.stringify(whileInView)}
|
||
data-viewport={JSON.stringify(viewport)}
|
||
data-transition={JSON.stringify(transition)}
|
||
className={className}
|
||
style={style}
|
||
ref={ref}
|
||
onMouseMove={onMouseMove}
|
||
onMouseEnter={onMouseEnter}
|
||
onMouseLeave={onMouseLeave}
|
||
onMouseUp={onMouseUp}
|
||
{...props}
|
||
>
|
||
{children}
|
||
</div>
|
||
)
|
||
),
|
||
p: ({ children, className, ...props }: any) => (
|
||
<p data-testid="motion-p" className={className} {...props}>{children}</p>
|
||
),
|
||
h1: ({ children, className, ...props }: any) => (
|
||
<h1 data-testid="motion-h1" className={className} {...props}>{children}</h1>
|
||
),
|
||
h2: ({ children, className, ...props }: any) => (
|
||
<h2 data-testid="motion-h2" className={className} {...props}>{children}</h2>
|
||
),
|
||
h3: ({ children, className, ...props }: any) => (
|
||
<h3 data-testid="motion-h3" className={className} {...props}>{children}</h3>
|
||
),
|
||
span: ({ children, className, ...props }: any) => (
|
||
<span data-testid="motion-span" className={className} {...props}>{children}</span>
|
||
),
|
||
button: ({ children, onClick, className, ...props }: any) => (
|
||
<button
|
||
data-testid="motion-button"
|
||
onClick={onClick}
|
||
className={className}
|
||
{...props}
|
||
>
|
||
{children}
|
||
</button>
|
||
),
|
||
a: ({ children, href, className, ...props }: any) => (
|
||
<a
|
||
data-testid="motion-a"
|
||
href={href}
|
||
className={className}
|
||
{...props}
|
||
>
|
||
{children}
|
||
</a>
|
||
),
|
||
li: ({ children, className, ...props }: any) => (
|
||
<li data-testid="motion-li" className={className} {...props}>{children}</li>
|
||
),
|
||
circle: ({ className, cx, cy, r, fill, stroke, strokeWidth, strokeLinecap, strokeDasharray, ...props }: any) => (
|
||
<circle
|
||
data-testid="motion-circle"
|
||
className={className}
|
||
cx={cx}
|
||
cy={cy}
|
||
r={r}
|
||
fill={fill}
|
||
stroke={stroke}
|
||
strokeWidth={strokeWidth}
|
||
strokeLinecap={strokeLinecap}
|
||
strokeDasharray={strokeDasharray}
|
||
{...props}
|
||
/>
|
||
),
|
||
},
|
||
useInView: jest.fn(() => true),
|
||
useMotionValue: jest.fn(() => ({ get: () => 0.5, set: jest.fn() })),
|
||
useTransform: jest.fn(() => ({ get: () => '0%' })),
|
||
useSpring: jest.fn(() => ({ get: () => 0 })),
|
||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||
}));
|
||
|
||
jest.mock('@/hooks/use-reduced-motion', () => ({
|
||
useReducedMotion: jest.fn(() => false),
|
||
}));
|
||
|
||
jest.mock('./micro-interactions', () => ({
|
||
TiltCard: ({ children, className }: any) => (
|
||
<div data-testid="tilt-card" className={className}>{children}</div>
|
||
),
|
||
PressableButton: ({ children, className, variant, onClick }: any) => (
|
||
<button
|
||
data-testid="pressable-button"
|
||
data-variant={variant}
|
||
className={className}
|
||
onClick={onClick}
|
||
>
|
||
{children}
|
||
</button>
|
||
),
|
||
HoverLink: ({ children, href, className }: any) => (
|
||
<a data-testid="hover-link" href={href} className={className}>{children}</a>
|
||
),
|
||
}));
|
||
|
||
jest.mock('./detail-case-study', () => ({
|
||
CaseStudyCard: ({ study, index }: any) => (
|
||
<div data-testid="case-study-card" data-index={index}>
|
||
<span data-testid="case-client">{study.client}</span>
|
||
<span data-testid="case-industry">{study.industry}</span>
|
||
</div>
|
||
),
|
||
}));
|
||
|
||
jest.mock('./detail-certification', () => ({
|
||
CertificationList: ({ certifications }: any) => (
|
||
<div data-testid="certification-list">
|
||
{certifications.map((cert: any, i: number) => (
|
||
<span key={i} data-testid="cert-name">{cert.name}</span>
|
||
))}
|
||
</div>
|
||
),
|
||
}));
|
||
|
||
jest.mock('lucide-react', () => {
|
||
const icons: Record<string, any> = {};
|
||
const names = [
|
||
'ArrowRight', 'Sparkles', 'ChevronDown', 'Zap', 'Shield',
|
||
'CheckCircle2', 'MessageCircle', 'Download', 'Package',
|
||
'Lightbulb', 'Wrench', 'TrendingUp', 'Clock', 'Award',
|
||
'Users', 'Cpu', 'ExternalLink',
|
||
];
|
||
for (const name of names) {
|
||
const Icon = (props: any) => (
|
||
<svg
|
||
data-testid={`icon-${name.toLowerCase().replace(/\s+/g, '-')}`}
|
||
className={props?.className}
|
||
/>
|
||
);
|
||
Icon.displayName = name;
|
||
icons[name] = Icon;
|
||
}
|
||
return icons;
|
||
});
|
||
|
||
jest.mock('@/lib/constants/design-system', () => ({
|
||
DESIGN_SYSTEM: {
|
||
animation: {
|
||
duration: { fast: '0.2s', normal: '0.4s', slow: '0.6s', slower: '0.8s', countUp: '2s' },
|
||
easing: { smooth: 'cubic-bezier(0.4, 0, 0.2, 1)', bounce: 'cubic-bezier(0.34, 1.56, 0.64, 1)', easeOut: 'cubic-bezier(0, 0, 0.2, 1)', easeInOut: 'cubic-bezier(0.4, 0, 0.6, 1)' },
|
||
delay: { stagger: 0.08, section: 0.15 },
|
||
},
|
||
spacing: {
|
||
section: { py: 'py-20 lg:py-28', pyCompact: 'py-16 lg:py-20' },
|
||
container: { default: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8', narrow: 'max-w-5xl mx-auto px-4 sm:px-6 lg:px-8', wide: 'max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8' },
|
||
grid: { gap: 'gap-6 lg:gap-8', gapSmall: 'gap-4 lg:gap-6' },
|
||
},
|
||
typography: {
|
||
hero: { title: 'text-4xl', subtitle: 'text-lg', description: 'text-base' },
|
||
section: { title: 'text-3xl', subtitle: 'text-lg', body: 'text-base' },
|
||
card: { title: 'text-lg', description: 'text-sm' },
|
||
},
|
||
effects: {
|
||
inkGlow: { border: 'conic-gradient(...)', glow: 'radial-gradient(...)', speed: '3s' },
|
||
hover: { translateY: '-4px', shadow: 'shadow-xl', transition: 'all 0.3s' },
|
||
scroll: {
|
||
fadeInUp: {
|
||
initial: { opacity: 0, y: 24 },
|
||
animate: { opacity: 1, y: 0 },
|
||
viewport: { once: true, margin: '-80px' },
|
||
transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] },
|
||
},
|
||
staggerChildren: { delay: 0.08 },
|
||
},
|
||
},
|
||
colors: {
|
||
brand: { primary: 'var(--color-brand)', light: 'var(--color-brand-bg)', lighter: 'rgba(var(--color-brand-rgb), 0.04)', gradient: 'linear-gradient(135deg, var(--color-brand) 0%, #99182d 100%)' },
|
||
neutral: { '50': 'var(--color-bg-primary)', '100': 'var(--color-bg-section)', '200': 'var(--color-border-primary)' },
|
||
ink: { light: 'rgba(0, 0, 0, 0.03)', medium: 'rgba(0, 0, 0, 0.06)' },
|
||
},
|
||
components: {
|
||
card: { base: 'rounded-2xl', hover: 'hover:-translate-y-1', padding: 'p-6' },
|
||
badge: { base: 'inline-flex', variants: { primary: 'bg-brand-soft', secondary: 'bg-bg-tertiary' } },
|
||
button: { primary: 'bg-brand', secondary: 'bg-white', ghost: 'bg-transparent' },
|
||
metric: { container: 'text-center', value: 'text-3xl', label: 'text-sm', description: 'text-xs' },
|
||
},
|
||
},
|
||
}));
|
||
|
||
// ─── Mock Data ───────────────────────────────────────────────────────────
|
||
|
||
const mockHeroTheme = {
|
||
id: 'erp',
|
||
name: 'ERP',
|
||
gradientFrom: '#f8f6f3',
|
||
gradientTo: '#f0ebe4',
|
||
gradientAngle: 135,
|
||
accentColor: '#1e3a5f',
|
||
accentBg: 'rgba(30, 58, 95, 0.06)',
|
||
texture: { type: 'grid' as const, opacity: 0.03 },
|
||
layout: 'left' as const,
|
||
badge: '企业套装',
|
||
};
|
||
|
||
const mockCenterTheme = {
|
||
...mockHeroTheme,
|
||
id: 'solution',
|
||
layout: 'center' as const,
|
||
badge: undefined,
|
||
};
|
||
|
||
const mockProduct = {
|
||
id: 'erp',
|
||
title: 'ERP 企业资源管理系统',
|
||
description: '企业资源管理系统',
|
||
image: '/images/erp.jpg',
|
||
category: 'Enterprise Software',
|
||
categoryId: 'enterprise' as const,
|
||
status: '已发布' as const,
|
||
overview: '覆盖企业全业务流程的数字化管理平台',
|
||
features: ['财务管理:总账、应收应付、资产管理', '供应链管理:采购、库存、物流'],
|
||
benefits: ['提升运营效率 300%', '降低人力成本 50%'],
|
||
process: ['需求调研', '方案设计'],
|
||
specs: ['支持 10 万并发', '99.9% 可用性'],
|
||
tags: ['ERP', '企业管理'],
|
||
heroThemeId: 'erp',
|
||
bundle: 'enterprise' as const,
|
||
caseStudies: [
|
||
{ client: '某制造企业', industry: '制造业', challenge: '库存管理混乱', solution: '实施ERP系统', result: '库存周转率提升200%' },
|
||
],
|
||
dataProofs: [
|
||
{ metric: '效率提升', value: '300%', description: '业务流程效率提升' },
|
||
],
|
||
certifications: [
|
||
{ name: 'ISO 27001', issuer: '国际认证', link: 'https://example.com' },
|
||
],
|
||
};
|
||
|
||
// ─── Tests ───────────────────────────────────────────────────────────────
|
||
|
||
describe('DetailHero', () => {
|
||
it('should render title and subtitle', async () => {
|
||
const { DetailHero } = await import('./detail-hero');
|
||
render(
|
||
<DetailHero
|
||
theme={mockHeroTheme}
|
||
title="ERP 系统"
|
||
subtitle="企业资源管理专家"
|
||
/>
|
||
);
|
||
expect(screen.getByText('ERP 系统')).toBeInTheDocument();
|
||
expect(screen.getByText('企业资源管理专家')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should render badge from theme', async () => {
|
||
const { DetailHero } = await import('./detail-hero');
|
||
render(
|
||
<DetailHero
|
||
theme={mockHeroTheme}
|
||
title="ERP"
|
||
subtitle="ERP Description"
|
||
/>
|
||
);
|
||
expect(screen.getByText('企业套装')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should render badge prop overriding theme badge', async () => {
|
||
const { DetailHero } = await import('./detail-hero');
|
||
render(
|
||
<DetailHero
|
||
theme={mockHeroTheme}
|
||
badge="新版"
|
||
title="ERP"
|
||
subtitle="ERP Description"
|
||
/>
|
||
);
|
||
expect(screen.getByText('新版')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should render description when provided', async () => {
|
||
const { DetailHero } = await import('./detail-hero');
|
||
render(
|
||
<DetailHero
|
||
theme={mockHeroTheme}
|
||
title="ERP"
|
||
subtitle="Expert"
|
||
description="Detailed description here"
|
||
/>
|
||
);
|
||
expect(screen.getByText('Detailed description here')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should not render description when not provided', async () => {
|
||
const { DetailHero } = await import('./detail-hero');
|
||
const { container } = render(
|
||
<DetailHero
|
||
theme={mockHeroTheme}
|
||
title="ERP"
|
||
subtitle="Expert"
|
||
/>
|
||
);
|
||
// Should still render subtitle but no extra description paragraph
|
||
expect(screen.getByText('Expert')).toBeInTheDocument();
|
||
// The description text should not be present
|
||
expect(container.querySelector('section')?.children.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('should render primary and secondary actions', async () => {
|
||
const { DetailHero } = await import('./detail-hero');
|
||
render(
|
||
<DetailHero
|
||
theme={mockHeroTheme}
|
||
title="ERP"
|
||
subtitle="Expert"
|
||
primaryAction={{ label: '了解更多', href: '/products/erp' }}
|
||
secondaryAction={{ label: '预约演示', href: '/contact' }}
|
||
/>
|
||
);
|
||
expect(screen.getByText('了解更多')).toBeInTheDocument();
|
||
expect(screen.getByText('预约演示')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should not render actions when not provided', async () => {
|
||
const { DetailHero } = await import('./detail-hero');
|
||
render(
|
||
<DetailHero
|
||
theme={mockHeroTheme}
|
||
title="ERP"
|
||
subtitle="Expert"
|
||
/>
|
||
);
|
||
expect(screen.queryByText('了解更多')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('should render no badge when theme has no badge and no badge prop', async () => {
|
||
const { DetailHero } = await import('./detail-hero');
|
||
render(
|
||
<DetailHero
|
||
theme={mockCenterTheme}
|
||
title="Solution"
|
||
subtitle="Desc"
|
||
/>
|
||
);
|
||
// Center theme has no badge - verify sparkles (badge icon) is not in document
|
||
expect(screen.queryByText('企业套装')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('should render with center layout', async () => {
|
||
const { DetailHero } = await import('./detail-hero');
|
||
const { container } = render(
|
||
<DetailHero
|
||
theme={mockCenterTheme}
|
||
title="Solution"
|
||
subtitle="Desc"
|
||
/>
|
||
);
|
||
// Center layout renders text-center class
|
||
const section = container.querySelector('section');
|
||
expect(section).toBeInTheDocument();
|
||
});
|
||
|
||
it('should use theme-aware bg token (not hard-coded bg-white) for light variant', async () => {
|
||
const { DetailHero } = await import('./detail-hero');
|
||
const { container } = render(
|
||
<DetailHero
|
||
theme={mockHeroTheme}
|
||
title="ERP"
|
||
subtitle="Expert"
|
||
/>
|
||
);
|
||
// 暗黑模式:浅色底色必须走 CSS 变量 token,dark 下才能随主题反色
|
||
const section = container.querySelector('section');
|
||
expect(section?.className).toContain('bg-bg-primary');
|
||
expect(section?.className).not.toContain('bg-white');
|
||
});
|
||
});
|
||
|
||
describe('ProductValueSection', () => {
|
||
it('should render product title and overview', async () => {
|
||
const { ProductValueSection } = await import('./detail-product-value');
|
||
render(<ProductValueSection product={mockProduct} />);
|
||
const heading = screen.getByRole('heading', { name: new RegExp(mockProduct.title) });
|
||
expect(heading).toHaveTextContent('核心能力');
|
||
expect(screen.getByText(mockProduct.overview)).toBeInTheDocument();
|
||
});
|
||
|
||
it('should render feature items', async () => {
|
||
const { ProductValueSection } = await import('./detail-product-value');
|
||
render(<ProductValueSection product={mockProduct} />);
|
||
expect(screen.getByText('产品功能')).toBeInTheDocument();
|
||
// Features are rendered via FeatureTags which splits on ":"
|
||
expect(screen.getByText('财务管理')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should render benefit items', async () => {
|
||
const { ProductValueSection } = await import('./detail-product-value');
|
||
render(<ProductValueSection product={mockProduct} />);
|
||
expect(screen.getByText('核心优势')).toBeInTheDocument();
|
||
expect(screen.getByText('提升运营效率 300%')).toBeInTheDocument();
|
||
expect(screen.getByText('降低人力成本 50%')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should render spec items', async () => {
|
||
const { ProductValueSection } = await import('./detail-product-value');
|
||
render(<ProductValueSection product={mockProduct} />);
|
||
expect(screen.getByText('技术规格')).toBeInTheDocument();
|
||
expect(screen.getByText('支持 10 万并发')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should render data proofs when available', async () => {
|
||
const { ProductValueSection } = await import('./detail-product-value');
|
||
render(<ProductValueSection product={mockProduct} />);
|
||
expect(screen.getByText('以可量化的目标约束交付')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should not render data proofs section when empty', async () => {
|
||
const { ProductValueSection } = await import('./detail-product-value');
|
||
const productWithoutData = { ...mockProduct, dataProofs: [] };
|
||
render(<ProductValueSection product={productWithoutData} />);
|
||
expect(screen.queryByText('以可量化的目标约束交付')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('指标计数动画由 effect 驱动并在卸载时取消排队帧(Q-6 回归)', async () => {
|
||
// Q-6:动画曾在 render 体内 setState + requestAnimationFrame——每帧 setState 又触发
|
||
// 重渲染并重启一条新动画链,且从不 cancelAnimationFrame;卸载后帧回调继续排队。
|
||
// 判别面:已排队的帧必须被逐一取消(sched === cancel),旧实现 sched>0 且 cancel=0 必红。
|
||
jest.useFakeTimers();
|
||
// faithful rAF 桩:cancel 必须真的撤销已排队的回调,否则「卸载后不再排队」无从判别
|
||
const pending = new Map<number, ReturnType<typeof setTimeout>>();
|
||
let handle = 0;
|
||
const sched = jest.fn((cb: FrameRequestCallback) => {
|
||
const id = ++handle;
|
||
pending.set(
|
||
id,
|
||
setTimeout(() => {
|
||
pending.delete(id);
|
||
cb(Date.now());
|
||
}, 16),
|
||
);
|
||
return id;
|
||
});
|
||
const cancel = jest.fn((id: number) => {
|
||
const t = pending.get(id);
|
||
if (t) clearTimeout(t);
|
||
pending.delete(id);
|
||
});
|
||
const prevRaf = global.requestAnimationFrame;
|
||
const prevCancel = global.cancelAnimationFrame;
|
||
global.requestAnimationFrame = sched as unknown as typeof requestAnimationFrame;
|
||
global.cancelAnimationFrame = cancel as unknown as typeof cancelAnimationFrame;
|
||
try {
|
||
const { ProductValueSection } = await import('./detail-product-value');
|
||
const { container, unmount } = render(<ProductValueSection product={mockProduct} />);
|
||
const card = container.querySelector('.tabular-nums') as HTMLElement;
|
||
expect(card.textContent).toBe('0');
|
||
await act(async () => {
|
||
jest.advanceTimersByTime(200);
|
||
});
|
||
// 动画真的在推进(中途值),而不是停在 0 或瞬间跳终值
|
||
expect(card.textContent).not.toBe('0');
|
||
expect(card.textContent).not.toBe('300%');
|
||
const scheduledBeforeUnmount = sched.mock.calls.length;
|
||
expect(scheduledBeforeUnmount).toBeGreaterThan(0);
|
||
unmount();
|
||
await act(async () => {
|
||
jest.advanceTimersByTime(3000);
|
||
});
|
||
// 泄漏判别:卸载后动画链必须死透——旧实现(render 期 rAF、从不 cancel)
|
||
// 在这里会继续排队每一帧;effect 版在 cleanup 里 cancel 掉最后一帧。
|
||
expect(sched.mock.calls.length).toBe(scheduledBeforeUnmount);
|
||
} finally {
|
||
global.requestAnimationFrame = prevRaf;
|
||
global.cancelAnimationFrame = prevCancel;
|
||
jest.useRealTimers();
|
||
}
|
||
});
|
||
|
||
});
|
||
|
||
describe('DetailTrustSection', () => {
|
||
it('should render nothing when all props are empty', async () => {
|
||
const { DetailTrustSection } = await import('./detail-trust-section');
|
||
const { container } = render(<DetailTrustSection />);
|
||
// Component returns null when no content
|
||
expect(container.innerHTML).toBe('');
|
||
});
|
||
|
||
it('should render data proofs', async () => {
|
||
const { DetailTrustSection } = await import('./detail-trust-section');
|
||
render(
|
||
<DetailTrustSection
|
||
dataProofs={mockProduct.dataProofs}
|
||
/>
|
||
);
|
||
expect(screen.getByText('目标口径')).toBeInTheDocument();
|
||
expect(screen.getByText(mockProduct.dataProofs[0]!.metric)).toBeInTheDocument();
|
||
// 未声明 basis 的 dataProofs 必须自动落最弱口径角注,不得裸奔
|
||
expect(screen.getByTestId('metrics-basis-note')).toHaveTextContent('目标口径,非既成结果');
|
||
});
|
||
|
||
it('should render case studies', async () => {
|
||
const { DetailTrustSection } = await import('./detail-trust-section');
|
||
render(
|
||
<DetailTrustSection
|
||
caseStudies={mockProduct.caseStudies}
|
||
/>
|
||
);
|
||
expect(screen.getByText('他们已经实现了转型突破')).toBeInTheDocument();
|
||
expect(screen.getByTestId('case-study-card')).toBeInTheDocument();
|
||
expect(screen.getByTestId('case-client')).toHaveTextContent('某制造企业');
|
||
});
|
||
|
||
it('should render certifications', async () => {
|
||
const { DetailTrustSection } = await import('./detail-trust-section');
|
||
render(
|
||
<DetailTrustSection
|
||
certifications={mockProduct.certifications}
|
||
/>
|
||
);
|
||
expect(screen.getByText('资质认证')).toBeInTheDocument();
|
||
expect(screen.getByTestId('certification-list')).toBeInTheDocument();
|
||
expect(screen.getByTestId('cert-name')).toHaveTextContent('ISO 27001');
|
||
});
|
||
|
||
it('should render all sections together', async () => {
|
||
const { DetailTrustSection } = await import('./detail-trust-section');
|
||
render(
|
||
<DetailTrustSection
|
||
caseStudies={mockProduct.caseStudies}
|
||
dataProofs={mockProduct.dataProofs}
|
||
certifications={mockProduct.certifications}
|
||
accentColor="#C41E3A"
|
||
/>
|
||
);
|
||
expect(screen.getByText('目标口径')).toBeInTheDocument();
|
||
expect(screen.getByText('他们已经实现了转型突破')).toBeInTheDocument();
|
||
expect(screen.getByText('资质认证')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('DetailCTASection', () => {
|
||
it('should render default title and description', async () => {
|
||
const { DetailCTASection } = await import('./detail-cta-section');
|
||
render(
|
||
<DetailCTASection
|
||
theme={mockHeroTheme}
|
||
primaryAction={{ label: '联系我们', href: '/contact' }}
|
||
/>
|
||
);
|
||
expect(screen.getByText('准备好开始了吗?')).toBeInTheDocument();
|
||
expect(screen.getByText('联系我们,获取专属方案和报价')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should render custom title and description', async () => {
|
||
const { DetailCTASection } = await import('./detail-cta-section');
|
||
render(
|
||
<DetailCTASection
|
||
theme={mockHeroTheme}
|
||
title="Custom CTA Title"
|
||
description="Custom CTA description"
|
||
primaryAction={{ label: '立即咨询', href: '/contact' }}
|
||
/>
|
||
);
|
||
expect(screen.getByText('Custom CTA Title')).toBeInTheDocument();
|
||
expect(screen.getByText('Custom CTA description')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should render primary action with link', async () => {
|
||
const { DetailCTASection } = await import('./detail-cta-section');
|
||
render(
|
||
<DetailCTASection
|
||
theme={mockHeroTheme}
|
||
primaryAction={{ label: '免费试用', href: '/trial' }}
|
||
/>
|
||
);
|
||
const link = screen.getByText('免费试用').closest('a');
|
||
expect(link).toHaveAttribute('href', '/trial');
|
||
});
|
||
|
||
it('should render secondary action when provided', async () => {
|
||
const { DetailCTASection } = await import('./detail-cta-section');
|
||
render(
|
||
<DetailCTASection
|
||
theme={mockHeroTheme}
|
||
primaryAction={{ label: '联系', href: '/contact' }}
|
||
secondaryAction={{ label: '下载资料', href: '/download' }}
|
||
/>
|
||
);
|
||
expect(screen.getByText('下载资料')).toBeInTheDocument();
|
||
const link = screen.getByText('下载资料').closest('a');
|
||
expect(link).toHaveAttribute('href', '/download');
|
||
});
|
||
|
||
it('should render free consultation badge', async () => {
|
||
const { DetailCTASection } = await import('./detail-cta-section');
|
||
render(
|
||
<DetailCTASection
|
||
theme={mockHeroTheme}
|
||
primaryAction={{ label: '联系', href: '/contact' }}
|
||
/>
|
||
);
|
||
expect(screen.getByText('免费咨询')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should render SLA text', async () => {
|
||
const { DetailCTASection } = await import('./detail-cta-section');
|
||
render(
|
||
<DetailCTASection
|
||
theme={mockHeroTheme}
|
||
primaryAction={{ label: '联系', href: '/contact' }}
|
||
/>
|
||
);
|
||
expect(screen.getByText(/平均响应时间 < 2小时/)).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
// 注:CrossRecommendGrid 相关测试已随组件删除(2026-08-31,组件生产 0 引用)
|