10-4-order-create-tests.md 14 KB

Story 10.4: 编写创建订单测试

Status: ready-for-dev

Story

作为测试开发者, 我想要编写创建订单的 E2E 测试, 以便验证订单创建功能。

Acceptance Criteria

Given 订单管理 Page Object 已创建 When 编写创建订单测试用例 Then 包含以下测试场景:

  1. 基本创建订单

    • 填写必填字段(订单名称、预计开始日期)
    • 验证订单创建成功
    • 验证订单出现在列表中
  2. 创建订单并选择平台

    • 选择平台
    • 使用 selectRadixOptionselectRadixOptionAsync
    • 验证平台关联正确
  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() - 等待对话框关闭

订单数据接口

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(订单筛选测试)的测试结构:

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="取消"

技术要求

导入依赖:

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.successtrue
  • 验证 FormSubmitResult.hasSuccesstrue
  • 验证订单出现在列表中(使用 orderExists() 方法)
  • 验证 Toast 消息显示成功提示

数据唯一性:

  • 使用 Date.now() 生成唯一订单名称,避免测试数据冲突
  • 示例:const orderName = '测试订单_' + Date.now();

工具使用

Select 工具使用(来自 Epic 1, 2):

import { selectRadixOption } from '@d8d/e2e-test-utils';

// 静态选择器(平台、公司、渠道)
await selectRadixOption(page, '平台', '58同城');
await selectRadixOption(page, '公司', '测试公司');
await selectRadixOption(page, '渠道', '网络招聘');

Page Object 使用:

// 方式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 注释

测试运行命令

# 在 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. 使用辅助函数消除代码重复

    // 提取辅助函数
    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 处理空数据场景

    // 如果数据库中没有测试数据,优雅处理
    try {
     await selectRadixOption(page, '平台', '58同城');
     // 验证结果
    } catch (error) {
     console.debug('平台数据不存在或选择器无选项:', error);
    }
    
  3. 验证实际数据变化,而非仅检查元素可见

    // 验证行数变化,而非仅检查表格可见
    expect(filteredCount).toBeLessThanOrEqual(initialCount);
    
  4. 使用 .toPass() 进行条件等待

    // 等待异步操作完成,最多重试 5 秒
    await expect(async () => {
     const exists = await orderManagementPage.orderExists(orderName);
     expect(exists).toBe(true);
    }).toPass({ timeout: 5000 });
    

References

Dev Agent Record

Agent Model Used

claude-opus-4-5-20251101

Debug Log References

Completion Notes List

File List