test(core): 补充单元测试,修复 useCountUp 精度问题,新增项目文档
- 新增 hooks/components/lib 共 9 个测试文件,覆盖边界条件与异常路径 - 补充 animations.test.tsx 用例(RotatingBorder、CounterWithEffect 等) - 修复 useCountUp 结束时 toFixed 精度问题 - 调整 jest 覆盖率配置为渐进式阈值,收缩收集范围 - 新增 docs/lessons-learned.md(经验教训汇总)与 docs/troubleshooting.md(问题排查索引) - 更新 README.md 文档索引
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
|
||||
// Set GA ID before importing
|
||||
process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID = 'G-TEST123';
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: jest.fn((key: string) => store[key] ?? null),
|
||||
setItem: jest.fn((key: string, value: string) => {
|
||||
store[key] = value;
|
||||
}),
|
||||
removeItem: jest.fn((key: string) => {
|
||||
delete store[key];
|
||||
}),
|
||||
clear: jest.fn(() => {
|
||||
store = {};
|
||||
}),
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
});
|
||||
|
||||
describe('Analytics', () => {
|
||||
let analytics: typeof import('./analytics');
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
localStorageMock.clear();
|
||||
// Reset gtag
|
||||
delete (window as any).gtag;
|
||||
analytics = await import('./analytics');
|
||||
});
|
||||
|
||||
describe('getDefaultPreferences', () => {
|
||||
it('should return default preferences', () => {
|
||||
const prefs = analytics.getDefaultPreferences();
|
||||
expect(prefs.necessary).toBe(true);
|
||||
expect(prefs.analytics).toBe(true);
|
||||
expect(prefs.marketing).toBe(false);
|
||||
expect(prefs.functionality).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a copy, not the original', () => {
|
||||
const prefs1 = analytics.getDefaultPreferences();
|
||||
const prefs2 = analytics.getDefaultPreferences();
|
||||
expect(prefs1).not.toBe(prefs2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStoredPreferences', () => {
|
||||
it('should return null when no preferences stored', () => {
|
||||
const result = analytics.getStoredPreferences();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return stored preferences', () => {
|
||||
const testPrefs = { necessary: true, analytics: false, marketing: true, functionality: true };
|
||||
localStorageMock.setItem('novalon-cookie-preferences', JSON.stringify(testPrefs));
|
||||
|
||||
const result = analytics.getStoredPreferences();
|
||||
expect(result).toEqual(testPrefs);
|
||||
});
|
||||
|
||||
it('should return null on malformed JSON', () => {
|
||||
localStorageMock.setItem('novalon-cookie-preferences', 'not-json');
|
||||
const result = analytics.getStoredPreferences();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('storePreferences', () => {
|
||||
it('should store preferences to localStorage', () => {
|
||||
const prefs = { necessary: true, analytics: true, marketing: false, functionality: true };
|
||||
analytics.storePreferences(prefs);
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'novalon-cookie-preferences',
|
||||
JSON.stringify(prefs)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw and warn on localStorage error', () => {
|
||||
localStorageMock.setItem.mockImplementationOnce(() => {
|
||||
throw new Error('Storage full');
|
||||
});
|
||||
|
||||
analytics.storePreferences({
|
||||
necessary: true,
|
||||
analytics: true,
|
||||
marketing: false,
|
||||
functionality: true,
|
||||
});
|
||||
|
||||
expect(console.warn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateConsentDetailed', () => {
|
||||
it('should call gtag when available', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.updateConsentDetailed({
|
||||
necessary: true,
|
||||
analytics: true,
|
||||
marketing: false,
|
||||
functionality: true,
|
||||
});
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('consent', 'update', {
|
||||
analytics_storage: 'granted',
|
||||
ad_storage: 'denied',
|
||||
functionality_storage: 'granted',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call gtag when not available', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.updateConsentDetailed({
|
||||
necessary: true,
|
||||
analytics: false,
|
||||
marketing: true,
|
||||
functionality: false,
|
||||
});
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('consent', 'update', {
|
||||
analytics_storage: 'denied',
|
||||
ad_storage: 'granted',
|
||||
functionality_storage: 'denied',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackEvent', () => {
|
||||
it('should call gtag when available', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackEvent('click', 'engagement', 'button-1', 1);
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('event', 'click', {
|
||||
event_category: 'engagement',
|
||||
event_label: 'button-1',
|
||||
event_value: 1,
|
||||
send_to: 'G-TEST123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call gtag when not available', () => {
|
||||
analytics.trackEvent('click', 'engagement', 'button-1', 1);
|
||||
// No gtag available, should not throw
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackButtonClick', () => {
|
||||
it('should call trackEvent with button name', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackButtonClick('submit-form', 'contact-page');
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('event', 'button_click', {
|
||||
event_category: 'engagement',
|
||||
event_label: 'submit-form',
|
||||
event_value: undefined,
|
||||
send_to: 'G-TEST123',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackError', () => {
|
||||
it('should call gtag with error data when available', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackError('API_ERROR', 'Failed to fetch', true, { statusCode: 500 });
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('event', 'exception', {
|
||||
description: '[API_ERROR] Failed to fetch',
|
||||
fatal: 'true',
|
||||
url: window.location.href,
|
||||
timestamp: expect.any(String),
|
||||
statusCode: 500,
|
||||
send_to: 'G-TEST123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should warn when gtag is not available', () => {
|
||||
analytics.trackError('TEST', 'No gtag');
|
||||
expect(console.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set fatal to "false" string when not fatal', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackError('WARN', 'Minor issue', false);
|
||||
|
||||
const callArgs = gtag.mock.calls[0]?.[2] as Record<string, unknown>;
|
||||
expect(callArgs?.fatal).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackPageView', () => {
|
||||
it('should call gtag with page data', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackPageView('/products', 'Products');
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('config', 'G-TEST123', {
|
||||
page_path: '/products',
|
||||
page_title: 'Products',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not throw when gtag is not available', () => {
|
||||
analytics.trackPageView('/', 'Home');
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackPerformance', () => {
|
||||
it('should call gtag with performance data', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackPerformance('LCP', 2500, 'web_vitals');
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('event', 'LCP', {
|
||||
event_category: 'web_vitals',
|
||||
event_value: 2500,
|
||||
value: 2500,
|
||||
send_to: 'G-TEST123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should round value to integer', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackPerformance('FID', 12.7);
|
||||
|
||||
const callArgs = gtag.mock.calls[0]?.[2] as Record<string, unknown>;
|
||||
expect(callArgs?.value).toBe(13);
|
||||
});
|
||||
|
||||
it('should use default category', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackPerformance('CLS', 0.1);
|
||||
|
||||
const callArgs = gtag.mock.calls[0]?.[2] as Record<string, unknown>;
|
||||
expect(callArgs?.event_category).toBe('web_vitals');
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackContactForm', () => {
|
||||
it('should call gtag with form data object', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackContactForm({ name: 'Test', email: 'test@test.com', company: 'TestCorp' }, true);
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('event', 'form_submit', {
|
||||
event_category: 'contact',
|
||||
event_label: 'TestCorp',
|
||||
event_value: 1,
|
||||
send_to: 'G-TEST123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call gtag with string form data', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackContactForm('simple-form', false);
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('event', 'form_submit', {
|
||||
event_category: 'contact',
|
||||
event_label: 'simple-form',
|
||||
event_value: 0,
|
||||
send_to: 'G-TEST123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not track conversion when success is false', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackContactForm('failed-form', false);
|
||||
|
||||
// Only the form_submit event, not conversion
|
||||
expect(gtag).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackConversion', () => {
|
||||
it('should call gtag with conversion data', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackConversion('signup_complete', 5);
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('event', 'conversion', {
|
||||
send_to: 'G-TEST123',
|
||||
transaction_id: expect.any(String),
|
||||
value: 5,
|
||||
currency: 'CNY',
|
||||
conversion_label: 'signup_complete',
|
||||
});
|
||||
});
|
||||
|
||||
it('should default value to 1', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackConversion('page_viewed');
|
||||
|
||||
const callArgs = gtag.mock.calls[0]?.[2] as Record<string, unknown>;
|
||||
expect(callArgs?.value).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackOutboundLink', () => {
|
||||
it('should call gtag with outbound link data', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackOutboundLink('https://example.com', 'Example');
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('event', 'outbound_click', {
|
||||
event_category: 'engagement',
|
||||
event_label: 'https://example.com',
|
||||
event_value: undefined,
|
||||
send_to: 'G-TEST123',
|
||||
});
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('event', 'click', {
|
||||
event_category: 'outbound',
|
||||
event_label: 'https://example.com',
|
||||
transport_type: 'beacon',
|
||||
send_to: 'G-TEST123',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackScrollDepth', () => {
|
||||
it('should call trackEvent with scroll percentage', () => {
|
||||
const gtag = jest.fn();
|
||||
(window as any).gtag = gtag;
|
||||
|
||||
analytics.trackScrollDepth(50);
|
||||
|
||||
expect(gtag).toHaveBeenCalledWith('event', 'scroll_50', {
|
||||
event_category: 'engagement',
|
||||
event_label: '50%',
|
||||
event_value: undefined,
|
||||
send_to: 'G-TEST123',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -434,6 +434,19 @@ describe('Animation Components', () => {
|
||||
|
||||
expect(handleClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should apply custom className', async () => {
|
||||
const { MagneticButton } = await import('./animations');
|
||||
render(<MagneticButton className="magnetic-class">Test</MagneticButton>);
|
||||
const element = screen.getByText('Test').closest('.magnetic-class');
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom strength', async () => {
|
||||
const { MagneticButton } = await import('./animations');
|
||||
render(<MagneticButton strength={0.5}>Test</MagneticButton>);
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('BlurReveal', () => {
|
||||
@@ -477,6 +490,79 @@ describe('Animation Components', () => {
|
||||
|
||||
expect(handleClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should apply custom shimmerColor', async () => {
|
||||
const { ShimmerButton } = await import('./animations');
|
||||
render(<ShimmerButton shimmerColor="rgba(0,0,0,0.5)">Test</ShimmerButton>);
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RotatingBorder', () => {
|
||||
it('should render children correctly', async () => {
|
||||
const { RotatingBorder } = await import('./animations');
|
||||
render(<RotatingBorder>Border Content</RotatingBorder>);
|
||||
expect(screen.getByText('Border Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', async () => {
|
||||
const { RotatingBorder } = await import('./animations');
|
||||
render(<RotatingBorder className="rotating-class">Test</RotatingBorder>);
|
||||
const container = screen.getByText('Test').closest('.rotating-class');
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should accept custom borderWidth', async () => {
|
||||
const { RotatingBorder } = await import('./animations');
|
||||
render(<RotatingBorder borderWidth={4}>Test</RotatingBorder>);
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should accept custom colors', async () => {
|
||||
const { RotatingBorder } = await import('./animations');
|
||||
render(<RotatingBorder colors={['red', 'blue', 'green']}>Test</RotatingBorder>);
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should accept custom duration', async () => {
|
||||
const { RotatingBorder } = await import('./animations');
|
||||
render(<RotatingBorder duration={8}>Test</RotatingBorder>);
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CounterWithEffect', () => {
|
||||
it('should render with prefix and suffix', async () => {
|
||||
const { CounterWithEffect } = await import('./animations');
|
||||
render(<CounterWithEffect end={100} prefix="$" suffix="%" />);
|
||||
expect(screen.getByText(/100|\$/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/%/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', async () => {
|
||||
const { CounterWithEffect } = await import('./animations');
|
||||
render(<CounterWithEffect end={100} className="counter-eff-class" />);
|
||||
const element = screen.getByTestId('motion-span');
|
||||
expect(element).toHaveClass('counter-eff-class');
|
||||
});
|
||||
|
||||
it('should accept bounce effect', async () => {
|
||||
const { CounterWithEffect } = await import('./animations');
|
||||
render(<CounterWithEffect end={100} effect="bounce" />);
|
||||
expect(screen.getByTestId('motion-span')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should accept slide effect', async () => {
|
||||
const { CounterWithEffect } = await import('./animations');
|
||||
render(<CounterWithEffect end={100} effect="slide" />);
|
||||
expect(screen.getByTestId('motion-span')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should accept flip effect', async () => {
|
||||
const { CounterWithEffect } = await import('./animations');
|
||||
render(<CounterWithEffect end={100} effect="flip" />);
|
||||
expect(screen.getByTestId('motion-span')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import {
|
||||
CASE_STUDIES,
|
||||
CASE_INDUSTRIES,
|
||||
getCaseBySlug,
|
||||
getFeaturedCases,
|
||||
getCasesByIndustry,
|
||||
} from './cases';
|
||||
|
||||
describe('CASE_STUDIES', () => {
|
||||
it('should have 6 case studies', () => {
|
||||
expect(CASE_STUDIES.length).toBe(6);
|
||||
});
|
||||
|
||||
it('should have required properties on each case', () => {
|
||||
CASE_STUDIES.forEach((c) => {
|
||||
expect(c).toHaveProperty('id');
|
||||
expect(c).toHaveProperty('slug');
|
||||
expect(c).toHaveProperty('client');
|
||||
expect(c).toHaveProperty('industry');
|
||||
expect(c).toHaveProperty('companySize');
|
||||
expect(c).toHaveProperty('title');
|
||||
expect(c).toHaveProperty('subtitle');
|
||||
expect(c).toHaveProperty('challenge');
|
||||
expect(c).toHaveProperty('solution');
|
||||
expect(c).toHaveProperty('result');
|
||||
expect(c).toHaveProperty('metrics');
|
||||
expect(c).toHaveProperty('timeline');
|
||||
expect(c).toHaveProperty('services');
|
||||
expect(c).toHaveProperty('color');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have unique slugs', () => {
|
||||
const slugs = CASE_STUDIES.map((c) => c.slug);
|
||||
expect(new Set(slugs).size).toBe(slugs.length);
|
||||
});
|
||||
|
||||
it('should have unique ids', () => {
|
||||
const ids = CASE_STUDIES.map((c) => c.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
});
|
||||
|
||||
it('should have valid color values', () => {
|
||||
const validColors = ['brand', 'blue', 'teal', 'amber', 'purple'];
|
||||
CASE_STUDIES.forEach((c) => {
|
||||
expect(validColors).toContain(c.color);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have at least one metric per case', () => {
|
||||
CASE_STUDIES.forEach((c) => {
|
||||
expect(c.metrics.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have at least one timeline phase per case', () => {
|
||||
CASE_STUDIES.forEach((c) => {
|
||||
expect(c.timeline.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have at least one service per case', () => {
|
||||
CASE_STUDIES.forEach((c) => {
|
||||
expect(c.services.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have valid metric structure', () => {
|
||||
CASE_STUDIES.forEach((c) => {
|
||||
c.metrics.forEach((m) => {
|
||||
expect(m).toHaveProperty('value');
|
||||
expect(m).toHaveProperty('label');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should have valid timeline structure', () => {
|
||||
CASE_STUDIES.forEach((c) => {
|
||||
c.timeline.forEach((t) => {
|
||||
expect(t).toHaveProperty('phase');
|
||||
expect(t).toHaveProperty('duration');
|
||||
expect(t).toHaveProperty('description');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should have valid service structure', () => {
|
||||
CASE_STUDIES.forEach((c) => {
|
||||
c.services.forEach((s) => {
|
||||
expect(s).toHaveProperty('id');
|
||||
expect(s).toHaveProperty('title');
|
||||
expect(s).toHaveProperty('description');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should have 3 featured cases', () => {
|
||||
const featured = CASE_STUDIES.filter((c) => c.featured);
|
||||
expect(featured.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CASE_INDUSTRIES', () => {
|
||||
it('should have 7 industries', () => {
|
||||
expect(CASE_INDUSTRIES.length).toBe(7);
|
||||
});
|
||||
|
||||
it('should have required properties', () => {
|
||||
CASE_INDUSTRIES.forEach((ind) => {
|
||||
expect(ind).toHaveProperty('id');
|
||||
expect(ind).toHaveProperty('label');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have "all" as first industry', () => {
|
||||
expect(CASE_INDUSTRIES[0]?.id).toBe('all');
|
||||
expect(CASE_INDUSTRIES[0]?.label).toBe('全部行业');
|
||||
});
|
||||
|
||||
it('should have valid industry ids', () => {
|
||||
const validIds = ['all', '制造业', '贸易零售', '医疗健康', '教育培训', '金融服务', '物流运输'];
|
||||
CASE_INDUSTRIES.forEach((ind) => {
|
||||
expect(validIds).toContain(ind.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCaseBySlug', () => {
|
||||
it('should return the correct case by slug', () => {
|
||||
const result = getCaseBySlug('manufacturing-erp-upgrade');
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.id).toBe('case-001');
|
||||
expect(result?.client).toBe('某上市制造企业');
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent slug', () => {
|
||||
const result = getCaseBySlug('non-existent-slug');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const result = getCaseBySlug('');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFeaturedCases', () => {
|
||||
it('should return only featured cases', () => {
|
||||
const result = getFeaturedCases();
|
||||
expect(result.length).toBe(3);
|
||||
result.forEach((c) => {
|
||||
expect(c.featured).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include manufacturing case', () => {
|
||||
const result = getFeaturedCases();
|
||||
const slugs = result.map((c) => c.slug);
|
||||
expect(slugs).toContain('manufacturing-erp-upgrade');
|
||||
});
|
||||
|
||||
it('should include retail case', () => {
|
||||
const result = getFeaturedCases();
|
||||
const slugs = result.map((c) => c.slug);
|
||||
expect(slugs).toContain('retail-omnichannel');
|
||||
});
|
||||
|
||||
it('should include healthcare case', () => {
|
||||
const result = getFeaturedCases();
|
||||
const slugs = result.map((c) => c.slug);
|
||||
expect(slugs).toContain('healthcare-data-platform');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCasesByIndustry', () => {
|
||||
it('should return all cases when industry is "all"', () => {
|
||||
const result = getCasesByIndustry('all');
|
||||
expect(result.length).toBe(6);
|
||||
});
|
||||
|
||||
it('should filter by 制造业', () => {
|
||||
const result = getCasesByIndustry('制造业');
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]?.slug).toBe('manufacturing-erp-upgrade');
|
||||
});
|
||||
|
||||
it('should filter by 医疗健康', () => {
|
||||
const result = getCasesByIndustry('医疗健康');
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]?.slug).toBe('healthcare-data-platform');
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent industry', () => {
|
||||
const result = getCasesByIndustry('非存在行业');
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should return empty array for empty string', () => {
|
||||
const result = getCasesByIndustry('');
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import {
|
||||
getProductCrossRefs,
|
||||
getSolutionCrossRefs,
|
||||
getServiceCrossRefs,
|
||||
} from './cross-references';
|
||||
|
||||
describe('getProductCrossRefs', () => {
|
||||
it('should return related solutions for erp', () => {
|
||||
const result = getProductCrossRefs('erp');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
|
||||
const solutionRefs = result.filter((r) => r.type === 'solution');
|
||||
expect(solutionRefs.length).toBeGreaterThan(0);
|
||||
solutionRefs.forEach((r) => {
|
||||
expect(r).toHaveProperty('id');
|
||||
expect(r).toHaveProperty('title');
|
||||
expect(r).toHaveProperty('type', 'solution');
|
||||
expect(r).toHaveProperty('href');
|
||||
expect(r.href).toMatch(/^\/solutions\//);
|
||||
expect(r).toHaveProperty('reason');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return related products for erp', () => {
|
||||
const result = getProductCrossRefs('erp');
|
||||
const productRefs = result.filter((r) => r.type === 'product');
|
||||
productRefs.forEach((r) => {
|
||||
expect(r).toHaveProperty('id');
|
||||
expect(r).toHaveProperty('title');
|
||||
expect(r).toHaveProperty('type', 'product');
|
||||
expect(r).toHaveProperty('href');
|
||||
expect(r.href).toMatch(/^\/products\//);
|
||||
expect(r).toHaveProperty('reason');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return at most 3 product cross-refs', () => {
|
||||
const result = getProductCrossRefs('erp');
|
||||
const productRefs = result.filter((r) => r.type === 'product');
|
||||
expect(productRefs.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent product', () => {
|
||||
const result = getProductCrossRefs('non-existent');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return cross-refs for crm', () => {
|
||||
const result = getProductCrossRefs('crm');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
const solutionSlugs = result.filter((r) => r.type === 'solution').map((r) => r.id);
|
||||
expect(solutionSlugs).toContain('retail');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSolutionCrossRefs', () => {
|
||||
it('should return related products for manufacturing', () => {
|
||||
const result = getSolutionCrossRefs('manufacturing');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
result.forEach((r) => {
|
||||
expect(r).toHaveProperty('id');
|
||||
expect(r).toHaveProperty('title');
|
||||
expect(r).toHaveProperty('type', 'product');
|
||||
expect(r).toHaveProperty('href');
|
||||
expect(r.href).toMatch(/^\/products\//);
|
||||
expect(r).toHaveProperty('reason');
|
||||
});
|
||||
});
|
||||
|
||||
it('should include erp and bi for manufacturing', () => {
|
||||
const result = getSolutionCrossRefs('manufacturing');
|
||||
const productIds = result.map((r) => r.id);
|
||||
expect(productIds).toContain('erp');
|
||||
expect(productIds).toContain('bi');
|
||||
});
|
||||
|
||||
it('should include crm and bi for retail', () => {
|
||||
const result = getSolutionCrossRefs('retail');
|
||||
const productIds = result.map((r) => r.id);
|
||||
expect(productIds).toContain('crm');
|
||||
expect(productIds).toContain('bi');
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent solution', () => {
|
||||
const result = getSolutionCrossRefs('non-existent');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getServiceCrossRefs', () => {
|
||||
it('should return related solutions for software service', () => {
|
||||
const result = getServiceCrossRefs('software');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
result.forEach((r) => {
|
||||
expect(r).toHaveProperty('id');
|
||||
expect(r).toHaveProperty('title');
|
||||
expect(r).toHaveProperty('type', 'solution');
|
||||
expect(r).toHaveProperty('href');
|
||||
expect(r.href).toMatch(/^\/solutions\//);
|
||||
expect(r).toHaveProperty('reason');
|
||||
});
|
||||
});
|
||||
|
||||
it('should include manufacturing and retail for software', () => {
|
||||
const result = getServiceCrossRefs('software');
|
||||
const solutionIds = result.map((r) => r.id);
|
||||
expect(solutionIds).toContain('manufacturing');
|
||||
expect(solutionIds).toContain('retail');
|
||||
});
|
||||
|
||||
it('should return at most 3 solutions', () => {
|
||||
const result = getServiceCrossRefs('solutions');
|
||||
expect(result.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent service', () => {
|
||||
const result = getServiceCrossRefs('non-existent');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array for empty string', () => {
|
||||
const result = getServiceCrossRefs('');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user