# 会议出勤（Meeting Attendance）- E2E 测试详细规范

> **版本**: v1.0
> **创建日期**: 2026-01-22
> **最后更新**: 2026-01-22
> **维护者**: 测试团队
> **参考文档**: `05-ui-interaction-spec.md`, `09-test-scenarios.md`

---

## ✅ 机器读取区（必填）

### 执行摘要

| 字段 | 内容 |
|------|------|
| 执行器 | AI Agent + Playwright MCP |
| 最小断言 | 到达 + 成功/稳定 |
| 鉴权策略 | storageState |
| 等待策略 | domcontentloaded + 条件等待 |

### 用例结构

| 字段 | 说明 |
|------|------|
| 用例标题 | `#### 测试场景 X.Y.Z: {标题}` |
| 优先级 | `P0/P1/P2` |
| 步骤 | 有序列表 |
| 断言 | `预期结果`/`验证点` |

---

## 🧭 人类阅读区（可选）

### 测试范围与说明

- 覆盖关键冒烟路径：会议列表、会议详情、签到页（系统/访客）、报表页。
- E2E 仅验证关键流程与稳定状态，不替代集成测试。
- 若数据不足，需先用 API 创建最小会议与参会人数据。

### 用例清单（冒烟建议）

#### 测试场景 1.1.1: 会议列表可访问并加载
- **优先级**: P0
- **前置条件**: 已登录（storageState），存在至少 1 条会议数据
- **步骤**:
  1. 打开 `/meetingattendance/meetings`
  2. 等待列表加载完成
- **预期结果**:
  - 到达断言：会议列表页标题或关键元素可见
  - 稳定断言：表格中出现至少 1 行会议数据
- **关联 API**: [001]

#### 测试场景 1.2.1: 会议详情可访问并展示二维码
- **优先级**: P0
- **前置条件**: 已登录（storageState），存在可访问会议 ID
- **步骤**:
  1. 打开 `/meetingattendance/meetings/:id`
  2. 进入二维码页
- **预期结果**:
  - 到达断言：详情页主区域可见
  - 稳定断言：线上/线下二维码至少一个可见
- **关联 API**: [003], [006]

#### 测试场景 1.3.1: 访客签到可提交并成功提示
- **优先级**: P0
- **前置条件**: 存在允许访客签到的会议与参会名单
- **步骤**:
  1. 打开 `/meetingattendance/checkin/guest?meetingId=:id`
  2. 输入姓名/邮箱并提交
- **预期结果**:
  - 到达断言：访客签到表单可见
  - 成功断言：出现签到成功提示或状态更新
- **关联 API**: [014]

#### 测试场景 1.4.1: 报表页可访问并加载统计
- **优先级**: P1
- **前置条件**: 已登录（storageState），存在统计数据
- **步骤**:
  1. 打开 `/meetingattendance/reports`
  2. 等待统计加载完成
- **预期结果**:
  - 到达断言：报表页标题或关键图表容器可见
  - 稳定断言：统计卡片或图表出现非空数据
- **关联 API**: [044], [045], [046]

---

### 议程能力 E2E 场景（v1.0）

> **MCP 选择器约定**：统一用 accessibility tree 的 `role` + `name`（按钮 / 文本框 / 列表项 / 标题 / 链接），不依赖 `data-testid`。
> **测试数据**：`t_` 前缀（如 `t_section_拖排序`、`t_item_新议题`）；测试结束按前缀清理。
> **鉴权**：默认 storageState；切角色场景在步骤里显式 logout → login。

#### 测试场景 2.1.1: 议程编辑页拖排序
- **优先级**: P0
- **路由**: `/meetingattendance/[meetingId]/agenda/edit`
- **权限**: `meeting:agenda:update`（manager / 会议 creator）
- **前置条件**: 已登录 manager；测试会议有 ≥ 2 个段（`t_section_A`、`t_section_B`），`t_section_A` 排序在前
- **步骤**:
  1. 浏览器导航到议程编辑页
  2. `browser_snapshot`：捕获议程 tree，记录段顺序为 `[t_section_A, t_section_B]`
  3. `browser_drag`：把 listitem `name="t_section_B"` 拖到 listitem `name="t_section_A"` 之上
  4. 等 toast `name="议程已更新"` 出现（domcontentloaded + 条件等待）
  5. `browser_snapshot`：再次捕获段顺序
  6. `browser_navigate`：刷新当前页
  7. `browser_snapshot`：再捕获段顺序
