# Story 11.5: 创建测试公司（需要先有平台）

Status: done

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

## Story

作为测试开发者，
我想要编写公司创建功能的 E2E 测试，
以便验证公司管理模块的创建功能正常工作。

## Acceptance Criteria

1. **AC1: 创建测试文件**
   - 文件路径: `web/tests/e2e/specs/admin/company-create.spec.ts`
   - 使用 Playwright test 框架
   - 使用 Story 11.4 创建的 `CompanyManagementPage` Page Object
   - 定义测试夹具（fixtures）包含 Page Object

2. **AC2: 测试基本创建流程（不选择平台）**
   - 导航到公司管理页面
   - 点击创建公司按钮
   - 仅填写必填字段（公司名称）
   - 提交表单
   - 验证创建成功（API 响应 + 列表中显示新公司）

3. **AC3: 测试完整表单字段（包含平台选择）**
   - 首先创建测试用的平台（使用 Page Object 方法）
   - 填写所有字段（平台、公司名称、联系人、联系电话、联系邮箱、地址）
   - 验证所有数据保存正确
   - 验证公司出现在列表中，平台关联正确

4. **AC4: 测试 PlatformSelector 集成**
   - 使用 `@d8d/e2e-test-utils` 的 `selectRadixOptionAsync` 选择平台
   - 验证异步加载的平台选项正确显示
   - 验证选择平台后表单提交成功

5. **AC5: 测试表单验证**
   - 未填写公司名称时提交
   - 验证内联错误提示显示（对话框保持打开）
   - 验证公司未被创建

6. **AC6: 测试数据唯一性**
   - 创建公司时使用时间戳确保公司名称唯一
   - 验证不同测试之间数据不冲突

7. **AC7: 测试取消和关闭操作**
   - 测试点击取消按钮关闭创建对话框
   - 测试点击对话框外部关闭对话框
   - 测试按 ESC 键关闭对话框
   - 验证取消后公司未被创建

8. **AC8: 代码质量标准**
   - TypeScript 类型检查通过（无类型错误）
   - 测试用例有清晰的描述

## Tasks / Subtasks

- [x] 任务 1: 创建测试文件和基础结构 (AC: 1, 8)
  - [x] 创建文件 `web/tests/e2e/specs/admin/company-create.spec.ts`
  - [x] 导入 Playwright test 和 CompanyManagementPage
  - [x] 定义测试夹具（adminLoginPage, companyManagementPage）
  - [x] 设置测试基础配置

- [x] 任务 2: 实现测试前置条件 (AC: 1, 2)
  - [x] 实现 `test.beforeEach()` 登录后台
  - [x] 导航到公司管理页面
  - [x] 验证页面加载完成

- [x] 任务 3: 实现基本创建流程测试（不选择平台） (AC: 2, 6)
  - [x] 编写测试用例：应该成功创建公司（仅填写公司名称）
  - [x] 使用时间戳生成唯一公司名称
  - [x] 验证 API 响应成功
  - [x] 验证公司出现在列表中
  - [x] 清理测试数据

- [x] 任务 4: 实现完整表单字段测试（包含平台选择） (AC: 3, 4, 6)
  - [x] 编写测试用例：应该成功创建公司（选择平台并填写所有字段）
  - [x] 使用 `createPlatform` 方法先创建测试平台
  - [x] 使用 `selectRadixOptionAsync` 选择平台
  - [x] 填写所有字段（公司名称、联系人、联系电话、联系邮箱、地址）
  - [x] 验证所有数据保存正确
  - [x] 验证平台关联正确显示
  - [x] 清理测试数据和平台

- [x] 任务 5: 实现表单验证测试 (AC: 5)
  - [x] 编写测试用例：未填写公司名称时应显示内联错误
  - [x] 尝试提交空表单
  - [x] 验证对话框保持打开（表单验证阻止了提交）

- [x] 任务 6: 实现取消和关闭操作测试 (AC: 7)
  - [x] 编写测试用例：应该能取消创建公司操作
  - [x] 编写测试用例：应该能通过关闭对话框取消创建
  - [x] 编写测试用例：应该能通过 ESC 键取消创建

