Files
lingxi/e2e/a3-client.spec.ts
T
Stev_Wang 140dc386c1 feat(client): A3 客户与行业目录(两级行业、客户组、字段审批)
背景:A3 需把行业 / 客户 / 客户组 / 客户级字段立为资产根节点;用户确认两级行业树与混搭 UI(行业 B / 客户列表 A / 详情 A / 新建 B / 客户组 B / 字段 A)。

改动:
- 新增 ADR-002,行业改为两级树,客户挂末级行业;同步 PRD / 技术架构 / 开发计划。
- 新增 client 模块:industries / customers / customer-groups / customer-fields 接口、真实 PrismaCustomerDirectory、客户状态机与客户转交、客户组数据范围解析、跨客户 404。
- 新增迁移与幂等种子(电商/金融两级行业、2 客户、1 客户组、4 条字段)。
- 前端新增行业/客户/客户组/字段审批页面与路由、导航更名(客户管理 / 行业管理 / 客户组),复用现有 token。
- 补充 shared 契约单测、client 集成用例、A3 浏览器 e2e。

影响面:client 模块与 client 表、shared 契约、web 路由/导航、ScopeContext 数据范围、OpenAPI 与种子;A1 客户转交占位由 client 实现替换。
回滚:回滚本提交;数据库保留新增表或按报告执行 down SQL;CUSTOMER_DIRECTORY 可切回空集实现。
验证:format:check / lint / typecheck / test 全绿(shared 131 / api 90 / web 125,共 346);test:int 5 文件 39 条;A3 e2e 1/1;build 8/8;真实浏览器六页验证通过。
2026-11-03 21:45:38 +08:00

121 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// <reference lib="dom" />
/**
* A3-QA-01 · 客户与行业目录端到端用例(真实 API + 真实 PG + 真实浏览器)
*
* 覆盖:登录 → 行业管理树/详情 → 客户列表 → 客户详情 → 新建客户向导 → 客户组 → 字段审批。
* 门控:仅 A3_E2E=1 时执行;默认不跑,避免影响 CI / smoke。
*
* 前置:API 3000、Web 5173(`pnpm dev`);用例自建 `qa3.e2e` 超管账号并在结束后删除。
* 运行:A3_E2E=1 pnpm exec playwright test e2e/a3-client.spec.ts --reporter=list
*/
import { randomBytes } from 'node:crypto';
import { expect, test } from '@playwright/test';
import { PrismaClient } from '@prisma/client';
const API_BASE = process.env.A3_E2E_API_BASE_URL ?? 'http://127.0.0.1:3000/api/v1';
const WEB_BASE = process.env.A3_E2E_BASE_URL ?? 'http://127.0.0.1:5173';
const ENABLED = process.env.A3_E2E === '1';
const PASSWORD = 'lingxi2026';
const PASSWORD_HASH =
'$argon2id$v=19$m=65536,p=4,t=3$Osdgg3Fz53AwcoC2kyGiGg$QVbxW7Ok/SlCNbML95go2J/6GMSfE1EG3jBTJgR4zO4';
const ACCOUNT = 'qa3.e2e';
const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
const prisma = new PrismaClient();
function ulid(): string {
let time = Date.now();
const timeChars = new Array<string>(10);
for (let index = 9; index >= 0; index -= 1) {
timeChars[index] = ENCODING[time % 32] ?? '0';
time = Math.floor(time / 32);
}
const random = randomBytes(16);
return `${timeChars.join('')}${Array.from(random, (byte) => ENCODING[byte & 31] ?? '0').join('')}`;
}
test.describe('A3 客户与行业目录', () => {
test.skip(!ENABLED, 'A3_E2E=1 时执行;需要 API 3000 + Web 5173');
test.beforeAll(async () => {
const [role, department] = await Promise.all([
prisma.role.findUniqueOrThrow({ where: { code: 'super_admin' } }),
prisma.department.findUniqueOrThrow({ where: { code: 'content-g1' } }),
]);
await prisma.user.deleteMany({ where: { account: ACCOUNT } });
await prisma.user.create({
data: {
id: ulid(),
name: 'A3 端到端',
account: ACCOUNT,
email: `${ACCOUNT}@lingxi.cn`,
phone: '13500008888',
passwordHash: PASSWORD_HASH,
departmentId: department.id,
mustChangePassword: false,
roles: { create: [{ roleId: role.id }] },
},
});
});
test.afterAll(async () => {
await prisma.user.deleteMany({ where: { account: ACCOUNT } });
await prisma.$disconnect();
});
test('行业 / 客户 / 客户组 / 字段审批六页可访问且数据加载', async ({ page, request }) => {
const login = await request.post(`${API_BASE}/auth/login`, {
data: { account: ACCOUNT, password: PASSWORD },
});
expect(login.ok()).toBe(true);
const session = (await login.json()) as { accessToken: string; refreshToken: string };
const meResponse = await request.get(`${API_BASE}/me`, {
headers: { authorization: `Bearer ${session.accessToken}` },
});
expect(meResponse.ok()).toBe(true);
const me = (await meResponse.json()) as {
user: { id: string; name: string; account: string };
roles: { name: string }[];
permissions: string[];
};
await page.addInitScript(
(value) => {
window.localStorage.setItem('lingxi.auth.session', JSON.stringify(value));
},
{
accessToken: session.accessToken,
refreshToken: session.refreshToken,
user: {
id: me.user.id,
name: me.user.name,
account: me.user.account,
roleName: me.roles[0]?.name,
},
permissions: me.permissions,
mustChangePassword: false,
},
);
const pages = [
{ path: '/system/industries', title: '行业管理' },
{ path: '/system/customers', title: '客户管理' },
{ path: '/system/customers/new', title: '新建客户' },
{ path: '/system/customer-groups', title: '客户组' },
{ path: '/system/customer-fields/approvals', title: '客户字段审批' },
];
for (const item of pages) {
await page.goto(`${WEB_BASE}${item.path}`, { waitUntil: 'networkidle' });
await expect(page.locator('h1').first()).toHaveText(item.title);
}
await page.goto(`${WEB_BASE}/system/customers`, { waitUntil: 'networkidle' });
const customerRows = page.locator('.lx-client-table tbody tr');
const count = await customerRows.count();
expect(count).toBeGreaterThan(0);
await customerRows.first().click();
await expect(page).toHaveURL(/\/system\/customers\/[A-Z0-9]+$/);
await expect(page.locator('h1').first()).toBeVisible();
});
});