新增 hero-particle-field 组件作为 Hero L3 层:品牌红点缀、Canvas + rAF 仅在 ~600ms 入场窗运行后冻结为静态帧,遵守「无持续循环动画」动效铁律; prefers-reduced-motion 退化为一次性静态帧。固定种子 PRNG 保证视觉快照可复现。 同步更新 home-content-v15 接入与单测、home 视觉基线。 Co-Authored-By: 阿睿 <workbuddy@tencent.com>
272 lines
11 KiB
TypeScript
272 lines
11 KiB
TypeScript
import { describe, it, expect, jest } from '@jest/globals';
|
||
import { render, screen } from '@testing-library/react';
|
||
import '@testing-library/jest-dom';
|
||
import HomeContentV15 from './home-content-v15';
|
||
|
||
jest.mock('framer-motion', () => ({
|
||
motion: {
|
||
div: ({ children, initial, animate, className, style, ...props }: any) => (
|
||
<div
|
||
data-testid="motion-div"
|
||
data-initial={JSON.stringify(initial)}
|
||
data-animate={JSON.stringify(animate)}
|
||
className={className}
|
||
style={style}
|
||
{...props}
|
||
>
|
||
{children}
|
||
</div>
|
||
),
|
||
h1: ({ children, className, ...props }: any) => (
|
||
<h1 data-testid="motion-h1" className={className} {...props}>{children}</h1>
|
||
),
|
||
p: ({ children, className, ...props }: any) => (
|
||
<p data-testid="motion-p" className={className} {...props}>{children}</p>
|
||
),
|
||
span: ({ children, className, ...props }: any) => (
|
||
<span data-testid="motion-span" className={className} {...props}>{children}</span>
|
||
),
|
||
a: ({ children, href, className, ...props }: any) => (
|
||
<a href={href} className={className} data-testid="motion-a" {...props}>{children}</a>
|
||
),
|
||
},
|
||
useInView: jest.fn(() => true),
|
||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||
}));
|
||
|
||
jest.mock('next/image', () => ({
|
||
__esModule: true,
|
||
default: ({ src, alt, className }: any) => (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img src={src} alt={alt} className={className} data-testid="mock-next-image" />
|
||
),
|
||
}));
|
||
|
||
jest.mock('@/components/ui/scroll-reveal', () => ({
|
||
ScrollReveal: ({ children, className }: any) => (
|
||
<div data-testid="scroll-reveal" className={className}>{children}</div>
|
||
),
|
||
StaggerReveal: ({ children, className }: any) => (
|
||
<div data-testid="stagger-reveal" className={className}>{children}</div>
|
||
),
|
||
}));
|
||
|
||
jest.mock('@/components/ui/badge', () => ({
|
||
Badge: ({ children, className }: any) => (
|
||
<span data-testid="mock-badge" className={className}>{children}</span>
|
||
),
|
||
}));
|
||
|
||
jest.mock('lucide-react', () => {
|
||
const mockIcon = (name: string) => {
|
||
const Icon = (props: any) => <svg data-testid={`icon-${name.toLowerCase()}`} className={props.className} />;
|
||
Icon.displayName = name;
|
||
return Icon;
|
||
};
|
||
return {
|
||
ArrowRight: mockIcon('arrow-right'),
|
||
ArrowUpRight: mockIcon('arrow-up-right'),
|
||
ArrowDownRight: mockIcon('arrow-down-right'),
|
||
Lightbulb: mockIcon('lightbulb'),
|
||
Database: mockIcon('database'),
|
||
Layers: mockIcon('layers'),
|
||
Code: mockIcon('code'),
|
||
Puzzle: mockIcon('puzzle'),
|
||
Server: mockIcon('server'),
|
||
Quote: mockIcon('quote'),
|
||
Newspaper: mockIcon('newspaper'),
|
||
Calendar: mockIcon('calendar'),
|
||
LayoutDashboard: mockIcon('layout-dashboard'),
|
||
BarChart3: mockIcon('bar-chart3'),
|
||
Package: mockIcon('package'),
|
||
Users: mockIcon('users'),
|
||
Settings: mockIcon('settings'),
|
||
Search: mockIcon('search'),
|
||
Bell: mockIcon('bell'),
|
||
TrendingUp: mockIcon('trending-up'),
|
||
AlertCircle: mockIcon('alert-circle'),
|
||
};
|
||
});
|
||
|
||
describe('HomeContentV15 - 浅色 Hero(暗黑模式 token 友好)', () => {
|
||
it('does not repeat the brand logo inside the hero (dedup with header)', () => {
|
||
render(<HomeContentV15 />);
|
||
|
||
const hero = screen.getByTestId('hero-section');
|
||
expect(hero).toBeInTheDocument();
|
||
// hero 内不应再有 Logo(导航栏已展示,避免重复);slogan badge 保留
|
||
expect(hero.querySelector('img')).toBeNull();
|
||
});
|
||
|
||
it('mounts the brand-red dynamic particle layer (entrance-only, no continuous loop)', () => {
|
||
render(<HomeContentV15 />);
|
||
|
||
const hero = screen.getByTestId('hero-section');
|
||
const particles = screen.getByTestId('hero-particle-field');
|
||
expect(particles.tagName).toBe('CANVAS');
|
||
expect(particles).toHaveAttribute('aria-hidden', 'true');
|
||
expect(hero.contains(particles)).toBe(true);
|
||
});
|
||
|
||
it('keeps a single primary CTA and demotes the secondary action to a text link', () => {
|
||
render(<HomeContentV15 />);
|
||
|
||
expect(screen.getByTestId('hero-primary-cta')).toBeInTheDocument();
|
||
expect(screen.getByTestId('hero-primary-cta')).toHaveAttribute('href', '/contact');
|
||
expect(screen.getByTestId('hero-secondary-link')).toBeInTheDocument();
|
||
});
|
||
|
||
it('keeps hero declaration-first: stats sink out of hero into the results band', () => {
|
||
render(<HomeContentV15 stats={[{ value: '100%', label: '私有化部署' }]} />);
|
||
|
||
const hero = screen.getByTestId('hero-section');
|
||
// 声明先行:hero 内不再内联数据条(一屏只负责一句话声明 + 一个行动)
|
||
expect(hero.querySelector('[data-testid="hero-stats"]')).toBeNull();
|
||
// 数据下沉为独立成果带,仍渲染(Glance 层滚动触发)
|
||
expect(screen.getByTestId('hero-stats')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders the abstract data-driven visual without product UI shell', () => {
|
||
render(<HomeContentV15 />);
|
||
|
||
expect(screen.getByTestId('hero-product-visual')).toBeInTheDocument();
|
||
expect(screen.getByText('数据驱动 · 让转型可量化')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('HomeContentV15 - 信任层与章节式叙事', () => {
|
||
it('renders verifiable trust signals with a source annotation', () => {
|
||
render(<HomeContentV15 />);
|
||
|
||
expect(screen.getByTestId('trust-section')).toBeInTheDocument();
|
||
expect(screen.getByText('100%')).toBeInTheDocument();
|
||
expect(screen.getByText('成都')).toBeInTheDocument();
|
||
// 可验证来源标注存在(每个数据卡一个)
|
||
expect(screen.getAllByText(/数据来源:/i).length).toBeGreaterThanOrEqual(4);
|
||
});
|
||
|
||
it('renders an early-access co-creation label instead of fabricated testimonials', () => {
|
||
render(<HomeContentV15 />);
|
||
|
||
expect(screen.getByTestId('early-access-banner')).toBeInTheDocument();
|
||
expect(screen.getAllByText('首批客户共创中').length).toBeGreaterThanOrEqual(1);
|
||
});
|
||
|
||
it('renders the problem-method-result narrative with chapter numbers', () => {
|
||
render(<HomeContentV15 />);
|
||
|
||
expect(screen.getByTestId('narrative-section')).toBeInTheDocument();
|
||
expect(screen.getAllByText('01').length).toBeGreaterThanOrEqual(1);
|
||
expect(screen.getAllByText('02').length).toBeGreaterThanOrEqual(1);
|
||
expect(screen.getAllByText('03').length).toBeGreaterThanOrEqual(1);
|
||
expect(screen.getAllByText(/从问题到结果/).length).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
|
||
describe('HomeContentV15 - 行业洞察区(对标 Accenture Insights)', () => {
|
||
it('renders the insights pillar with typed tags and verifiable methodology content', () => {
|
||
render(<HomeContentV15 />);
|
||
|
||
expect(screen.getByTestId('insights-section')).toBeInTheDocument();
|
||
// 类型标签(对标 Research Report / Perspective / Case Study)
|
||
expect(screen.getAllByText('方法论').length).toBeGreaterThanOrEqual(2);
|
||
expect(screen.getByText('观点')).toBeInTheDocument();
|
||
// 全部链接真实页面,不虚构研究报告
|
||
expect(screen.getByText('数字化转型诊断:从哪里开始,如何衡量')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('HomeContentV15 - 创始人观点区(对标 Accenture CEO Quote)', () => {
|
||
it('renders the founder quote with attribution (no fabricated persona)', () => {
|
||
render(<HomeContentV15 />);
|
||
|
||
expect(screen.getByTestId('founder-quote-section')).toBeInTheDocument();
|
||
expect(screen.getByText(/数字化不是一次性项目/)).toBeInTheDocument();
|
||
expect(screen.getByText('Novalon 创始团队')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('HomeContentV15 - 新闻动态区(对标 Accenture News)', () => {
|
||
it('renders the news section with latest items and a view-all entry', () => {
|
||
render(<HomeContentV15 />);
|
||
|
||
expect(screen.getByTestId('news-section')).toBeInTheDocument();
|
||
expect(screen.getByText('四川睿新致远科技有限公司正式成立')).toBeInTheDocument();
|
||
expect(screen.getByText('查看全部新闻')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders CMS-provided news when available', () => {
|
||
render(<HomeContentV15 news={[{
|
||
id: 'cms-news-1',
|
||
title: 'CMS 新闻条目',
|
||
excerpt: 'CMS 提供的新闻摘要',
|
||
date: '2026-08-30',
|
||
category: '研发动态',
|
||
image: '',
|
||
content: '',
|
||
}]} />);
|
||
|
||
expect(screen.getByText('CMS 新闻条目')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('HomeContentV15 - 案例区 Client Spotlight(对标 Accenture Client Spotlight)', () => {
|
||
it('renders a co-creation spotlight card (verifiable, no fabricated clients) when no cases exist', () => {
|
||
render(<HomeContentV15 />);
|
||
|
||
// 空态 spotlight:共创流程 3 步 + 深色卡 CTA
|
||
expect(screen.getAllByText('首批客户共创中').length).toBeGreaterThanOrEqual(1);
|
||
expect(screen.getAllByText('业务诊断').length).toBeGreaterThanOrEqual(1);
|
||
expect(screen.getByText('方案共创')).toBeInTheDocument();
|
||
expect(screen.getByText('验证发布')).toBeInTheDocument();
|
||
expect(screen.getAllByText('01').length).toBeGreaterThanOrEqual(1);
|
||
expect(screen.getByTestId('cases-co-creation-cta')).toHaveAttribute('href', '/contact');
|
||
});
|
||
|
||
it('renders a dark spotlight card with metrics for the featured case when data exists', () => {
|
||
const mockCases = [
|
||
{
|
||
slug: 'spotlight-demo',
|
||
industry: '制造业',
|
||
title: 'Spotlight 案例标题',
|
||
href: '/cases/spotlight-demo',
|
||
verified: true,
|
||
testimonial: {
|
||
quote: '这是一段客户引言,用于验证 spotlight 渲染。',
|
||
author: '客户 CIO',
|
||
role: '某制造企业',
|
||
},
|
||
metrics: [
|
||
{ value: '38%', label: '交付效率提升' },
|
||
{ value: '2x', label: '数据查询提速' },
|
||
{ value: '3周', label: '上线周期' },
|
||
],
|
||
},
|
||
{
|
||
slug: 'secondary-case',
|
||
industry: '贸易零售',
|
||
title: '次要案例标题',
|
||
href: '/cases/secondary-case',
|
||
metrics: [
|
||
{ value: '15%', label: '库存周转改善' },
|
||
{ value: '4h', label: '对账耗时' },
|
||
{ value: '9成', label: '预测准确率' },
|
||
],
|
||
},
|
||
];
|
||
|
||
render(<HomeContentV15 cases={mockCases as any} />);
|
||
|
||
// spotlight 大卡:深色画布 + 引言 + 授权 badge + 量化指标
|
||
expect(screen.getByText('Spotlight 案例标题')).toBeInTheDocument();
|
||
expect(screen.getByText('这是一段客户引言,用于验证 spotlight 渲染。')).toBeInTheDocument();
|
||
expect(screen.getByText('已获客户授权')).toBeInTheDocument();
|
||
expect(screen.getByText('38%')).toBeInTheDocument();
|
||
expect(screen.getByText('查看完整案例')).toBeInTheDocument();
|
||
|
||
// 次要案例保持轻量卡片
|
||
expect(screen.getByText('次要案例标题')).toBeInTheDocument();
|
||
expect(screen.getAllByText('了解详情').length).toBeGreaterThanOrEqual(1);
|
||
});
|
||
});
|