- [x] 任务 7: 实现测试后清理 (AC: 6)
  - [x] 每个测试用例内清理测试数据
  - [x] 删除测试创建的公司
  - [x] 删除测试创建的平台
  - [x] 验证清理成功

- [x] 任务 8: 运行测试并验证 (AC: 2, 3, 4, 5, 6, 7, 8)
  - [x] 运行测试: `pnpm test:e2e:chromium company-create.spec.ts`
  - [x] TypeScript 类型检查通过
  - [x] 修复发现的问题

## Dev Notes

### Epic 11 背景和目标

**Epic 11: 基础配置管理测试 (Epic F)**

为平台、公司、渠道配置管理编写 E2E 测试，为后续用户管理和跨端测试提供必要的测试数据。

**实体关系链：**
```
Platform (平台)
  ↓ 1:N
Company (公司) - 必须 platformId
  ↓ 1:N
Order (订单) - 必须 companyId
```

**Story 11.5 在 Epic 中的位置：**
- Story 11.1 → Story 11.2 → Story 11.3 → Story 11.4 (已完成) → Story 11.5 (当前) → Story 11.6
- Story 11.4 创建了 CompanyManagementPage Page Object
- Story 11.5 将编写公司创建功能的 E2E 测试
- Story 11.6 将编写公司列表显示验证测试

### 架构模式和约束

**测试文件结构参考：**

参考 Story 11.2（平台创建测试）的结构模式：
- `web/tests/e2e/specs/admin/platform-create.spec.ts`
- `web/tests/e2e/specs/admin/company-create.spec.ts`（当前）

**标准测试文件结构：**
```typescript
import { test, expect } from '@playwright/test';
import { CompanyManagementPage } from '../../pages/admin/company-management.page';

test.describe('公司创建功能', () => {
  test.beforeEach(async ({ adminLoginPage, companyManagementPage }) => {
    // 登录后台
    await adminLoginPage.goto();
    await adminLoginPage.login('admin', 'admin123');

    // 导航到公司管理页面
    await companyManagementPage.goto();
  });

  test.afterEach(async ({ companyManagementPage }) => {
    // 清理测试数据
  });

  test('应该成功创建公司', async ({ companyManagementPage }) => {
    // 测试逻辑
  });
});
```

**测试夹具配置：**

Story 11.4 已添加 `companyManagementPage` fixture 到 `web/tests/e2e/utils/test-setup.ts`：
```typescript
export const test = test.extend<{
  adminLoginPage: AdminLoginPage;
  companyManagementPage: CompanyManagementPage;
}>({
  adminLoginPage: async ({ page }, use) => {
    await use(new AdminLoginPage(page));
  },
  companyManagementPage: async ({ page }, use) => {
    await use(new CompanyManagementPage(page));
  },
});
```

### CompanyManagementPage API 参考

**来自 Story 11.4 的可用方法：**

| 方法 | 描述 | 返回值 |
|------|------|--------|
| `goto()` | 导航到公司管理页面 | `Promise<void>` |
| `createCompany(data, platformName)` | 创建公司（完整流程） | `Promise<FormSubmitResult>` |
| `editPlatform(name, data)` | 编辑公司 | `Promise<FormSubmitResult>` |
| `deleteCompany(name)` | 删除公司 | `Promise<boolean>` |
| `companyExists(name)` | 验证公司是否存在 | `Promise<boolean>` |
| `createPlatform(data)` | 创建平台（用于测试数据准备） | `Promise<FormSubmitResult>` |
| `deletePlatform(name)` | 删除平台 | `Promise<boolean>` |

**数据接口：**
```typescript
interface CompanyData {
  platformId?: number;       // 可选，平台ID
  companyName: string;       // 必填
  contactPerson?: string;
  contactPhone?: string;
  contactEmail?: string;
  address?: string;
}

interface PlatformData {
  platformName: string;      // 必填
  contactPerson?: string;
  contactPhone?: string;
  contactEmail?: string;
}

interface FormSubmitResult {
  success: boolean;
  hasError: boolean;
  hasSuccess: boolean;
  errorMessage?: string;
  successMessage?: string;
  responses?: NetworkResponse[];
}
```

