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;真实浏览器六页验证通过。
This commit is contained in:
1 parent
8626996bb1
commit
140dc386c1
54 files changed
+9018
-147
No files matched your search
@@ -0,0 +1,120 @@
|
||||
/// <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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user