# Story 12.6: 人才小程序 Page Object

Status: review

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

## Story

作为测试开发者，
我想要创建人才小程序的 Page Object，
以便组织人才小程序相关的页面元素和操作。

## Acceptance Criteria

### AC1: Page Object 基础结构
**Given** Playwright E2E 测试框架已配置
**When** 创建人才小程序 Page Object
**Then** 测试应满足以下要求：
- 创建 `web/tests/e2e/pages/mini/talent-mini.page.ts` 文件
- 定义 `TalentMiniPage` 类，继承 Playwright 的 Page 对象模式
- 实现基础选择器定义（登录表单、主页元素等）
- 所有选择器使用 `data-testid` 属性（优先级高于文本选择器）
- 遵循项目 Page Object 设计模式

### AC2: 小程序页面导航
**Given** 人才小程序 Page Object 已创建
**When** 实现页面导航方法
**Then** 测试应满足以下要求：
- 实现 `goto()` 方法导航到人才小程序 H5 页面 (`/talent-mini`)
- 实现 `expectToBeVisible()` 方法验证页面可见性
- 验证页面正确加载（检查关键元素存在）
- 处理页面加载超时情况

### AC3: 登录功能封装
**Given** 人才小程序 Page Object 已创建
**When** 实现登录相关方法
**Then** 测试应满足以下要求：
- 实现 `login(username, password)` 方法执行登录操作
- 实现填写用户名方法 `fillUsername(username)`
- 实现填写密码方法 `fillPassword(password)`
- 实现点击登录按钮方法 `clickLoginButton()`
- 实现验证登录成功方法 `expectLoginSuccess()`
- 处理登录失败场景（错误提示显示）

### AC4: Token 存储和管理
**Given** 小程序登录后需要存储 token 用于后续操作
**When** 实现 token 管理方法
**Then** 测试应满足以下要求：
- 实现 `getToken()` 方法获取当前存储的 token
- 实现 `setToken(token)` 方法设置 token（用于测试前置条件）
- 使用 localStorage 或 sessionStorage 存储 token
- 验证 token 在页面刷新后仍然有效

### AC5: 主页元素选择器
**Given** 人才小程序登录后进入主页
**When** 定义主页元素选择器
**Then** 测试应满足以下要求：
- 定义工作列表选择器（如适用）
- 定义导航菜单选择器
- 定义用户信息显示选择器
- 所有选择器使用 `data-testid` 属性
- 提供清晰的类型定义

### AC6: 代码质量标准
**Given** 遵循项目测试规范
**When** 编写 Page Object 代码
**Then** 代码应符合以下标准：
- TypeScript 类型安全，无 `any` 类型
- 所有公共方法有完整的 JSDoc 注释
- 使用 TIMEOUTS 常量定义超时
- 遵循项目命名约定（类名 PascalCase，方法名 camelCase）
- 通过 `pnpm typecheck` 类型检查

## Tasks / Subtasks