**关键方法签名（来自 Story 11.4）：**
```typescript
/**
 * 创建公司（完整流程）
 * @param data - 公司数据
 * @param platformName - 平台名称（用于选择平台）
 */
async createCompany(data: CompanyData, platformName?: string): Promise<FormSubmitResult>
```

### Company 实体结构

**Company Entity (来自 company.entity.ts):**
```typescript
{
  id: number;              // 主键ID
  platformId: number | null;  // 平台ID（外键，可选）
  companyName: string;     // 公司名称（必填，最大100字符）
  contactPerson: string | null;   // 联系人（可选，最大50字符）
  contactPhone: string | null;   // 联系电话（可选，最大20字符）
  contactEmail: string | null;   // 联系邮箱（可选，最大100字符）
  address: string | null;        // 地址（可选，最大200字符）
  status: number;          // 状态：1-正常，0-禁用（默认1）
  createTime: Date;        // 创建时间
  updateTime: Date;        // 更新时间
  platform?: Platform;     // 关联的平台信息（eager加载）
}
```

**唯一约束：**
- 同一平台下公司名称唯一：`idx_company_name_platform (companyName, platformId)`
- 如果不选择平台，公司名称全局唯一

### CompanyManagement UI 组件分析

**页面路径:** `/admin/companies`

**创建公司对话框字段（来自 CompanyManagement.tsx）：**

1. **platformId** - PlatformSelector（可选）
   - 使用 Radix UI Select 组件
   - 异步加载平台选项
   - data-testid: `create-company-platform-selector`
   - 需要使用 `selectRadixOptionAsync` 选择

2. **companyName** - Input（必填）
   - 占位符: "请输入公司名称"
   - data-testid: `create-company-name-input`

3. **contactPerson** - Input（可选）
   - 占位符: "请输入联系人"
   - data-testid: `create-company-contact-person-input`

4. **contactPhone** - Input（可选）
   - 占位符: "请输入联系电话"
   - data-testid: `create-company-contact-phone-input`

5. **contactEmail** - Input（可选，email 类型）
   - 占位符: "请输入联系邮箱"
   - data-testid: `create-company-contact-email-input`

6. **address** - Input（可选）
   - 占位符: "请输入地址"
   - data-testid: `create-company-address-input`

**按钮：**
- 取消按钮: `data-testid="cancel-company-button"`
- 提交按钮: `data-testid="submit-create-company-button"`

**对话框标题：**
- data-testid: `company-modal-title`
- 标题文本: "创建公司"

### 测试用例设计

**测试用例 1: 基本创建流程（不选择平台）**
```typescript
test('应该成功创建公司（不选择平台）', async ({ companyManagementPage }) => {
  const timestamp = Date.now();
  const companyName = `测试公司_${timestamp}`;

  // 创建公司（不选择平台）
  const result = await companyManagementPage.createCompany({
    companyName
  });

  // 验证 API 响应成功
  const createResponse = result.responses?.find(r => r.url.includes('createCompany'));
  expect(createResponse?.ok).toBe(true);

  // 验证公司出现在列表中
  const exists = await companyManagementPage.companyExists(companyName);
  expect(exists).toBe(true);

  // 清理
  await companyManagementPage.deleteCompany(companyName);
});
```