- **预期结果**:
  - 到达断言：议程 tree 区域可见
  - 排序断言：步骤 5 段顺序为 `[t_section_B, t_section_A]`
  - 持久化断言：步骤 7 段顺序仍为 `[t_section_B, t_section_A]`（刷新后 DB 持久化）
- **关联 API**: [104]

#### 测试场景 2.2.1: 议程项 CRUD
- **优先级**: P0
- **路由**: `/meetingattendance/[meetingId]/agenda/edit`
- **权限**: `meeting:agenda:update`
- **前置条件**: 已登录 manager；至少 1 个空段 `t_section_新建项`
- **步骤**:
  1. 在 `t_section_新建项` 段内点 button `name="添加议程项"`
  2. 弹窗出现：textbox `name="议程项标题"` 填 `t_item_新议题`；combobox `name="主讲人"` 选 `t_manager`；combobox `name="分类"` 选 `其他`（对应 `AgendaCategoryTag.OTHER`）
  3. 点 button `name="保存"`
  4. `browser_snapshot`：验证段下出现 listitem `name=/t_item_新议题/`
  5. 点该 listitem 上的 button `name="编辑"` → 弹窗 → 改 textbox `name="议程项标题"` 为 `t_item_新议题_v2` + 改主讲人 → 保存
  6. `browser_snapshot`：验证项标题已变 + 主讲人显示已变
- **预期结果**:
  - 到达断言：议程 tree 可见
  - 新增断言：步骤 4 listitem `t_item_新议题` 出现
  - 编辑断言：步骤 6 listitem 标题为 `t_item_新议题_v2` + 主讲人正确
- **关联 API**: [105], [106]

#### 测试场景 2.3.1: 分配上传任务
- **优先级**: P0
- **路由**: `/meetingattendance/[meetingId]/agenda/edit` + `/meetingattendance/my-tasks`
- **权限**: 分配 `meeting:upload-task:assign`；被分配方仅需登录
- **前置条件**: 至少 1 个议程项 `t_item_分配任务`；已存在测试用户 `t_assignee@ff.com`
- **步骤**:
  1. 议程编辑页找到 listitem `name=/t_item_分配任务/`，点 button `name="分配任务"`
  2. 弹窗内 combobox `name="选择参会人"` 多选 `t_assignee`，点 button `name="确认"`
  3. `browser_snapshot`：议程项卡片出现 badge `name=/待上传.*1/i` 或 `name=/PENDING.*1/i`
  4. 顶栏点用户菜单 → button `name="退出登录"`
  5. 用 `t_assignee@ff.com` 登录
  6. 顶部菜单点 link `name="我的待办"`，进入 `/meetingattendance/my-tasks`
  7. `browser_snapshot`：列表内出现一张卡片，含 text `t_item_分配任务`
- **预期结果**:
  - 分配断言：步骤 3 PENDING 徽章可见
  - 待办断言：步骤 7 assignee 我的待办列表含目标 task
- **关联 API**: [109], [113]

