Files
novalon-website/src/app/(marketing)/home-content-v15.test.tsx
T
zhangxiangand阿睿 549c151c6a feat(home): Hero 动态粒子层(入场浮现后静止)
新增 hero-particle-field 组件作为 Hero L3 层:品牌红点缀、Canvas + rAF
仅在 ~600ms 入场窗运行后冻结为静态帧,遵守「无持续循环动画」动效铁律;
prefers-reduced-motion 退化为一次性静态帧。固定种子 PRNG 保证视觉快照可复现。
同步更新 home-content-v15 接入与单测、home 视觉基线。

Co-Authored-By: 阿睿 <workbuddy@tencent.com>
2026-09-01 07:57:12 +08:00

272 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});