Files
youle_app_ios_v2/docs/Verification-Checklist.md
T
joywayerandClaude Opus 4.7 0602b9d4b6 Phase 3.A:srcIsloop 本地音频 + 3.B/3.C/3.D handler stub
3.A 本地音频完整落地(与 msext NewRootVC initJSdata 等价):

  Source/Audio/AudioPlayer.swift(@MainActor 状态机):
    - playOnce(url) 单次按钮音;buttonPlayers 数组保活池避免多次点击
      互相打断(duration + 0.5s 后清理)
    - loopBackground(url, type:) 循环背景音;numberOfLoops = -1 +
      backgroundType 记录用于同名停止
    - stopBackground(type:) 同名校验后才停止(msext 同款语义)

  Source/Bridge/Handlers/LocalAudioHandler.swift:
    - 注册 srcIsloop handler(@MainActor enum)
    - 入参 src + isloop,按 0/1/-1 分支调 AudioPlayer.shared 对应方法
    - audioFileURL 路径:{Caches}/{gamedir}/{gamestart}/assets/wav/{src}
      与 msext NewRootVC 路径约定 1:1(H5 zip 内 assets/wav 子目录)
    - audioFileURL 标 nonisolated 因 SandboxPaths 是 nonisolated 静态
    - cb 字面 "Response from srcIsloop" 严格保持

  Source/Bridge/Handlers/RemoteAudioHandler.swift(3.B/3.C/3.D stub):
    - prepareaudio + mediaTypeAudio 仅 cb 维持契约,无真实业务
    - 待 Phase 3.B(opencore-amr .a 接入)+ 3.C(七牛 SPM + token)+
      3.D(远端播放)完整后升级
    - 按 CLAUDE.md 原则 A:H5 调啥就得有人接,即使是 stub

WebContainerViewController.registerBridgeHandlers 接入:
  - LocalAudioHandler.register(完整实现)
  - RemoteAudioHandler.register(stub)

Plan §5 Phase 3.1 勾选;3.2 真机验证留待 H5 提供音频文件后做。

Verification-Checklist.md 新增 Phase 3 章节:
  - H. 本地音频(E.14-E.16 srcIsloop 三个 isloop 值)
  - I. prepareaudio / mediaTypeAudio stub(E.17 E.18)

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 19:11:34 +08:00

