@harmonyos-arkts/d2h 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.claude-plugin/marketplace.json +15 -13
  2. package/.claude-plugin/plugin.json +13 -7
  3. package/AGENTS.md +5 -5
  4. package/README.md +212 -212
  5. package/agents/android-to-hmos-00-orchestrator.md +318 -318
  6. package/agents/d2h.md +157 -157
  7. package/dist/bin/install-opencode.js +30 -30
  8. package/docs/environment-check.md +86 -86
  9. package/install-opencode.sh +160 -160
  10. package/opencode/agents.json +16 -16
  11. package/opencode/vscode-d2h-package.json +7 -7
  12. package/opencode/vscode-d2h-plugin.mjs +29 -29
  13. package/package.json +54 -54
  14. package/schemas/android-source-manifest.schema.json +25 -25
  15. package/schemas/checkpoint-provenance.schema.json +67 -67
  16. package/schemas/final-acceptance.schema.json +29 -29
  17. package/schemas/managed-evidence-index.schema.json +15 -15
  18. package/schemas/managed-evidence.schema.json +114 -114
  19. package/schemas/migration-config.schema.json +51 -51
  20. package/schemas/migration-report-index.schema.json +39 -39
  21. package/schemas/migration-report-item.schema.json +51 -51
  22. package/schemas/migration-report-summary.schema.json +25 -25
  23. package/schemas/migration-status.schema.json +202 -202
  24. package/schemas/preflight.schema.json +60 -60
  25. package/schemas/source-order-audit.schema.json +16 -16
  26. package/schemas/source-provenance-event.schema.json +19 -19
  27. package/schemas/spec-app-shard.schema.json +45 -45
  28. package/schemas/spec-fact-corrections-shard.schema.json +13 -13
  29. package/schemas/spec-features-shard.schema.json +43 -43
  30. package/schemas/spec-index.schema.json +26 -26
  31. package/schemas/spec-interactions-shard.schema.json +40 -40
  32. package/schemas/spec-page.schema.json +98 -98
  33. package/schemas/spec-pages-index.schema.json +1 -1
  34. package/schemas/spec-unresolved-shard.schema.json +1 -1
  35. package/schemas/task-envelope.schema.json +109 -109
  36. package/schemas/task-plan.schema.json +341 -341
  37. package/scripts/check-environment.mjs +320 -320
  38. package/skills/android2hmos_resources_convert/SKILL.md +180 -180
  39. package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -230
  40. package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -175
  41. package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -513
  42. package/skills/arkts-app-identity/SKILL.md +238 -238
  43. package/skills/arkts-i18n/SKILL.md +496 -496
  44. package/skills/arkts-i18n/evals/evals.json +84 -84
  45. package/skills/arkts-i18n/references/code-examples.md +302 -302
  46. package/skills/arkts-i18n/references/common-pitfalls.md +391 -391
  47. package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -604
  48. package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -348
  49. package/skills/arkts-i18n/references/language-codes.md +104 -104
  50. package/skills/arkts-i18n/references/resource-file-structure.md +775 -775
  51. package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -242
  52. package/skills/arkts-i18n/references/v1-compat.md +244 -244
  53. package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -174
  54. package/skills/arkts-icon-sizing/SKILL.md +211 -211
  55. package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -131
  56. package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -88
  57. package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -179
  58. package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -119
  59. package/skills/arkts-mvvm-architecture/SKILL.md +613 -613
  60. package/skills/d2h-hypium-forge/SKILL.md +309 -309
  61. package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -140
  62. package/skills/d2h-hypium-forge/references/official-api.md +135 -135
  63. package/skills/d2h-hypium-forge/references/pitfalls.md +98 -98
  64. package/skills/d2h-hypium-forge/references/testability-template.md +71 -71
  65. package/skills/d2h-hypium-forge/scripts/_common_template.py +659 -659
  66. package/skills/d2h-hypium-forge/scripts/analyze_results.py +165 -165
  67. package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -107
  68. package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -169
  69. package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -325
  70. package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -114
  71. package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -80
  72. package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -60
  73. package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -249
  74. package/skills/d2h-hypium-forge/scripts/precondition.py +140 -140
  75. package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -232
  76. package/skills/d2h-hypium-forge/scripts/probe.py +197 -197
  77. package/skills/d2h-hypium-forge/scripts/run_direct.py +823 -823
  78. package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -121
  79. package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -255
  80. package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -66
  81. package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -84
  82. package/skills/d2h-page-develop/SKILL.md +112 -112
  83. package/skills/d2h-page-develop/agents/openai.yaml +7 -7
  84. package/skills/d2h-page-spec-generate/SKILL.md +206 -206
  85. package/skills/d2h-page-spec-generate/scripts/build_batch_report.py +227 -227
  86. package/skills/d2h-utg-ai-generate/SKILL.md +128 -128
  87. package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -76
  88. package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -34
  89. package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -96
  90. package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -61
  91. package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -89
  92. package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -314
  93. package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -123
  94. package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -280
  95. package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -524
  96. package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -489
  97. package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -69
  98. package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -13
  99. package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -2
  100. package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -369
  101. package/skills/d2h-utg-static-generate/SKILL.md +73 -73
  102. package/skills/d2h-utg-static-generate/references/discoveries.md +38 -38
  103. package/skills/d2h-utg-static-generate/scripts/generate.py +499 -499
  104. package/skills/d2h-utg-static-generate/tests/test_static.py +335 -335
  105. package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -60
  106. package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -131
  107. package/skills/harmonyos-migration-playbook/SKILL.md +83 -83
  108. package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -7
  109. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -24
  110. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -53
  111. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -45
  112. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -23
  113. package/skills/harmonyos-migration-playbook/references/ui-automation.md +53 -53
  114. package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -38
  115. package/skills/maintaining-migration-report/SKILL.md +144 -144
  116. package/skills/maintaining-migration-report/references/final-acceptance.md +48 -48
  117. package/skills/maintaining-migration-report/references/source-provenance.md +66 -66
  118. package/skills/managing-d2h-toolchain/SKILL.md +24 -24
  119. package/skills/managing-d2h-toolchain/environment-requirements.json +16 -16
  120. package/skills/managing-d2h-toolchain/scripts/manage-toolchain.mjs +98 -98
  121. package/skills/managing-d2h-toolchain/toolchain.json +16 -16
  122. package/skills/native-library-substitution/SKILL.md +384 -384
  123. package/skills/native-library-substitution/references/native-library-substitution.json +56905 -56905
  124. package/skills/native-library-substitution/references/native-library-substitution.md +163 -163
  125. package/skills/preparing-migration-workspace/SKILL.md +131 -131
  126. package/skills/reviewing-migration-process/SKILL.md +62 -62
