# 端到端验证手册

目标：在**真实接口**下把手机端走通，并确认平板端没退化。只看编译通过是不够的——
本次适配里"筛选条件被静默清空"和"后端不认新参数"两个问题，都只有跑真机才暴露。

## 环境

```bash
~/Library/Android/sdk/emulator/emulator -list-avds    # Medium_Phone_API_36.1(手机) / Aerologic_Tablet(平板)
adb devices -l
adb -s <serial> shell wm size && adb -s <serial> shell wm density
```

判定形态：手机 1080×2400@420dpi → `PHONE(sw=411dp)`；平板 1280×800@160dpi → `TABLET(sw=800dp)`。

`ui_probe.sh` 封装了常用操作，比手写 adb 省事且更可靠：

```bash
P=.claude/skills/phone-adapt/scripts/ui_probe.sh
$P <serial> form                  # 打印形态识别结果
$P <serial> texts                 # 当前界面所有可见文字（做断言用）
$P <serial> bounds "已交接"        # 元素 bounds + 中心点
$P <serial> tap-text "确认"        # 按文字点击（自动取中心点）
$P <serial> wait-text "待交接"     # 等页面就绪
$P <serial> wait-gone "请稍候……"   # 等加载弹窗消失
$P <serial> shot /tmp/a.png       # 截图
$P <serial> req pageQuery         # 抓请求/响应体
$P <serial> crash                 # 崩溃次数 + 栈
```

## 构建安装

```bash
./gradlew assembleDebug 2>&1 | grep -E "^(BUILD|FAILURE)|error:"
adb -s <serial> install -r -d app/build/outputs/apk/debug/app-debug.apk
```

注意 gradle 输出里大量 `e: 注: ARouter::Compiler …` 是注解处理器的**提示**被误标成 `e:`，
不是错误；只看 `BUILD SUCCESSFUL` / `error:` 就行。

## 进入目标页面的两条路

### 正规路径（优先）：登录 → 首页菜单

```bash
adb -s <serial> shell monkey -p com.lukouguoji.aerologic -c android.intent.category.LAUNCHER 1
```