227 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 功能验证清单(开发期累积,统一验收)
> **定位**:各 Phase 实施期间陆续累积的验证项目,本文档**不是契约**(契约在 `H5-Native-Contract.md` §10),而是面向开发者的**指导步骤**——告诉验证者"应该做什么操作、看到什么现象、用什么工具"。
>
> **使用方式**
> - 每完成一个 Phase 子项 → 把对应验证步骤勾掉
> - **不要在 Phase 进行中频繁手动验证**,避免反复重装 app 浪费时间
> - 多个 Phase 累积后**统一跑一次**完整验证清单
> - 与 Contract §10 契约验收清单**并行**Contract §10 是黑盒契约边界("必须长这样"),本文档是**怎么验证它长这样**的操作手册
---
## 通用准备
### 环境
| 工具 | 用途 | 启用方式 |
|------|------|---------|
| Xcode console | 看原生 os.log 输出 | Xcode 底部输出窗口 |
| Safari Web Inspector | 看 H5 端 JS console / DOM / Network | macOS Safari → 开发菜单 → Simulator / 真机 → 当前 H5 页面 |
| 模拟器 Features 菜单 | 切换 Battery State / Network Link Conditioner / Toggle In-Call Status | Simulator → Features |
| 真机 Safari 调试 | 真机 H5 网页检查器 | iPhone → 设置 → Safari → 高级 → 网页检查器 = ON |
### 重装 app(每次跑完整验证前都做一遍)
按 CLAUDE.md「typical case 1launch snapshot 缓存粘滞」教训:
- 模拟器/真机:**长按 app → 删除** → Xcode 重新 Run
- 或:`xcrun simctl uninstall booted com.skyapp.ylgamehall` 然后 Xcode Run
- 这样可以清掉 iOS sandbox 内的 launch snapshot 缓存,避免改了 Info.plist / LaunchScreen 后还是看到旧版本
### 沙盒访问
```bash
xcrun simctl get_app_container booted com.skyapp.ylgamehall data
# 输出形如 /Users/.../Containers/Data/Application/<UUID>/
```
---
## Phase 1 — 启动闭环
### A. 启动图 + LaunchScreen
- [ ] **A.1 启动图方向正确**:横屏设备启动瞬间看到完整 logo + "快乐游戏 / 健康生活" 文字,画面正向(不是横躺)
- [ ] **A.2 状态栏方向**:电池/时间在画面顶部居中横向,**不在**画面左侧竖向(竖向 = iOS 错误地按 portrait 渲染 LaunchScreen,参 CLAUDE.md「典型案例 1」)
- [ ] **A.3 黑边合理**:比 16:9 更宽的 iPhone(如 16 Pro)启动图两侧补黑边可接受;iPad 4:3 上下补黑边可接受
### B. 启动流水线时序
预期 Xcode console 顺序:
```
[BundleConfig] 渠道注入读取结果(11 项)
[SandboxPaths] 路径计算结果
[ResourceUnzipper] ensureReady 完成
[RemoteConfigClient] 拉取成功 .parsed
[VersionResolver] chulishengji 双子树合并结果
[LocalVersionReader] 本地 vs 远端对比
[LobbyZipUpgrader] 无需升级 / 升级完成 N → M
[lobby] write app_data.js → ... ← Phase 1.7 落地(AppDataWriter
[lobby] write app_gamesname.js → ...
[lobby] write app_battery.js: app_getbattery=0.XX → ...
[lobby] write app_network.js: app_getnetwork=2 → ...
```
- [ ] **B.1 启动一气呵成**splash 显示 → "拉取配置中" → ("下载更新中 XX%") → "加载大厅..." → splash 0.3s 淡出,看到 H5 大厅
- [ ] **B.2 16:9 letterbox 黑边**:H5 大厅在屏幕中间,左右(或上下)有黑边,画面比例 16:9
- [ ] **B.3 启动总耗时 < 2s**(无升级路径),有升级时进度条实时更新
### C. `app_*.js` 文件落盘 + 全局变量命名
```bash
# 进入沙盒看 4 个文件
xcrun simctl get_app_container booted com.skyapp.ylgamehall data
# 然后 cat 4 个文件
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_data.js
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_battery.js
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_network.js
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_gamesname.js
```
- [ ] **C.1 4 个文件全部存在**且内容非空
- [ ] **C.2 app_data.js 内 12 个 var 命名 100% 正确**(特别注意大小写:`app_Launchtype` L 大写、`app_getwifisignalLevel` wifi 小写 + signal/Level 区分)
- [ ] **C.3 app_battery.js 变量名带 get**`var app_getbattery=N;`(不是 `app_battery`
- [ ] **C.4 app_network.js 变量名带 get**`var app_getnetwork=N;`
- [ ] **C.5 app_gamesname.js 沿用 msext 字面**`var app_gamesname=new Array('xxx');`var 后两个空格 + new Array(...)
- [ ] **C.6 不存在已撤销变量**`app_data.js` 内没有 `app_gameid` / `app_compareCode`(早期 Contract 误列,原 msext 不写)
### D. `window.app_*` H5 console 对账
Safari Web Inspector → Console
```js
console.log("app_gameconfig =", app_gameconfig); // 期望 ChannelConfig.plist gameconfig 实际值
console.log("app_market =", app_market); // 期望 "2"
console.log("app_agent =", app_agent); // 期望 ChannelConfig.plist agent 实际值
console.log("app_channel =", app_channel);
console.log("app_Launchtype =", app_Launchtype); // 大厅期望 0
console.log("app_appversion =", app_appversion); // 期望 '0' 字符串(审核切换标志非版本号)
console.log("app_getbattery =", app_getbattery); // 期望 0.XX
console.log("app_getnetwork =", app_getnetwork); // 期望 1/2/3
console.log("app_gamesname =", app_gamesname); // 期望 Array
console.log("app_gameid =", app_gameid); // 期望 undefined ✓
console.log("app_compareCode =", app_compareCode); // 期望 undefined ✓
```
- [ ] **D.1 所有 15 个 app_* 全局变量都 `!== undefined`**
- [ ] **D.2 关键字段都是 ChannelConfig.plist 的实际值**(不是 H5 zip 默认占位)
- [ ] **D.3 `app_appversion === '0'`**(审核标志,正常业务);`app_gameconfig` 取 BundleConfig.gameConfig
---
## Phase 2 — 大厅 handler + 反向 callback
### E. H5 → Native handler 响应
每项的验证方式:在 H5 console 调对应 `bridge.callHandler('xxx', data, function(resp){console.log(resp);})`,看 Xcode console 有无副作用 + H5 收到 cb。
- [ ] **E.1 vibrator** → 真机震动 + H5 cb "vibrator"
- [ ] **E.2 repeatvibrator** → 真机震动 + cb "repeatvibrator"
- [ ] **E.3 canclevibrator** → cb "canclevibrator"(无可感知副作用,正常)
- [ ] **E.4 gameCopytext('test123')** → 系统剪贴板含 "test123";其他 app 粘贴可验证;iOS 14+ 会出"已粘贴自 ylgamehall" banner(平台行为非 bug
- [ ] **E.5 gamepastetext** → cb 是当前剪贴板内容(不是字面 "gamepastetext"
- [ ] **E.6 SwitchShake(1) + SwitchShake(0)** → 内部 canVoice 状态切换(无可视化副作用,cb "SwitchShake"
- [ ] **E.7 startshake → 摇一摇手机 → H5 收到 `shakeEnd` 反向 callback**`bridge.registerHandler('shakeEnd', fn)` 先注册)
- [ ] **E.8 stopshake → 摇一摇 → H5 不再收到 shakeEnd**
- [ ] **E.9 voicePlaying(1) / voicePlaying(0)** → 内部 actor VoiceCenter 开关位(cb "Response from voicePlaying");mediaTypeAudio 实现后效果可验证
- [ ] **E.10 getphoneInfo** → cb "getphoneInfo"**大写 I**+ 反向 callback `getphoneinfo`**小写 i**)含 6 字段(PhoneAdresseMAC / PhoneDeviceBrand / PhoneIMEI / PhoneModel / PhoneProvidersName / PhoneVersion
- [ ] **E.11 browser('https://www.apple.com')** → Safari 打开苹果首页
- [ ] **E.12 opensaoma** → cb "opensaoma"(空实现,无可感知副作用,符合契约)
- [ ] **E.13 startlocation** → cb "startlocation"Phase 2 stub,无定位真实业务;Phase 5 升级后再验真实定位)
### F. Native → H5 反向 callback
H5 需先用 `bridge.registerHandler('callbackName', fn)` 注册:
- [ ] **F.1 模拟器 Features → Battery State 切换** → H5 收到 `getBattery` "0.XX" + `window.app_getbattery` 实时跟随
- [ ] **F.2 模拟器 Features → Network Link Conditioner 切换 100% Loss** → H5 收到 `getnetwork` "1" + `window.app_getnetwork === 1`;切回 WiFi → 收 "2";切 3G → 收 "3"
- [ ] **F.3 App 切后台**(Home 键 / 上滑)→ H5 收到 `appservice` "1";回前台 → 收 "2"
- [ ] **F.4 摇一摇** → shakeEnd(见 E.7
- [ ] **F.5 getphoneInfo 触发 getphoneinfo**(见 E.10
### G. ExternalSubscriptions 生命周期
> Phase 2.C 已实现 setup/teardown 配对,但 Phase 6 子游戏容器才能完整验证"防双发"。当前只验证基本生命周期。
- [ ] **G.1 viewWillAppear setup**app 启动后 onChange 闭包都已挂钩(F.1-F.4 都生效)
- [ ] **G.2 viewWillDisappear teardown**:手动触发场景待 Phase 6 子游戏 push 上来时验证
---
## Phase 3 — 音频体系(部分实现:3.A 完成;3.B/C/D 待外部资源)
### H. 本地音频(srcIsloop3.A 完成)
- [ ] **E.14 srcIsloop({src:"xxx.wav", isloop:0})** → 听到单次按钮音;cb "Response from srcIsloop"
- [ ] **E.15 srcIsloop({src:"bg.wav", isloop:1})** → 听到背景循环音 + 同时调 srcIsloop({src:"另一首.wav", isloop:1}) → 替换为新背景循环
- [ ] **E.16 srcIsloop({src:"bg.wav", isloop:-1})** → 停止 bg.wav 背景;如果当前背景不是 bg.wav 则无效(与 msext 同名 type 校验)
> **依赖**:测试需要 H5 zip 内 `assets/wav/` 目录有可用音频文件。
> H5 团队 / 项目方提供后做。
### I. prepareaudio / mediaTypeAudio stub3.B/3.C/3.D 待实现)
- [ ] **E.17 prepareaudio** → cb "Response from prepareaudio"stub 仅 cb,无录音 / 无 getaudiourl 反向)
- [ ] **E.18 mediaTypeAudio** → cb "Response from mediaTypeAudio"stub 仅 cb,无下载 / 无播放 / 无 gameui_play_voice 反向)
> 3.B AMR 转码 + 3.C 七牛上传 + 3.D 远端播放完整实现后升级。
---
## Phase 4 — 分享 + 登录(待实现)
> Phase 4 实施后填充。预备验证项:
> - accreditlogin 微信授权 + sharelogin 7 字段(含大写 Province
> - friendsSharetypeUrlToptitleDescript 分享微信 / 朋友圈 + sharesuccess
---
## Phase 5 — 定位(待实现)
> Phase 5 实施后填充。预备验证项:
> - startlocation 一次性 + 持续 + getlocationinfo 9 字段(含小写 province
---
## Phase 6 — 子游戏(待实现)
> Phase 6 实施后填充。预备验证项:
> - SwitchOverGameData push 子游戏
> - 子游戏 backgameData 回大厅 + getWebdata
> - 子游戏 push 上来后大厅 ExternalSubscriptions teardown(验证不会双发)
---
## Phase 7-10(待实现)
> 各 Phase 实施后陆续填充。
---
## 附:契约 §10 26 项验收清单映射
本文档每个章节对应 Contract §10 的 A/B/C/D/E 五节:
| 本文档章节 | Contract §10 节 | 状态 |
|---|---|---|
| A. 启动图 + LaunchScreen | §A 启动(启动相关) | Phase 1 实现 |
| B. 启动流水线时序 | §A 启动 | Phase 1 实现 |
| C. app_*.js 落盘 + 命名 | §A 启动 | Phase 1 实现 |
| D. window.app_* H5 console | §A 启动 + §B 桥接 | Phase 1 实现 |
| E. H5→Native handler | §B 桥接 | Phase 2 实现 |
| F. 反向 callback | §C 系统事件 + §B 桥接 | Phase 2 实现 |
| G. 生命周期 | Design §2.4.2 内部约束) | Phase 2 实现 |
| 音频 / 录音 / 远端语音 | §B 桥接(音频相关) | Phase 3 待实现 |
| 分享 / 登录 | §B 桥接 + §E 回归 | Phase 4 待实现 |
| 定位 | §B 桥接 | Phase 5 待实现 |
| 子游戏 / 视频房间 | §B + §D 视频房间 | Phase 6 + 8 待实现 |
---
## 维护规则
- 每完成一个 Phase 子项 → 在对应章节补 / 勾验证步骤
- **不要把验证步骤复制到 Contract §10**Contract §10 是契约边界("必须长这样"),本文档是验证操作手册("怎么验证它长这样"),两份保持独立
- 验证发现的 bug → 修复后 commit 时在 message 引用本文档对应 checkbox 编号(如 "修 E.10 getphoneInfo 大小写错"