test(e2e,unit): add navigation edge cases and component coverage

- Add missing-paths E2E spec for desktop/mobile navigation and contrast
- Add local Playwright config for dev server testing
- Expand unit tests for CMS data server, products, contact and UI components
- Adjust jest config for new test patterns
This commit is contained in:
张翔
2026-07-08 14:47:07 +08:00
parent 7f36211342
commit a6ea2cc72c
11 changed files with 1693 additions and 34 deletions
@@ -0,0 +1,436 @@
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
// ─── Mocks ───────────────────────────────────────────────────────────────
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, initial, animate, whileInView, viewport, 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)}
className={className}
style={style}
{...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>
),
span: ({ children, className, ...props }: any) => (
<span data-testid="motion-span" className={className} {...props}>{children}</span>
),
},
useInView: jest.fn(() => true),
AnimatePresence: ({ children }: any) => <>{children}</>,
}));
jest.mock('next/navigation', () => ({
useSearchParams: jest.fn(() => new URLSearchParams()),
}));
jest.mock('@/lib/analytics', () => ({
trackContactForm: jest.fn(),
trackConversion: jest.fn(),
}));
jest.mock('@/components/ui/button', () => ({
Button: ({ children, type, disabled, className, ...props }: any) => (
<button type={type} disabled={disabled} className={className} data-testid="mock-button" {...props}>
{children}
</button>
),
}));
jest.mock('@/components/ui/labeled-input', () => ({
LabeledInput: ({ label, error, id, placeholder, required, value, onChange, onBlur, name, type, 'data-testid': dataTestId, ...props }: any) => (
<div data-testid="labeled-input-wrapper">
{label && <label htmlFor={id} data-testid="input-label">{label}</label>}
<input
id={id}
name={name}
type={type || 'text'}
placeholder={placeholder}
required={required}
value={value || ''}
onChange={onChange}
onBlur={onBlur}
data-testid={dataTestId || 'labeled-input'}
{...props}
/>
{error && <span data-testid="input-error" className="text-brand text-sm mt-1">{error}</span>}
</div>
),
}));
jest.mock('@/components/ui/labeled-textarea', () => ({
LabeledTextarea: ({ label, error, id, placeholder, required, value, onChange, onBlur, name, rows, 'data-testid': dataTestId, ...props }: any) => (
<div data-testid="labeled-textarea-wrapper">
{label && <label htmlFor={id} data-testid="textarea-label">{label}</label>}
<textarea
id={id}
name={name}
placeholder={placeholder}
required={required}
value={value || ''}
onChange={onChange}
onBlur={onBlur}
rows={rows}
data-testid={dataTestId || 'labeled-textarea'}
{...props}
/>
{error && <span data-testid="textarea-error" className="text-brand text-sm mt-1">{error}</span>}
</div>
),
}));
jest.mock('@/components/ui/toast', () => ({
Toast: ({ message, type, onClose, ...props }: any) => (
<div data-testid="toast" data-toast-type={type} {...props}>
<span data-testid="toast-message">{message}</span>
<button data-testid="toast-close" onClick={onClose}>×</button>
</div>
),
}));
jest.mock('@/components/seo/structured-data', () => ({
BreadcrumbSchema: () => null,
}));
jest.mock('@/components/ui/tooltip', () => ({
LegacyTooltip: ({ children, content }: any) => (
<span data-testid="legacy-tooltip" data-tip={content}>{children}</span>
),
}));
jest.mock('sonner', () => ({
toast: {
success: jest.fn(),
error: jest.fn(),
info: jest.fn(),
},
}));
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 {
Mail: mockIcon('mail'),
MapPin: mockIcon('map-pin'),
Send: mockIcon('send'),
Loader2: mockIcon('loader2'),
Clock: mockIcon('clock'),
HeadphonesIcon: mockIcon('headphones'),
CheckCircle2: mockIcon('check-circle2'),
HelpCircle: mockIcon('help-circle'),
ArrowRight: mockIcon('arrow-right'),
};
});
// ─── Mock Data ────────────────────────────────────────────────────────────
const mockContactData = {
heroSubtitle: 'GET IN TOUCH',
heroTitle: '让我们\n开启对话',
heroDescription: '无论您有任何疑问或需求,我们的团队都会在24小时内回复。',
contactTitle: '联系方式',
emailLabel: '电子邮箱',
addressLabel: '公司地址',
email: 'contact@novalon.cn',
address: '成都市高新区天府大道中段',
workHoursTitle: '工作时间',
dayLabel: '周一至周五',
hours: '09:00 — 18:00',
commitmentTitle: '我们的承诺',
commitmentItems: [
'24小时内回复您的咨询',
'免费提供初步方案评估',
'严格保密您的业务信息',
],
formTitle: '发送消息',
successTitle: '消息已发送',
successMessage: '感谢您的留言,我们会尽快与您联系。',
submitLabel: '发送消息',
submittingLabel: '发送中...',
ctaSubtitle: 'Ready to Start',
ctaTitle: '准备好开启数字化转型了吗?',
ctaDescription: '立即预约免费咨询,我们的专家团队将为您量身定制最佳方案。',
ctaPrimaryLabel: '预约免费咨询',
ctaPrimaryHref: '/contact',
ctaSecondaryLabel: '了解更多',
ctaSecondaryHref: '/solutions',
};
// ─── Helpers ──────────────────────────────────────────────────────────────
function getSubmitButton(): HTMLButtonElement {
// "发送消息"同时出现在 form title 和 button label 中,需用 getAllByText 筛选
const elements = screen.getAllByText('发送消息');
const btnEl = elements.find(el => el.closest('button'));
return (btnEl?.closest('button') || elements[0]?.closest('button')) as HTMLButtonElement;
}
function renderContactContent(data: Record<string, unknown> = mockContactData) {
return import('./contact-content-v3').then((mod) => {
const Component = mod.default;
return render(<Component data={data} />);
});
}
function fillFormFields() {
const nameInput = screen.getByTestId('name-input') as HTMLInputElement;
const phoneInput = screen.getByTestId('phone-input') as HTMLInputElement;
const emailInput = screen.getByTestId('email-input') as HTMLInputElement;
const subjectInput = screen.getByTestId('subject-input') as HTMLInputElement;
const messageInput = screen.getByTestId('message-input') as HTMLTextAreaElement;
fireEvent.change(nameInput, { target: { value: '张三' } });
fireEvent.change(phoneInput, { target: { value: '13800138000' } });
fireEvent.change(emailInput, { target: { value: 'zhangsan@example.com' } });
fireEvent.change(subjectInput, { target: { value: '咨询产品方案' } });
fireEvent.change(messageInput, { target: { value: '您好,我想了解贵公司的ERP产品和CRM产品,请提供详细方案和报价。' } });
}
// ─── Tests ────────────────────────────────────────────────────────────────
describe('ContactContentV3 - 联系表单集成测试', () => {
beforeEach(() => {
jest.clearAllMocks();
// 模拟全局 fetch
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ success: 'true' }),
} as Response)
) as any;
// jsdom 默认不实现 scrollIntoView
Element.prototype.scrollIntoView = jest.fn();
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('Rendering', () => {
it('should render hero section with subtitle and title', async () => {
await renderContactContent();
expect(screen.getByText('GET IN TOUCH')).toBeInTheDocument();
expect(screen.getByText('开启对话')).toBeInTheDocument();
});
it('should render hero description', async () => {
await renderContactContent();
expect(screen.getByText(/无论您有任何疑问或需求/)).toBeInTheDocument();
});
it('should render contact info section', async () => {
await renderContactContent();
expect(screen.getByText('联系方式')).toBeInTheDocument();
expect(screen.getByText('contact@novalon.cn')).toBeInTheDocument();
expect(screen.getByText('成都市高新区天府大道中段')).toBeInTheDocument();
});
it('should render work hours section', async () => {
await renderContactContent();
expect(screen.getByText('工作时间')).toBeInTheDocument();
expect(screen.getByText('周一至周五')).toBeInTheDocument();
expect(screen.getByText('09:00 — 18:00')).toBeInTheDocument();
});
it('should render commitment items', async () => {
await renderContactContent();
expect(screen.getByText('24小时内回复您的咨询')).toBeInTheDocument();
expect(screen.getByText('免费提供初步方案评估')).toBeInTheDocument();
expect(screen.getByText('严格保密您的业务信息')).toBeInTheDocument();
});
it('should render form section with title', async () => {
await renderContactContent();
// "发送消息"同时出现在 form title 和 button 上
expect(screen.getAllByText('发送消息').length).toBeGreaterThanOrEqual(1);
});
it('should render all form fields', async () => {
await renderContactContent();
expect(screen.getByTestId('name-input')).toBeInTheDocument();
expect(screen.getByTestId('phone-input')).toBeInTheDocument();
expect(screen.getByTestId('email-input')).toBeInTheDocument();
expect(screen.getByTestId('subject-input')).toBeInTheDocument();
expect(screen.getByTestId('message-input')).toBeInTheDocument();
});
it('should render submit button', async () => {
await renderContactContent();
// "发送消息"同时出现在 form title 和 button 上,用 getAllByText
const elements = screen.getAllByText('发送消息');
const buttonElements = elements.filter((el) => el.closest('button'));
expect(buttonElements.length).toBeGreaterThanOrEqual(1);
});
it('should render CTA section', async () => {
await renderContactContent();
expect(screen.getByText('准备好开启数字化转型了吗?')).toBeInTheDocument();
expect(screen.getByText('预约免费咨询')).toBeInTheDocument();
expect(screen.getByText('了解更多')).toBeInTheDocument();
});
it('should render email link with correct href', async () => {
await renderContactContent();
const emailLink = screen.getByTestId('email-link');
expect(emailLink).toHaveAttribute('href', 'mailto:contact@novalon.cn');
});
});
describe('Empty State', () => {
it('should show empty state when no data is provided', async () => {
await renderContactContent({});
expect(screen.getByText('内容暂未发布')).toBeInTheDocument();
});
it('should show empty state when data is null-like', async () => {
await renderContactContent({} as any);
expect(screen.getByText('内容暂未发布')).toBeInTheDocument();
});
});
describe('Form Validation', () => {
it('should show validation errors when submitting empty form', async () => {
await renderContactContent();
const submitButton = getSubmitButton();
await act(async () => { submitButton.click(); });
// Toast should show with error count
await waitFor(() => {
expect(screen.getByTestId('toast')).toBeInTheDocument();
});
const toastMessage = screen.getByTestId('toast-message');
expect(toastMessage.textContent).toContain('请检查表单');
});
it('should show error for invalid phone number on blur', async () => {
await renderContactContent();
const phoneInput = screen.getByTestId('phone-input') as HTMLInputElement;
fireEvent.change(phoneInput, { target: { value: '12345' } });
fireEvent.blur(phoneInput, { target: { value: '12345' } });
// Validation error should be shown
const errorElements = screen.getAllByTestId('input-error');
const phoneError = errorElements.find((el) => el.textContent?.includes('手机号码'));
expect(phoneError).toBeTruthy();
});
it('should show error for invalid email on blur', async () => {
await renderContactContent();
const emailInput = screen.getByTestId('email-input') as HTMLInputElement;
fireEvent.change(emailInput, { target: { value: 'invalid-email' } });
fireEvent.blur(emailInput, { target: { value: 'invalid-email' } });
const errorElements = screen.getAllByTestId('input-error');
const emailError = errorElements.find((el) => el.textContent?.includes('邮箱'));
expect(emailError).toBeTruthy();
});
});
describe('Form Submission', () => {
it('should submit form successfully and show success state', async () => {
// Mock fetch to return success
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ success: 'true' }),
} as Response)
) as any;
await renderContactContent();
fillFormFields();
const submitButton = getSubmitButton();
await act(async () => { submitButton.click(); });
// Should show success state
await waitFor(() => {
expect(screen.getByText('消息已发送')).toBeInTheDocument();
});
// 成功消息同时出现在 form 成功状态和 toast 中,用 getAllByText
expect(screen.getAllByText('感谢您的留言,我们会尽快与您联系。').length).toBeGreaterThanOrEqual(1);
// Should have called analytics
const { trackContactForm, trackConversion } = await import('@/lib/analytics');
expect(trackContactForm).toHaveBeenCalled();
expect(trackConversion).toHaveBeenCalledWith('contact_form_submission');
});
it('should show error toast when API returns error', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: false,
json: () => Promise.resolve({ message: '服务器错误,请稍后重试' }),
} as Response)
) as any;
await renderContactContent();
fillFormFields();
const submitButton = getSubmitButton();
await act(async () => { submitButton.click(); });
await waitFor(() => {
expect(screen.getByTestId('toast')).toBeInTheDocument();
});
const toastMessage = screen.getByTestId('toast-message');
expect(toastMessage.textContent).toContain('服务器错误');
});
it('should show error toast on network failure', async () => {
global.fetch = jest.fn(() => Promise.reject(new Error('Network error'))) as any;
await renderContactContent();
fillFormFields();
const submitButton = getSubmitButton();
await act(async () => { submitButton.click(); });
await waitFor(() => {
expect(screen.getByTestId('toast')).toBeInTheDocument();
});
const toastMessage = screen.getByTestId('toast-message');
expect(toastMessage.textContent).toContain('网络错误');
});
});
describe('Accessibility', () => {
it('should have proper heading hierarchy', async () => {
await renderContactContent();
const h1 = screen.getByRole('heading', { level: 1 });
expect(h1).toBeInTheDocument();
// Should have section headings as h2 or h3
const headings = screen.getAllByRole('heading');
expect(headings.length).toBeGreaterThanOrEqual(2);
});
it('should have honeypot field for spam prevention', async () => {
await renderContactContent();
const honeypot = document.querySelector('input[aria-hidden="true"]');
expect(honeypot).toBeInTheDocument();
expect(honeypot).toHaveAttribute('tabIndex', '-1');
});
});
});
@@ -0,0 +1,304 @@
import { describe, it, expect, jest } from '@jest/globals';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import type { Product } from '@/lib/constants/products';
// ─── Mocks ───────────────────────────────────────────────────────────────
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, initial, animate, whileHover, whileInView, viewport, className, style, onMouseMove, onMouseEnter, onMouseLeave, ref, ...props }: any) => (
<div
data-testid="motion-div"
data-initial={JSON.stringify(initial)}
data-animate={JSON.stringify(animate)}
data-while-hover={JSON.stringify(whileHover)}
data-while-inview={JSON.stringify(whileInView)}
data-viewport={JSON.stringify(viewport)}
className={className}
style={style}
ref={ref}
onMouseMove={onMouseMove}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
{...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('@/hooks/use-reduced-motion', () => ({
useReducedMotion: jest.fn(() => false),
}));
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/button', () => ({
Button: ({ children, className, ...props }: any) => (
<button className={className} data-testid="mock-button" {...props}>
{children}
</button>
),
}));
jest.mock('@/components/ui/page-decoration', () => ({
SectionLabel: ({ children }: any) => <span data-testid="section-label">{children}</span>,
EASE_OUT: [0.22, 1, 0.36, 1],
}));
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'),
Package: mockIcon('package'),
Cpu: mockIcon('cpu'),
BarChart3: mockIcon('barchart3'),
Users: mockIcon('users'),
Settings: mockIcon('settings'),
FileText: mockIcon('file-text'),
CheckCircle2: mockIcon('check-circle2'),
};
});
// ─── Mock Data ────────────────────────────────────────────────────────────
const mockEnterpriseProducts: Product[] = [
{
id: 'erp',
title: '睿新ERP管理系统',
category: '企业旗舰系列',
categoryId: 'enterprise',
description: '覆盖企业财务、采购、库存、生产全流程',
overview: 'ERP 系统概述',
features: ['财务管理', '供应链管理'],
benefits: ['降本增效'],
tags: ['ERP', '核心系统'],
status: '已发布',
image: '/erp.png',
heroThemeId: 'erp',
bundle: 'enterprise',
scenario: '面向中大型制造与零售企业的核心运营系统',
metrics: [
{ value: '10 万级', label: 'SKU 支撑' },
{ value: '200+', label: '标准报表模板' },
{ value: '99.5%', label: '数据准确率' },
],
capabilities: ['财务核算', '采购管理', '生产计划', '库存控制'],
process: [],
specs: [],
caseStudies: [],
dataProofs: [],
certifications: [],
},
{
id: 'crm',
title: '睿新客户关系管理系统',
category: '企业旗舰系列',
categoryId: 'enterprise',
description: '全渠道客户管理平台',
overview: 'CRM 系统概述',
features: ['客户管理', '销售自动化'],
benefits: ['提升转化'],
tags: ['CRM', '客户管理'],
status: '已发布',
image: '/crm.png',
heroThemeId: 'crm',
bundle: 'enterprise',
scenario: '面向销售驱动型企业的一体化客户运营管理平台',
metrics: [
{ value: '360°', label: '客户画像' },
{ value: '3x', label: '转化率提升' },
{ value: '<2h', label: '线索响应' },
],
capabilities: ['线索管理', '商机跟进', '合同审批', '售后工单'],
process: [],
specs: [],
caseStudies: [],
dataProofs: [],
certifications: [],
},
];
const mockSpecializedProducts: Product[] = [
{
id: 'novavis',
title: 'NovaVis 数据可视化平台',
category: '专业产品',
categoryId: 'specialized',
description: '专业数据可视化工具',
overview: 'NovaVis 概述',
features: ['大屏展示', '报表设计'],
benefits: ['直观呈现'],
tags: ['数据可视化', 'BI'],
status: '研发中',
image: '/novavis.png',
heroThemeId: 'novavis',
bundle: 'standalone',
scenario: '面向执法机关的离线智能数据分析与资金关系图谱平台',
metrics: [
{ value: '100万+', label: '秒级处理' },
{ value: '<30秒', label: '分析耗时' },
{ value: '20层+', label: '资金追踪' },
],
capabilities: ['资金追踪', '关系图谱', 'AI 分析', '离线运行'],
process: [],
specs: [],
caseStudies: [],
dataProofs: [],
certifications: [],
},
];
const allMockProducts = [...mockEnterpriseProducts, ...mockSpecializedProducts];
// ─── Tests ────────────────────────────────────────────────────────────────
describe('ProductsContentV3 - 产品列表页集成测试', () => {
it('should render hero section with title', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
expect(screen.getByText('用自研产品组合')).toBeInTheDocument();
expect(screen.getByText('核心系统升级')).toBeInTheDocument();
});
it('should render hero description text', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
expect(screen.getByText(/从数据智能到业务协同/)).toBeInTheDocument();
});
it('should render enterprise products section', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
expect(screen.getAllByText('企业套装').length).toBeGreaterThanOrEqual(1);
});
it('should render specialized products section', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
expect(screen.getAllByText('专业产品').length).toBeGreaterThanOrEqual(1);
});
it('should render enterprise product cards with titles', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
// Each enterprise product title should appear 3 times: hero stats, product section, and suite combos section
expect(screen.getAllByText('睿新ERP管理系统').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('睿新客户关系管理系统').length).toBeGreaterThanOrEqual(1);
});
it('should render specialized product cards with titles', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
expect(screen.getAllByText('NovaVis 数据可视化平台').length).toBeGreaterThanOrEqual(1);
});
it('should render product capabilities on cards', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
expect(screen.getByText('财务核算')).toBeInTheDocument();
expect(screen.getByText('线索管理')).toBeInTheDocument();
});
it('should render product scenarios on cards', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
expect(screen.getAllByText('面向中大型制造与零售企业的核心运营系统').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('面向销售驱动型企业的一体化客户运营管理平台').length).toBeGreaterThanOrEqual(1);
});
it('should link product cards to detail pages', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
const erpLinks = screen.getAllByRole('link');
const erpDetailLink = erpLinks.find((link) => link.getAttribute('href') === '/products/erp');
expect(erpDetailLink).toBeTruthy();
});
it('should render suite combos section', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
expect(screen.getByText('推荐产品组合')).toBeInTheDocument();
});
it('should render CTA section', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
expect(screen.getByText('预约免费咨询')).toBeInTheDocument();
expect(screen.getByText('浏览行业方案')).toBeInTheDocument();
});
it('should render stats section with product count', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
expect(screen.getByText('产品线')).toBeInTheDocument();
expect(screen.getByText('自研率')).toBeInTheDocument();
expect(screen.getByText('技术覆盖')).toBeInTheDocument();
});
it('should handle empty products list gracefully', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={[]} />);
// Hero section should still render
expect(screen.getByText('用自研产品组合')).toBeInTheDocument();
// The enterprise section should still render (even without products)
expect(screen.getAllByText('企业套装').length).toBeGreaterThanOrEqual(1);
// No product detail links should be present when products are empty
const links = screen.queryAllByRole('link');
const productLinks = links.filter((l) => l.getAttribute('href')?.startsWith('/products/'));
expect(productLinks.length).toBe(0);
});
it('should have proper CTA links pointing to contact and solutions', async () => {
const { default: ProductsContentV3 } = await import('./products-content-v3');
render(<ProductsContentV3 products={allMockProducts} />);
const links = screen.getAllByRole('link');
const contactLinks = links.filter((l) => l.getAttribute('href') === '/contact');
const solutionsLinks = links.filter((l) => l.getAttribute('href') === '/solutions');
expect(contactLinks.length).toBeGreaterThanOrEqual(1);
expect(solutionsLinks.length).toBeGreaterThanOrEqual(1);
});
});