build-flow.admin/e2e/create-forms.spec.ts
向宁 77aac58f22
Some checks failed
CI / lint (push) Has been cancelled
CI / build (20, macOS-latest) (push) Has been cancelled
CI / build (20, ubuntu-latest) (push) Has been cancelled
CI / build (20, windows-latest) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
test: add playwright e2e suites for create-forms and obsidian-sync
2026-10-25 16:27:43 +08:00

298 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<MockState> {
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();
});
});