/** * Smoke Tests — Admin V2 连通性断裂探测 * * 6 个冒烟测试验证 Admin V2 页面与 SaaS 后端的完整连通性。 * 所有测试使用真实浏览器 + 真实 SaaS Server。 * * 前提条件: * - SaaS Server 运行在 http://localhost:8080 * - Admin V2 dev server 运行在 http://localhost:5173 * - 种子用户: testadmin / Admin123456 (super_admin) * * 运行: cd admin-v2 && npx playwright test smoke_admin */ import { test, expect, type Page } from '@playwright/test'; const SaaS_BASE = 'http://localhost:8080/api/v1'; const ADMIN_USER = 'admin'; const ADMIN_PASS = 'admin123'; // Helper: 通过 API 登录获取 HttpOnly cookie + 设置 localStorage async function apiLogin(page: Page) { const res = await page.request.post(`${SaaS_BASE}/auth/login`, { data: { username: ADMIN_USER, password: ADMIN_PASS }, }); const json = await res.json(); // 设置 localStorage 让 Admin V2 AuthGuard 认为已登录 await page.goto('/'); await page.evaluate((account) => { localStorage.setItem('zclaw_admin_account', JSON.stringify(account)); }, json.account); return json; } // Helper: 通过 API 登录 + 导航到指定路径 async function loginAndGo(page: Page, path: string) { await apiLogin(page); // 重新导航到目标路径 (localStorage 已设置,React 应识别为已登录) await page.goto(path, { waitUntil: 'networkidle' }); // 等待主内容区加载 await page.waitForSelector('#main-content', { timeout: 15000 }); } // ── A1: 登录→Dashboard ──────────────────────────────────────────── test('A1: 登录→Dashboard 5个统计卡片', async ({ page }) => { // 导航到登录页 await page.goto('/login'); await expect(page.getByPlaceholder('请输入用户名')).toBeVisible({ timeout: 10000 }); // 填写表单 await page.getByPlaceholder('请输入用户名').fill(ADMIN_USER); await page.getByPlaceholder('请输入密码').fill(ADMIN_PASS); // 提交 (Ant Design 按钮文本有全角空格 "登 录") const loginBtn = page.locator('button').filter({ hasText: /登/ }).first(); await loginBtn.click(); // 验证跳转到 Dashboard (可能需要等待 API 响应) await expect(page).toHaveURL(/\/(login)?$/, { timeout: 20000 }); // 验证 5 个统计卡片 await expect(page.getByText('总账号')).toBeVisible({ timeout: 10000 }); await expect(page.getByText('活跃服务商')).toBeVisible(); await expect(page.getByText('活跃模型')).toBeVisible(); await expect(page.getByText('今日请求')).toBeVisible(); await expect(page.getByText('今日 Token')).toBeVisible(); // 验证统计卡片有数值 (不是 loading 状态) const statCards = page.locator('.ant-statistic-content-value'); await expect(statCards.first()).not.toBeEmpty({ timeout: 10000 }); }); // ── A2: Provider CRUD ────────────────────────────────────────────── test('A2: Provider 创建→列表可见→禁用', async ({ page }) => { // 通过 API 创建 Provider await apiLogin(page); const createRes = await page.request.post(`${SaaS_BASE}/providers`, { data: { name: `smoke_provider_${Date.now()}`, provider_type: 'openai', base_url: 'https://api.smoke.test/v1', enabled: true, display_name: 'Smoke Test Provider', }, }); if (!createRes.ok()) { const body = await createRes.text(); console.log(`A2: Provider create failed: ${createRes.status()} — ${body.slice(0, 300)}`); } expect(createRes.ok()).toBeTruthy(); // 导航到 Model Services 页面 await page.goto('/model-services'); await page.waitForSelector('#main-content', { timeout: 15000 }); // 切换到 Provider tab (如果存在 tab 切换) const providerTab = page.getByRole('tab', { name: /服务商|Provider/i }); if (await providerTab.isVisible()) { await providerTab.click(); } // 验证 Provider 列表非空 const tableRows = page.locator('.ant-table-row'); await expect(tableRows.first()).toBeVisible({ timeout: 10000 }); expect(await tableRows.count()).toBeGreaterThan(0); }); // ── A3: Account 管理 ─────────────────────────────────────────────── test('A3: Account 列表加载→角色可见', async ({ page }) => { await loginAndGo(page, '/accounts'); // 验证表格加载 const tableRows = page.locator('.ant-table-row'); await expect(tableRows.first()).toBeVisible({ timeout: 10000 }); // 至少有 testadmin 自己 expect(await tableRows.count()).toBeGreaterThanOrEqual(1); // 验证有角色列 const roleText = await page.locator('.ant-table').textContent(); expect(roleText).toMatch(/super_admin|admin|user/); }); // ── A4: 知识管理 ─────────────────────────────────────────────────── test('A4: 知识分类→条目→搜索', async ({ page }) => { // 通过 API 创建分类和条目 await apiLogin(page); const catRes = await page.request.post(`${SaaS_BASE}/knowledge/categories`, { data: { name: `smoke_cat_${Date.now()}`, description: 'Smoke test category' }, }); expect(catRes.ok()).toBeTruthy(); const catJson = await catRes.json(); const itemRes = await page.request.post(`${SaaS_BASE}/knowledge/items`, { data: { title: 'Smoke Test Knowledge Item', content: 'This is a smoke test knowledge entry for E2E testing.', category_id: catJson.id, tags: ['smoke', 'test'], }, }); expect(itemRes.ok()).toBeTruthy(); // 导航到知识库页面 await page.goto('/knowledge'); await page.waitForSelector('#main-content', { timeout: 15000 }); // 验证页面加载 (有内容) const content = await page.locator('#main-content').textContent(); expect(content!.length).toBeGreaterThan(0); }); // ── A5: 角色权限 ─────────────────────────────────────────────────── test('A5: 角色页面加载→角色列表非空', async ({ page }) => { await loginAndGo(page, '/roles'); // 验证角色内容加载 await page.waitForTimeout(1000); // 检查页面有角色相关内容 (可能是表格或卡片) const content = await page.locator('#main-content').textContent(); expect(content!.length).toBeGreaterThan(0); // 通过 API 验证角色存在 const rolesRes = await page.request.get(`${SaaS_BASE}/roles`); expect(rolesRes.ok()).toBeTruthy(); const rolesJson = await rolesRes.json(); expect(Array.isArray(rolesJson) || rolesJson.roles).toBeTruthy(); }); // ── A6: 模型+Key池 ──────────────────────────────────────────────── test('A6: 模型服务页面加载→Provider和Model tab可见', async ({ page }) => { await loginAndGo(page, '/model-services'); // 验证页面标题或内容 const content = await page.locator('#main-content').textContent(); expect(content!.length).toBeGreaterThan(0); // 检查是否有 Tab 切换 (服务商/模型/API Key) const tabs = page.locator('.ant-tabs-tab'); if (await tabs.first().isVisible()) { const tabCount = await tabs.count(); expect(tabCount).toBeGreaterThanOrEqual(1); } // 通过 API 验证能列出 Provider const provRes = await page.request.get(`${SaaS_BASE}/providers`); expect(provRes.ok()).toBeTruthy(); });