test(qa): e2e 装配修正 + 集成/门禁测试与工具链重构

- e2e:修死选择器与蜜罐误命中、补断言、GA4/security-headers 用例诚实标注
  skip 边界;新增 assert-helpers/fixtures/hydrated/primary-nav/touch-targets
  等复用桩。
- 集成层:config/test/jest.integration.config.js + tests-integration/ 真库
  一次性 SQLite 用例,teardown 守卫开发库指纹。
- 门禁工具链:jest.setup 归位到根、scripts/accessibility 全站 axe 节点计数、
  check-brand-text-token/check-motion-constraints 机械守卫、stryker/lighthouse
  配置收敛。
This commit is contained in:
2026-09-28 10:48:08 +08:00
parent a366bd1400
commit 6bb7c557ee
142 changed files with 6597 additions and 2653 deletions
+135
View File
@@ -0,0 +1,135 @@
/**
* 真库集成测试的公共夹具。
*
* 这里没有任何 jest.mock —— 被测代码与 SQLite 之间不存在替身:
* `@/lib/db` 走的是 src/generated/prisma 的真实 PrismaClient,
* `@/lib/auth` 走的是真实 JWT 签名/校验(密钥由 config/test/itest/setup-files.js 注入,
* 刻意不读 .env*)。
*/
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/db';
import { generateTokens, hashPassword } from '@/lib/auth';
import type { FieldDefinition } from '@/lib/cms/types';
export { prisma };
let counter = 0;
/** 同一 worker 内共享一个库文件,唯一名避免用例间互相污染 */
export function unique(prefix: string): string {
counter += 1;
return `${prefix}-${Date.now().toString(36)}-${counter}`;
}
/** 按 FK 依赖顺序清空全部业务表(保留表结构) */
export async function resetDb(): Promise<void> {
await prisma.auditLog.deleteMany();
await prisma.notification.deleteMany();
await prisma.contentItem.deleteMany();
await prisma.contentZone.deleteMany();
await prisma.mediaAsset.deleteMany();
await prisma.permission.deleteMany();
await prisma.userRole.deleteMany();
await prisma.role.deleteMany();
await prisma.contentModel.deleteMany();
await prisma.user.deleteMany();
}
export async function seedRole(code: string, permissions: Array<{ modelCode: string; action: string }> = []): Promise<void> {
await prisma.role.create({ data: { code, name: code } });
for (const p of permissions) {
await prisma.permission.create({ data: { roleCode: code, modelCode: p.modelCode, action: p.action } });
}
}
export interface SeededUser {
id: string;
username: string;
password: string;
}
export async function seedUser(
username: string,
roleCodes: string[],
options: { password?: string; status?: number } = {},
): Promise<SeededUser> {
const password = options.password ?? 'Integration-test-2026';
const user = await prisma.user.create({
data: {
username,
// 真实 bcrypt 哈希:集成层不 mock @/lib/auth,登录/刷新走的是真校验
password: await hashPassword(password),
status: options.status ?? 1,
role: roleCodes[0] ?? 'readonly',
},
});
for (const roleCode of roleCodes) {
await prisma.userRole.create({ data: { userId: user.id, roleCode } });
}
return { id: user.id, username: user.username, password };
}
/** 真实签名的 access token(与 src/lib/auth 的 verifyAccessToken 对称) */
export function accessTokenFor(user: SeededUser, role = 'admin'): string {
return generateTokens({ userId: user.id, username: user.username, role }).accessToken;
}
export function refreshTokenFor(user: SeededUser, role = 'admin'): string {
return generateTokens({ userId: user.id, username: user.username, role }).refreshToken;
}
export interface RequestOptions {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: unknown;
token?: string;
headers?: Record<string, string>;
}
/**
* 构造真实 NextRequest —— 与仓库里既有的 route.test.ts 同一入口形态,
* 但这里不替换 headers/url/json,NextRequest 自己解析 cookie 与 searchParams。
*/
export function makeRequest(options: RequestOptions): NextRequest {
const method = options.method ?? 'GET';
const withBody = options.body !== undefined && method !== 'GET';
const headers: Record<string, string> = { ...options.headers };
if (options.token) headers.authorization = `Bearer ${options.token}`;
if (withBody) headers['content-type'] = 'application/json';
// 字面量直接内联,让 init 按 next/server 自己的 RequestInit 形状做上下文推断
return new NextRequest(options.url, {
method,
headers,
...(withBody ? { body: JSON.stringify(options.body) } : {}),
});
}
export const API = {
items: 'http://localhost:3000/api/admin/items',
users: 'http://localhost:3000/api/admin/users',
roles: 'http://localhost:3000/api/admin/roles',
refresh: 'http://localhost:3000/api/auth/refresh',
workflow: (id: string): string =>
`http://localhost:3000/api/admin/items/${id}/workflow`,
};
/** 读取 ContentItem.data 的**原始列值**(绕过任何解析,证明落库内容) */
export async function rawDataColumn(id: string): Promise<string> {
const rows = await prisma.$queryRawUnsafe<Array<{ data: string }>>(
`SELECT data FROM "ContentItem" WHERE id = ?`,
id,
);
return rows[0]?.data ?? '';
}
/** 以给定字段声明建一个内容模型,返回 { id, code } */
export async function seedModel(code: string, fields: FieldDefinition[]): Promise<{ id: string; code: string }> {
const model = await prisma.contentModel.create({
data: { code, name: code, fields: JSON.stringify(fields) },
});
return { id: model.id, code: model.code };
}
export async function disconnect(): Promise<void> {
await prisma.$disconnect();
}