**测试用例 2: 完整表单字段（包含平台选择）**
```typescript
test('应该成功创建公司（选择平台并填写所有字段）', async ({ companyManagementPage }) => {
  const timestamp = Date.now();

  // 首先创建测试平台
  const platformName = `测试平台_${timestamp}`;
  await companyManagementPage.createPlatform({
    platformName,
    contactPerson: '测试联系人',
    contactPhone: '13800138000',
    contactEmail: `test_${timestamp}@example.com`
  });

  // 创建公司（选择平台）
  const companyName = `测试公司_${timestamp}`;
  const result = await companyManagementPage.createCompany({
    companyName,
    contactPerson: '张三',
    contactPhone: '13900139000',
    contactEmail: `company_${timestamp}@example.com`,
    address: '北京市朝阳区'
  }, platformName);

  // 验证 API 响应成功
  const createResponse = result.responses?.find(r => r.url.includes('createCompany'));
  expect(createResponse?.ok).toBe(true);

  // 验证公司出现在列表中
  const exists = await companyManagementPage.companyExists(companyName);
  expect(exists).toBe(true);

  // 清理（先删除公司，再删除平台）
  await companyManagementPage.deleteCompany(companyName);
  await companyManagementPage.deletePlatform(platformName);
});
```

**测试用例 3: 表单验证（空公司名称）**
```typescript
test('未填写公司名称时应显示内联验证错误', async ({ companyManagementPage }) => {
  // 打开创建对话框
  await companyManagementPage.openCreateDialog();

  // 不填写任何字段，直接提交
  const submitButton = companyManagementPage.page.locator('[data-testid="submit-create-company-button"]');
  await submitButton.click();

  // 验证对话框仍然打开（表单验证阻止了提交）
  const dialog = companyManagementPage.page.locator('[role="dialog"]');
  await expect(dialog).toBeVisible();

  // 验证内联错误消息显示
  const errorMessage = companyManagementPage.page.locator('[data-testid="create-company-name-input"] + div[data-testid="form-message"]');
  await expect(errorMessage).toBeVisible();

  // 关闭对话框
  await companyManagementPage.cancelDialog();
});
```

**测试用例 4: 取消操作**
```typescript
test('应该能取消创建公司操作', async ({ companyManagementPage }) => {
  const timestamp = Date.now();
  const companyName = `测试公司_${timestamp}`;

  // 打开创建对话框并填写表单
  await companyManagementPage.openCreateDialog();
  await companyManagementPage.page.locator('[data-testid="create-company-name-input"]').fill(companyName);

  // 点击取消按钮
  await companyManagementPage.cancelDialog();

  // 验证对话框关闭
  const dialog = companyManagementPage.page.locator('[role="dialog"]');
  await expect(dialog).not.toBeVisible();

  // 验证公司未被创建
  const exists = await companyManagementPage.companyExists(companyName);
  expect(exists).toBe(false);
});
```

### 数据唯一性策略

**使用时间戳确保唯一性：**
```typescript
const timestamp = Date.now();
const uniqueId = `company_test_${timestamp}`;
const companyName = `测试公司_${uniqueId}`;
```

**测试数据清理：**
- 每个测试用例结束后清理自己创建的数据
- 如果创建了平台，先删除公司，再删除平台
- 验证删除成功后再结束测试

### PlatformSelector 集成要点

**关键注意事项（来自 Story 11.4）：**

1. **使用 `@d8d/e2e-test-utils` 的 `selectRadixOptionAsync`：**
   ```typescript
   import { selectRadixOptionAsync } from '@d8d/e2e-test-utils';

   // 在 Page Object 的 fillCompanyForm 中选择平台
   if (platformName) {
     await selectRadixOptionAsync(this.page, '平台', platformName);
   }
   ```

2. **测试数据准备顺序：**
   - 先创建测试平台（使用 `createPlatform`）
   - 然后创建公司并指定平台名称
   - 清理时先删除公司，再删除平台

3. **PlatformSelector 标签：**
   - 标签文本: "平台" 或 "选择平台"
   - 使用时需要确认实际的标签文本

### 项目结构约束

**测试文件存放路径：**
```
web/tests/e2e/specs/admin/
├── platform-create.spec.ts       # 平台创建测试（已完成）
├── platform-list.spec.ts         # 平台列表测试（已完成）
├── company-create.spec.ts        # 公司创建测试（当前）
└── company-list.spec.ts          # 公司列表测试（后续 Story）
```

