diff --git a/tests/manual/README.md b/tests/manual/README.md new file mode 100644 index 0000000..2bc1b7c --- /dev/null +++ b/tests/manual/README.md @@ -0,0 +1,64 @@ +# tests/manual/ — 真机回归手册 + +> mobile/ 模块依赖 UIAutomator2 直连真机,自动化测试以 Mock 为主(`tests/conftest.py`)。 +> **本目录**承担纯单测覆盖不到的真机验证:UI hierarchy、Damai App 实际响应、抢票热路径性能。 + +## 目录结构 + +``` +tests/manual/ +├── README.md 本文件 — 入口与使用方法 +├── runbook.md 标准回归流程(每次发版前跑) +├── android_compatibility_matrix.md 跨 Android 版本兼容性记录(持续更新) +├── templates/ 可复制模板 +│ ├── _smoke_log.md 冒烟测试模板 +│ ├── _nlp_5_show_log.md 5 演出 NLP 测试矩阵模板 +│ ├── _multi_session_log.md 多场次测试矩阵模板 +│ ├── _p2_scenarios_log.md P2 边界场景模板 +│ └── _findings.md 发现新 issue 的归档模板 +└── history/ 历史回归记录(按 YYYY-MM-DD 归档) + ├── 2026-05-15-W2-v0.4.0-rc1.md NLP autodetect (#26) + price 越界守护 (#31) + ├── 2026-05-15-findings.md W2 真机发现归档 + ├── 2026-05-22-W3-multi.md 多场次回归 (#25) + ├── 2026-05-22-W3-p2.md P2 三连 (#28 #23 #24) + └── 2026-05-22-W3-summary.md W3 综合回归汇总 +``` + +## 使用方法 + +### 一次完整回归(每次发版前) + +1. 打开 [`runbook.md`](runbook.md),按 6 步流程逐步执行。 +2. 每步从 [`templates/`](templates/) 复制对应模板到工作副本,填数据。 +3. 完成后把工作副本 `git mv` 到 [`history/`](history/),命名 `YYYY-MM-DD-.md`。 +4. 更新 [`android_compatibility_matrix.md`](android_compatibility_matrix.md) 一行。 +5. 任何 ❌ 的问题在 [`templates/_findings.md`](templates/_findings.md) 复制 Finding 模板登记 → `gh issue create` 上报。 + +### 临时只跑一类场景 + +如:仅验证某个 P0 hotfix → 直接复制 `templates/_smoke_log.md` 跑冒烟即可,归档到 `history/<日期>-hotfix-.md`。 + +## 触发条件(详见 runbook.md) + +- 任一 P0 / P1 PR 合入 +- 准备打 tag 发版 +- 收到大麦 App 更新通知(高优先级,可能需要立即兼容性回归) + +## SLA(详见 runbook.md) + +- P0 PR:24h 内完成回归 +- P1 PR:48h 内完成回归 +- 其他:与 sprint 同步 + +## 保密红线 + +- 演出名脱敏(首字母 + 类型,如 "XX 演唱会"、"ZZ 音乐节") +- 不写真实观演人姓名 / 手机号 / 身份证号 / 订单号 +- 截图、dump 文件、config 摘录均需脱敏后再纳入文档或 issue + +## 关联文档 + +- [`reference/06-test-gap-analysis.md`](../../reference/06-test-gap-analysis.md):测试缺口与维护节奏 +- [`reference/04-issues-matrix.md`](../../reference/04-issues-matrix.md):每个 issue 的真机验证状态 +- [`reference/08-ops-runbook.md`](../../reference/08-ops-runbook.md):抢票现场与回滚流程 +- [`mobile/scripts/benchmark_hot_path.sh`](../../mobile/scripts/benchmark_hot_path.sh):抢票热路径性能基准 diff --git a/tests/manual/android_compatibility_matrix.md b/tests/manual/android_compatibility_matrix.md new file mode 100644 index 0000000..7b91521 --- /dev/null +++ b/tests/manual/android_compatibility_matrix.md @@ -0,0 +1,33 @@ +# Android × Damai App 兼容性矩阵 + +> 持续更新。每次发版前的真机回归完成后,把环境维度追加 / 更新一行。 +> 用于:判断某次失败是否归因于已知组合;指导未来购买测试设备 / 选 emulator 镜像。 + +## 通过状态图例 + +- ✅:全部场景通过 +- ⚠️:部分场景失败,但有 workaround 或不影响主流程(详见对应 history 记录) +- ❌:主流程失败(需 hotfix) +- 空:尚未测试 + +## 矩阵 + +| 测试日期 | Android | 真机/模拟器 | Damai App | HaTickets commit | 冒烟 | NLP | 多场次 | P2 | 发现的 issue | 历史记录 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | + + + +## 当前已知风险点 + +> 在某个组合上反复出现的、尚未在 reference/ 中归档的兼容性问题。 +> 写法:` + : <一句话症状> → `。 + +- _ + +## 矩阵维护规则 + +- 每次回归后必更新本表(即使全部通过也要新增一行) +- "发现的 issue" 列填 `gh` 创建的 issue 编号;无则写 `none` +- "历史记录" 列引用 `history/<日期>-<标签>.md` +- 出现 ❌ 时,须在 [`reference/04-issues-matrix.md`](../../reference/04-issues-matrix.md) 同步登记 diff --git a/tests/manual/W2_nlp_realmachine_log.md b/tests/manual/history/2026-05-15-W2-v0.4.0-rc1.md similarity index 100% rename from tests/manual/W2_nlp_realmachine_log.md rename to tests/manual/history/2026-05-15-W2-v0.4.0-rc1.md diff --git a/tests/manual/W2_findings.md b/tests/manual/history/2026-05-15-findings.md similarity index 100% rename from tests/manual/W2_findings.md rename to tests/manual/history/2026-05-15-findings.md diff --git a/tests/manual/W3_multi_session_log.md b/tests/manual/history/2026-05-22-W3-multi.md similarity index 100% rename from tests/manual/W3_multi_session_log.md rename to tests/manual/history/2026-05-22-W3-multi.md diff --git a/tests/manual/W3_p2_log.md b/tests/manual/history/2026-05-22-W3-p2.md similarity index 100% rename from tests/manual/W3_p2_log.md rename to tests/manual/history/2026-05-22-W3-p2.md diff --git a/tests/manual/W3_regression_summary.md b/tests/manual/history/2026-05-22-W3-summary.md similarity index 100% rename from tests/manual/W3_regression_summary.md rename to tests/manual/history/2026-05-22-W3-summary.md diff --git a/tests/manual/runbook.md b/tests/manual/runbook.md new file mode 100644 index 0000000..4c1dcd4 --- /dev/null +++ b/tests/manual/runbook.md @@ -0,0 +1,75 @@ +# 真机回归 Runbook + +> 每次发版前必跑。从模板复制 → 填数据 → 归档到 `history/`。 + +## 触发条件 + +- 任一 P0 / P1 PR 合入 +- 准备打 tag 发版 +- 收到大麦 App 更新通知 + +## 流程(每次回归 1-2h) + +### 1. 冒烟(10 min) + +复制 `templates/_smoke_log.md` → 填: + +- 设备 / Android / Damai 版本 +- `poetry install` 通过 +- `mobile/scripts/start_ticket_grabbing.sh --probe --yes` 进到详情页 + +### 2. NLP 5 演出(30 min) + +复制 `templates/_nlp_5_show_log.md` → 跑 5 个不同演出。覆盖: + +- #26 NLP autodetect 4 类正常输入 + 1 类故意缺信息 +- #31 price_index 越界守护 3 子场景 +- 无价格卡片 / 缺货 dump + +### 3. 多场次(20 min) + +复制 `templates/_multi_session_log.md` → 跑 ≥2 个多场次活动。覆盖: + +- #25 多日同城 / 多日多城 / 单日多场 +- `rush_mode` alias 兼容 +- `rush_skip_session` 单场次/多场次行为 + +### 4. P2 边界(30 min) + +复制 `templates/_p2_scenarios_log.md` → 跑三类场景: + +- #28 `wait_for_home_ready` 超时与 dump +- #23 `select_search_result` 0/1/N 分流(含 strict 模式) +- #24 `PageProbe` unknown_threshold 阈值与 `force_state` + +### 5. 性能基准(10 min) + +```bash +bash mobile/scripts/benchmark_hot_path.sh --runs 5 +``` + +把中位数填到本次冒烟日志最末「性能数据」段。 + +### 6. 归档 + +- 把填好的文件 `git mv` 到 `history/<日期>-<标签>.md`,命名形如 `2026-MM-DD-.md`、`2026-MM-DD-findings.md`、`2026-MM-DD-summary.md`。 +- 更新 `android_compatibility_matrix.md` 一行:本次设备 / Android / Damai 版本 + 通过/失败摘要。 +- 把 findings 中的新 issue 用 `gh issue create` 上报,并在对应 PR 描述中回链。 + +## SLA + +- P0 PR:24h 内完成回归 +- P1 PR:48h 内完成回归 +- 其他:与 sprint 同步 + +## 不做的事 + +- ❌ 不在模板 / 历史日志中含真实演出名 / 真实姓名 / 手机号 / 身份证号 / 订单号 +- ❌ 不删除已有 history(只 `git mv` 重组) +- ❌ 不下调 80% 覆盖率门槛来"绕过"失败(应在本 PR 增补单测) + +## 关联 + +- `reference/06-test-gap-analysis.md`:测试维护节奏 +- `reference/05-fix-plans/`:每个 issue 的修复计划与回归点 +- `reference/08-ops-runbook.md`:抢票现场与回滚流程 diff --git a/tests/manual/templates/_findings.md b/tests/manual/templates/_findings.md new file mode 100644 index 0000000..a2384aa --- /dev/null +++ b/tests/manual/templates/_findings.md @@ -0,0 +1,90 @@ +# 真机回归 Findings 归档(_findings.md) + + + +> **用途**: 记录本次真机回归(冒烟 / NLP / 多场次 / P2 边界)中发现的、不在预期内的失败、回归或可用性问题。 +> **流程**: 每发现一个新问题 → +> 1. 在本文件追加一条「Finding 模板」并填写。 +> 2. 用 `gh issue create` 上报(含 OS / Android / Damai App 版本 / 脱敏 config / 错误日志摘要)。 +> 3. 在对应 PR 描述中引用新 issue 编号。 +> **保密要求**: 截图、日志、config 必须脱敏(演出名首字母 + 类型,无姓名 / 手机号 / 身份证号 / 订单号)。 + +--- + +## 已发现 Findings + +> [TODO] 暂无;qa 真机执行后追加。 + +--- + +## Finding 模板(复制后填写) + +```markdown +### Finding YYYY-MM-DD-NN:<一句话标题> + +- **发现时间**: YYYY-MM-DD HH:MM (Asia/Shanghai) +- **执行人 ID(脱敏)**: qa-XX +- **真机型号 / Android**: _ +- **Damai App 版本**: _ +- **仓库 commit**: `git rev-parse --short HEAD` 输出 +- **触发场景**: (冒烟 Task A/B / NLP Task A #N / NLP Task B-N / 多场次 #N / P2 #28-N / P2 #23-N / P2 #24-N / 其它) +- **复现步骤**: + 1. _ + 2. _ + 3. _ +- **预期**: _ +- **实际**: _ +- **关键日志(脱敏)**: + ``` + + ``` +- **附件**: + - tmp/price_dump_*.xml(路径,已脱敏) + - tmp/home_probe_*.xml / search_probe_*.xml / page_probe_unknown_*.xml(路径) + - tmp/failure_*.zip(W4-02 自动留档;路径) + - 截图链接(脱敏后存放位置) +- **严重度**: P0 / P1 / P2 / P3 +- **建议归类**: bug / docs / UX / 第三方变更 +- **关联 issue**: #N(gh issue create 创建后回填) +- **关联 PR**: #N(修复 PR,事后回填) +``` + +--- + +## gh issue 上报示例(便于 qa 复用) + +```bash +gh issue create \ + --title "[真机回归] <一句话现象>" \ + --label "bug,real-machine" \ + --body "$(cat <<'EOF' +## 现象 +<一句话描述> + +## 复现步骤 +1. ... +2. ... + +## 环境 +- 真机 / Android: _ +- Damai App: _ +- HaTickets commit: _ +- config 关键字段(脱敏): price_index=__, keyword=__ + +## 关键日志 +<脱敏后的 stderr / stack trace> + +## 关联 +- 真机日志: tests/manual/history/<本次回归文件名> +- 详细记录: tests/manual/history/<本次 findings 文件名> (Finding __) +EOF +)" +``` + +--- + +## 归档完成判定 + +- [ ] 本文件每条 Finding 都已建对应 GitHub issue(除非确认为操作失误) +- [ ] 每条 Finding 都标注严重度 +- [ ] 截图与 dump 路径不含真实演出名 / 姓名 / 手机号 / 身份证号 / 订单号 diff --git a/tests/manual/templates/_multi_session_log.md b/tests/manual/templates/_multi_session_log.md new file mode 100644 index 0000000..f15c458 --- /dev/null +++ b/tests/manual/templates/_multi_session_log.md @@ -0,0 +1,76 @@ +# 多场次回归矩阵(_multi_session_log.md) + + + +> **范围**: 多场次场景下场次选择 (#25) 的真机回归。 +> **预算**: 20 min。 +> **保密要求**: 演出名脱敏(首字母 + 类型),不写真实观演人姓名 / 手机号 / 身份证号 / 订单号。 +> **上游模板**: 抽自 `history/2026-05-22-W3-multi.md`。 +> **关联实现**: `mobile/event_navigator.py:select_session`。 + +--- + +## 测试环境 + +| 项 | 值(待 qa 填写) | +| --- | --- | +| 真机型号 / Android 版本 | [TODO] _ | +| Damai App 版本 | [TODO] _ | +| 仓库 commit (`git rev-parse --short HEAD`) | [TODO] _ | +| 操作者(脱敏 ID,如 qa-01) | [TODO] _ | +| 执行日期(YYYY-MM-DD) | [TODO] _ | + +--- + +## Task A:多场次 5 场景回归矩阵 + +> **填表说明** +> - `命中场次`:从 `mobile/scripts/start_ticket_grabbing.sh --probe --yes` 的日志中读取 `select_session: ... 命中 idx=K/N` 字段,记 `idx (K/N) - <场次描述>`,如 `0 (0/3) - 04.05 上海`。 +> - `通过` 列写 ✅/❌;❌ 行须在 `_findings.md` 开 issue 并回链。 +> - `# 4` 与 `# 5` 不需要真机活动,可在任意已 reach `sku_page` 的演出上验证;重点验证配置项的兼容/语义。 +> + +| # | 演出名(脱敏) | 类型 | date 配置 | city 配置 | 命中场次 | 通过 | 备注 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 1 | [TODO] 巡演 A 上海站 | 多日同城(同一城市多个日期) | [TODO] `04.05` | [TODO] `上海` | [TODO] _ | [TODO] _ | 期望唯一命中 04.05 | +| 2 | [TODO] 巡演 B 全国 | 多日多城(多个城市各一日期) | [TODO] `04.10` | [TODO] `北京` | [TODO] _ | [TODO] _ | 期望命中 北京 04.10;只填 date 应 ambiguous → 必须配合 city | +| 3 | [TODO] 音乐节 C 单日 | 多艺人单日(单日多场次卡片) | [TODO] `05.01` | [TODO] `上海` | [TODO] _ | [TODO] _ | 单日内若 SESSION_PICKER 不弹出(只有 1 场次),日志应说明 "skipped session selection";fallback_index 失效 | +| 4 | [TODO] rush_mode alias 兼容 | _ | _ | _ | _ | [TODO] _ | `config` 中 `rush_mode: true` 启动期应自动展开为三个子开关默认值,且日志显示 `rush_skip_session=false / rush_skip_price_dump=true / rush_aggressive_retry=true` | +| 5 | [TODO] rush_skip_session=true(单场次场景) | _ | _ | _ | _ | [TODO] _ | 单场次演出 + `rush_skip_session: true`:跳过场次选择直接 sku_page;多场次演出 + `rush_skip_session: true`:日志应有 warning 或 fail-fast | + +--- + +## Task B:执行步骤(人工 qa 真机现场操作) + +```bash +cd /Users/andrew/Documents/GitHub/HaTickets +git checkout master && git pull + +# 准备 config.local.jsonc,填入对应行的 keyword / date / city +# 然后逐场景执行: +bash mobile/scripts/start_ticket_grabbing.sh --probe --yes 2>&1 | tee tmp/multi_session_case_$N.log +# 把 select_session 命中行复制到 Task A 表格对应行 + +# 场景 4 验证:直接构造 alias 配置 +# config.local.jsonc: +# {"rush_mode": true, ...} +# 启动后日志应有:rush_mode alias → rush_skip_session=false, rush_skip_price_dump=true, rush_aggressive_retry=true + +# 场景 5:手动构造 rush_skip_session=true 单场次/多场次 各一次 +``` + +--- + +## Task C:发现问题汇总 + +> 任一行 ❌ 都要在此区列出 issue 链接 + 一句话症状。 +> 若已知症状但暂无 issue,先标记 "TBD" 并在 `_findings.md` 中提请创建。 + +- [TODO] _ + +--- + +## 归档 + +- 完成填写后,将本文件 `git mv` 到 `history/<日期>-<标签>.md`。 +- 若 5 场景全部 ✅,在 #25 issue 下评论:「真机回归全部通过 — log: history/<新文件名>@」。 diff --git a/tests/manual/templates/_nlp_5_show_log.md b/tests/manual/templates/_nlp_5_show_log.md new file mode 100644 index 0000000..97c84d0 --- /dev/null +++ b/tests/manual/templates/_nlp_5_show_log.md @@ -0,0 +1,91 @@ +# 5 演出 NLP 真机测试矩阵(_nlp_5_show_log.md) + + + +> **范围**: NLP autodetect (#26) + price_index 越界守护 (#31) 的回归矩阵。 +> **预算**: 30 min。 +> **保密要求**: 演出名脱敏(首字母 + 类型),不写真实观演人姓名 / 手机号 / 身份证号 / 订单号。 +> **上游模板**: 抽自 `history/2026-05-15-W2-v0.4.0-rc1.md`。 + +--- + +## 测试环境 + +| 项 | 值(待 qa 填写) | +| --- | --- | +| 真机型号 / Android 版本 | [TODO] _ | +| Damai App 版本 | [TODO] _ | +| 仓库 commit (`git rev-parse --short HEAD`) | [TODO] _ | +| 操作者(脱敏 ID,如 qa-01) | [TODO] _ | +| 执行日期(YYYY-MM-DD) | [TODO] _ | + +--- + +## Task A:5 演出 NLP probe 测试矩阵 + +> 在每台真机上执行 `bash mobile/scripts/run_from_prompt.sh --mode summary --yes "<自然语言输入>"`,把 stdout 的关键字段填入对应行。 +> `actionable=no` 视为预期失败(仅 #5 故意缺信息);其它 4 条若 actionable=no,需要在「diagnostics(关键)」列写出 `confidence` 与 `missing` 字段并在 `_findings.md` 中开 issue。 +> + +| # | 演出名(脱敏) | 类型 | 自然语言输入 | 预期 actionable | 实际 actionable | 命中演出名 | 命中场次 | 命中价格 | confidence | diagnostics(关键) | 通过 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 1 | [TODO] XX 演唱会 单场次 | 单场次 | [TODO] "<演出代号> <日期> <城市> <价格>" | yes | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | +| 2 | [TODO] YY 演唱会 全国巡演 | 多场次 | [TODO] "<演出代号> <日期> <城市> <价格>" | yes | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | +| 3 | [TODO] ZZ 音乐节 | 多日多场 | [TODO] "<演出代号> 五一 <城市> 2 张" | yes | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | +| 4 | [TODO] 日期模糊场景 | 单场次 | [TODO] "<演出代号> <城市> ¥A-B" | yes | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | +| 5 | [TODO] 故意缺信息 | 单场次 | [TODO] "<仅演出代号>" | no(应触发交互或 exit 5) | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | [TODO] _ | + +> **填表说明** +> - `命中演出名 / 命中场次 / 命中价格` 取自 summary 输出的 `selected_show / selected_session / selected_price` 字段。 +> - `confidence` 取 summary 顶层的同名字段。 +> - `diagnostics(关键)` 摘录 actionable=no 时的 `missing` / `errors` / `dump_path`(若有);actionable=yes 时记 "ok"。 +> - `通过` 列写 ✅/❌;❌ 行须在 `_findings.md` 开 issue。 + +--- + +## Task B:price_index 越界守护 + +```bash +# 先备份 config +cp mobile/config.local.jsonc mobile/config.local.jsonc.bak 2>/dev/null || cp mobile/config.jsonc mobile/config.local.jsonc + +# 设置越界值 +sed -i.bak 's/"price_index": [0-9]*/"price_index": 999/' mobile/config.local.jsonc +bash mobile/scripts/start_ticket_grabbing.sh --probe --yes +# 预期:启动期立即 ConfigError 退出,stderr 含 "price_index",进程不进入抢票流程 + +# 还原 +mv mobile/config.local.jsonc.bak mobile/config.local.jsonc +``` + +| 子场景 | 预期 | 实际(待填) | 通过 | +| --- | --- | --- | --- | +| price_index = 999(远超合法范围) | 启动期 ConfigError 立即退出 | [TODO] _ | [TODO] _ | +| price_index = -1(负数) | 启动期 ConfigError 立即退出 | [TODO] _ | [TODO] _ | +| price_index = 51(仅大于 warning 阈值) | 启动 warning,但流程继续 | [TODO] _ | [TODO] _ | + +--- + +## Task C:无价格卡片 dump + +| 触发方式 | tmp/price_dump_*.xml 是否生成 | dump 文件名(脱敏) | 通过 | +| --- | --- | --- | --- | +| 无价格卡片演出 / 已售罄 | [TODO] _ | [TODO] _ | [TODO] _ | + +--- + +## 执行人签字 + +| 角色 | ID(脱敏) | 完成时间 | 备注 | +| --- | --- | --- | --- | +| 真机执行 qa | [TODO] _ | [TODO] _ | _ | +| 复核 qa(可选) | [TODO] _ | [TODO] _ | _ | + +--- + +## 验收对照 + +- [ ] Task A 5 行全部填写,且 ≥4 行 actionable=yes +- [ ] Task B 三个子场景均按预期触发(前两行 ConfigError,第三行仅 warning) +- [ ] Task C 至少 1 行 dump 已生成,文件路径已记录 +- [ ] 本日志不含真实姓名 / 手机号 / 身份证号 / 订单号 diff --git a/tests/manual/templates/_p2_scenarios_log.md b/tests/manual/templates/_p2_scenarios_log.md new file mode 100644 index 0000000..5017ba7 --- /dev/null +++ b/tests/manual/templates/_p2_scenarios_log.md @@ -0,0 +1,176 @@ +# P2 边界场景回归(_p2_scenarios_log.md) + + + +> **范围**: P2 三连真机回归,覆盖: +> - #28 `wait_for_home_ready` 首页探测超时 + UI dump +> - #23 `select_search_result` 0/1/N 分流 + UI dump +> - #24 `PageProbe` unknown_threshold 阈值告警 + force_state +> +> **预算**: 30 min。 +> **保密要求**: 演出名脱敏,不写真实观演人姓名 / 手机号 / 身份证号 / 订单号。 +> **上游模板**: 抽自 `history/2026-05-22-W3-p2.md`。 + +--- + +## 测试环境 + +| 项 | 值(待 qa 填写) | +| --- | --- | +| 真机型号 / Android 版本 | [TODO] _ | +| Damai App 版本 | [TODO] _ | +| 仓库 commit (`git rev-parse --short HEAD`) | [TODO] _ | +| 操作者(脱敏 ID,如 qa-01) | [TODO] _ | +| 执行日期(YYYY-MM-DD) | [TODO] _ | + +--- + +## #28 首页探测超时 + +> 实现位于 `mobile/page_helpers.py:wait_for_home_ready`。 +> 期望:超时抛 `HomeNotReadyError`,异常 message 含 `dump=tmp/home_probe_.xml`,dump 文件已落地,日志输出当前可见 texts + resource_ids。 + +| # | 测试 | 步骤 | 预期 | 实际 | 通过 | +| --- | --- | --- | --- | --- | --- | +| 28-1 | 已在大麦首页 fast path | 启动脚本时已停留在大麦 MainActivity | 立即返回,state=homepage,<1.5s 完成 | [TODO] _ | [TODO] _ | +| 28-2 | 关闭大麦 App 后启动 | 强停大麦后启动脚本 | 8s 后抛 `HomeNotReadyError`,异常含 `首页未就绪` + dump 路径,文件存在 | [TODO] _ | [TODO] _ | +| 28-3 | 启动大麦但停在详情页 | 进入详情页后启动脚本 | 8s 后抛 `HomeNotReadyError`,dump 显示详情页 hierarchy(包含 `purchase_status_bar` 等关键 id) | [TODO] _ | [TODO] _ | +| 28-4 | dump 路径与命名 | 任一超时 case 后查看 `ls tmp/home_probe_*.xml` | 命名形如 `home_probe_YYYYMMDD_HHMMSS.xml`,体积 >0 | [TODO] _ | [TODO] _ | + +--- + +## #23 搜索结果 + +> 实现位于 `mobile/page_helpers.py:select_search_result`。 +> 0 结果 → `SearchEmptyError`(异常 message 含 keyword + dump);1 结果 → 自动选中点击;N 结果 + target_title 命中 → 模糊匹配选中;N 结果 + target_title 未命中(默认 `strict=False`)→ 回退首条并 warning;`strict=True` 未命中则 `SearchAmbiguousError`。 + +| # | 测试 | 关键词 | target_title | strict | 预期 | 实际 | 通过 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 23-1 | 0 结果 | [TODO] `xQyZpQ123abcdNOTEXIST` | `None` | `False` | `SearchEmptyError`,message 含 keyword + `dump=tmp/search_probe_*.xml` | [TODO] _ | [TODO] _ | +| 23-2 | 1 结果 | [TODO] `<极冷门演出全名脱敏>` | `None` | `False` | 自动选中并跳转 `ProjectDetailActivity` | [TODO] _ | [TODO] _ | +| 23-3 | N 结果 + target 匹配 | [TODO] `<热门关键词代号>` | [TODO] `<对应 target>` | `False` | 选中含 target 的卡片,跳转 ProjectDetailActivity | [TODO] _ | [TODO] _ | +| 23-4 | N 结果 + 无 target(fallback first) | [TODO] `<热门关键词代号>` | `None` | `False` | 选中第一个,warning `回退到 idx=0 title=...` | [TODO] _ | [TODO] _ | +| 23-5 | N 结果 + target 未命中 + strict | [TODO] `<热门关键词代号>` | [TODO] `<不存在的 target>` | `True` | `SearchAmbiguousError`,message 含 `共 N 条` + dump | [TODO] _ | [TODO] _ | +| 23-6 | dump 路径与命名 | 23-1 / 23-5 后查看 `ls tmp/search_probe_*.xml` | _ | _ | 命名形如 `search_probe_YYYYMMDD_HHMMSS.xml` | [TODO] _ | [TODO] _ | + +--- + +## #24 page_probe unknown 阈值 + +> 实现位于 `mobile/page_probe.py:PageProbe._track_unknown` 与 `force_state`。 +> 连续 N=`unknown_threshold` 次返回 `state="unknown"` 时:logger.warning(`UNKNOWN_THRESHOLD reached after N classifications`) + 自动写 `tmp/page_probe_unknown_.xml` + 设 `probe.dumped_xml_path`。 +> `force_state(state)` 短路返回;`force_state(None)` 清除并恢复真实探测。 + +| # | 测试 | 步骤 | 预期 | 实际 | 通过 | +| --- | --- | --- | --- | --- | --- | +| 24-1 | 在弹窗未关时连续 probe | 启动脚本时手动让首页弹窗保持显示,连续 3 次 `PageProbe.classify()` | 第 3 次返回 unknown 后 logger.warning `UNKNOWN_THRESHOLD reached after 3 classifications`;`probe.dumped_xml_path` 非 None;文件存在且为弹窗 hierarchy | [TODO] _ | [TODO] _ | +| 24-2 | 任一已知状态后计数重置 | 关闭弹窗 → probe 一次(应为 homepage)→ 再次让弹窗出现 → 仅 1 次 unknown 不应触发告警 | 不出现告警;`dumped_xml_path` 维持 24-1 的旧值 | [TODO] _ | [TODO] _ | +| 24-3 | `force_state(PageState.HOMEPAGE)` 短路 | 在任意页面调用 `probe.force_state(PageState.HOMEPAGE)`;后续 `classify()` | state=homepage,且未触发 dump_hierarchy / app_current 调用(adb 抓包/日志确认) | [TODO] _ | [TODO] _ | +| 24-4 | `force_state(None)` 恢复 | 24-3 后调用 `probe.force_state(None)` 然后 classify | 返回真实 state(与设备实际页面一致) | [TODO] _ | [TODO] _ | +| 24-5 | dump 路径与命名 | 24-1 后查看 `ls tmp/page_probe_unknown_*.xml` | 命名形如 `page_probe_unknown_YYYYMMDD_HHMMSS.xml` | [TODO] _ | [TODO] _ | + +--- + +## 执行步骤参考(人工 qa 真机现场操作) + +```bash +cd /Users/andrew/Documents/GitHub/HaTickets +git checkout master && git pull + +# === #28 === +# 28-1 +adb shell am start -n cn.damai/.homepage.MainActivity # 确保在首页 +poetry run python -c " +import time, uiautomator2 as u2 +from mobile.page_probe import PageProbe +from mobile.event_navigator import wait_for_home_ready, HomeNotReadyError +d = u2.connect() +probe = PageProbe(d, cache_ttl_s=0.0) +t0=time.time(); r=wait_for_home_ready(d, probe, timeout=8.0); print('elapsed_ms=', (time.time()-t0)*1000, 'state=', r['state']) +" + +# 28-2 +adb shell am force-stop cn.damai +poetry run python -c " +import time, uiautomator2 as u2 +from mobile.page_probe import PageProbe +from mobile.event_navigator import wait_for_home_ready, HomeNotReadyError +d = u2.connect() +probe = PageProbe(d, cache_ttl_s=0.0) +try: + wait_for_home_ready(d, probe, timeout=8.0) +except HomeNotReadyError as e: + print('OK:', e) +import glob; print('dumps:', glob.glob('tmp/home_probe_*.xml')) +" + +# === #23 === +# 进搜索页(首页 → 搜索按钮) +poetry run python -c " +import time, uiautomator2 as u2 +d = u2.connect() +d(resourceId='cn.damai:id/pioneer_homepage_header_search_btn').click(); time.sleep(1.2) +for _ in range(3): + btn = d(resourceId='cn.damai:id/damai_theme_dialog_cancel_btn') + if not btn.exists: break + btn.click(); time.sleep(0.6) +print('on search:', d.app_current().get('activity')) +" + +# 23-1 / 23-3 / 23-4 / 23-5:在搜索页执行 +poetry run python -c " +import time, uiautomator2 as u2 +from mobile.event_navigator import select_search_result, SearchEmptyError, SearchAmbiguousError +d = u2.connect() +inp = d(resourceId='cn.damai:id/header_search_v2_input') +inp.click(); time.sleep(0.3) +btn = d(resourceId='cn.damai:id/header_search_v2_input_delete') +if btn.exists: btn.click(); time.sleep(0.2) +keyword = '<填入测试关键词>' +inp.set_text(keyword); time.sleep(0.2) +d.press('enter'); time.sleep(1.0) +if d(text='演出').exists: d(text='演出').click(); time.sleep(0.5) +try: + chosen = select_search_result(d, keyword=keyword, target_title='<或 None>', strict=False, timeout=5.0) + print('picked:', chosen) +except (SearchEmptyError, SearchAmbiguousError) as e: + print(type(e).__name__, e) +" + +# === #24 === +adb shell am start -n cn.damai/.homepage.MainActivity +sleep 2 +poetry run python -c " +from mobile.page_probe import PageProbe, PageState +import uiautomator2 as u2 +d = u2.connect() +probe = PageProbe(d, cache_ttl_s=0.0, unknown_threshold=3, dump_dir='tmp') +import os +os.system('adb shell am start -a android.settings.SETTINGS') +import time; time.sleep(1.5) +for i in range(3): + r = probe.classify() + print(i, r['state']) +print('dumped:', probe.dumped_xml_path) + +probe.force_state(PageState.HOMEPAGE) +print('forced:', probe.classify()['state']) +probe.force_state(None) +print('cleared:', probe.classify()['state']) +" +``` + +--- + +## 发现问题汇总 + +> 任一行 ❌ 都要在此区列出 issue 链接 + 一句话症状。 + +- [TODO] _ + +--- + +## 归档 + +- 完成填写后,将本文件 `git mv` 到 `history/<日期>-<标签>.md`。 +- 三个 issue 的关键分支全部 ✅ 后,在各自 issue 下评论「真机回归通过 — log: history/<新文件名>@」。 diff --git a/tests/manual/templates/_smoke_log.md b/tests/manual/templates/_smoke_log.md new file mode 100644 index 0000000..8497c3e --- /dev/null +++ b/tests/manual/templates/_smoke_log.md @@ -0,0 +1,83 @@ +# 冒烟测试日志(_smoke_log.md) + + + + +> **范围**: 发版前最小集冒烟。验证 mobile/ 主干在目标真机上可启动并到达详情页。 +> **预算**: 10 min。 +> **保密要求**: 演出名脱敏(首字母 + 类型),不写真实观演人姓名 / 手机号 / 身份证号 / 订单号。 + +--- + +## 测试环境 + +| 项 | 值(待 qa 填写) | +| --- | --- | +| 真机型号 / Android 版本 | [TODO] _ | +| Damai App 版本 | [TODO] _ | +| 仓库 commit (`git rev-parse --short HEAD`) | [TODO] _ | +| 操作者(脱敏 ID,如 qa-01) | [TODO] _ | +| 执行日期(YYYY-MM-DD) | [TODO] _ | + +--- + +## Task A:依赖与基础启动 + +| # | 测试 | 命令 | 预期 | 实际(待填) | 通过 | +| --- | --- | --- | --- | --- | --- | +| 1 | poetry 安装 | `poetry install` | 0 错误,依赖落地 | [TODO] _ | [TODO] _ | +| 2 | adb 连接 | `adb devices` | 设备状态为 `device`(非 `unauthorized` / `offline`) | [TODO] _ | [TODO] _ | +| 3 | Damai 进程可启动 | `adb shell am start -n cn.damai/.homepage.MainActivity` | 首页可见,无 ANR | [TODO] _ | [TODO] _ | + +--- + +## Task B:probe 到详情页 + +```bash +bash mobile/scripts/start_ticket_grabbing.sh --probe --yes 2>&1 | tee tmp/smoke_probe.log +``` + +| # | 测试 | 预期 | 实际(待填) | 通过 | +| --- | --- | --- | --- | --- | +| 1 | 启动期 ConfigError 不触发 | 无 ConfigError,进入主流程 | [TODO] _ | [TODO] _ | +| 2 | 首页就绪 (#28) | 日志含 `wait_for_home_ready ... state=homepage` | [TODO] _ | [TODO] _ | +| 3 | 搜索 + 选中 (#23) | 日志含 `select_search_result picked` | [TODO] _ | [TODO] _ | +| 4 | 抵达详情页 | 日志含 `reach detail page` 或 `ProjectDetailActivity` | [TODO] _ | [TODO] _ | +| 5 | probe 模式安全退出 | 退出码 0 / 不下单 | [TODO] _ | [TODO] _ | + +--- + +## Task C:失败工件检查 + +> 如本次有任意 ❌,把 `tmp/` 下相关文件清单填入;正常路径可写「无」。 + +| 文件类型 | 命名样式 | 是否生成 | 路径(脱敏) | +| --- | --- | --- | --- | +| home_probe dump | `tmp/home_probe_*.xml` | [TODO] 无/有 | [TODO] _ | +| search_probe dump | `tmp/search_probe_*.xml` | [TODO] 无/有 | [TODO] _ | +| page_probe unknown | `tmp/page_probe_unknown_*.xml` | [TODO] 无/有 | [TODO] _ | +| 失败工件包 | `tmp/failure_*.zip`(W4-02 引入) | [TODO] 无/有 | [TODO] _ | + +--- + +## 验收 + +- [ ] Task A 三行全 ✅ +- [ ] Task B 五行全 ✅ +- [ ] 任一 ❌ 已在同次回归的 `_findings.md` 中追加 Finding +- [ ] 本日志不含真实姓名 / 手机号 / 身份证号 / 订单号 + +--- + +## 性能数据(在本次回归末尾追加) + +```bash +bash mobile/scripts/benchmark_hot_path.sh --runs 5 +``` + +| 指标 | 中位数 (ms) | 备注 | +| --- | --- | --- | +| wait_for_home_ready | [TODO] _ | _ | +| select_search_result | [TODO] _ | _ | +| select_session | [TODO] _ | _ | +| _submit_order_fast | [TODO] _ | _ |