#### 测试场景 2.4.1: 员工上传资料 + 自动完成任务
- **优先级**: P0
- **路由**: `/meetingattendance/my-tasks` → `/meetingattendance/meetings/:id`（议程项锚点）
- **权限**: `meeting:attachment:upload:assigned`
- **前置条件**: assignee 已登录；存在 PENDING task 对应议程项 `t_item_待上传`
- **步骤**:
  1. 进 `/meetingattendance/my-tasks`，点 button `name="去上传"`（或卡片本身）
  2. 跳转到 `/meetingattendance/meetings/:id#agenda-item-:itemId`，自动滚到对应议程项
  3. 在议程项展开区点 button `name="上传资料"`
  4. `browser_file_upload`：选 `testing/fixtures/t_slides.pdf`（约 2MB）
  5. 等 progressbar 推到 100% + toast `name=/上传成功/i`
  6. `browser_snapshot`：议程项徽章变为 `name=/已上传|UPLOADED/i`
  7. `browser_navigate` 回 `/meetingattendance/my-tasks`
  8. `browser_snapshot`：列表中 `t_item_待上传` 任务不再出现（默认 filter PENDING）
- **预期结果**:
  - 上传断言：步骤 5 toast 成功
  - 状态断言：步骤 6 任务徽章 UPLOADED
  - 列表断言：步骤 8 该任务从待办列表消失
- **关联 API**: [114], [113]

#### 测试场景 2.5.1: 上传 200MB 视频 + progress
- **优先级**: P1
- **路由**: `/meetingattendance/meetings/:id`
- **权限**: `meeting:attachment:upload:any` 或 assigned
- **前置条件**: 测试 fixture 准备 `t_video_200mb.mp4`（精确 200 \* 1024 \* 1024 字节）
- **步骤**:
  1. 进会议详情页议程区，议程项 `t_item_视频上传` 点 button `name="上传资料"`
  2. `browser_file_upload`：选 `t_video_200mb.mp4`
  3. `browser_snapshot` 多次：progressbar 至少出现一次值 ≥ 0 且 < 100，最后值 = 100
  4. 等 toast `name=/上传成功/i`
  5. `browser_snapshot`：议程项展开区附件列表新增 `t_video_200mb.mp4`
- **预期结果**:
  - 进度断言：步骤 3 progressbar 增长可见
  - 完成断言：步骤 5 附件列表出现新文件
- **关联 API**: [114]

#### 测试场景 2.6.1: 上传 > 200MB 文件前端拦截
- **优先级**: P0
- **路由**: `/meetingattendance/meetings/:id`
- **前置条件**: 测试 fixture `t_oversize_250mb.bin`（250MB）
- **步骤**:
  1. 议程项 `t_item_大文件` 点 button `name="上传资料"`
  2. `browser_file_upload`：选 `t_oversize_250mb.bin`
  3. `browser_snapshot`：立即出现 alert / toast `name=/超过.*200MB|文件过大/i`
  4. `browser_network_requests`：确认未发出 `POST /agenda-items/.../attachments`
- **预期结果**:
  - 拦截断言：步骤 3 错误提示可见
  - 零请求断言：步骤 4 上传 API 未被调用
- **关联 API**: [114]（前端阶段拦截）

#### 测试场景 2.7.1: 上传 .exe 前端 MIME 拒绝
- **优先级**: P0
- **路由**: `/meetingattendance/meetings/:id`
- **前置条件**: 测试 fixture `t_payload.exe`
- **步骤**:
  1. 议程项点 button `name="上传资料"`
  2. `browser_file_upload`：选 `t_payload.exe`
  3. `browser_snapshot`：立即出现 alert `name=/不支持的文件类型|MIME/i`
  4. `browser_network_requests`：上传 API 未发出
- **预期结果**:
  - 拒绝断言：错误提示可见
  - 零请求断言：API 未调用
- **关联 API**: [114]（前端阶段拦截）

#### 测试场景 2.8.1: 下载议程项附件
- **优先级**: P0
- **路由**: `/meetingattendance/meetings/:id`
- **权限**: `meeting:attachment:download` + 必须是参会人
- **前置条件**: 议程项 `t_item_下载` 已挂 attachment `t_doc.pdf`；当前用户是参会人
- **步骤**:
  1. 进会议详情页，议程项 `t_item_下载` 点击展开
  2. 附件列表点 link `name="t_doc.pdf"`
  3. 监听 `browser_network_request` 等待 `GET /attachments/agenda-item/:id/download`