**依赖文件：**
- `web/tests/e2e/pages/admin/company-management.page.ts` - Story 11.4 创建
- `web/tests/e2e/utils/test-setup.ts` - 已包含 companyManagementPage fixture

### 测试运行命令

**运行单个测试文件：**
```bash
cd web
pnpm test:e2e:chromium company-create.spec.ts
```

**运行单个测试用例：**
```bash
cd web
pnpm test:e2e:chromium company-create.spec.ts -g "应该成功创建公司"
```

**快速失败模式（调试）：**
```bash
cd web
timeout 60 pnpm test:e2e:chromium company-create.spec.ts
```

### 依赖关系

**Epic 11 内部依赖：**
- Story 11.1: ✅ 已完成（PlatformManagementPage 作为参考）
- Story 11.2: ✅ 已完成（平台创建测试参考）
- Story 11.3: ✅ 已完成（平台列表测试参考）
- Story 11.4: ✅ 已完成（CompanyManagementPage）
- Story 11.5: 公司创建测试（当前）
- Story 11.6: 公司列表测试（依赖当前 Story）

**外部依赖：**
- Epic 1, 2: `@d8d/e2e-test-utils` 包（已存在）
- `web/tests/e2e/utils/test-setup.ts`: 已包含 companyManagementPage 夹具

### 测试标准和规范

遵循项目测试标准：
- `docs/standards/testing-standards.md`
- `docs/standards/web-ui-testing-standards.md`

**关键测试原则：**
1. 测试独立性：每个测试用例独立运行
2. 数据清理：每个测试结束后清理自己创建的数据
3. 清晰断言：使用 expect() 明确断言预期结果
4. 等待策略：使用 Playwright 的 auto-waiting

### 前序 Story (11.1-11.4) 关键经验

从 Story 11.1-11.4 中学到的关键经验：

1. **Toast 检测不可靠：**
   - Toast 消息有时出现得很快
   - 已改用 API 响应验证作为主要验证方式

2. **表格列顺序：**
   - Company 表格列顺序：公司名称(0), 平台(1), 联系人(2), 联系电话(3), 状态(4), 创建时间(5), 操作(6)
   - 使用 `nth(0)` 检查公司名称列

3. **测试数据要求：**
   - 后端 Zod schema 要求 contactEmail 必须是有效邮箱（如果填写）
   - 空字符串会被拒绝，所以测试不填的字段应该设为 undefined

4. **页面刷新：**
   - 创建/删除数据后需要刷新页面或重新导航
   - 使用 `page.reload()` 或 `goto()` 刷新

5. **选择器优先级：**
   - 优先使用 data-testid（最稳定）
   - 其次使用 role + name（较稳定）
   - 避免使用文本选择器（可能变化）

6. **API 删除策略（来自 Story 11.2）：**
   - 使用 API 直接删除，绕过 UI 的不可靠性
   - 删除成功后刷新页面确保列表更新

### 已知问题和注意事项

1. **Platform 异步加载：**
   - PlatformSelector 选项是异步加载的
   - 需要等待选项加载完成再选择
   - 使用 `selectRadixOptionAsync` 确保选项加载完成

2. **公司名称唯一性：**
   - 同一平台下公司名称唯一
   - 测试使用时间戳确保唯一性
   - 如果不选择平台，公司名称全局唯一

3. **测试数据清理顺序：**
   - 必须先删除公司，再删除平台
   - 因为公司依赖平台（外键关系）

4. **内联验证消息：**
   - 使用 `FormMessage` 组件显示
   - data-testid 格式: `{fieldId}-form-message` 或类似
   - 需要验证实际的 data-testid

### 开发顺序建议

1. 创建测试文件和基础结构
2. 实现测试前置条件（登录、导航）
3. 实现基本创建流程测试（不选择平台）
4. 实现完整表单字段测试（包含平台选择）
5. 实现表单验证测试
6. 实现取消和关闭操作测试
7. 实现测试后清理
8. 运行测试并验证
9. TypeScript 类型检查

### References

