# Instructions - Following Playwright test failed. - Explain why, be concise, respect Playwright best practices. - Provide a snippet of code with the fix, if possible. # Test info - Name: smoke_admin.spec.ts >> A6: 模型服务页面加载→Provider和Model tab可见 - Location: tests\e2e\smoke_admin.spec.ts:179:1 # Error details ``` TimeoutError: page.waitForSelector: Timeout 15000ms exceeded. Call log: - waiting for locator('#main-content') to be visible ``` # Page snapshot ```yaml - generic [ref=e1]: - link "跳转到主要内容" [ref=e2] [cursor=pointer]: - /url: "#main-content" - generic [ref=e5]: - generic [ref=e9]: - generic [ref=e11]: Z - heading "ZCLAW" [level=1] [ref=e12] - paragraph [ref=e13]: AI Agent 管理平台 - paragraph [ref=e15]: 统一管理 AI 服务商、模型配置、API 密钥、用量监控与系统配置 - generic [ref=e17]: - heading "登录" [level=2] [ref=e18] - paragraph [ref=e19]: 输入您的账号信息以继续 - generic [ref=e22]: - generic [ref=e28]: - img "user" [ref=e30]: - img [ref=e31] - textbox "请输入用户名" [active] [ref=e33] - generic [ref=e40]: - img "lock" [ref=e42]: - img [ref=e43] - textbox "请输入密码" [ref=e45] - img "eye-invisible" [ref=e47] [cursor=pointer]: - img [ref=e48] - button "登 录" [ref=e51] [cursor=pointer]: - generic [ref=e52]: 登 录 ``` # Test source ```ts 1 | /** 2 | * Smoke Tests — Admin V2 连通性断裂探测 3 | * 4 | * 6 个冒烟测试验证 Admin V2 页面与 SaaS 后端的完整连通性。 5 | * 所有测试使用真实浏览器 + 真实 SaaS Server。 6 | * 7 | * 前提条件: 8 | * - SaaS Server 运行在 http://localhost:8080 9 | * - Admin V2 dev server 运行在 http://localhost:5173 10 | * - 种子用户: testadmin / Admin123456 (super_admin) 11 | * 12 | * 运行: cd admin-v2 && npx playwright test smoke_admin 13 | */ 14 | 15 | import { test, expect, type Page } from '@playwright/test'; 16 | 17 | const SaaS_BASE = 'http://localhost:8080/api/v1'; 18 | const ADMIN_USER = 'admin'; 19 | const ADMIN_PASS = 'admin123'; 20 | 21 | // Helper: 通过 API 登录获取 HttpOnly cookie + 设置 localStorage 22 | async function apiLogin(page: Page) { 23 | const res = await page.request.post(`${SaaS_BASE}/auth/login`, { 24 | data: { username: ADMIN_USER, password: ADMIN_PASS }, 25 | }); 26 | const json = await res.json(); 27 | // 设置 localStorage 让 Admin V2 AuthGuard 认为已登录 28 | await page.goto('/'); 29 | await page.evaluate((account) => { 30 | localStorage.setItem('zclaw_admin_account', JSON.stringify(account)); 31 | }, json.account); 32 | return json; 33 | } 34 | 35 | // Helper: 通过 API 登录 + 导航到指定路径 36 | async function loginAndGo(page: Page, path: string) { 37 | await apiLogin(page); 38 | // 重新导航到目标路径 (localStorage 已设置,React 应识别为已登录) 39 | await page.goto(path, { waitUntil: 'networkidle' }); 40 | // 等待主内容区加载 > 41 | await page.waitForSelector('#main-content', { timeout: 15000 }); | ^ TimeoutError: page.waitForSelector: Timeout 15000ms exceeded. 42 | } 43 | 44 | // ── A1: 登录→Dashboard ──────────────────────────────────────────── 45 | 46 | test('A1: 登录→Dashboard 5个统计卡片', async ({ page }) => { 47 | // 导航到登录页 48 | await page.goto('/login'); 49 | await expect(page.getByPlaceholder('请输入用户名')).toBeVisible({ timeout: 10000 }); 50 | 51 | // 填写表单 52 | await page.getByPlaceholder('请输入用户名').fill(ADMIN_USER); 53 | await page.getByPlaceholder('请输入密码').fill(ADMIN_PASS); 54 | 55 | // 提交 (Ant Design 按钮文本有全角空格 "登 录") 56 | const loginBtn = page.locator('button').filter({ hasText: /登/ }).first(); 57 | await loginBtn.click(); 58 | 59 | // 验证跳转到 Dashboard (可能需要等待 API 响应) 60 | await expect(page).toHaveURL(/\/(login)?$/, { timeout: 20000 }); 61 | 62 | // 验证 5 个统计卡片 63 | await expect(page.getByText('总账号')).toBeVisible({ timeout: 10000 }); 64 | await expect(page.getByText('活跃服务商')).toBeVisible(); 65 | await expect(page.getByText('活跃模型')).toBeVisible(); 66 | await expect(page.getByText('今日请求')).toBeVisible(); 67 | await expect(page.getByText('今日 Token')).toBeVisible(); 68 | 69 | // 验证统计卡片有数值 (不是 loading 状态) 70 | const statCards = page.locator('.ant-statistic-content-value'); 71 | await expect(statCards.first()).not.toBeEmpty({ timeout: 10000 }); 72 | }); 73 | 74 | // ── A2: Provider CRUD ────────────────────────────────────────────── 75 | 76 | test('A2: Provider 创建→列表可见→禁用', async ({ page }) => { 77 | // 通过 API 创建 Provider 78 | await apiLogin(page); 79 | const createRes = await page.request.post(`${SaaS_BASE}/providers`, { 80 | data: { 81 | name: `smoke_provider_${Date.now()}`, 82 | provider_type: 'openai', 83 | base_url: 'https://api.smoke.test/v1', 84 | enabled: true, 85 | display_name: 'Smoke Test Provider', 86 | }, 87 | }); 88 | if (!createRes.ok()) { 89 | const body = await createRes.text(); 90 | console.log(`A2: Provider create failed: ${createRes.status()} — ${body.slice(0, 300)}`); 91 | } 92 | expect(createRes.ok()).toBeTruthy(); 93 | 94 | // 导航到 Model Services 页面 95 | await page.goto('/model-services'); 96 | await page.waitForSelector('#main-content', { timeout: 15000 }); 97 | 98 | // 切换到 Provider tab (如果存在 tab 切换) 99 | const providerTab = page.getByRole('tab', { name: /服务商|Provider/i }); 100 | if (await providerTab.isVisible()) { 101 | await providerTab.click(); 102 | } 103 | 104 | // 验证 Provider 列表非空 105 | const tableRows = page.locator('.ant-table-row'); 106 | await expect(tableRows.first()).toBeVisible({ timeout: 10000 }); 107 | expect(await tableRows.count()).toBeGreaterThan(0); 108 | }); 109 | 110 | // ── A3: Account 管理 ─────────────────────────────────────────────── 111 | 112 | test('A3: Account 列表加载→角色可见', async ({ page }) => { 113 | await loginAndGo(page, '/accounts'); 114 | 115 | // 验证表格加载 116 | const tableRows = page.locator('.ant-table-row'); 117 | await expect(tableRows.first()).toBeVisible({ timeout: 10000 }); 118 | 119 | // 至少有 testadmin 自己 120 | expect(await tableRows.count()).toBeGreaterThanOrEqual(1); 121 | 122 | // 验证有角色列 123 | const roleText = await page.locator('.ant-table').textContent(); 124 | expect(roleText).toMatch(/super_admin|admin|user/); 125 | }); 126 | 127 | // ── A4: 知识管理 ─────────────────────────────────────────────────── 128 | 129 | test('A4: 知识分类→条目→搜索', async ({ page }) => { 130 | // 通过 API 创建分类和条目 131 | await apiLogin(page); 132 | 133 | const catRes = await page.request.post(`${SaaS_BASE}/knowledge/categories`, { 134 | data: { name: `smoke_cat_${Date.now()}`, description: 'Smoke test category' }, 135 | }); 136 | expect(catRes.ok()).toBeTruthy(); 137 | const catJson = await catRes.json(); 138 | 139 | const itemRes = await page.request.post(`${SaaS_BASE}/knowledge/items`, { 140 | data: { 141 | title: 'Smoke Test Knowledge Item', ```