- [x] 任务 1: 创建 Page Object 基础结构 (AC: #1, #6)
  - [x] 1.1 确认 `web/tests/e2e/pages/mini/` 目录存在
  - [x] 1.2 创建 `talent-mini.page.ts` 文件
  - [x] 1.3 定义 `TalentMiniPage` 类
  - [x] 1.4 定义基础选择器（使用 `data-testid`）

- [x] 任务 2: 实现页面导航功能 (AC: #2)
  - [x] 2.1 实现 `goto()` 方法（导航到 `/talent-mini`）
  - [x] 2.2 实现 `expectToBeVisible()` 方法
  - [x] 2.3 添加页面加载验证逻辑

- [x] 任务 3: 实现登录功能封装 (AC: #3)
  - [x] 3.1 实现 `fillUsername()` 方法（使用手机号或用户名）
  - [x] 3.2 实现 `fillPassword()` 方法
  - [x] 3.3 实现 `clickLoginButton()` 方法
  - [x] 3.4 实现 `login()` 完整登录方法
  - [x] 3.5 实现 `expectLoginSuccess()` 验证方法
  - [x] 3.6 实现 `expectLoginError()` 错误验证方法

- [x] 任务 4: 实现 Token 管理 (AC: #4)
  - [x] 4.1 实现 `getToken()` 方法
  - [x] 4.2 实现 `setToken()` 方法
  - [x] 4.3 实现 `clearAuth()` 清除认证方法

- [x] 任务 5: 定义主页元素选择器 (AC: #5)
  - [x] 5.1 定义工作列表选择器（待主页实现后添加对应 testid）
  - [x] 5.2 定义导航菜单选择器（待主页实现后添加对应 testid）
  - [x] 5.3 定义用户信息选择器（待主页实现后添加对应 testid）

- [x] 任务 6: 代码质量验证 (AC: #6)
  - [x] 6.1 运行 `pnpm typecheck` 验证类型检查
  - [x] 6.2 添加完整的 JSDoc 注释
  - [x] 6.3 验证选择器使用 data-testid

- [x] 任务 7: 更新 fixtures 文件 (AC: #6)
  - [x] 7.1 在 `web/tests/e2e/fixtures.ts` 中添加 `talentMiniPage` fixture
  - [x] 7.2 验证 fixture 正确初始化（类型检查通过）

- [x] 任务 8: 添加 data-testid 属性 (AC: #1)
  - [x] 8.1 在人才小程序登录页面添加 `data-testid` 属性
  - [x] 8.2 确保所有关键元素都有对应的 testid

## Dev Notes

### Epic 12 背景和依赖

**Epic 12 目标：** 为用户管理和小程序登录编写 E2E 测试，解锁小程序端的测试能力

**小程序技术要点（来自 Epic 12 文档）：**
- 企业小程序 H5 URL: `http://localhost:8080/mini`
- 人才小程序 H5 URL: `http://localhost:8080/talent-mini`
- 登录后存储 token 进行后续操作
- 使用 Playwright 测试 H5 页面
- 小程序只读，无写操作

**Epic 12 Story 依赖关系：**
```
Story 12.1: 用户管理 Page Object ✅ (已完成)
Story 12.2: 后台创建企业用户测试 ✅ (已完成)
Story 12.3: 后台创建人才用户测试 ✅ (已完成)
Story 12.4: 企业小程序 Page Object ✅ (已完成)
Story 12.5: 企业小程序登录测试 🔄 (进行中)
Story 12.6: 人才小程序 Page Object ← 当前 Story
Story 12.7: 人才小程序登录测试
Story 12.8: 用户权限验证测试
```

### Story 12.4 关键经验

从已完成的 Story 12.4（企业小程序 Page Object）中学习到的模式：

**企业小程序登录页面选择器模式：**
```typescript
private readonly selectors = {
  // 页面级选择器
  loginPage: '[data-testid="mini-login-page"]',
  pageTitle: '[data-testid="mini-page-title"]',

  // 登录表单选择器
  phoneInput: '[data-testid="mini-phone-input"]',
  passwordInput: '[data-testid="mini-password-input"]',
  loginButton: '[data-testid="mini-login-button"]',

  // 主页选择器（登录后）
  userInfo: '[data-testid="mini-user-info"]',
};
```

**人才小程序选择器命名建议：**
- 使用 `talent-` 前缀区分与企业小程序
- 例如：`talent-login-page`, `talent-phone-input`, `talent-password-input`

**Taro Input 组件处理方式：**
```typescript
// Taro 的 Input 组件使用自定义元素，使用 type() 代替 fill()
async fillPhone(phone: string): Promise<void> {
  await this.phoneInput.click();
  await this.page.keyboard.press('Control+A');
  await this.phoneInput.type(phone, { delay: 10 });
}
```

### 小程序登录流程

**预期登录流程：**
1. 导航到 `/talent-mini` 页面
2. 填写用户名（在测试中创建的人才用户）
3. 填写密码
4. 点击登录按钮
5. 验证登录成功（跳转到主页或显示用户信息）
6. 存储 token 用于后续请求

**测试用户创建（参考 Story 12.3）：**
```typescript
// Story 12.3 创建的人才用户
const talentUserData = {
  username: `test_talent_${Date.now()}`,
  password: 'password123',
  nickname: '测试人才用户',
  userType: UserType.TALENT,
  disabilityPersonId: 1,  // 关联残疾人
};
```

### Playwright MCP 探索结果

**页面 URL 和路由结构：**
- H5 URL: `http://localhost:8080/talent-mini` (生产环境)
- 开发环境: `http://localhost:10087/talent-mini`
- 配置源: `mini-talent/config/index.ts`
  - `publicPath: '/talent-mini/'`
  - `router.basename: '/talent-mini'`
- 路由配置: `mini-talent/src/app.config.ts`
  - 首页: `pages/index/index`
  - 登录页: `pages/login/index`

**登录表单元素选择器（当前无 data-testid）：**
- 源码位置: `mini-ui-packages/rencai-auth-ui/src/pages/LoginPage/LoginPage.tsx`
- **重要**: 当前登录页面**没有**添加 `data-testid` 属性，需要使用 role/text 选择器
- 表单结构:
  ```
  - 导航栏标题: "人才登录" (Navbar组件)
  - 页面标题: "人才服务平台" (text)
  - 副标题: "欢迎回来" (text)
  - 账号输入标签: "手机号/身份证号/残疾证号" (text)
  - 账号输入框: placeholder="请输入手机号/身份证号/残疾证号" (Input)
  - 密码输入标签: "密码" (text)
  - 密码输入框: placeholder="请输入密码" (Input, password模式)
  - 登录按钮: "登录" (Button)
  - 忘记密码链接: "忘记密码？" (text)
  ```

**Token 存储方式（通过源码确认）：**
- Token Key: `talent_token` (在 `rencai-auth-ui/src/hooks/useAuth.tsx` 第22行)
- User Key: `talent_user` (同上)
- 存储方式: `Taro.setStorageSync(TOKEN_KEY, token)`
- 在 H5 环境中，Taro.setStorageSync 会映射到 localStorage

**与企业小程序对比：**

| 特性 | 企业小程序 | 人才小程序 |
|------|-----------|-----------|
| H5 URL | `/mini` | `/talent-mini` |
| Token Key | `enterprise_token` | `talent_token` |
| User Key | `enterprise_user` | `talent_user` |
| 首页路径 | `/pages/yongren/dashboard/index` | `/pages/index/index` |
| 登录页标题 | "企业用户登录" | "人才登录" |
| 账号标签 | "请输入手机号" | "手机号/身份证号/残疾证号" |
| data-testid | ✅ 已添加 | ❌ 待添加 |

**源码位置参考：**
- 人才小程序登录页面: `mini-ui-packages/rencai-auth-ui/src/pages/LoginPage/LoginPage.tsx`
- 人才小程序桥接文件: `mini-talent/src/pages/login/index.tsx`
- 人才小程序路由配置: `mini-talent/src/app.config.ts`
- 人才小程序 H5 配置: `mini-talent/config/index.ts`
- 企业小程序登录页面: `mini-ui-packages/mini-enterprise-auth-ui/src/pages/login/Login.tsx`
- Auth Hook: `mini-ui-packages/rencai-auth-ui/src/hooks/useAuth.tsx`

### Token 管理策略

**存储位置：** localStorage (H5 环境，通过 Taro.setStorageSync 映射)

**Token 操作方法（使用 talent_token key）：**
```typescript
// 获取 token
async getToken(): Promise<string | null> {
  return await this.page.evaluate(() => {
    return localStorage.getItem('talent_token');
  });
}

// 设置 token（用于测试前置条件）
async setToken(token: string): Promise<void> {
  await this.page.evaluate((t) => {
    localStorage.setItem('talent_token', t);
  }, token);
}

// 清除所有认证存储
async clearAuth(): Promise<void> {
  await this.page.evaluate(() => {
    localStorage.removeItem('talent_token');
    localStorage.removeItem('talent_user');
  });
}
```

### Playwright Fixture 集成

**在 `web/tests/e2e/fixtures.ts` 中添加 fixture：**
```typescript
import { test as base } from '@playwright/test';
import { TalentMiniPage } from './pages/mini/talent-mini.page';

type TalentMiniFixtures = {
  talentMiniPage: TalentMiniPage;
};

export const test = base.extend<TalentMiniFixtures>({
  talentMiniPage: async ({ page }, use) => {
    const talentMiniPage = new TalentMiniPage(page);
    await use(talentMiniPage);
  },
});
```

### 项目结构

**新建文件：**
- `web/tests/e2e/pages/mini/talent-mini.page.ts`

**相关参考文件：**
- `web/tests/e2e/pages/mini/enterprise-mini.page.ts` (Story 12.4) - 企业小程序 Page Object
- `web/tests/e2e/pages/admin/user-management.page.ts` (Story 12.1) - 用户管理 Page Object
- `web/tests/e2e/utils/timeouts.ts` - TIMEOUTS 常量

### 小程序只读特性

根据 Epic 12 文档：
- 小程序只读，无写操作
- 这意味着 Page Object 不需要包含创建、编辑、删除等操作方法
- 主要关注登录、查看、验证等只读操作

### E2E 测试和主页实现的关联

**E2E 测试计划：**
- Story 12.6：人才小程序 Page Object 基础结构（当前 Story）
- Story 12.7：人才小程序登录 E2E 测试（将在该 Story 中实现完整的登录测试）

**主页元素选择器状态：**
- 任务 5.1（工作列表选择器）：待小程序主页实现后添加对应 testid
- 任务 5.2（导航菜单选择器）：待小程序主页实现后添加对应 testid
- 任务 5.3（用户信息选择器）：待主页实现后添加对应 testid

**原因说明：**
主页元素（工作列表、导航菜单、用户信息）的选择器需要在实际主页实现时才能确定对应的 testid。当前 Story 12.6 专注于 Page Object 的基础结构设计和登录功能，主页相关元素的选择器将在主页实现时同步添加，并在 Story 12.7 的 E2E 测试中验证。

**Token 持久性验证：**
Token 在页面刷新后的持久性验证将在 Story 12.7 的 E2E 测试中实现，包括：
- 验证 token 在 localStorage 中的存储
- 验证页面刷新后 token 仍然有效
- 验证使用已存储 token 可以继续访问需要认证的页面

### 选择器策略

**重要说明：人才小程序当前没有 data-testid 属性**

根据 Playwright MCP 探索结果，人才小程序登录页面 (`rencai-auth-ui/src/pages/LoginPage/LoginPage.tsx`) 目前**没有添加** `data-testid` 属性。与企业小程序不同：
- 企业小程序登录页面已在 Story 12.4 中添加 `data-testid` 属性
- 人才小程序需要在任务 8 中添加这些属性

**当前阶段的选择器策略：**

由于页面目前没有 `data-testid`，在实现 Page Object 时需要：

1. **优先使用 text 定位（临时方案）**：
```typescript
// 账号输入框 - 使用 placeholder
this.page.getByPlaceholder('请输入手机号/身份证号/残疾证号')

// 密码输入框 - 使用 placeholder
this.page.getByPlaceholder('请输入密码')

// 登录按钮 - 使用 text
this.page.getByRole('button', { name: '登录' })
```

2. **任务 8 添加 data-testid 后的最终方案**：
```typescript
private readonly selectors = {
  // ✅ 任务 8 完成后：data-testid（使用 talent- 前缀）
  loginPage: '[data-testid="talent-login-page"]',
  identifierInput: '[data-testid="talent-identifier-input"]',
  passwordInput: '[data-testid="talent-password-input"]',
  loginButton: '[data-testid="talent-login-button"]',

  // ⚠️ 当前阶段：使用 role/text 选择器（临时方案）
  // identifierInput: 'input[placeholder="请输入手机号/身份证号/残疾证号"]',
  // passwordInput: 'input[placeholder="请输入密码"]',
  // loginButton: 'button:has-text("登录")',
};
```

**标准优先级（任务 8 完成后）：**
1. `data-testid` 属性（最高优先级）
2. ARIA 属性 + role
3. 文本内容（最低优先级，避免使用）

### TypeScript 类型定义

**Page Object 类类型：**
```typescript
export class TalentMiniPage {
  readonly page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  // 所有方法应有明确的返回类型
  async goto(): Promise<void> { }
  async login(username: string, password: string): Promise<void> { }
  async expectLoginSuccess(): Promise<void> { }
}
```

### 测试超时配置

**使用 TIMEOUTS 常量：**
```typescript
import { TIMEOUTS } from '../utils/timeouts';

await expect(this.page.locator(this.selectors.loginButton)).toBeVisible({
  timeout: TIMEOUTS.default,
});
```

### 参考文档

**架构文档：**
- `_bmad-output/planning-artifacts/architecture.md`
- `docs/standards/e2e-radix-testing.md` (Radix UI 测试标准)

**相关 Story 文档：**
- `12-1-user-page-object.md` (用户管理 Page Object)
- `12-3-create-talent-user.md` (后台创建人才用户测试)
- `12-4-enterprise-mini-page-object.md` (企业小程序 Page Object)

## Dev Agent Record

### Agent Model Used

Claude (d8d-model)

### Debug Log References

_N/A - 无需调试_

### Completion Notes List

**实现完成 (2026-01-14):**

1. **TalentMiniPage 类创建**
   - 文件位置: `web/tests/e2e/pages/mini/talent-mini.page.ts`
   - 完整实现了人才小程序 Page Object，包含导航、登录、Token 管理功能
   - 所有公共方法都有完整的 JSDoc 注释
   - TypeScript 类型安全，无 `any` 类型

2. **页面导航功能 (AC2)**
   - `goto()` 方法导航到 `/talent-mini`
   - `expectToBeVisible()` 验证页面加载
   - `removeDevOverlays()` 移除开发覆盖层

3. **登录功能封装 (AC3)**
   - `fillIdentifier()` - 填写身份标识（手机号/身份证号/残疾证号）
   - `fillPassword()` - 填写密码
   - `clickLoginButton()` - 点击登录按钮
   - `login()` - 完整登录流程
   - `expectLoginSuccess()` - 验证登录成功
   - `expectLoginError()` - 验证登录失败

4. **Token 管理 (AC4)**
   - `getToken()` - 获取存储的 token (talent_token)
   - `setToken()` - 设置 token（用于测试前置条件）
   - `clearAuth()` - 清除认证存储

5. **主页元素选择器 (AC5)**
   - 定义了 `userInfo` 选择器
   - 主页详细选择器待主页实现后添加

6. **代码质量 (AC6)**
   - 类型检查通过
   - 完整 JSDoc 注释
   - 遵循项目命名约定

7. **Fixtures 更新 (任务 7)**
   - 在 `web/tests/e2e/fixtures.ts` 中添加 `TalentMiniFixtures` 接口
   - 添加 `testTalent` fixture 扩展

8. **data-testid 属性添加 (任务 8)**
   - 文件: `mini-ui-packages/rencai-auth-ui/src/pages/LoginPage/LoginPage.tsx`
   - 添加的 testid: `talent-login-page`, `talent-page-title`, `talent-identifier-input`, `talent-password-input`, `talent-login-button`

**技术要点:**
- 使用 `talent-` 前缀与企业小程序区分
- Token Key: `talent_token`
- H5 URL: `/talent-mini`
- 使用 data-testid 选择器（任务 8 已添加）

### File List

**新建文件:**
- `web/tests/e2e/pages/mini/talent-mini.page.ts`

**修改文件:**
- `web/tests/e2e/pages/mini/index.ts` - 添加 TalentMiniPage 导出
- `web/tests/e2e/fixtures.ts` - 添加 TalentMiniFixtures 和 testTalent
- `mini-ui-packages/rencai-auth-ui/src/pages/LoginPage/LoginPage.tsx` - 添加 data-testid 属性

## Change Log

- 2026-01-14: Story 12.6 创建完成
  - 人才小程序 Page Object 基础结构设计
  - 登录功能封装需求
  - Token 管理策略
  - 状态：ready-for-dev

- 2026-01-14: Dev Notes 更新 - 添加 Playwright MCP 探索结果
  - 页面 URL 和路由结构 (`/talent-mini`, 端口 10087)
  - 登录表单元素选择器（当前无 data-testid，需使用 role/text 选择器）
  - Token 存储方式确认 (`talent_token` in localStorage)
  - 与企业小程序对比（URL、Token Key、首页路径等区别）
  - 源码位置参考（登录页面、路由配置、H5 配置等）
  - 更新选择器策略，添加当前阶段的临时方案说明
  - 更新 Token 管理策略，使用 `talent_token` key

- 2026-01-14: Story 12.6 实现完成
  - 创建 TalentMiniPage 类，实现所有导航、登录、Token 管理功能
  - 添加 data-testid 属性到登录页面
  - 更新 fixtures 文件
  - 状态：review