- [Epic 11 基础配置管理测试](../planning-artifacts/epics.md#epic-11-基础配置管理测试-epic-f)
- [Story 11.1 Platform Page Object](./11-1-platform-page-object.story.md)
- [Story 11.2 平台创建测试](./11-2-platform-create-test.story.md)
- [Story 11.4 Company Page Object](./11-4-company-page-object.story.md)
- [Company Entity](../../allin-packages/company-module/src/entities/company.entity.ts)
- [Company Schema](../../allin-packages/company-module/src/schemas/company.schema.ts)
- [CompanyManagement UI](../../allin-packages/company-management-ui/src/components/CompanyManagement.tsx)
- [测试标准](../../docs/standards/testing-standards.md)
- [Web UI 测试标准](../../docs/standards/web-ui-testing-standards.md)
- [Architecture Decision - TypeScript + Playwright 陷阱](../planning-artifacts/architecture.md#typescript--playwright-常见陷阱)

### Project Structure Notes

**测试文件存放路径：**
```
web/tests/e2e/
├── pages/admin/
│   ├── platform-management.page.ts   # 平台管理 Page Object（已完成）
│   └── company-management.page.ts    # 公司管理 Page Object（已完成）
├── specs/admin/
│   ├── platform-create.spec.ts       # 平台创建测试（已完成）
│   ├── platform-list.spec.ts         # 平台列表测试（已完成）
│   └── company-create.spec.ts        # 公司创建测试（当前）
└── utils/
    └── test-setup.ts                  # 测试夹具配置（已包含 companyManagementPage）
```

## Dev Agent Record

### Agent Model Used

Claude (d8d-model)

### Debug Log References

### Completion Notes List

**Story 创建完成：**
1. ✅ 分析 Story 11.5 需求：编写公司创建功能的 E2E 测试
2. ✅ 创建完整的 Story 文档
3. ✅ 包含所有验收标准和任务分解
4. ✅ 提供详细的 Dev Notes 指导开发者
5. ✅ 参考 Story 11.2（平台创建测试）的模式
6. ✅ 分析 CompanyManagement.tsx UI 组件结构
7. ✅ 提供完整的测试用例示例
8. ✅ 说明 PlatformSelector 集成方法
9. ✅ 强调测试数据准备和清理顺序（平台 → 公司）

**Story 实施完成 (2026-01-12):**
1. ✅ 创建测试文件 `web/tests/e2e/specs/admin/company-create.spec.ts`
2. ✅ 实现所有 17 个测试用例：
   - 基本创建流程测试（不选择平台） - 2 个测试
   - 完整表单字段测试（包含平台选择） - 3 个测试
   - 表单验证测试 - 2 个测试
   - 取消和关闭操作测试 - 3 个测试
   - 对话框元素验证 - 1 个测试
   - 数据唯一性测试 - 2 个测试
   - 测试后清理验证 - 2 个测试
   - PlatformSelector 集成测试 - 2 个测试
3. ✅ 所有测试通过（17/17 passed）
4. ✅ 修复 CompanyManagementPage 中的 `platformSelector` 选择器
   - 原问题：使用了不存在的 `data-testid="create-company-platform-selector"`
   - 解决方案：改为使用 `page.getByRole('dialog').getByText(/^平台/)` 定位平台标签
5. ✅ 使用 `platformManagementPage` fixture 创建测试平台（因为 CompanyManagementPage 没有 createPlatform 方法）
6. ✅ 正确实现测试数据清理顺序（先删除公司，再删除平台）

### File List

**新创建的文件：**
- `web/tests/e2e/specs/admin/company-create.spec.ts` - 公司创建 E2E 测试文件

**修改的文件：**
- `web/tests/e2e/pages/admin/company-management.page.ts` - 修复 platformSelector 选择器

**依赖的已有文件：**
- `web/tests/e2e/pages/admin/company-management.page.ts` - Story 11.4 创建
- `web/tests/e2e/pages/admin/platform-management.page.ts` - Story 11.1 创建（用于创建测试平台）
- `web/tests/e2e/utils/test-setup.ts` - 已包含 companyManagementPage 和 platformManagementPage fixtures