测试环境账号在记忆文件 `test-env-login.md`（ADMIN / 用户已授权保存），可自主登录。
填表用 `input text`，**`#` 不需要转义**——新版 adb 的 input text 参数不经远端 shell 解析，
写 `\#` 反而会把 `\` 也输进去（实测多出一位）。

⚠️ **「记住密码」回填哈希坑（实测踩过）**：输入用户名后 App 会调 `queryUserByName`
把本地存储的 **bcrypt 哈希**回填进密码框，盖掉/混入你输入的明文 → 登录 200 但校验失败、
静默停在登录页（密码框点数对不上是判断信号：明文 9 位 vs 回填后 10 位显示）。
正确顺序：点密码框 → 等回填（sleep 1）→ `input keycombination 113 29`（CTRL+A）+
`KEYCODE_DEL` 清空 → 再输明文 → `KEYCODE_BACK` 收键盘 → 点登录。
键盘不收起时「登录」按钮可能点不到（tap 被吞），收起后再点。

底部有「首页 / 国内 / 国际 / 我的」Tab，国际业务页找对应入口
（底部 Tab 的「国际」与顶部筛选的「国际」同名，`tap-text "国际" 2` 指定第 2 处）。

**首页菜单是按权限过滤的**：当前账号角色没有该权限时，入口根本不显示（本次 ADMIN 登录后
国际业务只有「板箱过磅」「进港理货」，没有「出库交接」）。这时走下面的直启。

### 直启（权限缺失时的替代）：冷启动 + am start

`app/src/debug/AndroidManifest.xml` 里把调试页临时 `exported="true"`，就能直接拉起。
**必须冷启动**（先 force-stop），否则会撞上既有的 LoadingModel 崩溃：

```bash
adb -s <serial> shell am force-stop com.lukouguoji.aerologic
sleep 2
adb -s <serial> shell am start -n com.lukouguoji.aerologic/<完整类名>
```

登录态存在 SharedPreference 里，冷启动直启同样带 token，接口能正常返回真实数据。

> 先经首页再 `am start` 会崩在 `LoadingModel.showLoading` → XPopup `popupInfo is null`，
> 这是既有缺陷（未改动的页面也复现），不是本次改动引入的。

## 点击前先等就绪

加载弹窗（"请稍候……"）会盖住整屏并**吞掉 `input tap`**，表现为"点了没反应"。
本次验证就因此白跑了几轮。所以：

```bash
$P <serial> wait-text "待交接"      # 等目标元素出现
$P <serial> tap-text "已交接"
$P <serial> texts | grep -c "全选"  # 用文字变化确认点击确实生效
```

点完**一定要断言效果**（用 `texts` 或截图），别假设点中了。

## 逐项走查清单

每一步做完都跑一次 `crash`（必须为 0）：

- [ ] 列表加载：卡片字段、各种状态形态（如待处理 / 已完成两种卡片）
- [ ] Tab 切换：指示条位置、角标数字、底部条按设计显隐
- [ ] 筛选弹层：打开、各控件取值、重置、确认后重新查询
- [ ] 下拉字典：真实取到数据（不是空列表）
- [ ] 多选：单选、全选联动、"已选 N 项"计数
- [ ] 详情页/二级页：字段绑定、条件显示的区块
- [ ] 扫码入口能拉起

## 核对请求参数是否真的生效

新增的查询字段要确认两件事：客户端**发出去了**，服务端**认**。

```bash
$P <serial> req pageQuery
```

OkHttp 日志是多行 pretty JSON。看到请求体里有你的字段（如 `"handoverState": "0"`）只说明客户端对了。
**服务端是否生效要用行为验证**：切到另一个 Tab，看返回数据是否真的变了。

⚠️ 行为验证失败时**先怀疑参数名，再怀疑后端**：出库交接第一版传 `hoState`，两个 Tab 返回同一批数据，
一度误判为「后端没做过滤」；实际是参数名错了（api-doc 里是 `handoverState`），后端对不认识的参数
**静默忽略**。正确顺序：查 `references/api-doc.md` 核对参数名 → 用 token 直调后端做 A/B 对比
（同条件只改一个参数看 total 变化）→ 仍不生效才定性为后端缺陷并记入
`documents/后端接口对接问题清单.xlsx`。

## 无数据时怎么核对 UI

服务端当天可能没数据（列表空）。放宽条件（清空日期）再查；仍为空时，临时注入样例 Bean：

```kotlin
// 临时：覆盖各种形态各一条，验证后删除
binding.rv.postDelayed({
    binding.rv.commonAdapter()?.refresh(listOf(/* 待处理 / 危险品 / 已完成 各一条 */))
}, 2500)
```

截图核对完**必须删除**，然后重新构建 + grep 确认无残留：

```bash
grep -rn "injectMockForVisualCheck\|TODO 临时" module_base/src module_gjc/src | wc -l   # 应为 0
```

## 屏幕方向验证

新增/修改 Activity 后，确认手机端不会「先横屏再转竖屏」：

```bash
# 冷启动时连续采样：应全程 mRotation=0
adb -s <phone> shell am force-stop com.lukouguoji.aerologic; sleep 2
adb -s <phone> shell monkey -p com.lukouguoji.aerologic -c android.intent.category.LAUNCHER 1
for i in $(seq 1 10); do adb -s <phone> shell dumpsys window | grep -m1 -oE "mRotation=[0-9]+"; sleep 0.4; done
```

平板则应首屏就是横屏（截图宽 > 高）。若手机出现横屏帧，检查该 Activity 的 Manifest
是否写成了别的值——**必须是 `android:screenOrientation="unspecified"`**（写死 `userLandscape`
或用 `@integer/...` 资源引用都会出问题，详见 SKILL.md 阶段 4）。

## Pad 端回归（必做）

```bash
adb -s <tablet> install -r -d app/build/outputs/apk/debug/app-debug.apk
adb -s <tablet> shell am start -n com.lukouguoji.aerologic/<同一个类名>
$P <tablet> shot /tmp/tablet.png
$P <tablet> form     # 应为 TABLET(sw=800dp…)
```

对比改造前后：搜索条数量与顺序、日期默认值、按钮（平板可能比手机多按钮）、底部统计文案。
改过 `module_base` 里的公共基类或 Pad 系组件时，这一步不能省，且要多看几个页面。

## 定位"值被神秘修改"类问题

在 setter 里打带堆栈的日志，一次就能看到调用来源：

```kotlin
android.util.Log.d("DBG", "value '$field' -> '$v'", Throwable("trace"))
```

```bash
adb -s <serial> logcat -d -s DBG | grep -A14 "'旧值' -> ''" | grep "at com.lukouguoji\|at android"
```

本次靠这招定位到 `TextView.onRestoreInstanceState → EditText.setText → doOnTextChanged`，
即复合自定义 View 的 id 串档（见 `components.md`）。**排完删日志。**