@@ -1,659 +1,659 @@
1
- # coding: utf-8
2
- """d2h-hypium-forge 共享库(自动生成)
3
-
4
- 前置条件分组编排约定(TD-004):
5
- 需要登录态的用例文件顶部声明 PRECOND = 'logged_in'
6
- 首启态(bm clean)用例声明 PRECOND = 'first_launch'
7
- 缺省 PRECOND = 'anonymous'
8
- run_direct 按 anonymous→logged_in→first_launch 分组排序(破坏性递增,clean 类排尾),
9
- 组内软隔离(launch() 默认 reset_data=False,preferences 保留→登录态保持)。
10
- ensure_login 等 setup 必须【幂等】:先探测状态(抽屉用户名已就位)再决定是否走完整
11
- UI 登录——组首一次完整 setup,组内后续直接复用(省重复登录/注册)。
12
- 组首条执行时环境变量 FORGE_GROUP_FIRST=1(可选做完整冷启验证)。
13
- """
14
- import os as _os
15
- import time
16
- from hypium import *
17
- from hypium.model import MatchPattern, UiParam
18
-
19
- try:
20
- WAIT_SCALE = float(_os.environ.get('FORGE_WAIT_SCALE', '1') or 1)
21
- except ValueError:
22
- WAIT_SCALE = 1.0
23
-
24
-
25
- def _w(sec):
26
- """等待时长缩放(env FORGE_WAIT_SCALE,默认 1 不变):调试迭代期设 0.5
27
- 可把失败路径的超时烧穿时间减半(Grit 实证 FAIL 用例 17-36s 多为等待超时);
28
- 正式回归/出报告必须保持 1,防止慢设备真问题被截短掩盖。"""
29
- return max(0.5, sec * WAIT_SCALE)
30
-
31
- PACKAGE = "me.hackerchick.raisetoanswer"
32
- ABILITY = "EntryAbility"
33
- ENTRY_PAGE = "pages/Index"
34
-
35
-
36
- # ── Level 0 统一引导引擎(TD-008):权限/隐私/广告/引导弹窗 + tab 归位,一揽子消化 ──
37
- # 关卡模型:每关 = 探测特征文案集合 → 处置(点击命中项)→ 循环消化到无特征。
38
- # app 级配置 GATES(按需增删);bootstrap 幂等(主页锚可见即成功),超时输出页面快照供分诊。
39
-
40
- GATES = [
41
- {"name": "privacy", "texts": ["同意并继续", "同意", "知道了", "我知道了"]},
42
- {"name": "permission", "texts": ["仅在使用中允许", "使用应用时允许", "允许"]},
43
- {"name": "ad", "texts": ["跳过", "关闭广告", "不再显示", "以后再说", "暂不"]},
44
- ]
45
-
46
- def _page_texts(d):
47
- try:
48
- return [(c.getText() or '') for c in (d.find_all_components(BY.type("Text")) or [])
49
- if c.getText()]
50
- except Exception:
51
- return []
52
-
53
-
54
- def _visible_clickables(d):
55
- """一次拉全 Text+Button 可见文案集合(弹窗消化轮询的单 RPC 扫描,
56
- 替代逐文案 find_component 的 12+ RPC/轮——冷启消化从 ~3s/轮 降到 ~0.6s/轮)。"""
57
- out = []
58
- try:
59
- for by in (BY.type("Text"), BY.type("Button")):
60
- for c in (d.find_all_components(by) or []):
61
- t = c.getText() or ""
62
- if t:
63
- out.append(t)
64
- except Exception:
65
- pass
66
- return out
67
-
68
- def bootstrap(d, gates=None, home_tab="", anchor_key="", total=60, log=print):
69
- """冷启到可用基座。流程:轮询消化全部弹窗关卡(一轮多点,直到无特征)→
70
- tab 归位(点目标 tab + 验证锚)→ 主页锚可见返回 True。
71
- anchor_key 是"基座就绪"的唯一判据(app 级配置,如目标 tab 的内容控件 id)。"""
72
- gates = gates if gates is not None else GATES
73
- all_texts = [t for g in gates for t in g["texts"]]
74
- t0 = time.time()
75
- dismissed = []
76
- while time.time() - t0 < total:
77
- # 1) 主页锚已可见 → 提前成功(幂等)
78
- if anchor_key:
79
- try:
80
- if d.find_component(BY.key(anchor_key)) is not None:
81
- if dismissed:
82
- log(f" [L0] 弹窗消化: {dismissed}")
83
- return True
84
- except Exception:
85
- pass
86
- # 2) 消化所有弹窗关卡:一轮拉全可见文案(单 RPC 扫描),本地匹配命中再点击
87
- hit_any = False
88
- vis = set(_visible_clickables(d))
89
- for t in all_texts:
90
- if t == (home_tab or "") or t not in vis:
91
- continue
92
- try:
93
- d.touch(BY.text(t))
94
- dismissed.append(t)
95
- hit_any = True
96
- time.sleep(1.2)
97
- break # 点一个后重新扫(弹窗可能叠加/消失后露下一个)
98
- except Exception:
99
- pass
100
- if hit_any:
101
- continue
102
- pts = _page_texts(d)
103
- # 3) 无弹窗特征:tab 归位(点目标 tab,锚验证)
104
- if home_tab and home_tab in pts:
105
- if anchor_key:
106
- try:
107
- if d.find_component(BY.key(anchor_key)) is not None:
108
- return True
109
- except Exception:
110
- pass
111
- try:
112
- d.touch(BY.text(home_tab))
113
- time.sleep(2.0)
114
- except Exception:
115
- pass
116
- time.sleep(0.9)
117
- # 超时:输出页面快照供分诊
118
- log(f" [L0] ✗ bootstrap 超时 {total}s;页面文本快照: {_page_texts(d)[:15]}")
119
- return False
120
-
121
-
122
- # ── Level 0 前置三件套(TD-007:冷启自适应,用例不得自带)────────────────
123
- # ① 首启弹窗 dismiss:多文案轮询(隐私/权限/引导),幂等无弹窗秒过
124
- FIRST_RUN_DISMISS_TEXTS = ["同意并继续", "同意", "允许", "仅在使用中允许", "我知道了", "知道了", "暂不", "以后再说"]
125
-
126
- def dismiss_first_run(d, rounds=3, gap=1.2):
127
- for _ in range(rounds):
128
- hit = False
129
- for t in FIRST_RUN_DISMISS_TEXTS:
130
- try:
131
- c = d.find_component(BY.text(t))
132
- if c is not None:
133
- d.touch(BY.text(t)); time.sleep(gap); hit = True; break
134
- except Exception:
135
- pass
136
- if not hit:
137
- return
138
-
139
- # ② 首页 tab 归位:应用默认落在非目标 tab(如广场)时点回目标 tab(HOME_TAB 应用级配置)
140
- def ensure_home_tab(d, tab_text, timeout=10):
141
- t0 = time.time()
142
- while time.time() - t0 < timeout:
143
- try:
144
- if d.find_component(BY.text(tab_text)) is not None:
145
- d.touch(BY.text(tab_text)); time.sleep(1.5); return True
146
- except Exception:
147
- pass
148
- time.sleep(0.8)
149
- return False
150
-
151
- # ③ 冷启三件套:launch 内自动执行——弹窗 dismiss + tab 归位(HOME_TAB 非空时)
152
- HOME_TAB = "" # 应用级:主功能 tab 文案(如 "玩安卓");空串跳过归位
153
-
154
-
155
- def launch(d, want="", reset_data=False, force=False):
156
- """v3-①②(EXEC-034):want=启动参数('k=v;k2=v2' → hdc aa start --ps 透传,启动参数门控应用免人工补丁);
157
- reset_data=True → bm clean -n 清应用数据(用例级状态重置,防跨用例泄漏);
158
- force=True → 强制冷启(断言冷启行为的用例用)。
159
- 热启动(run_direct 组内非首条置 FORGE_WARM_LAUNCH=1):HOME_ANCHOR 已在位即软复用
160
- 当前会话跳过 stop/start/bootstrap(省冷启 8-12s/例);锚不可见自动回退完整冷启。"""
161
- import os as _os, subprocess as _sp
162
- if not want and not reset_data and not force and _os.environ.get("FORGE_WARM_LAUNCH") == "1":
163
- anchor = globals().get("HOME_ANCHOR", "")
164
- if anchor:
165
- try:
166
- if d.find_component(BY.key(anchor)) is not None:
167
- return # 基座已就位:软复用(上一用例恰好停在本页)
168
- except Exception:
169
- pass
170
- d.stop_app(PACKAGE)
171
- time.sleep(0.8)
172
- if reset_data:
173
- hdc = _os.environ.get("FORGE_HDC", "hdc")
174
- dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
175
- try:
176
- # DEVIATION-3: bm clean 不带 -d 时仅清 cache(-c),持久化不清,用例间状态泄漏
177
- # (TC02 关 answer 残留致 TC03/08/09 连锁失败);本机 bm 无 -b 选项,-d 清数据
178
- _sp.run([hdc, "-t", dev, "shell", "bm", "clean", "-n", PACKAGE, "-d"],
179
- capture_output=True, timeout=15)
180
- time.sleep(0.5)
181
- except Exception as e:
182
- print(" [reset] bm clean 失败:", e)
183
- if want:
184
- hdc = _os.environ.get("FORGE_HDC", "hdc")
185
- dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
186
- args = ["aa", "start", "-b", PACKAGE, "-a", ABILITY]
187
- for kv in want.split(";"):
188
- if "=" in kv:
189
- k, v = kv.split("=", 1)
190
- # DEVIATION-1: aa start 的 --ps 只接受空格分隔 key value,等号形式报
191
- # "invalid number of parameters for option --ps" 导致带参启动全部失败
192
- args += ["--ps", k, v]
193
- # EXEC-038 修复:带参启动 3 轮重试 + 实测探活(FORGE_WANT_PROBE=key,可寻才算活)
194
- probe = _os.environ.get("FORGE_WANT_PROBE", "")
195
- for attempt in range(3):
196
- try:
197
- _sp.run([hdc, "-t", dev, "shell", "aa", "force-stop", PACKAGE], capture_output=True, timeout=10)
198
- time.sleep(0.8)
199
- _sp.run([hdc, "-t", dev, "shell"] + args, capture_output=True, timeout=15)
200
- time.sleep(3.0)
201
- if probe:
202
- c = None
203
- try:
204
- c = d.find_component(BY.key(probe))
205
- except Exception:
206
- pass
207
- if c is None:
208
- print(f" [want] 第{attempt+1}轮探活失败(key={probe}),重试")
209
- time.sleep(1.5)
210
- continue
211
- _dismiss_first_run(d)
212
- return
213
- except Exception as e:
214
- print(" [want] 带参启动异常,重试:", e)
215
- time.sleep(1.5)
216
- print(" [want] 3 轮未探活成功,继续交由断言暴露")
217
- d.start_app(PACKAGE, ABILITY)
218
- ok = bootstrap(d, home_tab=HOME_TAB, anchor_key=HOME_ANCHOR)
219
- if not ok:
220
- raise AssertionError("L0 bootstrap failed(弹窗/tab 未消化,见日志快照)")
221
- time.sleep(1.5) # bootstrap 锚已验证就位,1.5s 落座余量(原 2.5s)
222
- _dismiss_first_run(d)
223
-
224
-
225
- def _dismiss_first_run(d):
226
- """首启弹窗一键处置(权限/隐私协议)——单 RPC 拉全文案本地匹配"""
227
- vis = set(_visible_clickables(d))
228
- for txt in ["同意并继续", "允许", "我知道了", "暂不", "以后再说", "同意"]:
229
- if txt not in vis:
230
- continue
231
- try:
232
- c = d.find_component(BY.text(txt))
233
- if c:
234
- c.click()
235
- time.sleep(0.8)
236
- vis = set(_visible_clickables(d)) # 点掉一层后重扫(可能叠弹窗)
237
- except Exception:
238
- pass
239
-
240
-
241
- def stop(d):
242
- d.stop_app(PACKAGE)
243
-
244
-
245
- def nav_to(d, page):
246
- """导航到目标页:入口页直接用;非入口页用返回键回根后再走用例自带链(由用例 steps 驱动)"""
247
- if page == ENTRY_PAGE:
248
- return
249
- # 返回根页(最多 5 次)
250
- for _ in range(5):
251
- try:
252
- if d.find_component(BY.key("__never__")) is None:
253
- pass
254
- except Exception:
255
- pass
256
- d.press_back()
257
- time.sleep(0.4)
258
-
259
-
260
- TAB_LABELS = ["首页", "视频", "我的"]
261
-
262
-
263
- def click_tab(d, label):
264
- """底部 Tab 切换:定位文本→bounds 中心 touch(比例坐标兜底)"""
265
- import time as _t
266
- for _ in range(8):
267
- try:
268
- comps = d.find_all_components(BY.text(label))
269
- if comps:
270
- c = comps[-1] # 底部 tab 是同名文本中最靠下的
271
- b = c.getBounds()
272
- cx = (b.left + b.right) // 2
273
- cy = (b.top + b.bottom) // 2
274
- d.touch((int(cx), int(cy)))
275
- _t.sleep(1.2)
276
- return
277
- except Exception:
278
- pass
279
- _t.sleep(0.25)
280
- idx = TAB_LABELS.index(label) if label in TAB_LABELS else 2
281
- w, h = d.get_display_size()
282
- d.touch((int(w * (idx + 0.5) / len(TAB_LABELS)), int(h * 0.945)))
283
- time.sleep(1.2)
284
-
285
-
286
- def click_text(d, txt, wait=6):
287
- """按可见文本点击(tab 名/中文按钮等无 ID 控件)。
288
- 先试 BY.key——无下划线的单词 id(如 fab)无法与文本区分(_is_id_like 局限),此处兜底。"""
289
- import time as _t
290
- try:
291
- c = d.find_component(BY.key(txt))
292
- if c:
293
- c.click()
294
- _t.sleep(1.0)
295
- return
296
- except Exception:
297
- pass
298
- deadline = _t.time() + _w(wait)
299
- while _t.time() < deadline:
300
- try:
301
- c = d.find_component(BY.text(txt, MatchPattern.CONTAINS))
302
- if c:
303
- c.click()
304
- _t.sleep(1.0)
305
- return
306
- except Exception:
307
- pass
308
- _t.sleep(0.25)
309
- raise AssertionError("text not clickable: " + txt)
310
-
311
-
312
- def click_type_first(d, ctype, wait=6):
313
- """点击指定类型的第一个组件(lineN:Type 引用的兜底点击)"""
314
- import time as _t
315
- deadline = _t.time() + _w(wait)
316
- while _t.time() < deadline:
317
- try:
318
- comps = d.find_all_components(BY.type(ctype))
319
- if comps:
320
- b = comps[0].getBounds()
321
- d.touch(((b.left + b.right) // 2, (b.top + b.bottom) // 2))
322
- _t.sleep(1.2)
323
- return
324
- except Exception:
325
- pass
326
- _t.sleep(0.25)
327
- raise AssertionError("type not clickable: " + ctype)
328
-
329
-
330
- def id_click(d, cid, wait=6):
331
- comp = _find(d, cid, wait)
332
- comp.click()
333
- time.sleep(1.0)
334
-
335
-
336
- def id_input(d, cid, text, wait=6):
337
- comp = _find(d, cid, wait)
338
- d.input_text(comp, text)
339
- time.sleep(0.6)
340
-
341
-
342
- def _find(d, cid, wait=6):
343
- import time as _t
344
- deadline = _t.time() + _w(wait)
345
- while _t.time() < deadline:
346
- try:
347
- c = d.find_component(BY.key(cid))
348
- if c:
349
- return c
350
- except Exception:
351
- pass
352
- try:
353
- c = d.find_component(BY.id(cid))
354
- if c:
355
- return c
356
- except Exception:
357
- pass
358
- _t.sleep(0.25)
359
- raise AssertionError("component not found: " + cid)
360
-
361
-
362
- def smart_click(d, cid, txt=None, wait=6):
363
- """key 优先点击;失败且有 txt 时文本点击(mine 类 HAR 模块 ID 丢失边界)"""
364
- import time as _t
365
- deadline = _t.time() + _w(wait)
366
- while _t.time() < deadline:
367
- try:
368
- c = d.find_component(BY.key(cid))
369
- if c:
370
- c.click()
371
- _t.sleep(1.0)
372
- return
373
- except Exception:
374
- pass
375
- _t.sleep(0.25)
376
- if txt and txt.startswith("line"):
377
- import re as _re
378
- m = _re.match(r"line\d+:(\w+)", txt)
379
- if m:
380
- click_type_first(d, m.group(1), wait)
381
- return
382
- if txt:
383
- click_text(d, txt, wait)
384
- return
385
- raise AssertionError("component not found: " + cid)
386
-
387
-
388
- def scroll_text_exist(d, txt, wait=6):
389
- """滚动查找文本,双向(EXEC-005):上滑 N 次找 → 未中则下滑回落 N 次找。
390
- 单向滚动在连续断言场景必挂:上一断言滑到底后,本断言目标在上方,单向永远回不去。"""
391
- import time as _t
392
- w, h = d.get_display_size()
393
- def _check():
394
- try:
395
- return d.find_component(BY.text(txt, MatchPattern.CONTAINS)) is not None
396
- except Exception:
397
- return False
398
- for direction in (UiParam.UP, UiParam.DOWN):
399
- for i in range(6):
400
- if _check():
401
- return True
402
- d.swipe(direction, distance=40)
403
- _t.sleep(1.0)
404
- return text_exist(d, txt, wait)
405
-
406
-
407
- def smart_check(d, cid, txt=None, wait=6):
408
- """key 优先断言;失败且有 txt 时降级文本断言(部分 HAR 模块 .id 运行时丢弃的边界)"""
409
- import time as _t
410
- deadline = _t.time() + _w(wait)
411
- while _t.time() < deadline:
412
- try:
413
- c = d.find_component(BY.key(cid))
414
- if c:
415
- return True
416
- except Exception:
417
- pass
418
- _t.sleep(0.25)
419
- if txt and txt.startswith("line"):
420
- import re as _re
421
- m = _re.match(r"line\d+:(\w+)", txt)
422
- if m:
423
- d.check_component_exist(BY.type(m.group(1)), wait_time=wait)
424
- return True
425
- if txt:
426
- return text_exist(d, txt, wait)
427
- raise AssertionError("component not found: " + cid)
428
-
429
-
430
- def text_equals(d, txt, wait=6):
431
- """精确文本断言(EQUALS)——CONTAINS 会被前缀污染(如 ❄️Gallery 含 Gallery),
432
- 恢复/状态标记类断言必须用本原语。存在且精确相等返回 True,否则抛。"""
433
- import time as _t
434
- deadline = _t.time() + _w(wait)
435
- while _t.time() < deadline:
436
- try:
437
- c = d.find_component(BY.text(txt))
438
- if c:
439
- return True
440
- except Exception:
441
- pass
442
- _t.sleep(0.25)
443
- raise AssertionError("text (exact) not found: " + txt)
444
-
445
-
446
- def text_absent(d, txt, wait=3):
447
- """文本不存在断言(反向;等待短暂稳定后确认消失)"""
448
- import time as _t
449
- _t.sleep(1.0)
450
- try:
451
- c = d.find_component(BY.text(txt, MatchPattern.CONTAINS))
452
- if c:
453
- raise AssertionError("text should be absent but found: " + txt)
454
- except AssertionError:
455
- raise
456
- except Exception:
457
- pass
458
- return True
459
-
460
-
461
- def step_shot(d, label="step"):
462
- """分步截图:一个动作/断言一张页面图(run_direct 设 FORGE_SHOT_DIR/FORGE_CASE 时启用)。
463
- 文件名 <case>_<序号>_<label>.jpeg,供 test-report.html 按步回放页面。
464
- FORGE_SHOT_MODE=off 时跳过;fail 模式下 PASS 用例的过程图由 run_direct 统一清理。"""
465
- import os as _os
466
- if _os.environ.get("FORGE_SHOT_MODE") == "off":
467
- return
468
- base = _os.environ.get("FORGE_SHOT_DIR")
469
- case = _os.environ.get("FORGE_CASE", "case")
470
- if not base:
471
- return
472
- n = step_shot.n.get(case, 0) + 1
473
- try:
474
- d.screenshot(_os.path.join(base, "%s_%02d_%s.jpeg" % (case, n, label)))
475
- step_shot.n[case] = n
476
- except Exception:
477
- pass
478
-
479
-
480
- step_shot.n = {}
481
-
482
-
483
- def wait_for(d, by, timeout=10, interval=0.5):
484
- """轮询等待控件出现(首屏锚点等待用);返回组件或 None,不抛异常"""
485
- import time as _t
486
- end = _t.time() + _w(timeout)
487
- while _t.time() < end:
488
- try:
489
- c = d.find_component(by)
490
- if c:
491
- return c
492
- except Exception:
493
- pass
494
- _t.sleep(interval)
495
- return None
496
-
497
-
498
- def text_exist(d, txt, wait=6):
499
- """文本存在断言(BY.text CONTAINS)。
500
- 契约:存在返回 True;不存在【抛 AssertionError】——永不返回 False。
501
- run_direct/analyze_results 依赖异常文本做 FAIL 判定与失败聚类,勿改为静默返回。"""
502
- import time as _t
503
- deadline = _t.time() + _w(wait)
504
- while _t.time() < deadline:
505
- try:
506
- c = d.find_component(BY.text(txt, MatchPattern.CONTAINS))
507
- if c:
508
- return True
509
- except Exception:
510
- pass
511
- _t.sleep(0.25)
512
- raise AssertionError("text not found: " + txt)
513
-
514
-
515
- def text_value_equals(d, cid, expected, wait=6):
516
- """值断言(EXEC-040 实证+反证):getText() 精确等于期望——锚定 key 控件动态内容。"""
517
- import time as _t
518
- deadline = _t.time() + _w(wait)
519
- while _t.time() < deadline:
520
- try:
521
- c = d.find_component(BY.key(cid))
522
- if c and c.getText() == expected:
523
- return True
524
- except Exception:
525
- pass
526
- _t.sleep(0.25)
527
- got = None
528
- try:
529
- c = d.find_component(BY.key(cid))
530
- got = c.getText() if c else '(not found)'
531
- except Exception:
532
- pass
533
- raise AssertionError(f"text value mismatch: expect {expected!r} got {got!r} (key={cid})")
534
-
535
-
536
- def text_value_contains(d, cid, part, wait=6):
537
- """值包含断言:getText() 包含子串(动态文案前缀不定时用)。"""
538
- import time as _t
539
- deadline = _t.time() + _w(wait)
540
- while _t.time() < deadline:
541
- try:
542
- c = d.find_component(BY.key(cid))
543
- if c and part in (c.getText() or ''):
544
- return True
545
- except Exception:
546
- pass
547
- _t.sleep(0.25)
548
- raise AssertionError(f"text value not containing {part!r} (key={cid})")
549
-
550
-
551
- def count_equals(d, target, expected, wait=6):
552
- """计数断言(EXEC-040 实证):key 或 CONTAINS 文本匹配的组件数==期望。"""
553
- import time as _t, re as _re
554
- deadline = _t.time() + _w(wait)
555
- last = -1
556
- while _t.time() < deadline:
557
- try:
558
- if _re.match(r'^[a-z][a-z0-9_]+$', target):
559
- comps = d.find_all_components(BY.key(target))
560
- else:
561
- comps = d.find_all_components(BY.text(target, MatchPattern.CONTAINS))
562
- last = len(comps) if comps else 0
563
- if last == expected:
564
- return True
565
- except Exception:
566
- pass
567
- _t.sleep(0.25)
568
- raise AssertionError(f"count mismatch: expect {expected} got {last} (target={target})")
569
-
570
-
571
- def is_below(d, cid, ref_cid, wait=6):
572
- """几何断言(EXEC-040 实证):cid 的 top >= ref_cid 的 bottom(垂直次序)。"""
573
- import time as _t
574
- deadline = _t.time() + _w(wait)
575
- while _t.time() < deadline:
576
- try:
577
- a = d.find_component(BY.key(cid)); b = d.find_component(BY.key(ref_cid))
578
- if a and b and a.getBounds().top >= b.getBounds().bottom:
579
- return True
580
- except Exception:
581
- pass
582
- _t.sleep(0.25)
583
- raise AssertionError(f"geometry: {cid} not below {ref_cid}")
584
-
585
-
586
- def toast_exist(d, txt, wait=4, poll=0.5):
587
- """Toast 断言(v3.1):轮询 hilog 捕获 Toast 文本。需 FORGE_HDC/FORGE_DEVICE。"""
588
- import os as _os, subprocess as _sp, time as _tt
589
- hdc = _os.environ.get("FORGE_HDC", "hdc"); dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
590
- deadline = _tt.time() + _w(wait)
591
- while _tt.time() < deadline:
592
- try:
593
- r = _sp.run([hdc, "-t", dev, "shell", "hilog", "-x"],
594
- capture_output=True, timeout=8, text=True, errors="ignore")
595
- if txt in (r.stdout or ""):
596
- return True
597
- except Exception:
598
- pass
599
- _tt.sleep(poll)
600
- raise AssertionError("toast not found in hilog: " + txt)
601
-
602
-
603
- # ── 强断言原语(2026-09-29 吸收自 none 臂实证:无 skill 的 LLM 凭通用能力即达成 34% 强断言,
604
- # B 线无理由低于此——Toast/系统事件/数据断言按下表选型,禁止只写 exist)──
605
- def _hdc(args, timeout=20):
606
- import subprocess as _sp, os as _os
607
- dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
608
- cmd = [HDC_BIN, "-t", dev] + args
609
- r = _sp.run(cmd, capture_output=True, timeout=timeout)
610
- return (r.stdout + r.stderr).decode("utf-8", "ignore")
611
-
612
-
613
- def _dump_layout(timeout=20):
614
- """取当前全量布局 JSON(含系统对话框/浮层),返回原始文本(Round-2:确保本地 tmp 目录存在)"""
615
- import json as _json, os as _os
616
- _os.makedirs("./tmp", exist_ok=True)
617
- _hdc(["shell", "uitest", "dumpLayout", "-p", "/data/local/tmp/forge_dump.json"], timeout=timeout)
618
- _hdc(["file", "recv", "/data/local/tmp/forge_dump.json", "./tmp/forge_dump.json"], timeout=timeout)
619
- try:
620
- return open("./tmp/forge_dump.json", encoding="utf-8").read()
621
- except Exception:
622
- return ""
623
-
624
-
625
- def hilog_clear():
626
- """清 hilog 缓冲,防止上一用例残留关键字造成假 PASS(tag=RaiseToAnswer)"""
627
- _hdc(["shell", "hilog", "-r"])
628
- time.sleep(0.3)
629
-
630
-
631
- HILOG_TAG = 'JSAPP'
632
-
633
-
634
- def hilog_tail(tag=None, n=400):
635
- tag = tag or HILOG_TAG
636
- """读取 tag 过滤的最近 n 行 hilog(诊断用)"""
637
- import subprocess as _sp, os as _os
638
- dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
639
- r = _sp.run([HDC_BIN, "-t", dev, "shell", "hilog", "-z", str(n), "-T", tag],
640
- capture_output=True, timeout=15)
641
- return r.stdout.decode("utf-8", "ignore")
642
-
643
-
644
- def toast_text_seen(d, txt, timeout=6):
645
- """Toast 文本断言(dump 版)。无 ID 例外:Toast 为瞬时浮层无稳定 ID,且 hypium find_component
646
- 实测看不到 Toast 子窗口,故以 uitest dumpLayout 原文轮询(首轮实测教训 Round-1)。"""
647
- deadline = time.time() + _w(timeout)
648
- while time.time() < deadline:
649
- raw = _dump_layout()
650
- if txt in raw:
651
- return True
652
- time.sleep(0.6)
653
- raise AssertionError("toast not seen: " + txt)
654
-
655
-
656
- def bm_clean():
657
- """清应用持久化数据(公开包装;import * 不导出 _ 前缀——Round-1 教训)"""
658
- _hdc(["shell", "bm", "clean", "-n", PACKAGE, "-d"])
659
- time.sleep(0.6)
1
+ # coding: utf-8
2
+ """d2h-hypium-forge 共享库(自动生成)
3
+
4
+ 前置条件分组编排约定(TD-004):
5
+ 需要登录态的用例文件顶部声明 PRECOND = 'logged_in'
6
+ 首启态(bm clean)用例声明 PRECOND = 'first_launch'
7
+ 缺省 PRECOND = 'anonymous'
8
+ run_direct 按 anonymous→logged_in→first_launch 分组排序(破坏性递增,clean 类排尾),
9
+ 组内软隔离(launch() 默认 reset_data=False,preferences 保留→登录态保持)。
10
+ ensure_login 等 setup 必须【幂等】:先探测状态(抽屉用户名已就位)再决定是否走完整
11
+ UI 登录——组首一次完整 setup,组内后续直接复用(省重复登录/注册)。
12
+ 组首条执行时环境变量 FORGE_GROUP_FIRST=1(可选做完整冷启验证)。
13
+ """
14
+ import os as _os
15
+ import time
16
+ from hypium import *
17
+ from hypium.model import MatchPattern, UiParam
18
+
19
+ try:
20
+ WAIT_SCALE = float(_os.environ.get('FORGE_WAIT_SCALE', '1') or 1)
21
+ except ValueError:
22
+ WAIT_SCALE = 1.0
23
+
24
+
25
+ def _w(sec):
26
+ """等待时长缩放(env FORGE_WAIT_SCALE,默认 1 不变):调试迭代期设 0.5
27
+ 可把失败路径的超时烧穿时间减半(Grit 实证 FAIL 用例 17-36s 多为等待超时);
28
+ 正式回归/出报告必须保持 1,防止慢设备真问题被截短掩盖。"""
29
+ return max(0.5, sec * WAIT_SCALE)
30
+
31
+ PACKAGE = "me.hackerchick.raisetoanswer"
32
+ ABILITY = "EntryAbility"
33
+ ENTRY_PAGE = "pages/Index"
34
+
35
+
36
+ # ── Level 0 统一引导引擎(TD-008):权限/隐私/广告/引导弹窗 + tab 归位,一揽子消化 ──
37
+ # 关卡模型:每关 = 探测特征文案集合 → 处置(点击命中项)→ 循环消化到无特征。
38
+ # app 级配置 GATES(按需增删);bootstrap 幂等(主页锚可见即成功),超时输出页面快照供分诊。
39
+
40
+ GATES = [
41
+ {"name": "privacy", "texts": ["同意并继续", "同意", "知道了", "我知道了"]},
42
+ {"name": "permission", "texts": ["仅在使用中允许", "使用应用时允许", "允许"]},
43
+ {"name": "ad", "texts": ["跳过", "关闭广告", "不再显示", "以后再说", "暂不"]},
44
+ ]
45
+
46
+ def _page_texts(d):
47
+ try:
48
+ return [(c.getText() or '') for c in (d.find_all_components(BY.type("Text")) or [])
49
+ if c.getText()]
50
+ except Exception:
51
+ return []
52
+
53
+
54
+ def _visible_clickables(d):
55
+ """一次拉全 Text+Button 可见文案集合(弹窗消化轮询的单 RPC 扫描,
56
+ 替代逐文案 find_component 的 12+ RPC/轮——冷启消化从 ~3s/轮 降到 ~0.6s/轮)。"""
57
+ out = []
58
+ try:
59
+ for by in (BY.type("Text"), BY.type("Button")):
60
+ for c in (d.find_all_components(by) or []):
61
+ t = c.getText() or ""
62
+ if t:
63
+ out.append(t)
64
+ except Exception:
65
+ pass
66
+ return out
67
+
68
+ def bootstrap(d, gates=None, home_tab="", anchor_key="", total=60, log=print):
69
+ """冷启到可用基座。流程:轮询消化全部弹窗关卡(一轮多点,直到无特征)→
70
+ tab 归位(点目标 tab + 验证锚)→ 主页锚可见返回 True。
71
+ anchor_key 是"基座就绪"的唯一判据(app 级配置,如目标 tab 的内容控件 id)。"""
72
+ gates = gates if gates is not None else GATES
73
+ all_texts = [t for g in gates for t in g["texts"]]
74
+ t0 = time.time()
75
+ dismissed = []
76
+ while time.time() - t0 < total:
77
+ # 1) 主页锚已可见 → 提前成功(幂等)
78
+ if anchor_key:
79
+ try:
80
+ if d.find_component(BY.key(anchor_key)) is not None:
81
+ if dismissed:
82
+ log(f" [L0] 弹窗消化: {dismissed}")
83
+ return True
84
+ except Exception:
85
+ pass
86
+ # 2) 消化所有弹窗关卡:一轮拉全可见文案(单 RPC 扫描),本地匹配命中再点击
87
+ hit_any = False
88
+ vis = set(_visible_clickables(d))
89
+ for t in all_texts:
90
+ if t == (home_tab or "") or t not in vis:
91
+ continue
92
+ try:
93
+ d.touch(BY.text(t))
94
+ dismissed.append(t)
95
+ hit_any = True
96
+ time.sleep(1.2)
97
+ break # 点一个后重新扫(弹窗可能叠加/消失后露下一个)
98
+ except Exception:
99
+ pass
100
+ if hit_any:
101
+ continue
102
+ pts = _page_texts(d)
103
+ # 3) 无弹窗特征:tab 归位(点目标 tab,锚验证)
104
+ if home_tab and home_tab in pts:
105
+ if anchor_key:
106
+ try:
107
+ if d.find_component(BY.key(anchor_key)) is not None:
108
+ return True
109
+ except Exception:
110
+ pass
111
+ try:
112
+ d.touch(BY.text(home_tab))
113
+ time.sleep(2.0)
114
+ except Exception:
115
+ pass
116
+ time.sleep(0.9)
117
+ # 超时:输出页面快照供分诊
118
+ log(f" [L0] ✗ bootstrap 超时 {total}s;页面文本快照: {_page_texts(d)[:15]}")
119
+ return False
120
+
121
+
122
+ # ── Level 0 前置三件套(TD-007:冷启自适应,用例不得自带)────────────────
123
+ # ① 首启弹窗 dismiss:多文案轮询(隐私/权限/引导),幂等无弹窗秒过
124
+ FIRST_RUN_DISMISS_TEXTS = ["同意并继续", "同意", "允许", "仅在使用中允许", "我知道了", "知道了", "暂不", "以后再说"]
125
+
126
+ def dismiss_first_run(d, rounds=3, gap=1.2):
127
+ for _ in range(rounds):
128
+ hit = False
129
+ for t in FIRST_RUN_DISMISS_TEXTS:
130
+ try:
131
+ c = d.find_component(BY.text(t))
132
+ if c is not None:
133
+ d.touch(BY.text(t)); time.sleep(gap); hit = True; break
134
+ except Exception:
135
+ pass
136
+ if not hit:
137
+ return
138
+
139
+ # ② 首页 tab 归位:应用默认落在非目标 tab(如广场)时点回目标 tab(HOME_TAB 应用级配置)
140
+ def ensure_home_tab(d, tab_text, timeout=10):
141
+ t0 = time.time()
142
+ while time.time() - t0 < timeout:
143
+ try:
144
+ if d.find_component(BY.text(tab_text)) is not None:
145
+ d.touch(BY.text(tab_text)); time.sleep(1.5); return True
146
+ except Exception:
147
+ pass
148
+ time.sleep(0.8)
149
+ return False
150
+
151
+ # ③ 冷启三件套:launch 内自动执行——弹窗 dismiss + tab 归位(HOME_TAB 非空时)
152
+ HOME_TAB = "" # 应用级:主功能 tab 文案(如 "玩安卓");空串跳过归位
153
+
154
+
155
+ def launch(d, want="", reset_data=False, force=False):
156
+ """v3-①②(EXEC-034):want=启动参数('k=v;k2=v2' → hdc aa start --ps 透传,启动参数门控应用免人工补丁);
157
+ reset_data=True → bm clean -n 清应用数据(用例级状态重置,防跨用例泄漏);
158
+ force=True → 强制冷启(断言冷启行为的用例用)。
159
+ 热启动(run_direct 组内非首条置 FORGE_WARM_LAUNCH=1):HOME_ANCHOR 已在位即软复用
160
+ 当前会话跳过 stop/start/bootstrap(省冷启 8-12s/例);锚不可见自动回退完整冷启。"""
161
+ import os as _os, subprocess as _sp
162
+ if not want and not reset_data and not force and _os.environ.get("FORGE_WARM_LAUNCH") == "1":
163
+ anchor = globals().get("HOME_ANCHOR", "")
164
+ if anchor:
165
+ try:
166
+ if d.find_component(BY.key(anchor)) is not None:
167
+ return # 基座已就位:软复用(上一用例恰好停在本页)
168
+ except Exception:
169
+ pass
170
+ d.stop_app(PACKAGE)
171
+ time.sleep(0.8)
172
+ if reset_data:
173
+ hdc = _os.environ.get("FORGE_HDC", "hdc")
174
+ dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
175
+ try:
176
+ # DEVIATION-3: bm clean 不带 -d 时仅清 cache(-c),持久化不清,用例间状态泄漏
177
+ # (TC02 关 answer 残留致 TC03/08/09 连锁失败);本机 bm 无 -b 选项,-d 清数据
178
+ _sp.run([hdc, "-t", dev, "shell", "bm", "clean", "-n", PACKAGE, "-d"],
179
+ capture_output=True, timeout=15)
180
+ time.sleep(0.5)
181
+ except Exception as e:
182
+ print(" [reset] bm clean 失败:", e)
183
+ if want:
184
+ hdc = _os.environ.get("FORGE_HDC", "hdc")
185
+ dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
186
+ args = ["aa", "start", "-b", PACKAGE, "-a", ABILITY]
187
+ for kv in want.split(";"):
188
+ if "=" in kv:
189
+ k, v = kv.split("=", 1)
190
+ # DEVIATION-1: aa start 的 --ps 只接受空格分隔 key value,等号形式报
191
+ # "invalid number of parameters for option --ps" 导致带参启动全部失败
192
+ args += ["--ps", k, v]
193
+ # EXEC-038 修复:带参启动 3 轮重试 + 实测探活(FORGE_WANT_PROBE=key,可寻才算活)
194
+ probe = _os.environ.get("FORGE_WANT_PROBE", "")
195
+ for attempt in range(3):
196
+ try:
197
+ _sp.run([hdc, "-t", dev, "shell", "aa", "force-stop", PACKAGE], capture_output=True, timeout=10)
198
+ time.sleep(0.8)
199
+ _sp.run([hdc, "-t", dev, "shell"] + args, capture_output=True, timeout=15)
200
+ time.sleep(3.0)
201
+ if probe:
202
+ c = None
203
+ try:
204
+ c = d.find_component(BY.key(probe))
205
+ except Exception:
206
+ pass
207
+ if c is None:
208
+ print(f" [want] 第{attempt+1}轮探活失败(key={probe}),重试")
209
+ time.sleep(1.5)
210
+ continue
211
+ _dismiss_first_run(d)
212
+ return
213
+ except Exception as e:
214
+ print(" [want] 带参启动异常,重试:", e)
215
+ time.sleep(1.5)
216
+ print(" [want] 3 轮未探活成功,继续交由断言暴露")
217
+ d.start_app(PACKAGE, ABILITY)
218
+ ok = bootstrap(d, home_tab=HOME_TAB, anchor_key=HOME_ANCHOR)
219
+ if not ok:
220
+ raise AssertionError("L0 bootstrap failed(弹窗/tab 未消化,见日志快照)")
221
+ time.sleep(1.5) # bootstrap 锚已验证就位,1.5s 落座余量(原 2.5s)
222
+ _dismiss_first_run(d)
223
+
224
+
225
+ def _dismiss_first_run(d):
226
+ """首启弹窗一键处置(权限/隐私协议)——单 RPC 拉全文案本地匹配"""
227
+ vis = set(_visible_clickables(d))
228
+ for txt in ["同意并继续", "允许", "我知道了", "暂不", "以后再说", "同意"]:
229
+ if txt not in vis:
230
+ continue
231
+ try:
232
+ c = d.find_component(BY.text(txt))
233
+ if c:
234
+ c.click()
235
+ time.sleep(0.8)
236
+ vis = set(_visible_clickables(d)) # 点掉一层后重扫(可能叠弹窗)
237
+ except Exception:
238
+ pass
239
+
240
+
241
+ def stop(d):
242
+ d.stop_app(PACKAGE)
243
+
244
+
245
+ def nav_to(d, page):
246
+ """导航到目标页:入口页直接用;非入口页用返回键回根后再走用例自带链(由用例 steps 驱动)"""
247
+ if page == ENTRY_PAGE:
248
+ return
249
+ # 返回根页(最多 5 次)
250
+ for _ in range(5):
251
+ try:
252
+ if d.find_component(BY.key("__never__")) is None:
253
+ pass
254
+ except Exception:
255
+ pass
256
+ d.press_back()
257
+ time.sleep(0.4)
258
+
259
+
260
+ TAB_LABELS = ["首页", "视频", "我的"]
261
+
262
+
263
+ def click_tab(d, label):
264
+ """底部 Tab 切换:定位文本→bounds 中心 touch(比例坐标兜底)"""
265
+ import time as _t
266
+ for _ in range(8):
267
+ try:
268
+ comps = d.find_all_components(BY.text(label))
269
+ if comps:
270
+ c = comps[-1] # 底部 tab 是同名文本中最靠下的
271
+ b = c.getBounds()
272
+ cx = (b.left + b.right) // 2
273
+ cy = (b.top + b.bottom) // 2
274
+ d.touch((int(cx), int(cy)))
275
+ _t.sleep(1.2)
276
+ return
277
+ except Exception:
278
+ pass
279
+ _t.sleep(0.25)
280
+ idx = TAB_LABELS.index(label) if label in TAB_LABELS else 2
281
+ w, h = d.get_display_size()
282
+ d.touch((int(w * (idx + 0.5) / len(TAB_LABELS)), int(h * 0.945)))
283
+ time.sleep(1.2)
284
+
285
+
286
+ def click_text(d, txt, wait=6):
287
+ """按可见文本点击(tab 名/中文按钮等无 ID 控件)。
288
+ 先试 BY.key——无下划线的单词 id(如 fab)无法与文本区分(_is_id_like 局限),此处兜底。"""
289
+ import time as _t
290
+ try:
291
+ c = d.find_component(BY.key(txt))
292
+ if c:
293
+ c.click()
294
+ _t.sleep(1.0)
295
+ return
296
+ except Exception:
297
+ pass
298
+ deadline = _t.time() + _w(wait)
299
+ while _t.time() < deadline:
300
+ try:
301
+ c = d.find_component(BY.text(txt, MatchPattern.CONTAINS))
302
+ if c:
303
+ c.click()
304
+ _t.sleep(1.0)
305
+ return
306
+ except Exception:
307
+ pass
308
+ _t.sleep(0.25)
309
+ raise AssertionError("text not clickable: " + txt)
310
+
311
+
312
+ def click_type_first(d, ctype, wait=6):
313
+ """点击指定类型的第一个组件(lineN:Type 引用的兜底点击)"""
314
+ import time as _t
315
+ deadline = _t.time() + _w(wait)
316
+ while _t.time() < deadline:
317
+ try:
318
+ comps = d.find_all_components(BY.type(ctype))
319
+ if comps:
320
+ b = comps[0].getBounds()
321
+ d.touch(((b.left + b.right) // 2, (b.top + b.bottom) // 2))
322
+ _t.sleep(1.2)
323
+ return
324
+ except Exception:
325
+ pass
326
+ _t.sleep(0.25)
327
+ raise AssertionError("type not clickable: " + ctype)
328
+
329
+
330
+ def id_click(d, cid, wait=6):
331
+ comp = _find(d, cid, wait)
332
+ comp.click()
333
+ time.sleep(1.0)
334
+
335
+
336
+ def id_input(d, cid, text, wait=6):
337
+ comp = _find(d, cid, wait)
338
+ d.input_text(comp, text)
339
+ time.sleep(0.6)
340
+
341
+
342
+ def _find(d, cid, wait=6):
343
+ import time as _t
344
+ deadline = _t.time() + _w(wait)
345
+ while _t.time() < deadline:
346
+ try:
347
+ c = d.find_component(BY.key(cid))
348
+ if c:
349
+ return c
350
+ except Exception:
351
+ pass
352
+ try:
353
+ c = d.find_component(BY.id(cid))
354
+ if c:
355
+ return c
356
+ except Exception:
357
+ pass
358
+ _t.sleep(0.25)
359
+ raise AssertionError("component not found: " + cid)
360
+
361
+
362
+ def smart_click(d, cid, txt=None, wait=6):
363
+ """key 优先点击;失败且有 txt 时文本点击(mine 类 HAR 模块 ID 丢失边界)"""
364
+ import time as _t
365
+ deadline = _t.time() + _w(wait)
366
+ while _t.time() < deadline:
367
+ try:
368
+ c = d.find_component(BY.key(cid))
369
+ if c:
370
+ c.click()
371
+ _t.sleep(1.0)
372
+ return
373
+ except Exception:
374
+ pass
375
+ _t.sleep(0.25)
376
+ if txt and txt.startswith("line"):
377
+ import re as _re
378
+ m = _re.match(r"line\d+:(\w+)", txt)
379
+ if m:
380
+ click_type_first(d, m.group(1), wait)
381
+ return
382
+ if txt:
383
+ click_text(d, txt, wait)
384
+ return
385
+ raise AssertionError("component not found: " + cid)
386
+
387
+
388
+ def scroll_text_exist(d, txt, wait=6):
389
+ """滚动查找文本,双向(EXEC-005):上滑 N 次找 → 未中则下滑回落 N 次找。
390
+ 单向滚动在连续断言场景必挂:上一断言滑到底后,本断言目标在上方,单向永远回不去。"""
391
+ import time as _t
392
+ w, h = d.get_display_size()
393
+ def _check():
394
+ try:
395
+ return d.find_component(BY.text(txt, MatchPattern.CONTAINS)) is not None
396
+ except Exception:
397
+ return False
398
+ for direction in (UiParam.UP, UiParam.DOWN):
399
+ for i in range(6):
400
+ if _check():
401
+ return True
402
+ d.swipe(direction, distance=40)
403
+ _t.sleep(1.0)
404
+ return text_exist(d, txt, wait)
405
+
406
+
407
+ def smart_check(d, cid, txt=None, wait=6):
408
+ """key 优先断言;失败且有 txt 时降级文本断言(部分 HAR 模块 .id 运行时丢弃的边界)"""
409
+ import time as _t
410
+ deadline = _t.time() + _w(wait)
411
+ while _t.time() < deadline:
412
+ try:
413
+ c = d.find_component(BY.key(cid))
414
+ if c:
415
+ return True
416
+ except Exception:
417
+ pass
418
+ _t.sleep(0.25)
419
+ if txt and txt.startswith("line"):
420
+ import re as _re
421
+ m = _re.match(r"line\d+:(\w+)", txt)
422
+ if m:
423
+ d.check_component_exist(BY.type(m.group(1)), wait_time=wait)
424
+ return True
425
+ if txt:
426
+ return text_exist(d, txt, wait)
427
+ raise AssertionError("component not found: " + cid)
428
+
429
+
430
+ def text_equals(d, txt, wait=6):
431
+ """精确文本断言(EQUALS)——CONTAINS 会被前缀污染(如 ❄️Gallery 含 Gallery),
432
+ 恢复/状态标记类断言必须用本原语。存在且精确相等返回 True,否则抛。"""
433
+ import time as _t
434
+ deadline = _t.time() + _w(wait)
435
+ while _t.time() < deadline:
436
+ try:
437
+ c = d.find_component(BY.text(txt))
438
+ if c:
439
+ return True
440
+ except Exception:
441
+ pass
442
+ _t.sleep(0.25)
443
+ raise AssertionError("text (exact) not found: " + txt)
444
+
445
+
446
+ def text_absent(d, txt, wait=3):
447
+ """文本不存在断言(反向;等待短暂稳定后确认消失)"""
448
+ import time as _t
449
+ _t.sleep(1.0)
450
+ try:
451
+ c = d.find_component(BY.text(txt, MatchPattern.CONTAINS))
452
+ if c:
453
+ raise AssertionError("text should be absent but found: " + txt)
454
+ except AssertionError:
455
+ raise
456
+ except Exception:
457
+ pass
458
+ return True
459
+
460
+
461
+ def step_shot(d, label="step"):
462
+ """分步截图:一个动作/断言一张页面图(run_direct 设 FORGE_SHOT_DIR/FORGE_CASE 时启用)。
463
+ 文件名 <case>_<序号>_<label>.jpeg,供 test-report.html 按步回放页面。
464
+ FORGE_SHOT_MODE=off 时跳过;fail 模式下 PASS 用例的过程图由 run_direct 统一清理。"""
465
+ import os as _os
466
+ if _os.environ.get("FORGE_SHOT_MODE") == "off":
467
+ return
468
+ base = _os.environ.get("FORGE_SHOT_DIR")
469
+ case = _os.environ.get("FORGE_CASE", "case")
470
+ if not base:
471
+ return
472
+ n = step_shot.n.get(case, 0) + 1
473
+ try:
474
+ d.screenshot(_os.path.join(base, "%s_%02d_%s.jpeg" % (case, n, label)))
475
+ step_shot.n[case] = n
476
+ except Exception:
477
+ pass
478
+
479
+
480
+ step_shot.n = {}
481
+
482
+
483
+ def wait_for(d, by, timeout=10, interval=0.5):
484
+ """轮询等待控件出现(首屏锚点等待用);返回组件或 None,不抛异常"""
485
+ import time as _t
486
+ end = _t.time() + _w(timeout)
487
+ while _t.time() < end:
488
+ try:
489
+ c = d.find_component(by)
490
+ if c:
491
+ return c
492
+ except Exception:
493
+ pass
494
+ _t.sleep(interval)
495
+ return None
496
+
497
+
498
+ def text_exist(d, txt, wait=6):
499
+ """文本存在断言(BY.text CONTAINS)。
500
+ 契约:存在返回 True;不存在【抛 AssertionError】——永不返回 False。
501
+ run_direct/analyze_results 依赖异常文本做 FAIL 判定与失败聚类,勿改为静默返回。"""
502
+ import time as _t
503
+ deadline = _t.time() + _w(wait)
504
+ while _t.time() < deadline:
505
+ try:
506
+ c = d.find_component(BY.text(txt, MatchPattern.CONTAINS))
507
+ if c:
508
+ return True
509
+ except Exception:
510
+ pass
511
+ _t.sleep(0.25)
512
+ raise AssertionError("text not found: " + txt)
513
+
514
+
515
+ def text_value_equals(d, cid, expected, wait=6):
516
+ """值断言(EXEC-040 实证+反证):getText() 精确等于期望——锚定 key 控件动态内容。"""
517
+ import time as _t
518
+ deadline = _t.time() + _w(wait)
519
+ while _t.time() < deadline:
520
+ try:
521
+ c = d.find_component(BY.key(cid))
522
+ if c and c.getText() == expected:
523
+ return True
524
+ except Exception:
525
+ pass
526
+ _t.sleep(0.25)
527
+ got = None
528
+ try:
529
+ c = d.find_component(BY.key(cid))
530
+ got = c.getText() if c else '(not found)'
531
+ except Exception:
532
+ pass
533
+ raise AssertionError(f"text value mismatch: expect {expected!r} got {got!r} (key={cid})")
534
+
535
+
536
+ def text_value_contains(d, cid, part, wait=6):
537
+ """值包含断言:getText() 包含子串(动态文案前缀不定时用)。"""
538
+ import time as _t
539
+ deadline = _t.time() + _w(wait)
540
+ while _t.time() < deadline:
541
+ try:
542
+ c = d.find_component(BY.key(cid))
543
+ if c and part in (c.getText() or ''):
544
+ return True
545
+ except Exception:
546
+ pass
547
+ _t.sleep(0.25)
548
+ raise AssertionError(f"text value not containing {part!r} (key={cid})")
549
+
550
+
551
+ def count_equals(d, target, expected, wait=6):
552
+ """计数断言(EXEC-040 实证):key 或 CONTAINS 文本匹配的组件数==期望。"""
553
+ import time as _t, re as _re
554
+ deadline = _t.time() + _w(wait)
555
+ last = -1
556
+ while _t.time() < deadline:
557
+ try:
558
+ if _re.match(r'^[a-z][a-z0-9_]+$', target):
559
+ comps = d.find_all_components(BY.key(target))
560
+ else:
561
+ comps = d.find_all_components(BY.text(target, MatchPattern.CONTAINS))
562
+ last = len(comps) if comps else 0
563
+ if last == expected:
564
+ return True
565
+ except Exception:
566
+ pass
567
+ _t.sleep(0.25)
568
+ raise AssertionError(f"count mismatch: expect {expected} got {last} (target={target})")
569
+
570
+
571
+ def is_below(d, cid, ref_cid, wait=6):
572
+ """几何断言(EXEC-040 实证):cid 的 top >= ref_cid 的 bottom(垂直次序)。"""
573
+ import time as _t
574
+ deadline = _t.time() + _w(wait)
575
+ while _t.time() < deadline:
576
+ try:
577
+ a = d.find_component(BY.key(cid)); b = d.find_component(BY.key(ref_cid))
578
+ if a and b and a.getBounds().top >= b.getBounds().bottom:
579
+ return True
580
+ except Exception:
581
+ pass
582
+ _t.sleep(0.25)
583
+ raise AssertionError(f"geometry: {cid} not below {ref_cid}")
584
+
585
+
586
+ def toast_exist(d, txt, wait=4, poll=0.5):
587
+ """Toast 断言(v3.1):轮询 hilog 捕获 Toast 文本。需 FORGE_HDC/FORGE_DEVICE。"""
588
+ import os as _os, subprocess as _sp, time as _tt
589
+ hdc = _os.environ.get("FORGE_HDC", "hdc"); dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
590
+ deadline = _tt.time() + _w(wait)
591
+ while _tt.time() < deadline:
592
+ try:
593
+ r = _sp.run([hdc, "-t", dev, "shell", "hilog", "-x"],
594
+ capture_output=True, timeout=8, text=True, errors="ignore")
595
+ if txt in (r.stdout or ""):
596
+ return True
597
+ except Exception:
598
+ pass
599
+ _tt.sleep(poll)
600
+ raise AssertionError("toast not found in hilog: " + txt)
601
+
602
+
603
+ # ── 强断言原语(2026-09-29 吸收自 none 臂实证:无 skill 的 LLM 凭通用能力即达成 34% 强断言,
604
+ # B 线无理由低于此——Toast/系统事件/数据断言按下表选型,禁止只写 exist)──
605
+ def _hdc(args, timeout=20):
606
+ import subprocess as _sp, os as _os
607
+ dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
608
+ cmd = [HDC_BIN, "-t", dev] + args
609
+ r = _sp.run(cmd, capture_output=True, timeout=timeout)
610
+ return (r.stdout + r.stderr).decode("utf-8", "ignore")
611
+
612
+
613
+ def _dump_layout(timeout=20):
614
+ """取当前全量布局 JSON(含系统对话框/浮层),返回原始文本(Round-2:确保本地 tmp 目录存在)"""
615
+ import json as _json, os as _os
616
+ _os.makedirs("./tmp", exist_ok=True)
617
+ _hdc(["shell", "uitest", "dumpLayout", "-p", "/data/local/tmp/forge_dump.json"], timeout=timeout)
618
+ _hdc(["file", "recv", "/data/local/tmp/forge_dump.json", "./tmp/forge_dump.json"], timeout=timeout)
619
+ try:
620
+ return open("./tmp/forge_dump.json", encoding="utf-8").read()
621
+ except Exception:
622
+ return ""
623
+
624
+
625
+ def hilog_clear():
626
+ """清 hilog 缓冲,防止上一用例残留关键字造成假 PASS(tag=RaiseToAnswer)"""
627
+ _hdc(["shell", "hilog", "-r"])
628
+ time.sleep(0.3)
629
+
630
+
631
+ HILOG_TAG = 'JSAPP'
632
+
633
+
634
+ def hilog_tail(tag=None, n=400):
635
+ tag = tag or HILOG_TAG
636
+ """读取 tag 过滤的最近 n 行 hilog(诊断用)"""
637
+ import subprocess as _sp, os as _os
638
+ dev = _os.environ.get("FORGE_DEVICE", "127.0.0.1:5555")
639
+ r = _sp.run([HDC_BIN, "-t", dev, "shell", "hilog", "-z", str(n), "-T", tag],
640
+ capture_output=True, timeout=15)
641
+ return r.stdout.decode("utf-8", "ignore")
642
+
643
+
644
+ def toast_text_seen(d, txt, timeout=6):
645
+ """Toast 文本断言(dump 版)。无 ID 例外:Toast 为瞬时浮层无稳定 ID,且 hypium find_component
646
+ 实测看不到 Toast 子窗口,故以 uitest dumpLayout 原文轮询(首轮实测教训 Round-1)。"""
647
+ deadline = time.time() + _w(timeout)
648
+ while time.time() < deadline:
649
+ raw = _dump_layout()
650
+ if txt in raw:
651
+ return True
652
+ time.sleep(0.6)
653
+ raise AssertionError("toast not seen: " + txt)
654
+
655
+
656
+ def bm_clean():
657
+ """清应用持久化数据(公开包装;import * 不导出 _ 前缀——Round-1 教训)"""
658
+ _hdc(["shell", "bm", "clean", "-n", PACKAGE, "-d"])
659
+ time.sleep(0.6)