# Story 10.4: 编写创建订单测试

Status: ready-for-dev

<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->

## 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<FormSubmitResult>` - 提交表单
   - 返回结果包含：success, hasError, hasSuccess, errorMessage, successMessage, responses

4. **完整流程:**
   - `createOrder(data: OrderData): Promise<FormSubmitResult>` - 创建订单（完整流程）
   - `orderExists(orderName: string): Promise<boolean>` - 验证订单是否存在

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<number> {
     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
