/// /** * 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(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(); }); });