- 手机版列表(三色状态标签卡片 + 筛选弹层 + 浮动新增 + 全选/批量删除)、 一套 activity_uld_edit 手机布局按 pageType 覆盖 ULD 新增/修改/详情三态 (详情右上铅笔进修改、状态底部选择面板、所在港默认 HFE、手机端必填校验); 平板布局迁至 layout-sw600dp,Kotlin 双端共用,平板端实机回归零变化 - 新增公共组件 PhoneFormRow(行式表单,INPUT/SELECT/DATE/TEXT 四形态, SELECT 内置底部选择面板);PhoneBottomBar 增加 actionDanger 危险按钮样式; 新增 FAB/danger按钮/浅红标签/垃圾桶/铅笔等资源 - ULDBean 补解析接口既有出参 ifNo/efNo/checkInDate/checkOutDate 并预留 source; 批量删除以队列串行调用单条 deleteUld 实现 - 手机首页「国际」Tab 接入 ULD管理入口(ARouter + ComprehensiveUld 权限), 登录→菜单→页面全链路实机验证通过,方向无闪屏,crash=0 - 后端缺口 5 项(状态三态、来源字段、筛选入参、批量删除接口、字段语义) 登记《后端接口对接问题清单.xlsx》#7~#11;skill 沉淀本次踩坑 (FAB elevation 盖弹层、Spinner 异步回调竞态、登录哈希回填) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
182 lines
8.0 KiB
Markdown
182 lines
8.0 KiB
Markdown
# 端到端验证手册
|
||
|
||
目标:在**真实接口**下把手机端走通,并确认平板端没退化。只看编译通过是不够的——
|
||
本次适配里"筛选条件被静默清空"和"后端不认新参数"两个问题,都只有跑真机才暴露。
|
||
|
||
## 环境
|
||
|
||
```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`)。**排完删日志。**
|