- jest.setup.js: Proxy 兜底所有 lucide 命名导入,防 P5 CTAButton 后 未显式 mock 的图标 undefined 导致渲染崩溃(测试内显式 mock 仍可覆盖) - playwright.config.ts: outputDir 显式指向 e2e/test-results,避免项目根 test-results 触发 WorkBuddy 沙箱 safe-delete BULK_GUARD(fs.rm ≥50 被拦)
234 lines
6.6 KiB
JavaScript
234 lines
6.6 KiB
JavaScript
// @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 → <ArrowRight /> 渲染崩。
|
||
// 用 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);
|
||
}
|
||
};
|