test: add 30 smoke tests for break detection across SaaS/Admin/Desktop
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Layer 1 断裂探测矩阵: - S1-S6: SaaS API 端到端 (auth/lockout/relay/permissions/billing/knowledge) - A1-A6: Admin V2 连通性 (login/dashboard/CRUD/knowledge/roles/models) - D1-D6: Desktop 聊天流 (gateway/kernel/relay/cancel/offline/error) - F1-F6: Desktop 功能闭环 (agent/hands/pipeline/memory/butler/skills) - X1-X6: 跨系统闭环 (provider→desktop/disabled user/knowledge/stats/totp/billing) Also adds: admin-v2 Playwright config, updated spec doc with cross-reference Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
50
admin-v2/playwright.config.ts
Normal file
50
admin-v2/playwright.config.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Admin V2 E2E 测试配置
|
||||
*
|
||||
* 断裂探测冒烟测试 — 验证 Admin V2 页面与 SaaS 后端的连通性
|
||||
*
|
||||
* 前提条件:
|
||||
* - SaaS Server 运行在 http://localhost:8080
|
||||
* - Admin V2 dev server 运行在 http://localhost:5173
|
||||
* - 数据库有种子数据 (super_admin: testadmin/Admin123456)
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
timeout: 60000,
|
||||
expect: {
|
||||
timeout: 10000,
|
||||
},
|
||||
fullyParallel: false,
|
||||
retries: 0,
|
||||
workers: 1,
|
||||
reporter: [
|
||||
['list'],
|
||||
['html', { outputFolder: 'test-results/html-report' }],
|
||||
],
|
||||
use: {
|
||||
baseURL: 'http://localhost:5173',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
actionTimeout: 10000,
|
||||
navigationTimeout: 30000,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
viewport: { width: 1280, height: 720 },
|
||||
},
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'pnpm dev --port 5173',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: true,
|
||||
timeout: 30000,
|
||||
},
|
||||
outputDir: 'test-results/artifacts',
|
||||
});
|
||||
182
admin-v2/tests/e2e/smoke_admin.spec.ts
Normal file
182
admin-v2/tests/e2e/smoke_admin.spec.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* 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 = 'testadmin';
|
||||
const ADMIN_PASS = 'Admin123456';
|
||||
|
||||
// Helper: 通过 API 登录获取 HttpOnly cookie,避免 UI 登录的复杂性
|
||||
async function apiLogin(page: Page) {
|
||||
await page.request.post(`${SaaS_BASE}/auth/login`, {
|
||||
data: { username: ADMIN_USER, password: ADMIN_PASS },
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: 通过 API 登录 + 导航到指定路径
|
||||
async function loginAndGo(page: Page, path: string) {
|
||||
await apiLogin(page);
|
||||
await page.goto(path);
|
||||
// 等待主内容区加载
|
||||
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);
|
||||
|
||||
// 提交
|
||||
await page.getByRole('button', { name: /登录/ }).click();
|
||||
|
||||
// 验证跳转到 Dashboard
|
||||
await expect(page).toHaveURL(/\/$/, { timeout: 15000 });
|
||||
|
||||
// 验证 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,
|
||||
},
|
||||
});
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user