- **预期结果**:
  - 请求断言：响应 200；`Content-Disposition` 含 `attachment; filename="t_doc.pdf"`
  - 内容断言：响应体非空（size > 0）
- **关联 API**: [120]

#### 测试场景 2.9.1: 议程项软删连带确认弹窗
- **优先级**: P0
- **路由**: `/meetingattendance/[meetingId]/agenda/edit`
- **权限**: `meeting:agenda:update`
- **前置条件**: 议程项 `t_item_删除` 含 2 个 attachment + 1 个 PENDING task
- **步骤**:
  1. 议程编辑页找到 listitem `name=/t_item_删除/`，点 button `name="删项"`
  2. 弹出 dialog `name=/删除议程项/i`，文案含「将连带删除 2 个附件、1 个任务（30 天内可联系 admin 恢复）」
  3. 点 button `name="确认删除"`
  4. `browser_snapshot`：议程项从议程树消失
  5. （后台校验）GET `/agenda-items/:itemId/attachments` 返回 404；DB 中对应 attachment / task 行**仍存在但 `deletedAt` 非空**（30 天软删窗口）
- **预期结果**:
  - 确认断言：弹窗显示连带数 `2 + 1` 与恢复窗口提示
  - 删除断言：步骤 4 议程树不再含 `t_item_删除`
  - 级联软删断言：步骤 5 关联 DB 行 `deletedAt` 全部非空；list/find 默认过滤
- **关联 API**: [107]

#### 测试场景 2.10.1: 议程能力双语切换
- **优先级**: P0
- **路由**: `/meetingattendance/[meetingId]/agenda/edit` + `/meetingattendance/my-tasks`
- **前置条件**: 已登录 manager
- **步骤**:
  1. 议程编辑页右上角切语言到 `en-US`
  2. `browser_snapshot`：button `name="Add Section"` / `name="Add Agenda Item"` / column header `name="Presenter"` / `name="Duration (min)"` / category options `Mixed` / `Other` 均为英文
  3. 触发一个错误（如未填标题保存）→ snapshot：错误文案为英文
  4. `browser_console_messages`：确认无 `missing key` warning
  5. 切回 `zh-CN`，重复 snapshot 验证中文文案
  6. 进 `/meetingattendance/my-tasks` 重复 en/zh 切换 + snapshot
- **预期结果**:
  - 文案断言：英文模式所有按钮 / label / 错误文案显示英文
  - i18n 断言：console 无 `missing key`
  - 中文断言：切回 zh-CN 文案完整
- **关联 API**: 无（纯 i18n）

#### 测试场景 2.11.1: 普通员工只读议程
- **优先级**: P0
- **路由**: `/meetingattendance/meetings/:id` + `/meetingattendance/[meetingId]/agenda/edit`
- **权限**: 普通 Employee（参会人，无 `meeting:agenda:update`）
- **前置条件**: 用 `t_employee@ff.com` 登录；该用户是测试会议参会人，非 manager / 非 creator
- **步骤**:
  1. 进会议详情页 `/meetingattendance/meetings/:id`
  2. `browser_snapshot`：议程区可见；button `name=/编辑议程/i` **不存在**；button `name=/分配任务/i` **不存在**；议程项 listitem 上**无**拖拽 handle（role="button" `name=/拖动/i` 不存在）
  3. 直接导航 `browser_navigate` 到 `/meetingattendance/[meetingId]/agenda/edit`
  4. `browser_snapshot` + `browser_wait_for`：要么页面显示 403 / `name=/无权限/i`，要么自动 redirect 回 `/meetingattendance/meetings/:id`
- **预期结果**:
  - 入口隐藏断言：步骤 2 三个 manager 入口均不可见
  - 强访问拦截断言：步骤 4 出现 403 或被重定向到详情页
- **关联 API**: [100]（只读）；[101]-[112]、[114] 不应被调用

> 议程能力 E2E 场景小计：11 个 MCP 场景（覆盖 CRUD + 上传/下载 + 大文件/MIME 边界 + 级联删除 + i18n + 权限）。

