// @ts-nocheck require('@testing-library/jest-dom'); const { TextEncoder, TextDecoder } = require('util'); global.TextEncoder = TextEncoder; global.TextDecoder = TextDecoder; jest.mock('nanoid', () => ({ nanoid: jest.fn(() => 'test-id-123'), })); jest.mock('@/generated/prisma/client', () => ({ PrismaClient: jest.fn().mockImplementation(() => ({ contentItem: { findMany: jest.fn().mockResolvedValue([]), findFirst: jest.fn().mockResolvedValue(null), findUnique: jest.fn().mockResolvedValue(null), }, contentModel: { findMany: jest.fn().mockResolvedValue([]), }, contentZone: { findMany: jest.fn().mockResolvedValue([]), findUnique: jest.fn().mockResolvedValue(null), }, })), })); jest.mock('@/lib/cms/data-server', () => ({ getPublishedItems: jest.fn().mockResolvedValue([]), getPublishedItemBySlug: jest.fn().mockResolvedValue(null), getItemById: jest.fn().mockResolvedValue(null), getPageZones: jest.fn().mockResolvedValue([]), getZone: jest.fn().mockResolvedValue(null), getContentModels: jest.fn().mockResolvedValue([]), getCases: jest.fn().mockResolvedValue([]), getNews: jest.fn().mockResolvedValue([]), getServices: jest.fn().mockResolvedValue([]), getProducts: jest.fn().mockResolvedValue([]), getSolutions: jest.fn().mockResolvedValue([]), getStats: jest.fn().mockResolvedValue([]), getHeroBanners: jest.fn().mockResolvedValue([]), getCaseBySlug: jest.fn().mockResolvedValue(null), getNewsBySlug: jest.fn().mockResolvedValue(null), getServiceBySlug: jest.fn().mockResolvedValue(null), getProductBySlug: jest.fn().mockResolvedValue(null), getSolutionBySlug: jest.fn().mockResolvedValue(null), getHomePageZones: jest.fn().mockResolvedValue([]), getAllPublishedSlugs: jest.fn().mockResolvedValue([]), })); // 全局 mock lucide-react 防止 missing named export 导致测试渲染崩溃 // 解决 P5 切换 CTAButton 后暴露的问题:lucide-react 是 ESM,jest 转换默认排除 node_modules, // 测试中未显式 mock 的 lucide 命名导入会拿到 undefined → 渲染崩。 // 用 Proxy 兜底所有图标(任何 named import 自动返回 stub 组件), // 对齐 services-content-v3.test.tsx 等显式 mock 范式(data-testid: icon-{name.toLowerCase()})。 // 测试内的显式 jest.mock('lucide-react', ...) 会覆盖此兜底(保留细粒度 mock 兼容性)。 jest.mock('lucide-react', () => { const React = require('react'); const makeIcon = (name) => { const Icon = (props) => React.createElement('svg', { 'data-testid': `icon-${name.toLowerCase()}`, className: props?.className, }); Icon.displayName = name; return Icon; }; const lucide = new Proxy({ __esModule: true }, { get(_target, prop) { if (prop === '__esModule') return true; if (prop === 'default') return lucide; if (typeof prop === 'symbol') return undefined; return makeIcon(String(prop)); }, }); return lucide; }); jest.mock('next/dynamic', () => ({ __esModule: true, default: (_importFn, _options) => { const MockComponent = (_props) => null; MockComponent.displayName = 'DynamicComponent'; MockComponent.preload = () => Promise.resolve(); return MockComponent; }, })); jest.mock('next/server', () => ({ NextRequest: class MockNextRequest { constructor(input, init = {}) { this.url = typeof input === 'string' ? input : input.url; this.method = init.method || 'GET'; this.headers = new Headers(init.headers); this.body = init.body; } async json() { return typeof this.body === 'string' ? JSON.parse(this.body) : this.body; } }, NextResponse: { json: (body, init = {}) => ({ status: init.status || 200, json: async () => body, }), }, })); global.console = { ...console, error: jest.fn(), warn: jest.fn(), log: jest.fn(), }; class MockIntersectionObserver { constructor(callback, options = {}) { this.callback = callback; this.options = options; this.elements = new Set(); this.observationEntries = []; } observe(element) { this.elements.add(element); const entry = { isIntersecting: true, target: element, boundingClientRect: element.getBoundingClientRect ? element.getBoundingClientRect() : {}, intersectionRatio: 1, intersectionRect: {}, rootBounds: {}, time: Date.now(), }; this.observationEntries.push(entry); this.callback(this.observationEntries, this); } unobserve(element) { this.elements.delete(element); this.observationEntries = this.observationEntries.filter( entry => entry.target !== element ); } disconnect() { this.elements.clear(); this.observationEntries = []; } takeRecords() { return this.observationEntries; } } global.IntersectionObserver = MockIntersectionObserver; global.IntersectionObserverEntry = class IntersectionObserverEntry { constructor() { this.isIntersecting = true; this.target = {}; this.boundingClientRect = {}; this.intersectionRatio = 1; this.intersectionRect = {}; this.rootBounds = {}; this.time = Date.now(); } }; Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation(query => ({ matches: false, media: query, onchange: null, addListener: jest.fn(), removeListener: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), })), }); global.Request = class Request { constructor(input, init = {}) { this.url = typeof input === 'string' ? input : input.url; this.method = init.method || 'GET'; this.headers = new Headers(init.headers); this.body = init.body; } async json() { return typeof this.body === 'string' ? JSON.parse(this.body) : this.body; } }; global.Headers = class Headers { constructor(init = {}) { this.headers = {}; if (init) { Object.entries(init).forEach(([key, value]) => { this.headers[key.toLowerCase()] = value; }); } } get(name) { return this.headers[name.toLowerCase()]; } set(name, value) { this.headers[name.toLowerCase()] = value; } }; global.Response = class Response { constructor(body, init = {}) { this.body = body; this.status = init.status || 200; this.statusText = init.statusText || 'OK'; this.headers = new Headers(init.headers); this.ok = this.status >= 200 && this.status < 300; } async json() { return typeof this.body === 'string' ? JSON.parse(this.body) : this.body; } async text() { return String(this.body); } };