import { expect, type Page, test } from '@playwright/test'; /** * 新建表单冒烟 · UI 级端到端测试 * 覆盖本轮补齐的「未实现功能」:创建知识库 / 文件上传 / 新建用户 / * 新建 AI 模型 / 新建流程定义。IM SignalR 为真实 WebSocket,不在 mock 范围内。 * * 后端 API 通过 page.route 拦截 mock(envelope {code,data,message} 与 * ApiResponseMiddleware 对齐),保证确定性、无外部基础设施依赖。 */ const envelope = (data: unknown) => ({ code: 0, data, message: 'ok' }); const json = (body: unknown, status = 200) => ({ status, contentType: 'application/json', body: JSON.stringify(body), }); interface CapturedRequest { path: string; method: string; /** JSON body 或 multipart 原始文本 */ body: string; } interface MockState { authed: boolean; posts: CapturedRequest[]; } /** 安装全量 /api mock;POST 请求体统一捕获到 state.posts 供断言。 */ async function installApiMocks(page: Page): Promise { const state: MockState = { authed: false, posts: [] }; await page.route('**/api/**', async (route) => { const req = route.request(); const url = new URL(req.url()); const path = url.pathname; if (path === '/api/auth/login' && req.method() === 'POST') { state.authed = true; return route.fulfill( json( envelope({ accessToken: 'e2e-access-token', refreshToken: 'e2e-refresh-token', expiresAt: new Date(Date.now() + 3600_000).toISOString(), }), ), ); } if (!state.authed) { return route.fulfill( json({ code: 'unauthorized', data: null, message: '未登录' }, 401), ); } if (path === '/api/user/info' && req.method() === 'GET') { return route.fulfill( json( envelope({ userId: 'u-e2e', username: 'admin', realName: 'E2E 管理员', homePath: '/welcome', roles: ['admin'], email: 'admin@e2e.local', }), ), ); } if (req.method() === 'POST') { state.posts.push({ path, method: 'POST', body: req.postData() ?? '' }); return route.fulfill(json(envelope({ id: 'created-e2e-id' }))); } // 各列表页 GET:按页面期望的返回结构给空数据 if (path === '/api/workflow-definitions') { return route.fulfill( json(envelope({ items: [], total: 0, pageIndex: 1, pageSize: 20 })), ); } return route.fulfill(json(envelope([]))); }); return state; } /** 走完登录流程。 */ async function login(page: Page) { await page.goto('/user/login'); await page.getByPlaceholder('邮箱 / 手机号 / 用户名').fill('admin'); await page.getByPlaceholder('请输入密码').fill('e2e-password'); await page.getByRole('button', { name: /登\s*录|登录/ }).click(); await page.waitForURL(/\/(dashboard|welcome)?$/, { timeout: 30_000 }); } test.describe('创建知识库', () => { test('填写名称提交后 POST /knowledge-bases 且名称正确', async ({ page }) => { const state = await installApiMocks(page); await login(page); await page.goto('/ai/knowledge'); await page .getByRole('button', { name: /创建知识库/ }) .first() .click(); await page.getByTestId('kb-name-input').fill('E2E 冒烟知识库'); await page.getByTestId('kb-create-submit').click(); await expect(page.getByText('E2E 冒烟知识库')).toBeHidden({ timeout: 10_000, }); const post = state.posts.find((p) => p.path === '/api/knowledge-bases'); const body = JSON.parse((post as CapturedRequest).body); expect(body.name).toBe('E2E 冒烟知识库'); // 未填的可选项一律传 null,与后端默认值约定一致 expect(body.chunkingModeEnum).toBeNull(); expect(body.retrievalModeEnum).toBeNull(); }); test('名称为空时前端必填校验拦截,不发起请求', async ({ page }) => { const state = await installApiMocks(page); await login(page); await page.goto('/ai/knowledge'); await page .getByRole('button', { name: /创建知识库/ }) .first() .click(); await page.getByTestId('kb-create-submit').click(); await expect(page.getByText('请输入知识库名称')).toBeVisible(); expect( state.posts.find((p) => p.path === '/api/knowledge-bases'), ).toBeUndefined(); }); }); test.describe('文件上传', () => { test('选择文件后以 multipart 提交 /files/upload(含 ObjectKey 与 ContentHash)', async ({ page, }) => { const state = await installApiMocks(page); await login(page); await page.goto('/files/list'); await expect(page.getByTestId('file-upload-input')).toBeAttached(); await page.getByTestId('file-upload-input').setInputFiles({ name: 'e2e-note.md', mimeType: 'text/markdown', buffer: Buffer.from('# E2E 冒烟文件\n'), }); await expect( page.locator('.ant-message').getByText(/上传完成:1 个文件/), ).toBeVisible({ timeout: 10_000, }); const post = state.posts.find((p) => p.path === '/api/files/upload'); // multipart 原始体中包含三个表单字段 expect((post as CapturedRequest).body).toContain('name="File"'); expect((post as CapturedRequest).body).toContain('name="ObjectKey"'); expect((post as CapturedRequest).body).toContain('uploads/'); expect((post as CapturedRequest).body).toContain('name="ContentHash"'); expect((post as CapturedRequest).body).toContain('e2e-note.md'); }); }); test.describe('系统管理 · 新建用户', () => { test('填写用户名/邮箱/密码提交后 POST /users', async ({ page }) => { const state = await installApiMocks(page); await login(page); await page.goto('/system/user'); await page .getByRole('button', { name: /新建用户/ }) .first() .click(); await page.getByPlaceholder('登录账号').fill('e2e-user'); await page.getByPlaceholder('name@example.com').fill('e2e@example.com'); await page.getByPlaceholder('至少 8 位,含字母与数字').fill('Passw0rd123'); await page.getByTestId('create-form-submit').click(); await expect( page.locator('.ant-message').getByText('用户已创建'), ).toBeVisible({ timeout: 10_000 }); const post = state.posts.find((p) => p.path === '/api/users'); const body = JSON.parse((post as CapturedRequest).body); expect(body).toEqual({ username: 'e2e-user', email: 'e2e@example.com', password: 'Passw0rd123', }); }); }); test.describe('AI 模型 · 新建模型', () => { test('完整填写后 POST /ai/models,枚举值与后端对齐', async ({ page }) => { const state = await installApiMocks(page); await login(page); await page.goto('/ai/models'); await expect(page.getByText('暂无模型配置')).toBeVisible(); await page.getByRole('button', { name: /新建模型/ }).click(); await page.getByTestId('ai-model-name').fill('E2E 对话模型'); // antd Select:点开 combobox,再点下拉选项(role 定位,不依赖 antd 类名) await page.getByRole('combobox', { name: /提供商/ }).click(); await page.getByText('Ollama · 本地推理', { exact: true }).click(); await page.getByRole('combobox', { name: /模型类型/ }).click(); await page.getByText('Chat · 对话/生成', { exact: true }).click(); await page .getByPlaceholder('例如 qwen3.6-27b / gpt-4o-mini') .fill('qwen3.6-27b'); await page .getByPlaceholder('例如 http://localhost:1234/v1') .fill('http://localhost:11434/v1'); await page.getByPlaceholder('sk-...').fill('e2e-placeholder-key'); await page.getByTestId('ai-model-submit').click(); await page.waitForTimeout(500); const post = state.posts.find((p) => p.path === '/api/ai/models'); const body = JSON.parse((post as CapturedRequest).body); expect(body.name).toBe('E2E 对话模型'); expect(body.provider).toBe(2); // Ollama expect(body.modelType).toBe(0); // Chat expect(body.modelId).toBe('qwen3.6-27b'); expect(body.baseUrl).toBe('http://localhost:11434/v1'); expect(body.apiKey).toBe('e2e-placeholder-key'); expect(body.isDefault).toBe(false); }); test('必填项缺失时前端校验拦截,不发起请求', async ({ page }) => { const state = await installApiMocks(page); await login(page); await page.goto('/ai/models'); await page.getByRole('button', { name: /新建模型/ }).click(); await page.getByTestId('ai-model-submit').click(); await expect(page.getByText('请输入显示名称')).toBeVisible(); expect( state.posts.find((p) => p.path === '/api/ai/models'), ).toBeUndefined(); }); }); test.describe('工作流 · 新建流程定义', () => { test('填写名称与编码提交后 POST /workflow-definitions', async ({ page }) => { const state = await installApiMocks(page); await login(page); await page.goto('/workflow/list'); await expect(page.getByText(/暂无流程定义/)).toBeVisible(); await page .getByRole('button', { name: /新建流程/ }) .first() .click(); await page.getByPlaceholder('例如:请假审批流').fill('E2E 审批流'); await page .getByPlaceholder('唯一标识,如 leave-approval') .fill('e2e-approval'); await page.getByTestId('create-form-submit').click(); await expect( page.locator('.ant-message').getByText(/流程定义已创建/), ).toBeVisible({ timeout: 10_000 }); const post = state.posts.find( (p) => p.path === '/api/workflow-definitions', ); const body = JSON.parse((post as CapturedRequest).body); expect(body.name).toBe('E2E 审批流'); expect(body.code).toBe('e2e-approval'); expect(body.description).toBeNull(); expect(body.formDefinitionId).toBeNull(); }); test('编码为空时前端必填校验拦截,不发起请求', async ({ page }) => { const state = await installApiMocks(page); await login(page); await page.goto('/workflow/list'); await page .getByRole('button', { name: /新建流程/ }) .first() .click(); await page.getByPlaceholder('例如:请假审批流').fill('只有名称'); await page.getByTestId('create-form-submit').click(); await expect(page.getByText('请输入流程编码')).toBeVisible(); expect( state.posts.find((p) => p.path === '/api/workflow-definitions'), ).toBeUndefined(); }); });