# Story 10.4: 编写创建订单测试 Status: ready-for-dev ## Story 作为测试开发者, 我想要编写创建订单的 E2E 测试, 以便验证订单创建功能。 ## Acceptance Criteria **Given** 订单管理 Page Object 已创建 **When** 编写创建订单测试用例 **Then** 包含以下测试场景: 1. **基本创建订单** - 填写必填字段(订单名称、预计开始日期) - 验证订单创建成功 - 验证订单出现在列表中 2. **创建订单并选择平台** - 选择平台 - 使用 `selectRadixOption` 或 `selectRadixOptionAsync` - 验证平台关联正确 3. **创建订单并选择公司** - 选择公司 - 验证公司关联正确 4. **创建订单并选择渠道** - 选择渠道 - 验证渠道关联正确 5. **创建完整订单(所有字段)** - 填写所有可选字段 - 验证所有数据保存正确 6. **表单验证测试** - 未填写必填字段时提交 - 验证错误提示显示正确 **测试文件:** `web/tests/e2e/specs/admin/order-create.spec.ts` ## Tasks / Subtasks - [ ] 创建订单创建测试文件 (AC: When) - [ ] 创建 `web/tests/e2e/specs/admin/order-create.spec.ts` - [ ] 导入必要的测试依赖(Playwright fixtures、OrderManagementPage) - [ ] 配置测试文件的基本结构 - [ ] 编写基本创建订单测试 (AC: Then #1) - [ ] 测试只填写必填字段创建订单 - [ ] 验证创建成功提示 - [ ] 验证订单出现在列表中 - [ ] 编写选择平台创建订单测试 (AC: Then #2) - [ ] 测试创建订单时选择平台 - [ ] 验证平台正确保存 - [ ] 使用 `selectRadixOption` 工具 - [ ] 编写选择公司创建订单测试 (AC: Then #3) - [ ] 测试创建订单时选择公司 - [ ] 验证公司正确保存 - [ ] 编写选择渠道创建订单测试 (AC: Then #4) - [ ] 测试创建订单时选择渠道 - [ ] 验证渠道正确保存 - [ ] 编写创建完整订单测试 (AC: Then #5) - [ ] 测试填写所有字段创建订单 - [ ] 验证所有数据正确保存 - [ ] 编写表单验证测试 (AC: Then #6) - [ ] 测试不填写必填字段提交 - [ ] 验证错误提示显示 - [ ] 测试取消创建操作 - [ ] 确保所有测试通过 (AC: And) ## Dev Notes ### Epic Context **Epic 10: 订单管理 E2E 测试 (Epic C - 业务测试 Epic)** - **目标**: 测试开发者可以为订单管理功能编写完整的 E2E 测试,验证订单的 CRUD、状态流转、人员关联和附件管理功能 - **业务分组**: Epic C(业务测试 Epic) - **背景**: 订单管理是招聘系统的核心业务功能,涉及复杂表单(多选择器联动)、状态流转、人员关联等场景 - **模式**: 业务测试为主,工具包支持为辅(遵循 Epic A 成功模式) **依赖:** - Epic 1: ✅ 已完成(Select 工具基础框架) - Epic 2: ✅ 已完成(Select 工具在真实 E2E 测试中验证) - Story 10.1: ✅ 已完成(订单管理 Page Object) - Story 10.2: ✅ 已完成(订单列表查看测试) - Story 10.3: ✅ 已完成(订单搜索和筛选测试) ### 前序 Story 情报 (Story 10.1, 10.2, 10.3) **Page Object 已有的创建订单功能:** `web/tests/e2e/pages/admin/order-management.page.ts` 包含以下方法: 1. **打开创建对话框:** - `openCreateDialog()` - 打开创建订单对话框 - `addOrderButton` - 创建订单按钮选择器(`data-testid="create-order-button"`) 2. **填写表单:** - `fillOrderForm(data: OrderData)` - 填写订单表单 - 支持字段:name, expectedStartDate, platformName, companyName, channelName, status, workStatus 3. **提交表单:** - `submitForm(): Promise` - 提交表单 - 返回结果包含:success, hasError, hasSuccess, errorMessage, successMessage, responses 4. **完整流程:** - `createOrder(data: OrderData): Promise` - 创建订单(完整流程) - `orderExists(orderName: string): Promise` - 验证订单是否存在 5. **对话框控制:** - `cancelDialog()` - 取消对话框 - `waitForDialogClosed()` - 等待对话框关闭 ### 订单数据接口 ```typescript interface OrderData { name: string; // 订单名称(必填) expectedStartDate?: string; // 预计开始日期(必填) platformName?: string; // 平台名称(可选) companyName?: string; // 公司名称(可选) channelName?: string; // 渠道名称(可选) status?: OrderStatus; // 订单状态(编辑模式) workStatus?: WorkStatus; // 工作状态(编辑模式) } // 订单状态常量 ORDER_STATUS = { DRAFT: 'draft', CONFIRMED: 'confirmed', IN_PROGRESS: 'in_progress', COMPLETED: 'completed', } // 订单状态显示名称 ORDER_STATUS_LABELS = { draft: '草稿', confirmed: '已确认', in_progress: '进行中', completed: '已完成', } ``` ### 测试文件结构参考 参考 Story 10.2(订单列表查看测试)和 Story 10.3(订单筛选测试)的测试结构: ```typescript import { test, expect } from '../../utils/test-setup'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { selectRadixOption } from '@d8d/e2e-test-utils'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const testUsers = JSON.parse(readFileSync(join(__dirname, '../../fixtures/test-users.json'), 'utf-8')); test.describe.serial('创建订单测试', () => { test.beforeEach(async ({ adminLoginPage, orderManagementPage }) => { await adminLoginPage.goto(); await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password); await adminLoginPage.expectLoginSuccess(); await orderManagementPage.goto(); }); test.describe('基本创建订单', () => { test('应该能创建只填写必填字段的订单', async ({ orderManagementPage, page }) => { // 生成唯一订单名称 const orderName = `测试订单_${Date.now()}`; // 创建订单 const result = await orderManagementPage.createOrder({ name: orderName, expectedStartDate: '2025-01-15', }); // 验证创建成功 expect(result.success).toBe(true); expect(result.hasSuccess).toBe(true); // 验证订单出现在列表中 await expect(async () => { const exists = await orderManagementPage.orderExists(orderName); expect(exists).toBe(true); }).toPass({ timeout: 5000 }); }); }); // 更多测试... }); ``` ### 创建订单对话框元素(预期) **创建订单对话框结构(基于 Page Object 分析):** - 打开方式:点击"创建订单"按钮(`data-testid="create-order-button"`) - 对话框:`role="dialog"` - 订单名称输入框:`label` 包含 "订单名称" 或 "名称" - 预计开始日期输入框:`label` 包含 "预计开始日期" 或 "开始日期" - 平台选择器:Radix UI Select,使用 `selectRadixOption` 工具 - 公司选择器:Radix UI Select,使用 `selectRadixOption` 工具 - 渠道选择器:Radix UI Select,使用 `selectRadixOption` 工具 - 创建按钮:`role="button"`, name="创建" - 取消按钮:`role="button"`, name="取消" ### 技术要求 **导入依赖:** ```typescript import { test, expect } from '../../utils/test-setup'; import { OrderManagementPage, ORDER_STATUS, WORK_STATUS } from '../../pages/admin/order-management.page'; import { selectRadixOption } from '@d8d/e2e-test-utils'; ``` **测试 Fixtures:** - 使用 `adminLoginPage` fixture 进行登录 - 使用 `orderManagementPage` fixture 操作页面 - 使用 `page` fixture 直接操作页面元素 - 使用 `test.describe.serial()` 确保测试按顺序执行 **断言策略:** - 验证 `FormSubmitResult.success` 为 `true` - 验证 `FormSubmitResult.hasSuccess` 为 `true` - 验证订单出现在列表中(使用 `orderExists()` 方法) - 验证 Toast 消息显示成功提示 **数据唯一性:** - 使用 `Date.now()` 生成唯一订单名称,避免测试数据冲突 - 示例:`const orderName = '测试订单_' + Date.now();` ### 工具使用 **Select 工具使用(来自 Epic 1, 2):** ```typescript import { selectRadixOption } from '@d8d/e2e-test-utils'; // 静态选择器(平台、公司、渠道) await selectRadixOption(page, '平台', '58同城'); await selectRadixOption(page, '公司', '测试公司'); await selectRadixOption(page, '渠道', '网络招聘'); ``` **Page Object 使用:** ```typescript // 方式1:使用完整流程方法 const result = await orderManagementPage.createOrder({ name: '测试订单', expectedStartDate: '2025-01-15', platformName: '58同城', }); // 方式2:分步操作(适合需要验证中间状态) await orderManagementPage.openCreateDialog(); await orderManagementPage.fillOrderForm({ name: '测试订单' }); const result = await orderManagementPage.submitForm(); await orderManagementPage.waitForDialogClosed(); ``` ### 测试标准参考 遵循以下测试标准文档: - `docs/standards/testing-standards.md` - 测试规范 - `docs/standards/web-ui-testing-standards.md` - Web UI 测试规范 - `docs/standards/e2e-radix-testing.md` - Radix UI E2E 测试标准 **选择器优先级:** 1. `data-testid` - 最高优先级 2. `aria-label + role` 3. `text content + role` - 兜底 ### Project Structure Notes **文件位置:** - 测试文件: `web/tests/e2e/specs/admin/order-create.spec.ts` - Page Object: `web/tests/e2e/pages/admin/order-management.page.ts` - Fixtures 目录: `web/tests/e2e/fixtures/` **对齐统一项目结构:** - 遵循 `project-context.md` 中的 TypeScript 严格模式规则 - 函数参数、返回值必须有明确类型注解 - 禁止使用 `any` 类型 - 公共 API 必须包含完整 JSDoc 注释 ### 测试运行命令 ```bash # 在 web 目录下运行单个测试文件 cd web pnpm test:e2e:chromium order-create # 快速失败模式(推荐调试时使用) timeout 60 pnpm test:e2e:chromium order-create ``` ### 测试注意事项 **数据唯一性:** - 必须使用 `Date.now()` 或 `crypto.randomUUID()` 生成唯一订单名称 - 避免测试数据冲突导致测试失败 **必填字段:** - 订单名称:必填 - 预计开始日期:必填(格式:YYYY-MM-DD) **可选字段:** - 平台:可选,需要数据库中存在平台数据 - 公司:可选,需要数据库中存在公司数据 - 渠道:可选,需要数据库中存在渠道数据 **表单验证:** - 未填写必填字段时提交应显示错误提示 - 验证错误提示使用 `data-sonner-toast` 属性定位 **Toast 消息:** - 成功消息:`[data-sonner-toast][data-type="success"]` - 错误消息:`[data-sonner-toast][data-type="error"]` **网络响应:** - `submitForm()` 方法已监听网络响应 - 使用 `waitForLoadState('networkidle')` 等待网络请求完成 ### 测试覆盖场景清单 **基本创建:** - [ ] 只填写必填字段创建订单 - [ ] 验证创建成功提示 - [ ] 验证订单出现在列表中 **平台选择:** - [ ] 创建订单时选择平台 - [ ] 验证平台正确保存 **公司选择:** - [ ] 创建订单时选择公司 - [ ] 验证公司正确保存 **渠道选择:** - [ ] 创建订单时选择渠道 - [ ] 验证渠道正确保存 **完整订单:** - [ ] 填写所有字段创建订单 - [ ] 验证所有数据正确保存 **表单验证:** - [ ] 不填写订单名称提交 - [ ] 不填写预计开始日期提交 - [ ] 验证错误提示显示 - [ ] 测试取消创建操作 ### 前序 Story 关键学习点 **从 Story 10.3(订单筛选测试)学到的经验:** 1. **使用辅助函数消除代码重复**: ```typescript // 提取辅助函数 async function applyFilters(page) { await page.getByTestId('search-button').click(); await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { console.debug('筛选后没有网络请求或请求已完成'); }); } async function getTableRowCount(page): Promise { const tbody = page.locator('table tbody'); const rows = tbody.locator('tr'); return await rows.count(); } function formatLocalDate(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } ``` 2. **使用 try-catch 处理空数据场景**: ```typescript // 如果数据库中没有测试数据,优雅处理 try { await selectRadixOption(page, '平台', '58同城'); // 验证结果 } catch (error) { console.debug('平台数据不存在或选择器无选项:', error); } ``` 3. **验证实际数据变化,而非仅检查元素可见**: ```typescript // 验证行数变化,而非仅检查表格可见 expect(filteredCount).toBeLessThanOrEqual(initialCount); ``` 4. **使用 `.toPass()` 进行条件等待**: ```typescript // 等待异步操作完成,最多重试 5 秒 await expect(async () => { const exists = await orderManagementPage.orderExists(orderName); expect(exists).toBe(true); }).toPass({ timeout: 5000 }); ``` ### References - [Source: _bmad-output/planning-artifacts/epics.md#Story 10.4](../planning-artifacts/epics.md) - [Source: _bmad-output/planning-artifacts/prd.md](../planning-artifacts/prd.md) - [Source: _bmad-output/planning-artifacts/architecture.md](../planning-artifacts/architecture.md) - [Source: web/tests/e2e/pages/admin/order-management.page.ts](../../web/tests/e2e/pages/admin/order-management.page.ts) - [Source: _bmad-output/implementation-artifacts/10-1-order-page-object.md](10-1-order-page-object.md) - [Source: _bmad-output/implementation-artifacts/10-2-order-list-tests.md](10-2-order-list-tests.md) - [Source: _bmad-output/implementation-artifacts/10-3-order-filter-tests.md](10-3-order-filter-tests.md) ## Dev Agent Record ### Agent Model Used claude-opus-4-5-20251101 ### Debug Log References ### Completion Notes List ### File List