From 373a8bd6fa62c991d4ea562a8636dafd7a7ff7d3 Mon Sep 17 00:00:00 2001 From: woan <798680981@qq.com> Date: Mon, 1 Jun 2026 16:10:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BF=85=E6=B5=8B=E9=A1=B9=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8C=96=E9=94=9A=E7=82=B9=20+=20=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E9=94=9A=E7=82=B9=20+=20=E5=8F=8C=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E7=BD=91=E7=BB=9C=E5=88=87=E6=8D=A2=20+=20ONES=20muta?= =?UTF-8?q?tion=20=E5=9B=9E=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ONES 回写: - ones-sync 改为 ones graphql mutation(updateTestcasePlanCase/Step),修复 token 打码导致的 401;支持 step 级回写、按 case 聚合 - gen-writeback-params.ts + ones-writeback-params.json:固化用例/步骤 uuid,给 plan 链接即可回写 必测项锚点(reporter.record 名带 [P0][ONES:号(#step)][协议],it 标题带 [ONES:号] 备注): - 添加:15 个 connect - 控制:bot/color_bulb/lock/plug/strip_light/fan/sensor/humidifier/ceiling_light/curtain/meter/urc(双协议,PROTO 切换) - 同品类多型号:ones-anchor.helper 动态解析(curtain/lock/plug/sensor/humidifier/hub/meter/robot),换 _DEVICE 重跑分别回写 双协议网络前置(network.helper + driver.activateApp): - Android adb(svc wifi + am start 蓝牙设置点 switch_widget;实测 svc bluetooth 不可用) - iOS 系统设置 UI(蓝牙 name=BLUETOOTH / WiFi name=无线局域网,iOS26.5 实测) 测试计划:generate_test_plan.py 重构为优先级四层(必测项→探索→全功能→平台)+ AI人力估算 + 双平台/夜测/硬件并行;去除机械臂4款(82→78) 提示词:must_test_conversion / ones_to_automation(含 iOS 快速连接) / test_plan_conversion Co-Authored-By: Claude Opus 4.8 --- docs/UI自动化测试计划.docx | Bin 49059 -> 51210 bytes docs/generate_test_plan.py | 115 +++- drivers/android-driver.ts | 5 + drivers/hubshow-driver.ts | 5 + drivers/types.ts | 3 + drivers/wda-driver.ts | 4 + iOS连接修复指南.md | 8 +- package.json | 3 +- prompts/must_test_conversion.md | 80 ++- prompts/ones_to_automation.md | 89 ++- scripts/gen-writeback-params.ts | 78 +++ scripts/sync-ones-results.ts | 51 +- test-plan/ones-writeback-params.json | 607 ++++++++++++++++++ .../air_condition_connect.test.ts | 8 +- tests/bot/bot_card.test.ts | 18 +- tests/bot/bot_connect.test.ts | 8 +- .../ceiling_light_connect.test.ts | 8 +- .../ceiling_light_control.test.ts | 12 +- tests/color_bulb/color_bulb_connect.test.ts | 8 +- tests/color_bulb/color_bulb_control.test.ts | 8 +- tests/curtain/curtain_connect.test.ts | 12 +- tests/curtain/curtain_control.test.ts | 12 +- tests/fan/fan_connect.test.ts | 8 +- tests/fan/fan_control.test.ts | 12 +- tests/hub/hub_connect.test.ts | 10 +- tests/humidifier/humidifier_connect.test.ts | 10 +- tests/humidifier/humidifier_control.test.ts | 12 +- tests/keypad/keypad_connect.test.ts | 8 +- tests/lock/lock_connect.test.ts | 10 +- tests/lock/lock_control.test.ts | 13 +- tests/meter/meter_connect.test.ts | 10 +- tests/meter/meter_control.test.ts | 11 +- tests/osc/osc_connect.test.ts | 10 +- tests/plug/plug_connect.test.ts | 10 +- tests/plug/plug_control.test.ts | 8 +- tests/robot/robot_connect.test.ts | 12 +- tests/sensor/sensor_control.test.ts | 8 +- tests/strip_light/strip_light_connect.test.ts | 8 +- tests/strip_light/strip_light_control.test.ts | 12 +- tests/urc/urc_control.test.ts | 11 +- utils/common/index.ts | 2 + utils/common/network.helper.ts | 121 ++++ utils/common/ones-anchor.helper.ts | 74 +++ utils/ones-sync.ts | 104 ++- utils/wda-helper.ts | 6 + 45 files changed, 1366 insertions(+), 266 deletions(-) create mode 100644 scripts/gen-writeback-params.ts create mode 100644 test-plan/ones-writeback-params.json create mode 100644 utils/common/network.helper.ts create mode 100644 utils/common/ones-anchor.helper.ts diff --git a/docs/UI自动化测试计划.docx b/docs/UI自动化测试计划.docx index 6aaa311c1fb2d96e1029956a96b949a7eff7a6e4..32aec98ec35dc9275517fe3a954e0fd179ff10cf 100644 GIT binary patch delta 15741 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 delta 13601 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 diff --git a/docs/generate_test_plan.py b/docs/generate_test_plan.py index e56861e..f138f20 100644 --- a/docs/generate_test_plan.py +++ b/docs/generate_test_plan.py @@ -106,7 +106,7 @@ add_table_with_header(doc, ['设备', '脚本文件数', '覆盖模块', '通过 doc.add_heading('1.4 目标', level=2) doc.add_paragraph('• P0 必测项:各单品添加 + 核心控制(双协议)准入闸门,来源 ONES 必测项-AI自动化') -doc.add_paragraph('• P1 单品探索:82 款单品主流程冒烟,覆盖广度') +doc.add_paragraph('• P1 单品探索:78 款单品主流程冒烟,覆盖广度') doc.add_paragraph('• P2 全功能:全维度回归(card/control/setting/scene/logs),覆盖深度') doc.add_paragraph('• P3 平台:账号/房间/消息/家庭分享/场景等平台功能') doc.add_paragraph('• 按优先级 backlog 依次交付,各层有独立退出标准(不绑定固定周次)') @@ -114,7 +114,7 @@ doc.add_paragraph('• 按优先级 backlog 依次交付,各层有独立退出 doc.add_page_break() # ======================== 2. 设备清单 ======================== -doc.add_heading('2. 待覆盖设备清单(82款)', level=1) +doc.add_heading('2. 待覆盖设备清单(78款)', level=1) device_categories = { '窗帘 (9款)': [ @@ -214,11 +214,7 @@ device_categories = { ('AI PinNote', '需新写', '全新品类'), ('Bot Rechargeable', '复用bot', '调试适配'), ], - '机器人+联名+其他 (8款)': [ - ('OBBOTO 1.0', '需新写', '全新品类'), - ('Robotic Actuator', '需新写', '全新品类'), - ('Robotic Arm', '需新写', '全新品类'), - ('Robotic Picker', '需新写', '全新品类'), + '机器人+联名+其他 (4款)': [ ('SwitchBot KATAフレンズ KUMAMON ver.', '复用bot', '特殊UI定制'), ('KATA Friends 国行版', '复用bot', '特殊UI定制'), ('Outdoor PTC', '需新写', '全新品类'), @@ -257,7 +253,7 @@ add_table_with_header(doc, ) doc.add_paragraph() -doc.add_paragraph('用例数量估算:每设备平均 25-40 条,82款设备总计约 2000-3000 条自动化用例。') +doc.add_paragraph('用例数量估算:每设备平均 25-40 条,78款设备总计约 2000-3000 条自动化用例。') doc.add_paragraph('AI 提效口径:单维度脚本首台 0.2-1 人日、复用台 0.1-0.3 人日;串口添加框架为一次性投入(见 §5.2)。下列各层人力预估均按此口径,单位为人日(1 人)。') doc.add_page_break() @@ -271,8 +267,8 @@ doc.add_paragraph( rows = [ ('P0 必测项', '各单品添加 + 核心控制(双协议)', 'ONES 必测项-AI自动化 (CQz9YCNX)', '187', '每次提测/版本必跑', '通过率≥95%'), - ('P1 单品探索', '每单品主流程冒烟(card+核心control)', '按单品模板', '82款各1遍', '每迭代', '全单品冒烟通过'), - ('P2 全功能', '全维度回归(card/control/setting/scene/logs)', '各品类用例库', '~2366', '版本回归', '通过率≥85%'), + ('P1 单品探索', '每单品主流程冒烟(card+核心control)', '按单品模板', '78款各1遍', '每迭代', '全单品冒烟通过'), + ('P2 全功能', '全维度回归(card/control/setting/scene/logs)', '各品类用例库', '~2250', '版本回归', '通过率≥85%'), ('P3 平台', '账号/房间/消息/家庭分享/场景等平台功能', 'App平台用例库', '平台模块', '版本回归', '平台用例通过'), ] add_table_with_header(doc, @@ -350,14 +346,17 @@ doc.add_page_break() # ======================== 6. P1 单品探索 ======================== doc.add_heading('6. P1 单品探索(覆盖广度)', level=1) doc.add_paragraph( - '必测项达标后,为全部 82 款单品各跑通一遍主流程冒烟,确保每款设备最低限度可用。' - '范围 = card(首页卡片)+ 核心 control(开关/主模式),不含设置/场景/日志等深度回归。' + '必测项达标后,为全部 78 款单品覆盖一遍探索测试:card + 核心 control,并扩展到' + '全品类核心功能的「部分全功能用例」(每品类挑代表性主功能,不做完整深度回归)。' ) -doc.add_paragraph('• 目标:82 款单品各 1 套冒烟,广度优先、深度浅。') -doc.add_paragraph('• 复用必测项已建的 control helper,单品只补 card 与主流程断言。') -doc.add_paragraph('• 退出标准:每款单品冒烟用例通过(card 可见 + 核心控制响应)。') +doc.add_paragraph('• 目标:78 款单品各 1 套探索集(card + 核心控制 + 品类代表性主功能),广度优先。') +doc.add_paragraph('• 已有/复用品类:复用 P0 已建 control helper,补 card 与主功能断言,较快。') +doc.add_paragraph('• 新品未支持:扫地机新机 / Lock Ultra·Vision / AI产品 / Art Frame 等 ~26 款为全新 UI,' + 'P1 需先建该 UI 的探索脚本支持,这部分较贵、是 P1 的主要成本。') +doc.add_paragraph('• 退出标准:每款单品探索集通过(card 可见 + 核心控制响应 + 代表性主功能可用)。') doc.add_paragraph('• 运行频率:每迭代跑一遍,作为单品级回归基线。') -doc.add_paragraph('• 人力预估(AI 辅助):约 12-16 人日(建立在 P0 已建 control helper 之上,82 款多为同品类复用,首台 ~0.3、复用台 ~0.1 人日/款)。') +doc.add_paragraph('• 旧代码转换提效:部分用例由以前旧代码转换而来,已含测试路径/步骤,只需定位元素位置使脚本可执行(Midscene AI 定位较快)——这类省去写用例的工作。新品无旧码、不受益。') +doc.add_paragraph('• 人力预估(AI 辅助):约 16-22 人日(已有/复用品类 ~52 款较快,部分还有旧码转换的现成路径;新品 ~26 款需首次建新 UI 探索支持,为主要成本)。') doc.add_page_break() @@ -368,11 +367,20 @@ doc.add_paragraph( '同品类 UI 高度相似,调通首台后改设备名配置快速扩展,并沉淀通用 helper。' '不绑定周次,按复用度从高到低排批次。' ) +doc.add_paragraph( + '脚本状态分三层成本(见第 2 节):「已有脚本」只需在真机上调试适配,最快;' + '「复用模板」改设备名/配置次之;「需新写」(全新 UI)最贵。估算按此分层。' +) +doc.add_paragraph( + '进一步提效:功能页/设置页「入口」全单品通用(沉淀一个全局导航 helper,不逐设备重写);' + '同品类功能测试点高度雷同,首台调通后其余只调「差异功能」。' + '另有部分用例由旧代码转换而来、已含路径,只需定位元素即可执行。故 P2 实际只在每设备增量调试少量差异点。' +) rows = [ - ('批1·高复用', '窗帘9 / 锁12 / 插座开关6 / 灯光10', '37', '脚本×37 + curtain/lock/relay/light helper', '~25'), - ('批2·中复杂', '扫地机9 / 传感器温控7 / 风扇空净加湿6 / 摄像头门铃3', '25', '脚本×25 + robot/sensor/climate helper', '~27'), - ('批3·新品特殊', 'Hub门控安防5 / AI产品5 / 机器人联名5', '15+', '脚本×15 + 全新UI框架', '~28'), + ('批1·高复用', '窗帘9 / 锁12 / 插座开关6 / 灯光10', '37', '差异功能 + 通用入口 helper', '~12'), + ('批2·中复杂', '扫地机9 / 传感器温控7 / 风扇空净加湿6 / 摄像头门铃3', '25', '差异功能 + robot/sensor/climate helper', '~14'), + ('批3·新品特殊', 'Hub门控安防 / AI产品 / 联名其他(KATA/FindCard/PTC)', '16', '深度回归(UI支持已在P1建好)', '~10'), ] add_table_with_header(doc, ['批次', '品类', '设备数', '交付物', 'AI预估(人日)'], @@ -383,10 +391,10 @@ add_table_with_header(doc, doc.add_paragraph() p = doc.add_paragraph() p.add_run('覆盖目标:').bold = True -p.add_run('全部 82 款单品全维度回归,~2366 条用例,单品功能覆盖率 100%。') +p.add_run('全部 78 款单品全维度回归,~2250 条用例,单品功能覆盖率 100%。') p = doc.add_paragraph() p.add_run('P2 合计:').bold = True -p.add_run('约 75-90 人日(仅 card/control/setting 约 60-70,含 scene/logs 约 75-90)。约 50 款为“已有/复用”可低成本适配;真正“需新写”的新品 UI 约 30 款(扫地机新机/Lock Ultra·Vision/AI产品/机器人)需 Figma/UX 重新分析,AI 辅助有限、为主要成本;add/control 已被 P0/P1 覆盖可复用。') +p.add_run('约 25-35 人日。入口通用、同品类只调差异功能、card/control 与新品 UI 已在 P0/P1 建好;且已有品类不少用例由旧代码转换、已含路径,只需定位元素(Midscene AI 定位)——P2 主要剩真机跑通+少量差异深度。已接近实际地板:每设备至少真机验一次 + flaky 复跑不可再压。注:P0+P1+P2 总量守恒。') doc.add_heading('7.1 高复用品类要点', level=2) doc.add_paragraph('• 窗帘: 位置控制、校准流程、定时器、群组控制 → curtain_helper') @@ -404,7 +412,7 @@ doc.add_heading('7.3 新品+特殊要点', level=2) doc.add_paragraph('• Hub 3 / AI Hub Show: 设备管理、红外学习、Matter、带屏交互') doc.add_paragraph('• 门控安防: Garage Door、Safety Alarm、Radiator Thermostat') doc.add_paragraph('• AI产品: Art Frame、AI Pet、AI PinNote(全新UI,需从Figma/UX重新分析)') -doc.add_paragraph('• 机器人+联名: OBBOTO、Robotic Actuator/Arm/Picker、KATA Friends(Bot复用+定制皮肤)') +doc.add_paragraph('• 联名+其他: KATA Friends(Bot复用+定制皮肤)、FindCard(已有脚本调试适配)、Outdoor PTC(需新写)') doc.add_page_break() @@ -424,7 +432,7 @@ rows = [ add_table_with_header(doc, ['模块', '内容', '来源'], rows) doc.add_heading('8.2 自动化/场景联动', level=2) -doc.add_paragraph('验证设备间联动可靠性(复用已有 automation 脚本基础):') +doc.add_paragraph('验证设备间联动可靠性(复用已有 automation 脚本基础)。场景联动里的设备功能入口/控制 P0-P2 已调通,联动部分调试较快;账号/房间/消息/分享为纯平台功能,不复用设备脚本。') doc.add_paragraph('• 创建自动化:手动/条件触发/定时触发') doc.add_paragraph('• 执行验证:触发条件满足后动作执行') doc.add_paragraph('• 编辑/删除:修改条件或动作、删除自动化') @@ -452,7 +460,7 @@ doc.add_paragraph( rows = [ ('P0 · 控制轨', '自动化(1人)', '—', '先行:双协议控制 105 step,不待串口'), ('P0 · 添加轨', '自动化(1人)', '嵌入式(串口协议+框架)', '串口就绪后插入:73 添加 + 9 功能'), - ('P1 单品探索', '自动化(1人)', '—', 'P0 达标后:82 款冒烟'), + ('P1 单品探索', '自动化(1人)', '—', 'P0 达标后:78 款冒烟'), ('P2 全功能', '自动化(1人)', '—', '按品类批量,复用 helper'), ('P3 平台', '自动化(1人)', '—', '最后:平台功能 + 场景联动'), ] @@ -475,12 +483,18 @@ add_table_with_header(doc, ['用途', '设备需求', '到位时间'], rows) doc.add_heading('9.3 环境', level=2) rows = [ - ('测试手机', 'Samsung (Android) + iPhone (iOS)', '已有'), - ('Appium Server', 'v2.x + UIAutomator2', '已有'), + ('测试机', 'Mac×2(双调试位)+ Android×2(Samsung 等)+ iOS×2(iPhone)', '已有'), + ('Appium Server', 'v2.x + UIAutomator2 / WebDriverAgent', '已有'), ('网络环境', '稳定Wi-Fi (Deco)', '已有'), ('CI/CD', 'Jenkins/GitHub Actions (可选)', '待搭建'), ] add_table_with_header(doc, ['项目', '说明', '状态'], rows) +doc.add_paragraph('硬件并行(2 Mac + 2 Android + 2 iOS)压缩日历:') +doc.add_paragraph('• Android∥iOS 并行:Mac1+Android、Mac2+iOS 同时推进,iOS 增量(25-45 人日)与 Android 时间线重叠,不再串行累加(最大杠杆)。') +doc.add_paragraph('• 每平台 2 台:一台调试、另一台跑全量回归/flaky 复跑(白天+夜间),执行类 wall-clock 再砍。') +doc.add_paragraph('• 双 Mac 各挂一个 AI agent(Claude Code/Midscene)并行“边跑边写”,人只做 review——唯一能突破“1 人”瓶颈的方式。') +doc.add_paragraph('• 上限:仍 1 人,手动交互调试不可一人并干两台(Amdahl);正确用法=「1 台主调 + 1 台挂 AI/跑回归」。') +doc.add_paragraph('• 净效果:双平台日历 ~90-130 → ~65-90 工作日(≈ 压回单平台水平),人日(工作量)不变;提速幅度取决于多少调试可交给 AI 自动跑。') doc.add_page_break() @@ -489,20 +503,53 @@ doc.add_heading('10. 优先级 backlog 一览', level=1) rows = [ ('P0 必测项', '添加73 + 功能9 + 控制105step(双协议)', '187条', '~11-15', 'serial框架 + connect + 控制断言 + step回写', '通过率≥95%'), - ('P1 单品探索', '82款单品主流程冒烟(card+核心control)', '82款×1', '~12-16', '每单品冒烟用例', '全单品冒烟通过'), - ('P2 全功能', '全维度回归,按品类批量(高复用→新品)', '~2366条', '~75-90', '85+脚本 + 8个通用helper', '通过率≥85%、覆盖100%'), - ('P3 平台', '账号/房间/消息/家庭分享/场景等', '平台模块', '~12-15', '平台用例 + 场景联动', '平台用例通过'), + ('P1 单品探索', '全品类 card+核心control + 代表性主功能(部分全功能);含新品UI首次支持', '78款各1套', '~16-22', '探索集脚本 + 新品UI支持', '全品类探索集通过'), + ('P2 全功能', '全维度深度回归(setting/scene/logs+完整功能点)', '~2250条', '~25-35', '深度回归(入口/路径多已就绪,补定位)', '通过率≥85%、覆盖100%'), + ('P3 平台', '账号/房间/消息/家庭分享/场景等', '平台模块', '~10-13', '平台用例 + 场景联动(复用设备入口)', '平台用例通过'), ] add_table_with_header(doc, - ['优先级', '范围', '用例量', 'AI预估(人日)', '交付物', '退出标准'], + ['优先级', '范围', '用例量', 'AI预估(人日,单平台)', '交付物', '退出标准'], rows, - col_widths=[2, 4.3, 1.5, 2, 4.2, 2.5] + col_widths=[2, 4.3, 1.5, 2.2, 4, 2.5] ) doc.add_paragraph() p = doc.add_paragraph() -p.add_run('人力总计(AI 辅助口径):').bold = True -p.add_run('约 105-135 人日 ≈ 5-7 人月(1 人,按 20 人日/月)。P0 大幅复用既有 control、串口硬件联调由嵌入式主导不计入;P2 “需新写”新品 UI 占比最高、最不可压缩。') +p.add_run('人力总计(AI 辅助口径,单平台):').bold = True +p.add_run('约 65-85 人日 ≈ 3.5-4.5 人月(1 人,按 20 人日/月)。P1 含新品(~26款)全新 UI 首次探索支持,是 P1 主成本;P2 因入口通用、card/control 与新品 UI 已在 P0-P1 建好、且不少用例由旧代码转换已含路径(只需定位元素),只做剩余深度回归。已接近地板:每设备至少真机验一次 + 新品 UI 分析 + flaky 复跑不可再压。P0+P1+P2 总量守恒。P3 纯平台功能不复用设备脚本。串口硬件联调由嵌入式主导、不计入。') + +doc.add_paragraph() +p = doc.add_paragraph() +p.add_run('双平台(Android + iOS):').bold = True +p.add_run('以上为单平台口径(主调 Android)。iOS 增量约 +40-60%(非 ×2):用例逻辑/路径/helper 共享,' + 'Midscene AI 视觉定位跨平台大多可复用(省去两套选择器重写);增量主要是 iOS 真机执行+flaky 复跑、' + '及平台差异(iOS 权限弹窗 / 返回交互 / 配网与添加流程 / 个别布局)。各层增量按其真机/差异占比分摊(P0 偏低,见下),分布如下:') + +rows = [ + ('P0 必测项', '11-15', '3-5', '14-20'), + ('P1 单品探索', '16-22', '6-13', '22-35'), + ('P2 全功能', '25-35', '10-21', '35-56'), + ('P3 平台', '10-13', '4-8', '14-21'), + ('合计', '65-85', '25-45', '90-130'), +] +add_table_with_header(doc, ['优先级', '单平台(人日)', 'iOS增量(人日)', '双平台合计(人日)'], rows) +doc.add_paragraph('iOS 增量大头在 P2(深度回归真机执行最多)。P0 偏低:添加流程 app 侧通用(iOS 只适配一次 add 流程 + 配置驱动验证),' + '串口框架(对硬件)与 step 回写(ONES API)与平台无关、iOS 零增量,故 P0 仅控制+少量添加差异有增量。' + 'P3 仅权限/登录小幅增量。') + +doc.add_paragraph() +p = doc.add_paragraph() +p.add_run('夜间并行 iOS 压缩日历(省周期,不省人力):').bold = True +p.add_run('白天调 Android,iOS 夜间挂测——让 Midscene/AI 无人值守跑全量+flaky 复跑+自动改定位,次日早上 review 失败项。' + 'iOS 增量(~26-51 人日)中“机器执行类”可藏进夜间闲置时段(约占一半到三分之二);' + '“人工判断类”(iOS 特有差异、AI 改不动的)仍占用同一人白天工时、省不掉。') +rows = [ + ('串行双平台', '~90-130 工作日', 'Android 调完再调 iOS'), + ('夜间并行 iOS', '~70-100 工作日(省约 15-35 天)', '理想可压回接近单平台'), +] +add_table_with_header(doc, ['方式', '日历(1人)', '说明'], rows) +doc.add_paragraph('省多少取决于:① iOS 真机夜间在位、app 状态可自动复位;② iOS 平台差异多少;③ AI 自动修定位成功率。' + '注意:压缩的是日历周期,人日(工作量)不变;夜测靠 AI 跑+早上 review,非人工加班,可持续。') doc.add_paragraph( '人力构成说明:以上为 AI 辅助口径(较手工已下调约 2-3 倍)。其中“脚本编写”仅占约 1/3——AI 提效主要在此;' @@ -530,7 +577,7 @@ doc.add_paragraph('• 脚本可在 Android/iOS 双平台运行(通过平台 doc.add_heading('11.2 优先级层验收标准', level=2) rows = [ ('P0 必测项', '187条全部实现并跑通(添加+控制双协议)', '通过率≥95%,作为提测门禁'), - ('P1 单品探索', '82款单品冒烟全部通过', '广度100%覆盖'), + ('P1 单品探索', '78款单品冒烟全部通过', '广度100%覆盖'), ('P2 全功能', '全维度回归,通过率≥85%', '单品功能覆盖率100%'), ('P3 平台', '平台功能 + 场景联动通过', '全流程覆盖'), ] diff --git a/drivers/android-driver.ts b/drivers/android-driver.ts index 4ff90b7..904bdf2 100644 --- a/drivers/android-driver.ts +++ b/drivers/android-driver.ts @@ -57,6 +57,11 @@ export class AndroidDriver implements DeviceDriver { this.sessionId = null; } + async activateApp(appId: string): Promise { + await this.request('POST', `/session/${this.sessionId}/appium/device/activate_app`, { appId }); + await new Promise(r => setTimeout(r, 2000)); + } + async findElement(locator: ElementLocator): Promise { if (!locator.android) return null; return this.findElementRaw(locator.android.using, locator.android.value); diff --git a/drivers/hubshow-driver.ts b/drivers/hubshow-driver.ts index 40ef7e3..ba49e4b 100644 --- a/drivers/hubshow-driver.ts +++ b/drivers/hubshow-driver.ts @@ -62,6 +62,11 @@ export class HubShowDriver implements DeviceDriver { this.sessionId = null; } + // Hub Show 直连设备固件 UI,不管理 App;activateApp 为空实现以满足接口。 + async activateApp(_appId: string): Promise { + /* no-op */ + } + async findElement(locator: ElementLocator): Promise { if (!locator.android) return null; return this.findElementRaw(locator.android.using, locator.android.value); diff --git a/drivers/types.ts b/drivers/types.ts index db77ac6..8d67f36 100644 --- a/drivers/types.ts +++ b/drivers/types.ts @@ -19,6 +19,9 @@ export interface DeviceDriver { createSession(): Promise; destroySession(): Promise; + /** 激活/拉起指定 app(Android appPackage / iOS bundleId)。用于切到系统设置再切回。 */ + activateApp(appId: string): Promise; + findElement(locator: ElementLocator): Promise; findElements(locator: ElementLocator): Promise; findElementRaw(using: string, value: string): Promise; diff --git a/drivers/wda-driver.ts b/drivers/wda-driver.ts index 0b68772..d10cecc 100644 --- a/drivers/wda-driver.ts +++ b/drivers/wda-driver.ts @@ -17,6 +17,10 @@ export class WDADriver implements DeviceDriver { await this.wda.destroySession(); } + async activateApp(appId: string): Promise { + await this.wda.activateAppById(appId); + } + async findElement(locator: ElementLocator): Promise { if (!locator.ios) throw new Error(`Locator "${locator.name}" has no iOS strategy`); return this.wda.findElement(locator.ios.using, locator.ios.value); diff --git a/iOS连接修复指南.md b/iOS连接修复指南.md index c9ebfd2..ca8962b 100644 --- a/iOS连接修复指南.md +++ b/iOS连接修复指南.md @@ -43,7 +43,7 @@ from appium.options.ios import XCUITestOptions caps = XCUITestOptions() caps.platform_name = 'iOS' -caps.platform_version = '26.3.1' # 实际 iOS 版本 +caps.platform_version = '26.5' # 实际 iOS 版本 caps.device_name = 'iPhone' caps.udid = '00008110-001A34303AE9801E' # 设备 UDID caps.bundle_id = 'com.demo.wohand' @@ -63,7 +63,7 @@ cd /Users/woan/.appium/node_modules/appium-xcuitest-driver/node_modules/appium-w xcodebuild -project WebDriverAgent.xcodeproj \ -scheme WebDriverAgentRunner \ -destination "id=YOUR_DEVICE_UDID" \ - IPHONEOS_DEPLOYMENT_TARGET=26.3.1 \ + IPHONEOS_DEPLOYMENT_TARGET=26.5 \ -configuration Debug \ build ``` @@ -73,7 +73,7 @@ xcodebuild -project WebDriverAgent.xcodeproj \ **重要**:配置中的 platform_version 必须与设备实际版本一致 - 查询设备版本:`ideviceinfo | grep ProductVersion` -- 当前测试设备:iOS 26.3.1(iPhone 14 Pro) +- 当前测试设备:iOS 26.5(iPhone 14 Pro) - 设备 UDID:00008110-001A34303AE9801E ## 常见错误 @@ -94,7 +94,7 @@ from appium import webdriver from appium.options.ios import XCUITestOptions caps = XCUITestOptions() caps.platform_name = 'iOS' -caps.platform_version = '26.3.1' +caps.platform_version = '26.5' caps.udid = '00008110-001A34303AE9801E' caps.bundle_id = 'com.demo.wohand' caps.automation_name = 'XCUITest' diff --git a/package.json b/package.json index 4f08e45..2b41980 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "test:card": "vitest run tests/bot/bot_card.test.ts", "test:logs": "vitest run tests/bot/bot_logs.test.ts", "test:scene": "vitest run tests/bot/bot_scene.test.ts", - "gen:must-test": "ts-node scripts/gen-must-test-manifest.ts" + "gen:must-test": "ts-node scripts/gen-must-test-manifest.ts", + "gen:writeback-params": "ts-node scripts/gen-writeback-params.ts" }, "keywords": [ "ui-automation", diff --git a/prompts/must_test_conversion.md b/prompts/must_test_conversion.md index 6e3c13e..428c035 100644 --- a/prompts/must_test_conversion.md +++ b/prompts/must_test_conversion.md @@ -106,18 +106,42 @@ export const MUST_TEST: MustTestItem[] = [ ## 6. P0 标记约定(带 ONES 锚点) -代码里用 `it` 名称打标,锚点指向 ONES,便于筛选与回写: +**关键:锚点必须打在 `reporter.record(名称, ...)` 的名称里**——结果写入 `reports/.results.json` 用的是 record 名称,`buildAnchoredPayloads` 从中解析 `[ONES:号(#step)]` 做回写。`it()` 标题里也加同一锚点作**备注**(测试报告里可见、便于追溯),但回写不读 it 标题。 ```ts -// 添加:锚点 = 用例号 -it(`[P0][ONES:91013] 通过BLE添加${deviceName}`, async () => { ... }); +// it 标题:加 [ONES:号] 作备注(可读/可追溯) +it(`[ONES:15968] 通过BLE添加${deviceName}设备`, async () => { + // ... + // reporter.record 名称:加 [P0][ONES:号] —— 这是回写真正依据 + reporter.record(`[P0][ONES:15968] 添加${deviceName}`, 'PASS', dur, detail); +}); -// 控制:锚点 = 用例号#step_uuid;协议标在中括号里(双协议则两条 it 或参数化) -it(`[P0][ONES:15975#${stepUuid}][ble] 开/关 ${deviceName}`, async () => { ... }); -it(`[P0][ONES:15974#${stepUuid}][wifi] 开/关 ${deviceName}`, async () => { ... }); +// 控制(协议相关):record 名称带 用例号#step_uuid + 协议;协议由 PROTO 环境变量切(见 §7) +const CTRL = process.env.PROTO === 'wifi' ? '[P0][ONES:15974#][wifi]' : '[P0][ONES:15975#][ble]'; +reporter.record(`${CTRL} 开/关 ${deviceName}`, status, dur, detail); ``` -筛选:`vitest -t '\[P0\]'`(全量) / `-t '\[ble\]'` / `-t '\[wifi\]'`。 +- 一条 ONES step 可由多个用例覆盖 → 用同一 step 锚点,回写自动聚合(fail>skip>pass)。 +- 筛选:`vitest -t '\[P0\]'`(全量) / 结果文件里按 `[ble]`/`[wifi]` 区分协议。 + +### 同品类多型号:设备维度动态锚点 + +一个品类的脚本只测默认设备,但同品类多个 UI 相似型号(如 Curtain/Curtain3/BlindTilt)应共用脚本、各自回写。**不要写死 ONES 号**,改用 `utils/common/ones-anchor.helper.ts` 按当前设备动态解析: + +```ts +import { onesAdd, onesCtrl } from '../../utils/common'; +const ADD_ANCHOR = onesAdd('curtain', deviceName); // 添加: 按 CURTAIN_DEVICE 解析 +const CTRL_CURTAIN = onesCtrl('curtain', deviceName); // 控制: 按设备 + PROTO 解析 +reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, ...); +``` + +- `ANCHORS` map(在 `ones-anchor.helper.ts`)按 `品类 → 设备名(DEVICE_CONFIG) → {add, ctrlBle, ctrlWifi}` 维护;**新增型号补一行即可**。 +- 跑法:**换 `_DEVICE` 环境变量按型号多跑几遍**(× PROTO),每遍写到该型号的 ONES 用例。 + ```bash + CURTAIN_DEVICE='Curtain3 2B' PROTO=ble npx vitest run tests/curtain + ``` +- 约束:每个型号要是账号里真实存在的设备才能跑(变体覆盖上限 = 真机数量)。 +- UI **不相似**的型号(需新写)→ 单独脚本 + 自己的锚点,不走此复用。 --- @@ -151,19 +175,39 @@ it(`[P0][ONES:15974#${stepUuid}][wifi] 开/关 ${deviceName}`, async () => { ... --- -## 9. step 级结果回写 ONES +## 9. step 级结果回写 ONES(正确方法:GraphQL mutation) -主提示词反写 API(`.../testcase/plan/{plan_uuid}/cases/update`)的 `cases[].steps` 数组**支持按步回写**。必测项据此: +> ⚠️ **必须走 `ones graphql` mutation,不要用 curl 直连 REST `.../cases/update`。** +> `ones config show` 会把 token 打码成 `***`,curl 直连必然 `401 AuthFailure.InvalidToken`; +> 而 `ones graphql` 复用 CLI 登录认证、无需 token/PAT,且已在权限白名单内。 +> 此机制已在 `utils/ones-sync.ts` 的 `postPayloads` 实现,跑 `scripts/sync-ones-results.ts` 即用。 -- **添加 case**:整 case 回写,`steps: []`,`result` = PASS→`passed` / FAIL→`failed` / SKIP→`skipped`(映射见主提示词第 2 节)。 -- **控制用例 15974 / 15975**:`uuid` = 该控制用例的 `testcaseCase.uuid`,`steps` 数组按 `step_uuid` 逐条填结果: - ```json - { "uuid":"Vp7vuhbu", "executor":"", "result":"passed", - "steps":[ { "uuid":"", "result":"passed", "actual_result":"开/关成功" }, ... ] } - ``` - case 级 `result` 由其所有 step 聚合(全 pass→passed,有 fail→failed)。 -- 用例名只有 2 条、靠 LCS 匹配会误配 → 控制用例**改按 `[ONES:号#step]` 锚点精确匹配**,扩展 `utils/ones-sync.ts` / `scripts/sync-ones-results.ts` 支持 step 维度。 -- 协议:`[ble]` 结果回 15975,`[wifi]` 结果回 15974。 +**key 拼接规则(确定式):** +- case: `testcase_plan_case--` +- step: `testcase_plan_case_step---` + +**写入 mutation:** +```bash +# case 级 +ones graphql 'mutation { updateTestcasePlanCase(key: "testcase_plan_case-CQz9YCNX-", result: "passed") { key } }' +# step 级(result 字段名是 step_result,不是 execute_result/result) +ones graphql 'mutation { updateTestcasePlanCaseStep(key: "testcase_plan_case_step-CQz9YCNX--", step_result: "passed", actual_result: "开/关成功") { key } }' +``` +- `result` / `step_result` 取值:`passed` / `failed` / `skipped` / `to_do`(PASS→passed、FAIL→failed、SKIP→skipped)。 + +**必测项据此:** +- **按用例聚合、一趟写完(批量规则)**:同一用例的所有 step 结果先聚合,在**一次回写流程里**写完该用例的全部 step、再写 case 级 result——不要把一个用例的步骤分散到多次回写里逐个触发。(`buildAnchoredPayloads` 已按 caseUUID 聚合 step,`postPayloads` 对一个用例的 steps 连续写完再写 case,即满足此规则。GraphQL 不支持单请求多 mutation,故实现上是同一趟内连续多次 mutation,效果即"一次性更新该用例多个步骤"。) +- **添加/功能 case**:只写 case 级 `updateTestcasePlanCase`。 +- **控制用例 15974 / 15975**:case 级 result 由 step 聚合(全跑完才 passed/failed,否则 `to_do`)。`[ble]`→15975、`[wifi]`→15974。 +- 匹配靠测试名 `[ONES:号#step]` 锚点(已在 `buildAnchoredPayloads` 实现),不用 LCS 误配。 + +**回写参数已固化(用例固定、仅新增)**:`test-plan/ones-writeback-params.json` 保存了计划全部用例(号→`uuid`)+ 15974/15975 的步骤 uuid。用例与步骤 uuid 与具体计划无关,**plan UUID 是回写时唯一变量**——后续给定任意必测项 plan 链接,取出 planUUID 即可按上面 key 规则拼出所有 case/step key 直接回写,无需重新查 ONES。用例新增后重跑 `npm run gen:writeback-params` 刷新。 + +**读回校验:** +```bash +ones graphql '{ testcasePlanCaseSteps(filter: { testcasePlan: { uuid_in: ["CQz9YCNX"] }, testcaseCase: { uuid_in: [""] } }, limit: 60) { key stepResult actualResult } }' +``` +(注意:查询用驼峰 `stepResult`/`actualResult`,filter 用 `testcasePlan`/`testcaseCase` 嵌套 `uuid_in`。) --- diff --git a/prompts/ones_to_automation.md b/prompts/ones_to_automation.md index 611d31b..4f464fc 100644 --- a/prompts/ones_to_automation.md +++ b/prompts/ones_to_automation.md @@ -33,6 +33,42 @@ --- +## iOS 真机快速连接 + +> 来源 `iOS连接修复指南.md`。iOS 跑测/双协议(iOS)前按此连。核心坑:**8100 端口被 iproxy 占用导致 WDA 起不来**。 + +**4 步快速连接:** +```bash +# 1. 取设备信息(UDID / iOS 版本) +idevicepair pair # 手机弹窗点「信任」 +ideviceinfo | grep -E "UniqueDeviceID|ProductVersion" +# 2. 清占用 8100 的进程(关键) +pkill -f iproxy +# 3. 确认 Appium 在跑 +curl -s http://localhost:4723/status +``` +4. 用 **XCUITestOptions** 连接,`wdaLocalPort` 设为 **8200**(避开 8100);`platform_version` 必须与设备实际版本一致: +```python +from appium import webdriver +from appium.options.ios import XCUITestOptions +caps = XCUITestOptions() +caps.platform_name = 'iOS' +caps.platform_version = '26.5' # 与 ideviceinfo 实际版本一致 +caps.udid = '00008110-001A34303AE9801E' # ideviceinfo 取 +caps.bundle_id = 'com.wohand.wohand' # 项目 SwitchBot bundleId(指南示例 com.demo.wohand 为 demo 包) +caps.automation_name = 'XCUITest' +caps.set_capability('wdaLocalPort', 8200) +driver = webdriver.Remote('http://127.0.0.1:4723', options=caps) +``` + +**当前 iOS 真机**:iPhone 14 Pro,iOS 26.5,UDID `00008110-001A34303AE9801E`。 + +**常见错误**:`Port 8100 occupied`→`pkill -f iproxy` + 用 8200;`accept trust dialog`→手机点信任;`WDA build failed`→手动 `xcodebuild` 构建 WDA(见 `iOS连接修复指南.md` 第4节);`Connection refused`→确认 WDA 已在设备运行。 + +注:仓库自定义 `WDADriver` 走 8100(iproxy 8100:8100);若 8100 冲突,按上面切 8200 或先 `pkill -f iproxy` 再起。 + +--- + ## DeviceDriver 可用接口 ```typescript @@ -840,43 +876,32 @@ ONES测试计划 → 读取用例列表 → 转换为自动化脚本 → 执行 | `'SKIP'` | `skipped` | | 未执行 | `to_do` | -### 3. 结果反写 (已确认可行) +### 3. 结果反写 (正确方法:GraphQL mutation,已验证) -API 端点: -``` -POST /project/api/project/team/{team_uuid}/testcase/plan/{plan_uuid}/cases/update -``` +> ⚠️ **必须走 `ones graphql` mutation,不要用 curl 直连 REST `.../cases/update`。** +> `ones config show` 把 token 打码成 `***`,curl 直连必然 `401 AuthFailure.InvalidToken`; +> `ones graphql` 复用 CLI 登录认证、无需 token/PAT,已在权限白名单。`utils/ones-sync.ts` 的 `postPayloads` 已按此实现。 -请求体 (JSON 对象,cases 数组包裹): -```json -{ - "cases": [ - { - "uuid": "用例UUID (testcaseCase.uuid)", - "executor": "执行人UUID (user_id)", - "note": "", - "result": "passed|failed|skipped|to_do", - "steps": [] - } - ] -} -``` +**key 拼接(确定式):** +- case: `testcase_plan_case--` +- step: `testcase_plan_case_step---` -响应: -```json -{ - "success_cases": ["BZfZGRcF"], - "not_found_cases": [], - "no_permission_cases": [], - "not_handle_cases": [] -} +**写入:** +```bash +# case 级结果 +ones graphql 'mutation { updateTestcasePlanCase(key: "testcase_plan_case--", result: "passed") { key } }' +# step 级结果(字段名 step_result,可带 actual_result) +ones graphql 'mutation { updateTestcasePlanCaseStep(key: "testcase_plan_case_step---", step_result: "passed", actual_result: "...") { key } }' ``` +取值 `passed|failed|skipped|to_do`(PASS→passed、FAIL→failed、SKIP→skipped)。 -说明: -- `uuid`: 测试用例的 UUID(不是 plan_case 的 key,是 `testcaseCase.uuid`) -- `executor`: 执行人 UUID,从 `ones config show` 的 `user_id` 获取 -- `steps`: 步骤结果数组,无步骤时传空数组 `[]` -- 支持批量提交多条 +**读回校验:** +```bash +ones graphql '{ testcasePlanCaseSteps(filter: { testcasePlan: { uuid_in: [""] }, testcaseCase: { uuid_in: [""] } }, limit: 60) { key stepResult actualResult } }' +``` +(查询字段驼峰 `stepResult`/`actualResult`;filter 用 `testcasePlan`/`testcaseCase` 嵌套 `uuid_in`。) + +可用 mutation:`updateTestcasePlanCase` / `updateTestcasePlanCaseStep`(经 `ones graphql '{ __schema { mutationType { fields { name } } } }'` 可查全部写操作)。 ### 4. 完整同步命令 diff --git a/scripts/gen-writeback-params.ts b/scripts/gen-writeback-params.ts new file mode 100644 index 0000000..bf242c2 --- /dev/null +++ b/scripts/gen-writeback-params.ts @@ -0,0 +1,78 @@ +/** + * 生成 ONES 回写参数 test-plan/ones-writeback-params.json + * + * 必测项用例固定、仅新增;用例与步骤的 UUID 与具体测试计划无关(plan UUID 是回写时的变量)。 + * 本文件保存:计划内全部用例(号→uuid)+ 控制用例 15974/15975 的步骤 uuid。 + * 后续给定任意必测项 plan UUID,即可按 key 规则拼出 key 直接回写: + * case: testcase_plan_case-- + * step: testcase_plan_case_step--- + * + * 用法: npx ts-node scripts/gen-writeback-params.ts [--plan CQz9YCNX] [--out test-plan/ones-writeback-params.json] + * 用例新增后重跑刷新即可。 + */ +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; + +const ONES_CLI = '/Users/woan/local/bin/ones'; +const DEFAULT_PLAN = 'CQz9YCNX'; // 必测项-AI自动化 +const CONTROL = [ + { number: 15975, name: '蓝牙控制设备', proto: 'ble' }, + { number: 15974, name: 'WiFi控制设备', proto: 'wifi' }, +]; + +function ones(args: string): any { + return JSON.parse(execSync(`${ONES_CLI} ${args}`, { encoding: 'utf-8', timeout: 30000 })); +} +function gql(q: string): any { + return JSON.parse(execSync(`${ONES_CLI} graphql '${q.replace(/'/g, "'\\''")}'`, { encoding: 'utf-8', timeout: 30000 })); +} + +function argVal(flag: string): string | undefined { + const i = process.argv.indexOf(flag); + return i >= 0 ? process.argv[i + 1] : undefined; +} + +function main() { + const plan = argVal('--plan') || DEFAULT_PLAN; + const out = argVal('--out') || 'test-plan/ones-writeback-params.json'; + + const pc = gql(`{ testcasePlanCases(filter: { testcasePlan: { uuid_in: ["${plan}"] } }, limit: 300) { testcaseCase { uuid number name } } }`) + .data.testcasePlanCases; + const cases = pc + .map((c: any) => ({ number: c.testcaseCase.number, uuid: c.testcaseCase.uuid, name: c.testcaseCase.name })) + .sort((a: any, b: any) => a.number - b.number); + + const controlCases: Record = {}; + for (const c of CONTROL) { + const found = ones(`testcase case search --key ${c.number}`).cases?.[0]; + controlCases[c.number] = { + name: c.name, + uuid: found?.uuid, + proto: c.proto, + steps: (found?.steps || []).map((s: any) => s.uuid), + }; + } + + const data = { + _note: + '必测项回写参数。用例固定仅新增;回写时 plan UUID 为变量。' + + 'key 规则: case=testcase_plan_case--; step=testcase_plan_case_step---。' + + '刷新: 重跑 scripts/gen-writeback-params.ts。', + defaultPlan: plan, + cases, + controlCases, + }; + + const outPath = path.resolve(__dirname, '..', out); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, JSON.stringify(data, null, 2), 'utf-8'); + console.log( + `已生成 ${out}: ${cases.length} 用例; ` + + Object.entries(controlCases) + .map(([n, v]: any) => `${n}=${v.steps.length}步`) + .join(', ') + ); +} + +main(); diff --git a/scripts/sync-ones-results.ts b/scripts/sync-ones-results.ts index 94cc418..109331d 100644 --- a/scripts/sync-ones-results.ts +++ b/scripts/sync-ones-results.ts @@ -6,9 +6,9 @@ * * 流程: * 1. 读取 reports/.results.json (自动化执行后的结果) - * 2. 从 ONES 拉取测试计划用例列表 - * 3. 优先按测试名锚点 [ONES:号(#step)] 精确匹配(支持 step 级回写), - * 无锚点的回退到用例名 LCS 模糊匹配 + * 2. 从本地参数文件 test-plan/ones-writeback-params.json 取用例(号→uuid)+ 步骤总数 + * —— 不再读 ONES(用例固定仅新增,刷新参数用 npm run gen:writeback-params) + * 3. 按测试名锚点 [ONES:号(#step)] 精确匹配(支持 step 级);无锚点回退用例名 LCS * 4. 反写结果到 ONES (dry-run 仅打印 payload) */ @@ -16,17 +16,17 @@ import * as fs from 'fs'; import * as path from 'path'; import { TestResult } from '../utils/test-reporter'; import { - fetchPlanCases, matchResults, buildAnchoredPayloads, postPayloads, - fetchCaseSteps, + OnesPlanCase, OnesUpdatePayload, } from '../utils/ones-sync'; import { execSync } from 'child_process'; const ONES_CLI = '/Users/woan/local/bin/ones'; const RESULTS_FILE = path.resolve(__dirname, '../reports/.results.json'); +const PARAMS_FILE = path.resolve(__dirname, '../test-plan/ones-writeback-params.json'); function parseArgs() { const args = process.argv.slice(2); @@ -61,6 +61,28 @@ function loadResults(): TestResult[] { return data.results || []; } +/** 从本地参数文件取 用例(OnesPlanCase[]) + 控制用例步骤总数(号→step数)。不读 ONES。 */ +function loadParams(): { planCases: OnesPlanCase[]; totalStepsByNumber: Map } { + if (!fs.existsSync(PARAMS_FILE)) { + console.error(`参数文件不存在: ${PARAMS_FILE}`); + console.error('请先运行: npm run gen:writeback-params'); + process.exit(1); + } + const p = JSON.parse(fs.readFileSync(PARAMS_FILE, 'utf-8')); + const planCases: OnesPlanCase[] = (p.cases || []).map((c: any) => ({ + key: '', + caseUUID: c.uuid, + caseName: c.name, + caseNumber: c.number, + currentResult: 'to_do', + })); + const totalStepsByNumber = new Map(); + for (const [num, c] of Object.entries(p.controlCases || {})) { + totalStepsByNumber.set(Number(num), (c.steps || []).length); + } + return { planCases, totalStepsByNumber }; +} + function main() { const { planUUID, dryRun } = parseArgs(); @@ -82,27 +104,18 @@ function main() { const skipped = testResults.filter(r => r.status === 'SKIP').length; console.log(` PASS: ${passed} | FAIL: ${failed} | SKIP: ${skipped}`); - // 2. 从 ONES 拉取计划用例 - console.log(`\n[2/4] 从 ONES 拉取测试计划用例 ...`); - const planCases = fetchPlanCases(planUUID); - console.log(` 计划共 ${planCases.length} 条用例`); + // 2. 从本地参数文件取用例 + 步骤总数(不读 ONES) + console.log(`\n[2/4] 读取本地回写参数 ...`); + const { planCases, totalStepsByNumber } = loadParams(); + console.log(` 参数: ${planCases.length} 用例, ${totalStepsByNumber.size} 个控制用例步骤数`); // 3. 匹配:锚点优先(支持 step 级),无锚点回退 LCS console.log(`\n[3/4] 匹配 (锚点优先 + LCS 兜底) ...`); - // 含 step 锚点的用例需列全量 step → 先取其完整 step 列表 - const stepNums = new Set(); - for (const tr of testResults) { - const m = /\[ONES:(\d+)#/.exec(tr.name); - if (m) stepNums.add(parseInt(m[1], 10)); - } - const fullStepsByNumber = new Map(); - for (const num of stepNums) fullStepsByNumber.set(num, fetchCaseSteps(num)); - const { payloads: anchored, unanchored } = buildAnchoredPayloads( planCases, testResults, executor, - { fullStepsByNumber } + { totalStepsByNumber } ); const anchoredUUIDs = new Set(anchored.map(p => p.uuid)); const stepCount = anchored.reduce((n, p) => n + p.steps.filter(s => s.execute_result).length, 0); diff --git a/test-plan/ones-writeback-params.json b/test-plan/ones-writeback-params.json new file mode 100644 index 0000000..365e08a --- /dev/null +++ b/test-plan/ones-writeback-params.json @@ -0,0 +1,607 @@ +{ + "_note": "必测项回写参数。用例固定仅新增;回写时 plan UUID 为变量。key 规则: case=testcase_plan_case--; step=testcase_plan_case_step---。刷新: 重跑 scripts/gen-writeback-params.ts。", + "defaultPlan": "CQz9YCNX", + "cases": [ + { + "number": 15940, + "uuid": "6yxCKHLc", + "name": "账户注册验证" + }, + { + "number": 15941, + "uuid": "9TYHacGi", + "name": "忘记密码验证" + }, + { + "number": 15942, + "uuid": "79wW7aGj", + "name": "第三方登录" + }, + { + "number": 15943, + "uuid": "61CCdeGk", + "name": "账户登出验证" + }, + { + "number": 15944, + "uuid": "6KfLxzGc", + "name": "创建房间验证" + }, + { + "number": 15945, + "uuid": "SuNW9m9B", + "name": "feedback提交验证" + }, + { + "number": 15946, + "uuid": "3eHivSnk", + "name": "添加Blind Tilt验证" + }, + { + "number": 15947, + "uuid": "2s4GbZAh", + "name": "添加Pan/Tilt Cam 2K验证" + }, + { + "number": 15948, + "uuid": "YCHSGAPP", + "name": "添加keypad touch验证" + }, + { + "number": 15949, + "uuid": "KDcB3xju", + "name": "添加keypad验证" + }, + { + "number": 15950, + "uuid": "PhJTMDYV", + "name": "添加吸顶灯验证(Pro和lte)" + }, + { + "number": 15951, + "uuid": "TBp2WxHN", + "name": "添加Robot Vacuum S1 Plus验证" + }, + { + "number": 15952, + "uuid": "EGzyeS4Z", + "name": "添加Robot Vacuum S1验证" + }, + { + "number": 15953, + "uuid": "4zJjZbQr", + "name": "学习others遥控器" + }, + { + "number": 15954, + "uuid": "KHUtwWFg", + "name": "学习自定义遥控器" + }, + { + "number": 15955, + "uuid": "ASgigVHm", + "name": "智能匹配遥控器" + }, + { + "number": 15956, + "uuid": "5ujCPVBF", + "name": "添加Meter Plus验证" + }, + { + "number": 15957, + "uuid": "7KAwsNmC", + "name": "添加Pan/Tilt Cam验证" + }, + { + "number": 15958, + "uuid": "GdqgjWmu", + "name": "添加Plug Mini验证(JP和US)" + }, + { + "number": 15959, + "uuid": "PgeMXwx6", + "name": "添加Strip Light验证" + }, + { + "number": 15960, + "uuid": "XQGshXkv", + "name": "添加Color Bulb验证" + }, + { + "number": 15961, + "uuid": "VaxZgabA", + "name": "添加Indoor Cam验证" + }, + { + "number": 15962, + "uuid": "VVTCNWiX", + "name": "添加Contact Sensor验证" + }, + { + "number": 15963, + "uuid": "U45e5n66", + "name": "添加Motion Sensor验证" + }, + { + "number": 15964, + "uuid": "JjvbRHYU", + "name": "添加Humidifier验证" + }, + { + "number": 15965, + "uuid": "8UFaR2qu", + "name": "添加Meter验证" + }, + { + "number": 15966, + "uuid": "36y6sC3z", + "name": "添加Plug验证" + }, + { + "number": 15967, + "uuid": "L4C4AVQe", + "name": "添加Remote验证" + }, + { + "number": 15968, + "uuid": "K6LFdz81", + "name": "添加Bot验证" + }, + { + "number": 15969, + "uuid": "3XahnWS4", + "name": "添加Curtain验证" + }, + { + "number": 15970, + "uuid": "SKAj9t7t", + "name": "添加Lock验证" + }, + { + "number": 15971, + "uuid": "NSQT1PGJ", + "name": "添加Hub Plus验证" + }, + { + "number": 15972, + "uuid": "Fi2V2XPY", + "name": "添加Hub Mini验证" + }, + { + "number": 15973, + "uuid": "SP75BSp2", + "name": "keypad、keypad touch添加密码、指纹、卡片验证" + }, + { + "number": 15974, + "uuid": "Vp7vuhbu", + "name": "WiFi控制设备" + }, + { + "number": 15975, + "uuid": "Lqpkx6mp", + "name": "蓝牙控制设备" + }, + { + "number": 15976, + "uuid": "6qQ6Pmm2", + "name": "场景操作验证" + }, + { + "number": 15977, + "uuid": "Be91sD22", + "name": "第三方操作验证" + }, + { + "number": 15978, + "uuid": "76VscU5y", + "name": "覆盖安装测试" + }, + { + "number": 21787, + "uuid": "SF9cqxki", + "name": "添加Hub2 验证" + }, + { + "number": 35871, + "uuid": "PRvriHQj", + "name": "添加IOSensor验证" + }, + { + "number": 40954, + "uuid": "8GpJwfuo", + "name": "添加K10+扫地机" + }, + { + "number": 40955, + "uuid": "y9ksiDTo", + "name": "添加curtain 3验证" + }, + { + "number": 47214, + "uuid": "L3dTTpJU", + "name": "添加PTC Plus 3MP" + }, + { + "number": 48159, + "uuid": "Dh4gScBV", + "name": "添加Battery Circulator Fan验证" + }, + { + "number": 48734, + "uuid": "Cvu7v7dt", + "name": "插件热更" + }, + { + "number": 51440, + "uuid": "5F6H4XW4", + "name": "添加lock&kit验证" + }, + { + "number": 65336, + "uuid": "9CT8TcY4", + "name": "添加Hub Mini Matter验证" + }, + { + "number": 66694, + "uuid": "CNK9w5gf", + "name": "添加Lock Pro验证" + }, + { + "number": 74078, + "uuid": "g7y2EGXF", + "name": "APP切换温湿度单位显示" + }, + { + "number": 74104, + "uuid": "PJ3bJLTg", + "name": "添加Humidifier2验证" + }, + { + "number": 74105, + "uuid": "XTNTvNyF", + "name": "强绑定设备的自动解绑申请" + }, + { + "number": 78369, + "uuid": "7mrfzZr1", + "name": "添加S10扫地机" + }, + { + "number": 105436, + "uuid": "D2c4eLE5", + "name": "添加lock Pro&kit验证" + }, + { + "number": 113941, + "uuid": "B7CRfHh4", + "name": "添加OSC验证" + }, + { + "number": 113942, + "uuid": "5JYiBYff", + "name": "添加OSC 2K验证" + }, + { + "number": 113943, + "uuid": "S4f2GnKM", + "name": "添加PTC Plus 5MP" + }, + { + "number": 122737, + "uuid": "9aCBMCZM", + "name": "添加K10+Pro扫地机" + }, + { + "number": 122775, + "uuid": "svdK6AdD", + "name": "添加 Circulator Fan验证(无电池款)" + }, + { + "number": 162261, + "uuid": "2E5kn5TV", + "name": "添加K10+ Pro Combo" + }, + { + "number": 162263, + "uuid": "JCqQmM1y", + "name": "K10+ Pro Combo绑定手持吸尘器" + }, + { + "number": 162264, + "uuid": "UvH2Zoiw", + "name": "K10+ Pro Combo解除绑定手持" + }, + { + "number": 190801, + "uuid": "XeoBqvEX", + "name": "添加URC验证" + }, + { + "number": 190802, + "uuid": "WcEHdNnS", + "name": "同步URC设备、控制" + }, + { + "number": 191100, + "uuid": "LiFS1jPW", + "name": "添加Roller Shade验证" + }, + { + "number": 196802, + "uuid": "HhrjuktG", + "name": "添加空气净化器验证(基础款和table款)" + }, + { + "number": 197389, + "uuid": "SZZq5NDW", + "name": "添加Doorbell" + }, + { + "number": 205248, + "uuid": "4jt45mtk", + "name": "添加S20扫地机" + }, + { + "number": 227290, + "uuid": "XY3yL3yL", + "name": "欧区账号登录" + }, + { + "number": 265933, + "uuid": "T4C5Bc4h", + "name": "添加curtain 3 2025验证" + }, + { + "number": 267366, + "uuid": "8h8m2oMK", + "name": "添加Meter pro co2验证" + }, + { + "number": 267367, + "uuid": "AdAxoQss", + "name": "添加Meter pro验证" + }, + { + "number": 273787, + "uuid": "4qEc6uDh", + "name": "添加K11+扫地机" + }, + { + "number": 298968, + "uuid": "YSyKzd5b", + "name": "消息中心" + }, + { + "number": 301585, + "uuid": "MaLqK2LR", + "name": "添加RGBWW Strip light3(彩色灯带3) 验证 " + }, + { + "number": 301586, + "uuid": "DXWPJi2t", + "name": "添加RGBWW Floor Lamp (落地灯)验证" + }, + { + "number": 302288, + "uuid": "ABWcTyTS", + "name": "添加Relay Switch验证" + }, + { + "number": 302289, + "uuid": "SwhcmiAT", + "name": "添加Garage Door Opener验证" + }, + { + "number": 304130, + "uuid": "5EZBMbyC", + "name": "添加Lock Ultra验证" + }, + { + "number": 304131, + "uuid": "8FytfzPz", + "name": "添加Keypad Vision验证" + }, + { + "number": 304132, + "uuid": "CquFqWd6", + "name": "添加Lock Lite验证" + }, + { + "number": 304133, + "uuid": "UtDXaxef", + "name": "添加Lock Pro WiFi验证" + }, + { + "number": 310703, + "uuid": "GRuid7nr", + "name": "添加Climate Panel验证" + }, + { + "number": 310704, + "uuid": "4e4Anr8B", + "name": "添加暖气阀Smart Radiator Thermostat验证" + }, + { + "number": 311018, + "uuid": "Evr4zoiU", + "name": "添加RGBICWW Strip Light(炫彩灯带) 验证" + }, + { + "number": 311019, + "uuid": "2mnWm4kU", + "name": "添加RGBICWW Floor Lamp(炫彩落地灯)验证" + }, + { + "number": 311020, + "uuid": "NrBUAnYg", + "name": "添加RGBIC Neon Wire Rope Light(炫彩钢丝霓虹灯)验证" + }, + { + "number": 319303, + "uuid": "3ibhGBWz", + "name": "添加Hub Mini Matter S3验证" + }, + { + "number": 319304, + "uuid": "Vu6x1gpU", + "name": "添加Hub2 S3验证" + }, + { + "number": 319305, + "uuid": "2qvq8JZa", + "name": "添加Hub Mini S3验证" + }, + { + "number": 332917, + "uuid": "8UFZSbeC", + "name": "添加人体存在传感器" + }, + { + "number": 332918, + "uuid": "Wmyuosou", + "name": "添加eu plug" + }, + { + "number": 332919, + "uuid": "8CXeNBcG", + "name": "添加safety alarm" + }, + { + "number": 393234, + "uuid": "9t9W6kLx", + "name": "添加Standing Circulator fan验证" + }, + { + "number": 393235, + "uuid": "87y8Y8Ni", + "name": "添加RGBIC Neon Rope Light(炫彩霓虹灯)验证" + }, + { + "number": 393236, + "uuid": "DAFQxUYA", + "name": "添加Candle lamp(融蜡灯)验证(融蜡灯需要插110V电压)" + } + ], + "controlCases": { + "15974": { + "name": "WiFi控制设备", + "uuid": "Vp7vuhbu", + "proto": "wifi", + "steps": [ + "Qt4hcgB4", + "7cEkbh4Y", + "GPT3wJd9", + "63kDVNx1", + "8yGTCPwY", + "Ay79Pj23", + "8tx3Rg76", + "Kaj2UdKf", + "QgX4dX7X", + "3nXsFW9n", + "N7fSHmH3", + "JKSgHHbC", + "4oeeJEV8", + "16szaUdD", + "Hj5Z9jSf", + "SaKmFWLw", + "LDqSDPPT", + "HoDdVUm1", + "WH7kyypF", + "J8BQ1p9G", + "KNk1HaB2", + "67oRHoYJ", + "YAeiQE6n", + "TcnL3gFM", + "WL8KSQLb", + "6gv1WLKE", + "B5N4u7Fj", + "5yYJmwLL", + "GYtvdMTW", + "44FEY1z2", + "6RC22BGo", + "Virku9zd", + "BhNuXgg7", + "9bcyjgJD", + "KNyzKhn6", + "W7nK4VEB", + "GWAbic5S", + "ScBpcSfJ", + "AWVhA6za", + "MmodMTvh", + "VbqWg59G", + "N5i3oQP2", + "LAtdvNBV", + "N1kzcb7S", + "2hm6sXrk", + "XvFF8AK7", + "Y9gosqbQ", + "Vro1QBrg", + "c34Tk92u", + "2nRaC87F", + "MAwYyCaC", + "ML8CSkWX", + "5sJcsTgi", + "U5z9wocr", + "BiEXxZDr", + "RXm3iNLR" + ] + }, + "15975": { + "name": "蓝牙控制设备", + "uuid": "Lqpkx6mp", + "proto": "ble", + "steps": [ + "6e4uZSVe", + "5YEfmpJz", + "774V8bND", + "NvpUv9Tt", + "3Jm5FAdW", + "QfYLdj8s", + "GkkrZpaC", + "TNhSAuUC", + "4poyiVwD", + "2528azD6", + "V15M64cP", + "2jSFfY4k", + "UZCtZRPy", + "9mheLMDJ", + "GSZv8BYm", + "HrigrD2X", + "A8C4BsCm", + "3frMRDZj", + "Qbm9VB72", + "QwnZJARA", + "Ncg7BDeR", + "k6cqKqtZ", + "QqsnPMc3", + "CiFPEbNS", + "LnPNEJmS", + "3PmZMk1s", + "JBszgxRS", + "SmzuHtWi", + "7qxA1ajE", + "R5PDp8BR", + "3wM6fW8W", + "JggzNXDo", + "L967RGHa", + "8Wzaj5pB", + "3bYsmiZG", + "NDqRQxVx", + "DxKG9Acs", + "3ob75cjf", + "JVnRz7xY", + "MKgLq1oy", + "A1TLxsFw", + "PG62s725", + "AafJ5tj5", + "E5YRm4Pg", + "QAHNrXrm", + "AfxmoAEq", + "E2QCS8xy", + "TSVKWcJ3", + "XWmNA9UL" + ] + } + } +} \ No newline at end of file diff --git a/tests/air_condition/air_condition_connect.test.ts b/tests/air_condition/air_condition_connect.test.ts index 726ee3b..3937b13 100644 --- a/tests/air_condition/air_condition_connect.test.ts +++ b/tests/air_condition/air_condition_connect.test.ts @@ -40,13 +40,13 @@ describe('Air Condition Connect - 添加红外空调设备', () => { await driver.destroySession(); }); - it('添加红外空调设备', async () => { + it('[ONES:196802] 添加红外空调设备', async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('添加红外空调设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record('[P0][ONES:196802] 添加红外空调设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -135,10 +135,10 @@ describe('Air Condition Connect - 添加红外空调设备', () => { || await waitForSource(driver, 'Air Conditioner', 5000); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record('添加红外空调设备', 'PASS', Date.now() - start, `红外空调添加完成, 耗时${elapsed}s, 首页可见=${found}`); + reporter.record('[P0][ONES:196802] 添加红外空调设备', 'PASS', Date.now() - start, `红外空调添加完成, 耗时${elapsed}s, 首页可见=${found}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('添加红外空调设备', 'FAIL', Date.now() - start, e.message, ss); + reporter.record('[P0][ONES:196802] 添加红外空调设备', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/bot/bot_card.test.ts b/tests/bot/bot_card.test.ts index e963de7..f5edc3b 100644 --- a/tests/bot/bot_card.test.ts +++ b/tests/bot/bot_card.test.ts @@ -6,6 +6,7 @@ import { BOT_LOCATORS } from '../../locators/bot-locators'; import { TestReporter } from '../../utils/test-reporter'; import { getDeviceName } from '../../config/device.config'; import { sleep } from '../../utils/common'; +import { applyProtoNetwork } from '../../utils/common'; import * as dotenv from 'dotenv'; import * as path from 'path'; @@ -13,6 +14,11 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('bot', 'BOT_DEVICE'); +// 必测项控制步骤锚点(协议相关,由 PROTO 环境变量切换): BLE→15975#6e4uZSVe / WiFi→15974#Qt4hcgB4 +// 该 step「点击控制Bot 不加密开&不加密关&加密按压」由 ON/OFF切换 + 加密按压 两个用例共同覆盖,回写时按 step 聚合 +const PROTO = process.env.PROTO === 'wifi' ? 'wifi' : 'ble'; +const CTRL = PROTO === 'wifi' ? '[P0][ONES:15974#Qt4hcgB4][wifi]' : '[P0][ONES:15975#6e4uZSVe][ble]'; + describe('Bot Card - 首页卡片操作', () => { let driver: DeviceDriver; let bot: BotHelper; @@ -23,6 +29,8 @@ describe('Bot Card - 首页卡片操作', () => { await driver.createSession(); bot = new BotHelper(driver); reporter = new TestReporter('Bot_Card', driver.platform.toUpperCase()); + // 双协议前置:按 PROTO 切手机蓝牙/WiFi(ble→开蓝牙关WiFi / wifi→关蓝牙开WiFi),无人值守自动切 + await applyProtoNetwork(driver, PROTO); }); beforeEach(async () => { @@ -256,7 +264,7 @@ describe('Bot Card - 首页卡片操作', () => { console.log('切换后状态:', statusBefore); if (statusBefore === 'unknown') { - reporter.record('ON/OFF切换', 'SKIP', Date.now() - start, '切换Switch Mode后仍无法识别状态'); + reporter.record(`${CTRL} 不加密开/关`, 'SKIP', Date.now() - start, '切换Switch Mode后仍无法识别状态'); return; } } @@ -284,7 +292,7 @@ describe('Bot Card - 首页卡片操作', () => { expect(statusAfter).not.toBe(statusBefore); const detail = `${statusBefore} → ${statusAfter}`; - reporter.record('ON/OFF切换', 'PASS', Date.now() - start, detail); + reporter.record(`${CTRL} 不加密开/关`, 'PASS', Date.now() - start, detail); // Restore state await tapBotAndWaitPopup(); @@ -298,7 +306,7 @@ describe('Bot Card - 首页卡片操作', () => { await sleep(5000); } catch (e: any) { const ss = await captureScreenshot(); - reporter.record('ON/OFF切换', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL} 不加密开/关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); @@ -532,10 +540,10 @@ describe('Bot Card - 首页卡片操作', () => { const stillPress = source.includes('Press Mode'); console.log('按压后状态:', stillPress ? 'Press Mode' : '已执行'); - reporter.record('加密-按压操作', 'PASS', Date.now() - start, `加密按压完成, Press=${stillPress}`); + reporter.record(`${CTRL} 加密按压`, 'PASS', Date.now() - start, `加密按压完成, Press=${stillPress}`); } catch (e: any) { const ss = await captureScreenshot(); - reporter.record('加密-按压操作', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL} 加密按压`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/bot/bot_connect.test.ts b/tests/bot/bot_connect.test.ts index 166e7e4..f74a3f9 100644 --- a/tests/bot/bot_connect.test.ts +++ b/tests/bot/bot_connect.test.ts @@ -31,13 +31,13 @@ describe('Bot Connect - 添加Bot设备', () => { await driver.destroySession(); }); - it(`通过BLE添加${deviceName}设备`, async () => { + it(`[ONES:15968] 通过BLE添加${deviceName}设备`, async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record(`添加${deviceName}`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record(`[P0][ONES:15968] 添加${deviceName}`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -53,10 +53,10 @@ describe('Bot Connect - 添加Bot设备', () => { expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record(`添加${deviceName}`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); + reporter.record(`[P0][ONES:15968] 添加${deviceName}`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record(`添加${deviceName}`, 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`[P0][ONES:15968] 添加${deviceName}`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/ceiling_light/ceiling_light_connect.test.ts b/tests/ceiling_light/ceiling_light_connect.test.ts index 9a0e2f3..d408a47 100644 --- a/tests/ceiling_light/ceiling_light_connect.test.ts +++ b/tests/ceiling_light/ceiling_light_connect.test.ts @@ -43,13 +43,13 @@ describe('CeilingLight Connect - 添加吸顶灯设备', () => { await driver.destroySession(); }); - it('通过BLE添加吸顶灯', async () => { + it('[ONES:15950] 通过BLE添加吸顶灯', async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('添加吸顶灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record('[P0][ONES:15950] 添加吸顶灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -65,10 +65,10 @@ describe('CeilingLight Connect - 添加吸顶灯设备', () => { expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record('添加吸顶灯设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); + reporter.record('[P0][ONES:15950] 添加吸顶灯设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('添加吸顶灯设备', 'FAIL', Date.now() - start, e.message, ss); + reporter.record('[P0][ONES:15950] 添加吸顶灯设备', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/ceiling_light/ceiling_light_control.test.ts b/tests/ceiling_light/ceiling_light_control.test.ts index 128d9cc..18ade62 100644 --- a/tests/ceiling_light/ceiling_light_control.test.ts +++ b/tests/ceiling_light/ceiling_light_control.test.ts @@ -28,6 +28,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('ceilingLight', 'CEILING_LIGHT_DEVICE'); +// 必测项控制锚点: 点击控制吸顶灯pro开/关灯 BLE 15975#2528azD6 / WiFi 15974#TcnL3gFM +const PROTO = process.env.PROTO === 'wifi' ? 'wifi' : 'ble'; +const CTRL_CEIL = PROTO === 'wifi' ? '[P0][ONES:15974#TcnL3gFM][wifi]' : '[P0][ONES:15975#2528azD6][ble]'; + describe('CeilingLight Control - 吸顶灯功能页', () => { let driver: DeviceDriver; let reporter: TestReporter; @@ -72,10 +76,10 @@ describe('CeilingLight Control - 吸顶灯功能页', () => { const isOn = source.includes('ON') || source.includes('Night Light') || source.includes('Full'); expect(isOn).toBe(true); - reporter.record('打开开关', 'PASS', Date.now() - start, `吸顶灯已开启`); + reporter.record(`${CTRL_CEIL} 打开开关`, 'PASS', Date.now() - start, `吸顶灯已开启`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('打开开关', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_CEIL} 打开开关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); @@ -254,10 +258,10 @@ describe('CeilingLight Control - 吸顶灯功能页', () => { !source.includes('Night Light'); console.log('吸顶灯已关闭:', isOff); - reporter.record('关闭开关', 'PASS', Date.now() - start, `吸顶灯已关闭`); + reporter.record(`${CTRL_CEIL} 关闭开关`, 'PASS', Date.now() - start, `吸顶灯已关闭`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('关闭开关', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_CEIL} 关闭开关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/color_bulb/color_bulb_connect.test.ts b/tests/color_bulb/color_bulb_connect.test.ts index f9d9947..3b28a9e 100644 --- a/tests/color_bulb/color_bulb_connect.test.ts +++ b/tests/color_bulb/color_bulb_connect.test.ts @@ -43,13 +43,13 @@ describe('ColorBulb Connect - 添加彩灯设备', () => { await driver.destroySession(); }); - it('通过BLE添加彩灯', async () => { + it('[ONES:15960] 通过BLE添加彩灯', async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('添加彩灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record('[P0][ONES:15960] 添加彩灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -65,10 +65,10 @@ describe('ColorBulb Connect - 添加彩灯设备', () => { expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record('添加彩灯设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); + reporter.record('[P0][ONES:15960] 添加彩灯设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('添加彩灯设备', 'FAIL', Date.now() - start, e.message, ss); + reporter.record('[P0][ONES:15960] 添加彩灯设备', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/color_bulb/color_bulb_control.test.ts b/tests/color_bulb/color_bulb_control.test.ts index 176220a..d3a0580 100644 --- a/tests/color_bulb/color_bulb_control.test.ts +++ b/tests/color_bulb/color_bulb_control.test.ts @@ -28,6 +28,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('colorBulb', 'COLOR_BULB_DEVICE'); +// 必测项控制锚点(协议相关): 点击控制Bulb 开/关 BLE 15975#TNhSAuUC / WiFi 15974#Kaj2UdKf +const PROTO = process.env.PROTO === 'wifi' ? 'wifi' : 'ble'; +const CTRL_BULB = PROTO === 'wifi' ? '[P0][ONES:15974#Kaj2UdKf][wifi]' : '[P0][ONES:15975#TNhSAuUC][ble]'; + describe('ColorBulb Control - 彩灯功能页', () => { let driver: DeviceDriver; let reporter: TestReporter; @@ -72,10 +76,10 @@ describe('ColorBulb Control - 彩灯功能页', () => { const isOn = source.includes('ON') || source.includes('Dynamic') || source.includes('Color'); expect(isOn).toBe(true); - reporter.record('打开开关', 'PASS', Date.now() - start, `彩灯已开启`); + reporter.record(`${CTRL_BULB} 打开开关`, 'PASS', Date.now() - start, `彩灯已开启`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('打开开关', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_BULB} 打开开关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/curtain/curtain_connect.test.ts b/tests/curtain/curtain_connect.test.ts index 481626b..be8f8d9 100644 --- a/tests/curtain/curtain_connect.test.ts +++ b/tests/curtain/curtain_connect.test.ts @@ -15,6 +15,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('curtain', 'CURTAIN_DEVICE'); +// 设备维度动态添加锚点:按当前 CURTAIN_DEVICE 解析(Curtain/Curtain3/BlindTilt 各自的 ONES 号) +import { onesAdd } from '../../utils/common'; +const ADD_ANCHOR = onesAdd('curtain', deviceName); + describe('Curtain Connect - 通过BLE添加窗帘设备', () => { let driver: DeviceDriver; let reporter: TestReporter; @@ -30,13 +34,13 @@ describe('Curtain Connect - 通过BLE添加窗帘设备', () => { await driver.destroySession(); }); - it('通过BLE添加窗帘设备', async () => { + it(`${ADD_ANCHOR} 通过BLE添加窗帘设备`, async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('添加窗帘设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -53,10 +57,10 @@ describe('Curtain Connect - 通过BLE添加窗帘设备', () => { expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record('添加窗帘设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); + reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('添加窗帘设备', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/curtain/curtain_control.test.ts b/tests/curtain/curtain_control.test.ts index dfed257..4271fea 100644 --- a/tests/curtain/curtain_control.test.ts +++ b/tests/curtain/curtain_control.test.ts @@ -19,6 +19,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('curtain', 'CURTAIN_DEVICE'); +// 设备维度动态控制锚点:按当前 CURTAIN_DEVICE + PROTO 解析(Curtain/Curtain3/BlindTilt 各自的 step) +import { onesCtrl } from '../../utils/common'; +const CTRL_CURTAIN = onesCtrl('curtain', deviceName); + describe('Curtain Control - 窗帘控制功能', () => { let driver: DeviceDriver; let reporter: TestReporter; @@ -68,10 +72,10 @@ describe('Curtain Control - 窗帘控制功能', () => { source.includes('Opening') || source.includes('Opened'); console.log('打开窗帘状态:', statusChanged); - reporter.record('打开窗帘', 'PASS', Date.now() - start, `Open按钮点击成功, 状态变化=${statusChanged}`); + reporter.record(`${CTRL_CURTAIN} 打开窗帘`, 'PASS', Date.now() - start, `Open按钮点击成功, 状态变化=${statusChanged}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('打开窗帘', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_CURTAIN} 打开窗帘`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); @@ -91,10 +95,10 @@ describe('Curtain Control - 窗帘控制功能', () => { source.includes('Closing') || source.includes('Closed'); console.log('关闭窗帘状态:', statusChanged); - reporter.record('关闭窗帘', 'PASS', Date.now() - start, `Close按钮点击成功, 状态变化=${statusChanged}`); + reporter.record(`${CTRL_CURTAIN} 关闭窗帘`, 'PASS', Date.now() - start, `Close按钮点击成功, 状态变化=${statusChanged}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('关闭窗帘', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_CURTAIN} 关闭窗帘`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/fan/fan_connect.test.ts b/tests/fan/fan_connect.test.ts index 4689b0e..efaabfa 100644 --- a/tests/fan/fan_connect.test.ts +++ b/tests/fan/fan_connect.test.ts @@ -53,13 +53,13 @@ describe('Fan Connect - 添加风扇设备', () => { await driver.destroySession(); }); - it('通过BLE添加风扇', async () => { + it('[ONES:122775] 通过BLE添加风扇', async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('通过BLE添加风扇', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record('[P0][ONES:122775] 通过BLE添加风扇', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -75,10 +75,10 @@ describe('Fan Connect - 添加风扇设备', () => { expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record('通过BLE添加风扇', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); + reporter.record('[P0][ONES:122775] 通过BLE添加风扇', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('通过BLE添加风扇', 'FAIL', Date.now() - start, e.message, ss); + reporter.record('[P0][ONES:122775] 通过BLE添加风扇', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/fan/fan_control.test.ts b/tests/fan/fan_control.test.ts index c579650..6285888 100644 --- a/tests/fan/fan_control.test.ts +++ b/tests/fan/fan_control.test.ts @@ -32,6 +32,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('fan', 'FAN_DEVICE'); +// 必测项控制锚点: 无电池款 Circulator Fan 首页控制开关 BLE 15975#JBszgxRS / WiFi 15974#KNyzKhn6 +const PROTO = process.env.PROTO === 'wifi' ? 'wifi' : 'ble'; +const CTRL_FAN = PROTO === 'wifi' ? '[P0][ONES:15974#KNyzKhn6][wifi]' : '[P0][ONES:15975#JBszgxRS][ble]'; + describe('Fan Control - 风扇控制页', () => { let driver: DeviceDriver; let reporter: TestReporter; @@ -81,10 +85,10 @@ describe('Fan Control - 风扇控制页', () => { const isOn = source.includes('ON') || source.includes('Normal') || source.includes('Speed'); console.log('风扇开启:', isOn); - reporter.record('打开开关', 'PASS', Date.now() - start, `Power已点击, ON=${isOn}`); + reporter.record(`${CTRL_FAN} 打开开关`, 'PASS', Date.now() - start, `Power已点击, ON=${isOn}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('打开开关', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_FAN} 打开开关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); @@ -390,10 +394,10 @@ describe('Fan Control - 风扇控制页', () => { const isOff = source.includes('OFF') || source.includes('off') || source.includes('Disconnected'); console.log('风扇关闭:', isOff); - reporter.record('关闭开关', 'PASS', Date.now() - start, `Power OFF=${isOff}`); + reporter.record(`${CTRL_FAN} 关闭开关`, 'PASS', Date.now() - start, `Power OFF=${isOff}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('关闭开关', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_FAN} 关闭开关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/hub/hub_connect.test.ts b/tests/hub/hub_connect.test.ts index af5943f..99ed4ef 100644 --- a/tests/hub/hub_connect.test.ts +++ b/tests/hub/hub_connect.test.ts @@ -14,6 +14,8 @@ import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('hub', 'HUB_DEVICE'); +import { onesAdd } from "../../utils/common"; +const ADD_ANCHOR = onesAdd("hub", deviceName); describe('Hub Connect - 添加Hub设备', () => { let driver: DeviceDriver; @@ -30,13 +32,13 @@ describe('Hub Connect - 添加Hub设备', () => { await driver.destroySession(); }); - it('通过BLE添加Hub设备', async () => { + it(`${ADD_ANCHOR} 通过BLE添加Hub设备`, async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('添加Hub设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record(`${ADD_ANCHOR} 添加Hub设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -52,10 +54,10 @@ describe('Hub Connect - 添加Hub设备', () => { expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record('添加Hub设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); + reporter.record(`${ADD_ANCHOR} 添加Hub设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('添加Hub设备', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${ADD_ANCHOR} 添加Hub设备`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/humidifier/humidifier_connect.test.ts b/tests/humidifier/humidifier_connect.test.ts index f857b3a..85f5cbe 100644 --- a/tests/humidifier/humidifier_connect.test.ts +++ b/tests/humidifier/humidifier_connect.test.ts @@ -14,6 +14,8 @@ import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('humidifier', 'HUMIDIFIER_DEVICE'); +import { onesAdd } from "../../utils/common"; +const ADD_ANCHOR = onesAdd("humidifier", deviceName); describe('Humidifier Connect - 添加加湿器设备', () => { let driver: DeviceDriver; @@ -30,13 +32,13 @@ describe('Humidifier Connect - 添加加湿器设备', () => { await driver.destroySession(); }); - it('通过BLE添加加湿器设备', async () => { + it(`${ADD_ANCHOR} 通过BLE添加加湿器设备`, async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('添加加湿器设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record(`${ADD_ANCHOR} 添加加湿器设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -52,10 +54,10 @@ describe('Humidifier Connect - 添加加湿器设备', () => { expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record('添加加湿器设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); + reporter.record(`${ADD_ANCHOR} 添加加湿器设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('添加加湿器设备', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${ADD_ANCHOR} 添加加湿器设备`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/humidifier/humidifier_control.test.ts b/tests/humidifier/humidifier_control.test.ts index bce96d2..b142a38 100644 --- a/tests/humidifier/humidifier_control.test.ts +++ b/tests/humidifier/humidifier_control.test.ts @@ -17,6 +17,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('humidifier', 'HUMIDIFIER_DEVICE'); +// 必测项控制锚点: Humidifier 快捷弹窗开启加湿器(自动/低/中/高/关) BLE 15975#GkkrZpaC / WiFi 15974#8tx3Rg76 +import { onesCtrl } from "../../utils/common"; +const CTRL_HUMID = onesCtrl("humidifier", deviceName); + describe('Humidifier Control - 功能页操作', () => { let driver: DeviceDriver; let reporter: TestReporter; @@ -61,10 +65,10 @@ describe('Humidifier Control - 功能页操作', () => { const isOn = source.includes('ON') || source.includes('Auto') || source.includes('Manual'); expect(isOn).toBe(true); - reporter.record('功能页打开开关', 'PASS', Date.now() - start, `加湿器已开启, ON=${isOn}`); + reporter.record(`${CTRL_HUMID} 功能页打开开关`, 'PASS', Date.now() - start, `加湿器已开启, ON=${isOn}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('功能页打开开关', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_HUMID} 功能页打开开关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); @@ -279,10 +283,10 @@ describe('Humidifier Control - 功能页操作', () => { !source.includes('Auto'); console.log('加湿器已关闭:', isOff); - reporter.record('功能页关闭开关', 'PASS', Date.now() - start, `加湿器已关闭`); + reporter.record(`${CTRL_HUMID} 功能页关闭开关`, 'PASS', Date.now() - start, `加湿器已关闭`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('功能页关闭开关', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_HUMID} 功能页关闭开关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/keypad/keypad_connect.test.ts b/tests/keypad/keypad_connect.test.ts index c488dde..9a06f93 100644 --- a/tests/keypad/keypad_connect.test.ts +++ b/tests/keypad/keypad_connect.test.ts @@ -53,13 +53,13 @@ describe('Keypad Connect - 添加Keypad设备', () => { await driver.destroySession(); }); - it('通过BLE添加Keypad', async () => { + it('[ONES:15949] 通过BLE添加Keypad', async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('通过BLE添加Keypad', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record('[P0][ONES:15949] 通过BLE添加Keypad', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -75,10 +75,10 @@ describe('Keypad Connect - 添加Keypad设备', () => { expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record('通过BLE添加Keypad', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); + reporter.record('[P0][ONES:15949] 通过BLE添加Keypad', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('通过BLE添加Keypad', 'FAIL', Date.now() - start, e.message, ss); + reporter.record('[P0][ONES:15949] 通过BLE添加Keypad', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/lock/lock_connect.test.ts b/tests/lock/lock_connect.test.ts index ba611c9..d3b4bed 100644 --- a/tests/lock/lock_connect.test.ts +++ b/tests/lock/lock_connect.test.ts @@ -14,6 +14,8 @@ import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('lock', 'LOCK_DEVICE'); +import { onesAdd } from "../../utils/common"; +const ADD_ANCHOR = onesAdd("lock", deviceName); describe('Lock Connect - 添加Lock设备', () => { let driver: DeviceDriver; @@ -30,13 +32,13 @@ describe('Lock Connect - 添加Lock设备', () => { await driver.destroySession(); }); - it('通过BLE添加Lock设备', async () => { + it(`${ADD_ANCHOR} 通过BLE添加Lock设备`, async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('添加Lock设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record(`${ADD_ANCHOR} 添加Lock设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -53,10 +55,10 @@ describe('Lock Connect - 添加Lock设备', () => { expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record('添加Lock设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); + reporter.record(`${ADD_ANCHOR} 添加Lock设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('添加Lock设备', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${ADD_ANCHOR} 添加Lock设备`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/lock/lock_control.test.ts b/tests/lock/lock_control.test.ts index 1a7f61a..bc6f320 100644 --- a/tests/lock/lock_control.test.ts +++ b/tests/lock/lock_control.test.ts @@ -15,6 +15,11 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('lock', 'LOCK_DEVICE'); +// 必测项控制锚点(协议相关): 点击控制Lock 开锁/解锁 BLE 15975#5YEfmpJz / WiFi 15974#7cEkbh4Y +// 上锁+解锁两个用例同覆盖此 step,回写按 step 聚合 +import { onesCtrl } from "../../utils/common"; +const CTRL_LOCK = onesCtrl("lock", deviceName); + describe('Lock Control - 功能页操作', () => { let driver: DeviceDriver; let reporter: TestReporter; @@ -68,10 +73,10 @@ describe('Lock Control - 功能页操作', () => { console.log('上锁状态:', isLocked); expect(isLocked).toBe(true); - reporter.record('功能页-上锁', 'PASS', Date.now() - start, '门锁已上锁'); + reporter.record(`${CTRL_LOCK} 上锁`, 'PASS', Date.now() - start, '门锁已上锁'); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('功能页-上锁', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_LOCK} 上锁`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); @@ -105,10 +110,10 @@ describe('Lock Control - 功能页操作', () => { await sleep(5000); } - reporter.record('功能页-解锁', 'PASS', Date.now() - start, '门锁已解锁'); + reporter.record(`${CTRL_LOCK} 解锁`, 'PASS', Date.now() - start, '门锁已解锁'); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('功能页-解锁', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_LOCK} 解锁`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/meter/meter_connect.test.ts b/tests/meter/meter_connect.test.ts index 2849f28..eff5712 100644 --- a/tests/meter/meter_connect.test.ts +++ b/tests/meter/meter_connect.test.ts @@ -14,6 +14,8 @@ import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('meter', 'METER_DEVICE'); +import { onesAdd } from "../../utils/common"; +const ADD_ANCHOR = onesAdd("meter", deviceName); describe('Meter Connect - 添加Meter设备', () => { let driver: DeviceDriver; @@ -30,13 +32,13 @@ describe('Meter Connect - 添加Meter设备', () => { await driver.destroySession(); }); - it('通过BLE添加Meter设备', async () => { + it(`${ADD_ANCHOR} 通过BLE添加Meter设备`, async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('添加Meter设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record(`${ADD_ANCHOR} 添加Meter设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -52,10 +54,10 @@ describe('Meter Connect - 添加Meter设备', () => { expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record('添加Meter设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); + reporter.record(`${ADD_ANCHOR} 添加Meter设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('添加Meter设备', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${ADD_ANCHOR} 添加Meter设备`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/meter/meter_control.test.ts b/tests/meter/meter_control.test.ts index 7f00c95..56b41bc 100644 --- a/tests/meter/meter_control.test.ts +++ b/tests/meter/meter_control.test.ts @@ -14,6 +14,9 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('meter', 'METER_DEVICE'); +// 必测项: meter 控制步(报警/校正)在此文件无对应用例,跳过;切换单位对应 feature 用例 74078(case 级,无协议) +const FEAT_UNIT = '[P0][ONES:74078]'; + describe('Meter Control - 功能页操作', () => { let driver: DeviceDriver; let reporter: TestReporter; @@ -58,10 +61,10 @@ describe('Meter Control - 功能页操作', () => { const hasCelsius = source.includes('°C') || source.includes('℃'); expect(hasCelsius).toBe(true); - reporter.record('切换单位为°C', 'PASS', Date.now() - start, '温度单位已切换为°C'); + reporter.record(`${FEAT_UNIT} 切换单位为°C`, 'PASS', Date.now() - start, '温度单位已切换为°C'); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('切换单位为°C', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${FEAT_UNIT} 切换单位为°C`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); @@ -91,10 +94,10 @@ describe('Meter Control - 功能页操作', () => { await sleep(1000); } - reporter.record('切换单位为°F', 'PASS', Date.now() - start, '温度单位已切换为°F并还原'); + reporter.record(`${FEAT_UNIT} 切换单位为°F`, 'PASS', Date.now() - start, '温度单位已切换为°F并还原'); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('切换单位为°F', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${FEAT_UNIT} 切换单位为°F`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/osc/osc_connect.test.ts b/tests/osc/osc_connect.test.ts index 8d66ad2..827b8fb 100644 --- a/tests/osc/osc_connect.test.ts +++ b/tests/osc/osc_connect.test.ts @@ -40,13 +40,13 @@ describe('OSC Connect - 通过BLE添加OSC', () => { await driver.destroySession(); }); - it('通过BLE添加OSC', async () => { + it('[ONES:113942] 通过BLE添加OSC', async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('通过BLE添加OSC', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record('[P0][ONES:113942] 通过BLE添加OSC', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -67,14 +67,14 @@ describe('OSC Connect - 通过BLE添加OSC', () => { const elapsed = ((Date.now() - start) / 1000).toFixed(1); if (success) { - reporter.record('通过BLE添加OSC', 'PASS', Date.now() - start, `OSC添加成功, 耗时${elapsed}s`); + reporter.record('[P0][ONES:113942] 通过BLE添加OSC', 'PASS', Date.now() - start, `OSC添加成功, 耗时${elapsed}s`); } else { - reporter.record('通过BLE添加OSC', 'FAIL', Date.now() - start, `OSC添加失败, 耗时${elapsed}s`); + reporter.record('[P0][ONES:113942] 通过BLE添加OSC', 'FAIL', Date.now() - start, `OSC添加失败, 耗时${elapsed}s`); throw new Error('OSC添加失败'); } } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('通过BLE添加OSC', 'FAIL', Date.now() - start, e.message, ss); + reporter.record('[P0][ONES:113942] 通过BLE添加OSC', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/plug/plug_connect.test.ts b/tests/plug/plug_connect.test.ts index ff191ae..3096e22 100644 --- a/tests/plug/plug_connect.test.ts +++ b/tests/plug/plug_connect.test.ts @@ -14,6 +14,8 @@ import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('plug', 'PLUG_DEVICE'); +import { onesAdd } from "../../utils/common"; +const ADD_ANCHOR = onesAdd("plug", deviceName); describe('Plug Connect - 添加Plug设备', () => { let driver: DeviceDriver; @@ -30,13 +32,13 @@ describe('Plug Connect - 添加Plug设备', () => { await driver.destroySession(); }); - it('通过BLE添加Plug设备', async () => { + it(`${ADD_ANCHOR} 通过BLE添加Plug设备`, async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('添加Plug设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record(`${ADD_ANCHOR} 添加Plug设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -52,10 +54,10 @@ describe('Plug Connect - 添加Plug设备', () => { expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record('添加Plug设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); + reporter.record(`${ADD_ANCHOR} 添加Plug设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('添加Plug设备', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${ADD_ANCHOR} 添加Plug设备`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/plug/plug_control.test.ts b/tests/plug/plug_control.test.ts index 6ebe849..3f7ebef 100644 --- a/tests/plug/plug_control.test.ts +++ b/tests/plug/plug_control.test.ts @@ -19,6 +19,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('plug', 'PLUG_DEVICE'); +// 必测项控制锚点: 点击控制Plug 开/关 仅 WiFi 必测(15974#Ay79Pj23);BLE 无 base Plug 必测步 → ble 模式不打锚点 +import { onesCtrl } from "../../utils/common"; +const CTRL_PLUG = onesCtrl("plug", deviceName); + describe('Plug Control - 功能页操作', () => { let driver: DeviceDriver; let reporter: TestReporter; @@ -88,10 +92,10 @@ describe('Plug Control - 功能页操作', () => { await sleep(5000); expect(statusChanged).toBe(true); - reporter.record('功能页打开/关闭开关', 'PASS', Date.now() - start, `控制页开关切换成功`); + reporter.record(`${CTRL_PLUG}功能页打开/关闭开关`, 'PASS', Date.now() - start, `控制页开关切换成功`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('功能页打开/关闭开关', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_PLUG}功能页打开/关闭开关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/robot/robot_connect.test.ts b/tests/robot/robot_connect.test.ts index baf71fe..6d8af6a 100644 --- a/tests/robot/robot_connect.test.ts +++ b/tests/robot/robot_connect.test.ts @@ -24,6 +24,8 @@ import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('robot', 'ROBOT_DEVICE'); +import { onesAdd } from "../../utils/common"; +const ADD_ANCHOR = onesAdd("robot", deviceName); describe('Robot Connect - 添加扫地机', () => { let driver: DeviceDriver; @@ -40,13 +42,13 @@ describe('Robot Connect - 添加扫地机', () => { await driver.destroySession(); }); - it('添加扫地机', async () => { + it(`${ADD_ANCHOR} 添加扫地机`, async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('添加扫地机', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -67,14 +69,14 @@ describe('Robot Connect - 添加扫地机', () => { const elapsed = ((Date.now() - start) / 1000).toFixed(1); if (success) { - reporter.record('添加扫地机', 'PASS', Date.now() - start, `扫地机添加成功, 耗时${elapsed}s`); + reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'PASS', Date.now() - start, `扫地机添加成功, 耗时${elapsed}s`); } else { - reporter.record('添加扫地机', 'FAIL', Date.now() - start, `扫地机添加失败, 耗时${elapsed}s`); + reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'FAIL', Date.now() - start, `扫地机添加失败, 耗时${elapsed}s`); throw new Error('扫地机添加失败'); } } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('添加扫地机', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/sensor/sensor_control.test.ts b/tests/sensor/sensor_control.test.ts index 2045caf..61b5484 100644 --- a/tests/sensor/sensor_control.test.ts +++ b/tests/sensor/sensor_control.test.ts @@ -15,6 +15,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('sensor', 'SENSOR_DEVICE'); +// 必测项控制锚点: Contact Sensor 功能页显示开关状态 BLE 15975#A8C4BsCm / WiFi 15974#JKSgHHbC +import { onesCtrl } from "../../utils/common"; +const CTRL_SENSOR = onesCtrl("sensor", deviceName); + describe('Sensor Control - 功能页操作', () => { let driver: DeviceDriver; let reporter: TestReporter; @@ -64,10 +68,10 @@ describe('Sensor Control - 功能页操作', () => { console.log(detail); expect(hasDetectionStatus || hasIndicatorLight).toBe(true); - reporter.record('功能页显示', 'PASS', Date.now() - start, detail); + reporter.record(`${CTRL_SENSOR} 功能页显示`, 'PASS', Date.now() - start, detail); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('功能页显示', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_SENSOR} 功能页显示`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/strip_light/strip_light_connect.test.ts b/tests/strip_light/strip_light_connect.test.ts index 19e3941..4f78ed6 100644 --- a/tests/strip_light/strip_light_connect.test.ts +++ b/tests/strip_light/strip_light_connect.test.ts @@ -43,13 +43,13 @@ describe('StripLight Connect - 添加灯带设备', () => { await driver.destroySession(); }); - it('通过BLE添加灯带', async () => { + it('[ONES:15959] 通过BLE添加灯带', async () => { const start = Date.now(); try { const alreadyExists = await isDeviceOnHomepage(driver, deviceName); if (alreadyExists) { console.log(`${deviceName}已在首页,跳过重新添加`); - reporter.record('添加灯带设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); + reporter.record('[P0][ONES:15959] 添加灯带设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); return; } @@ -65,10 +65,10 @@ describe('StripLight Connect - 添加灯带设备', () => { expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); - reporter.record('添加灯带设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); + reporter.record('[P0][ONES:15959] 添加灯带设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('添加灯带设备', 'FAIL', Date.now() - start, e.message, ss); + reporter.record('[P0][ONES:15959] 添加灯带设备', 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/strip_light/strip_light_control.test.ts b/tests/strip_light/strip_light_control.test.ts index ba5aacc..b9dbab5 100644 --- a/tests/strip_light/strip_light_control.test.ts +++ b/tests/strip_light/strip_light_control.test.ts @@ -28,6 +28,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('stripLight', 'STRIP_LIGHT_DEVICE'); +// 必测项控制锚点: 点击控制Strip Light开/关 BLE 15975#4poyiVwD / WiFi 15974#QgX4dX7X +const PROTO = process.env.PROTO === 'wifi' ? 'wifi' : 'ble'; +const CTRL_STRIP = PROTO === 'wifi' ? '[P0][ONES:15974#QgX4dX7X][wifi]' : '[P0][ONES:15975#4poyiVwD][ble]'; + describe('StripLight Control - 灯带功能页', () => { let driver: DeviceDriver; let reporter: TestReporter; @@ -72,10 +76,10 @@ describe('StripLight Control - 灯带功能页', () => { const isOn = source.includes('ON') || source.includes('White') || source.includes('Scene'); expect(isOn).toBe(true); - reporter.record('打开开关', 'PASS', Date.now() - start, `灯带已开启`); + reporter.record(`${CTRL_STRIP} 打开开关`, 'PASS', Date.now() - start, `灯带已开启`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('打开开关', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_STRIP} 打开开关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); @@ -245,10 +249,10 @@ describe('StripLight Control - 灯带功能页', () => { !source.includes('Scene'); console.log('灯带已关闭:', isOff); - reporter.record('关闭开关', 'PASS', Date.now() - start, `灯带已关闭`); + reporter.record(`${CTRL_STRIP} 关闭开关`, 'PASS', Date.now() - start, `灯带已关闭`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('关闭开关', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${CTRL_STRIP} 关闭开关`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/tests/urc/urc_control.test.ts b/tests/urc/urc_control.test.ts index a37481c..5e36dfa 100644 --- a/tests/urc/urc_control.test.ts +++ b/tests/urc/urc_control.test.ts @@ -25,6 +25,9 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const deviceName = getDeviceName('urc', 'URC_DEVICE'); +// 必测项 feature: 同步URC设备、控制 → 用例 190802(case 级,无协议) +const FEAT_URC = '[P0][ONES:190802]'; + describe('URC Control - 万能遥控器功能操作', () => { let driver: DeviceDriver; let reporter: TestReporter; @@ -159,10 +162,10 @@ describe('URC Control - 万能遥控器功能操作', () => { || source.includes('完成') || source.includes('Device Management'); console.log('同步一个设备:', syncDone); - reporter.record('设备管理-同步一个设备', 'PASS', Date.now() - start, `同步完成=${syncDone}`); + reporter.record(`${FEAT_URC} 设备管理-同步一个设备`, 'PASS', Date.now() - start, `同步完成=${syncDone}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('设备管理-同步一个设备', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${FEAT_URC} 设备管理-同步一个设备`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); @@ -200,10 +203,10 @@ describe('URC Control - 万能遥控器功能操作', () => { const syncDone = source.includes('Synced') || source.includes('Success') || source.includes('完成') || source.includes('Device Management'); - reporter.record('设备管理-同步多个设备', 'PASS', Date.now() - start, `同步${selectCount}个设备完成=${syncDone}`); + reporter.record(`${FEAT_URC} 设备管理-同步多个设备`, 'PASS', Date.now() - start, `同步${selectCount}个设备完成=${syncDone}`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); - reporter.record('设备管理-同步多个设备', 'FAIL', Date.now() - start, e.message, ss); + reporter.record(`${FEAT_URC} 设备管理-同步多个设备`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); diff --git a/utils/common/index.ts b/utils/common/index.ts index bd3da20..e86a675 100644 --- a/utils/common/index.ts +++ b/utils/common/index.ts @@ -8,3 +8,5 @@ export * from './room.helper'; export * from './timer.helper'; export * from './scene.helper'; export * from './feedback.helper'; +export * from './network.helper'; +export * from './ones-anchor.helper'; diff --git a/utils/common/network.helper.ts b/utils/common/network.helper.ts new file mode 100644 index 0000000..43ac544 --- /dev/null +++ b/utils/common/network.helper.ts @@ -0,0 +1,121 @@ +/** + * 双协议网络前置:按 PROTO 切手机蓝牙/WiFi,用于必测项控制用例的 BLE/WiFi 两种模式。 + * + * - ble 模式: 开蓝牙、关 WiFi → app 走 BLE 直连 + * - wifi 模式: 关蓝牙、开 WiFi → app 走 WiFi/云 + * + * 平台能力(已在三星真机实测): + * - Android WiFi : adb `svc wifi enable/disable` 可行(dumpsys 实测真关/开) + * - Android 蓝牙 : adb `svc bluetooth` 被新系统禁用(exit 137)、`cmd bluetooth_manager` 无实现、 + * `settings put bluetooth_on` 不动 radio → 改为 `am start 蓝牙设置` + 点 switch_widget(实测可切 ON↔BLE_ON) + * - iOS : 无公开 API,走系统设置(com.apple.Preferences)UI;locator 需 iOS 真机校准 + */ +import { execSync } from 'child_process'; +import { DeviceDriver } from '../../drivers/types'; +import { APP_CONFIG } from '../../config/app.config'; +import { sleep } from './element.helper'; + +function adbShell(cmd: string): string { + return execSync(`adb shell ${cmd}`, { encoding: 'utf-8', timeout: 20000 }); +} + +// ---------- Android(纯 adb,已实测) ---------- +async function androidSetWifi(on: boolean): Promise { + adbShell(`svc wifi ${on ? 'enable' : 'disable'}`); + await sleep(2000); +} + +async function androidSetBluetooth(on: boolean): Promise { + adbShell('am start -a android.settings.BLUETOOTH_SETTINGS'); + await sleep(2500); + adbShell('uiautomator dump /sdcard/ui.xml'); + const xml = adbShell('cat /sdcard/ui.xml'); + // 三星实测节点: resource-id="com.android.settings:id/switch_widget" class=Switch checked=.. bounds=[x1,y1][x2,y2] + const m = xml.match(/switch_widget"[^>]*?checked="(true|false)"[^>]*?bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/); + if (!m) { + console.warn('[network] 未找到蓝牙开关 switch_widget,跳过(请真机校准 locator)'); + return; + } + const isOn = m[1] === 'true'; + if (isOn !== on) { + const cx = Math.round((Number(m[2]) + Number(m[4])) / 2); + const cy = Math.round((Number(m[3]) + Number(m[5])) / 2); + adbShell(`input tap ${cx} ${cy}`); + await sleep(3000); + } +} + +// ---------- iOS(系统设置 UI,locator 已在 iPhone/iOS26.5 实测) ---------- +async function iosSetBluetooth(driver: DeviceDriver, on: boolean): Promise { + await driver.activateApp('com.apple.Preferences'); + await sleep(2000); + // 进入蓝牙二级页:设置首页 cell(文案本地化,中文"蓝牙"/英文"Bluetooth") + const entry = + (await driver.findElementRaw('accessibility id', '蓝牙').catch(() => null)) || + (await driver.findElementRaw('accessibility id', 'Bluetooth').catch(() => null)); + if (entry) { + await driver.tapElement(entry); + await sleep(2000); + } + // 蓝牙开关:name="BLUETOOTH"(与语言无关),value "1"=开 / "0"=关 + const sw = await driver.findElementRaw('accessibility id', 'BLUETOOTH').catch(() => null); + if (!sw) { + console.warn('[network] iOS 未找到蓝牙开关(accessibility id=BLUETOOTH),跳过'); + return; + } + const val = await driver.getElementAttribute(sw, 'value').catch(() => ''); + const isOn = val === '1' || val === 'true'; + if (isOn !== on) { + await driver.tapElement(sw); + await sleep(2500); + } +} + +// iOS WiFi(无 adb,走系统设置;locator 已在 iOS26.5 实测:开关 name="无线局域网" value 1/0) +async function iosSetWifi(driver: DeviceDriver, on: boolean): Promise { + await driver.activateApp('com.apple.Preferences'); + await sleep(2000); + // 进入 WiFi 页:cell 文案本地化(中文"无线局域网" / 英文"Wi-Fi"/"WLAN") + const entry = + (await driver.findElementRaw('accessibility id', '无线局域网').catch(() => null)) || + (await driver.findElementRaw('accessibility id', 'Wi-Fi').catch(() => null)) || + (await driver.findElementRaw('accessibility id', 'WLAN').catch(() => null)); + if (entry) { + await driver.tapElement(entry); + await sleep(2000); + } + // WiFi 开关:name="无线局域网"(本地化), value "1"=开 / "0"=关 + const sw = + (await driver.findElementRaw('accessibility id', '无线局域网').catch(() => null)) || + (await driver.findElementRaw('accessibility id', 'Wi-Fi').catch(() => null)); + if (!sw) { + console.warn('[network] iOS 未找到 WiFi 开关,跳过'); + return; + } + const val = await driver.getElementAttribute(sw, 'value').catch(() => ''); + const isOn = val === '1' || val === 'true'; + if (isOn !== on) { + await driver.tapElement(sw); + await sleep(2500); + } +} + +/** + * 按协议设置手机网络状态,然后切回 SwitchBot app。 + * 无人值守可用(Android 全自动;iOS 走设置 UI)。 + */ +export async function applyProtoNetwork(driver: DeviceDriver, proto: 'ble' | 'wifi'): Promise { + const wantBluetooth = proto === 'ble'; + const wantWifi = proto === 'wifi'; + + if (driver.platform === 'android') { + await androidSetWifi(wantWifi); + await androidSetBluetooth(wantBluetooth); + await driver.activateApp(APP_CONFIG.android.appPackage); + } else { + await iosSetWifi(driver, wantWifi); + await iosSetBluetooth(driver, wantBluetooth); + await driver.activateApp(APP_CONFIG.ios.bundleId); + } + await sleep(2000); +} diff --git a/utils/common/ones-anchor.helper.ts b/utils/common/ones-anchor.helper.ts new file mode 100644 index 0000000..dea357a --- /dev/null +++ b/utils/common/ones-anchor.helper.ts @@ -0,0 +1,74 @@ +/** + * 设备维度的 ONES 锚点解析:同品类不同型号(UI 相似)用同一脚本,按当前设备动态解析锚点, + * 换 _DEVICE 环境变量重跑即可分别回写各型号的 ONES 用例。 + * + * 映射 key = DEVICE_CONFIG 里的设备名;值 = 该型号的 添加用例号 / 控制 step uuid(ble/wifi)。 + * 控制步在两条协议超级用例里:ble→15975 / wifi→15974。 + * 新型号:在此补一行即可被脚本识别。 + */ +export interface DeviceAnchor { + add?: number; // 添加用例 ONES number + ctrlBle?: string; // 15975 下的 step uuid + ctrlWifi?: string; // 15974 下的 step uuid +} + +const ANCHORS: Record> = { + curtain: { + 'Curtain 1A': { add: 15969, ctrlBle: '774V8bND', ctrlWifi: 'GPT3wJd9' }, + 'Curtain3 2B': { add: 40955, ctrlBle: 'QqsnPMc3', ctrlWifi: 'GYtvdMTW' }, + 'BlindTilt 3C': { add: 15946, ctrlBle: '3frMRDZj', ctrlWifi: '6gv1WLKE' }, + // 'Roller Shade': { add: 191100, ctrlBle: 'SmzuHtWi', ctrlWifi: 'ScBpcSfJ' }, + // 'Curtain3 2025': { add: 265933, ctrlBle: '3wM6fW8W', ctrlWifi: 'VbqWg59G' }, + }, + lock: { + 'Lock 6F': { add: 15970, ctrlBle: '5YEfmpJz', ctrlWifi: '7cEkbh4Y' }, + 'LockPro 7G': { add: 66694, ctrlBle: 'LnPNEJmS', ctrlWifi: 'Virku9zd' }, + }, + plug: { + // base Plug 仅 WiFi 必测控制(BLE 无) + 'Plug 4D': { add: 15966, ctrlWifi: 'Ay79Pj23' }, + 'PlugMini 5E': { add: 15958, ctrlBle: '2jSFfY4k', ctrlWifi: '3nXsFW9n' }, + }, + sensor: { + 'Contact Sensor 5O': { add: 15962, ctrlBle: 'A8C4BsCm', ctrlWifi: 'JKSgHHbC' }, + 'Motion Sensor 6P': { add: 15963, ctrlBle: 'HrigrD2X', ctrlWifi: 'N7fSHmH3' }, + }, + humidifier: { + 'Humidifier 1K': { add: 15964, ctrlBle: 'GkkrZpaC', ctrlWifi: '8tx3Rg76' }, + // Humidifier2 控制必测是"绑定温湿度计",非简单开关 → 现有 control 用例不覆盖,仅锚添加 + 'Humidifier2 2L': { add: 74104 }, + }, + hub: { + // hub 无控制必测,仅添加 + 'Hub2 8H': { add: 21787 }, + 'HubMini 9I': { add: 15972 }, + 'HubMiniMatter 0J': { add: 65336 }, + }, + meter: { + // meter 控制必测(报警/校正)现有 control 未覆盖;仅锚添加。Outdoor Meter 暂无明确 ONES 对应,未映射 + 'Meter 3M': { add: 15965 }, + }, + robot: { + // robot 控制必测(清扫/暂停/回充)为 WiFi,现有无 _control;仅锚添加 + 'Robot S1': { add: 15952 }, + 'Robot S1P': { add: 15951 }, + 'Robot K10+': { add: 40954 }, + 'Robot S10': { add: 78369 }, + }, +}; + +const curProto = (): 'ble' | 'wifi' => (process.env.PROTO === 'wifi' ? 'wifi' : 'ble'); + +/** 当前设备的添加锚点前缀,如 `[P0][ONES:15969]`;无映射返回空串(不打锚点)。 */ +export function onesAdd(cat: string, deviceName: string): string { + const a = ANCHORS[cat]?.[deviceName]; + return a?.add ? `[P0][ONES:${a.add}]` : ''; +} + +/** 当前设备 + 当前协议的控制锚点前缀,如 `[P0][ONES:15975#774V8bND][ble]`;无对应返回空串。 */ +export function onesCtrl(cat: string, deviceName: string): string { + const a = ANCHORS[cat]?.[deviceName]; + if (!a) return ''; + if (curProto() === 'wifi') return a.ctrlWifi ? `[P0][ONES:15974#${a.ctrlWifi}][wifi]` : ''; + return a.ctrlBle ? `[P0][ONES:15975#${a.ctrlBle}][ble]` : ''; +} diff --git a/utils/ones-sync.ts b/utils/ones-sync.ts index 9ea68af..8f029ba 100644 --- a/utils/ones-sync.ts +++ b/utils/ones-sync.ts @@ -141,36 +141,22 @@ function lcs(a: string, b: string): number { return dp[m][n]; } -/** - * 取某用例的全部 step uuid(按定义顺序),用于回写时列全量 step。 - */ -export function fetchCaseSteps(caseNumber: number): string[] { - try { - const out = execSync(`${ONES_CLI} testcase case search --key ${caseNumber}`, { - encoding: 'utf-8', - timeout: 30000, - }); - const c = JSON.parse(out).cases?.[0]; - return (c?.steps || []).map((s: any) => s.uuid); - } catch { - return []; - } -} - /** * 按测试名锚点构建回写 payload(支持 case 级与 step 级)。 * - [ONES:] → case 级结果 * - [ONES:#] → step 级结果 * 无锚点的结果不在此处理(交由 matchResults LCS 兜底)。 * - * opts.fullStepsByNumber: 用例号 → 全部 step uuid。提供时按 ONES 要求列全量 step, - * 只给跑过的填 execute_result,未跑的仅列 uuid;全部跑完才聚合 case 结果,否则保持 to_do。 + * 只写「跑过的」step(不读取/不列出原有 step,提高效率)。 + * opts.totalStepsByNumber: 用例号 → 该用例步骤总数(来自本地参数文件)。 + * 提供时:跑过的 step 数 < 总数 → case 保持 to_do(部分执行);全跑完才聚合 passed/failed。 + * 不提供时:按已有结果聚合。 */ export function buildAnchoredPayloads( planCases: OnesPlanCase[], testResults: TestResult[], executor: string, - opts: { fullStepsByNumber?: Map } = {} + opts: { totalStepsByNumber?: Map } = {} ): { payloads: OnesUpdatePayload[]; unanchored: TestResult[] } { const byNumber = new Map(); for (const pc of planCases) byNumber.set(pc.caseNumber, pc); @@ -204,19 +190,12 @@ export function buildAnchoredPayloads( const payloads: OnesUpdatePayload[] = []; - // step 级用例:列全量 step(未跑的仅 uuid),全部跑完才聚合 case 结果 + // step 级用例:只写跑过的 step;全跑完才聚合 case 结果,否则 to_do for (const [caseUUID, stepMap] of runSteps) { const num = numberByUUID.get(caseUUID)!; - const full = opts.fullStepsByNumber?.get(num); - let steps: OnesStepResult[]; - let allRun: boolean; - if (full && full.length) { - steps = full.map((uuid) => stepMap.get(uuid) ?? { uuid }); - allRun = full.every((uuid) => stepMap.has(uuid)); - } else { - steps = Array.from(stepMap.values()); - allRun = true; // 无全量列表时,以已有结果为准 - } + const steps = Array.from(stepMap.values()); + const total = opts.totalStepsByNumber?.get(num); + const allRun = total != null ? stepMap.size >= total : true; let agg: SyncStatus | undefined; for (const s of stepMap.values()) if (s.execute_result) agg = mergeStatus(agg, s.execute_result); const result: OnesUpdatePayload['result'] = allRun ? agg ?? 'to_do' : 'to_do'; @@ -235,32 +214,62 @@ export function buildAnchoredPayloads( /** * 分批 POST 回写 payload 到 ONES 测试计划。 */ +/** + * 执行一个 GraphQL mutation(经 ones CLI,复用登录认证)。失败抛错。 + */ +function runOnesMutation(body: string): void { + const resp = runOnesGraphQL(`mutation { ${body} }`); + if (!resp || !resp.data) { + throw new Error(JSON.stringify(resp?.detail || resp || {}).slice(0, 300)); + } +} + +/** + * 回写 payload 到 ONES 测试计划,走 GraphQL mutation(updateTestcasePlanCase / + * updateTestcasePlanCaseStep),用 ones CLI 的登录认证,无需 token/PAT。 + * + * key 规则: + * case: testcase_plan_case-- + * step: testcase_plan_case_step--- + */ export function postPayloads( planUUID: string, payload: OnesUpdatePayload[] ): { success: number; failed: number } { - if (payload.length === 0) return { success: 0, failed: 0 }; - const batchSize = 50; let success = 0; let failed = 0; - for (let i = 0; i < payload.length; i += batchSize) { - const batch = payload.slice(i, i + batchSize); + let stepsWritten = 0; + + for (const p of payload) { try { - execSync(buildCurlCommand(planUUID, batch), { encoding: 'utf-8', timeout: 30000 }); - success += batch.length; + // 先写 step(仅跑过的) + for (const s of p.steps) { + if (!s.execute_result) continue; + const sk = `testcase_plan_case_step-${planUUID}-${p.uuid}-${s.uuid}`; + const ar = s.actual_result ? `, actual_result: ${JSON.stringify(s.actual_result)}` : ''; + runOnesMutation( + `updateTestcasePlanCaseStep(key: ${JSON.stringify(sk)}, step_result: ${JSON.stringify(s.execute_result)}${ar}) { key }` + ); + stepsWritten++; + } + // 再写 case 级结果 + const ck = `testcase_plan_case-${planUUID}-${p.uuid}`; + runOnesMutation( + `updateTestcasePlanCase(key: ${JSON.stringify(ck)}, result: ${JSON.stringify(p.result)}) { key }` + ); + success++; } catch (e: any) { - console.error(`ONES sync batch failed: ${e.message}`); - failed += batch.length; + console.error(`[ONES] 用例 ${p.uuid} 回写失败: ${e.message}`); + failed++; } } + + if (stepsWritten) console.log(`[ONES] 写入 ${stepsWritten} 个 step 结果`); return { success, failed }; } /** - * 反写结果到 ONES 测试计划 - * - * API: POST /project/api/project/team/{team_uuid}/testcase/plan/{plan_uuid}/cases/update - * Body: { cases: [{ uuid, executor, note, result, steps: [{ uuid, execute_result }] }] } + * 反写结果到 ONES 测试计划(case 级)。步骤级请用带 steps 的 payload 走 postPayloads。 */ export function syncResultsToOnes( planUUID: string, @@ -274,17 +283,6 @@ export function syncResultsToOnes( return postPayloads(planUUID, payload); } -function buildCurlCommand(planUUID: string, payload: OnesUpdatePayload[]): string { - const configStr = execSync(`${ONES_CLI} config show`, { encoding: 'utf-8' }); - const config = JSON.parse(configStr); - const { base_url, team_uuid, token } = config; - - const url = `${base_url}/project/api/project/team/${team_uuid}/testcase/plan/${planUUID}/cases/update`; - const body = JSON.stringify({ cases: payload }).replace(/'/g, "'\\''"); - - return `curl -s -X POST '${url}' -H 'Authorization: Bearer ${token}' -H 'Content-Type: application/json' -d '${body}'`; -} - /** * 一键同步: 读取计划用例 → 匹配自动化结果 → 反写 */ diff --git a/utils/wda-helper.ts b/utils/wda-helper.ts index 559a37f..373df06 100644 --- a/utils/wda-helper.ts +++ b/utils/wda-helper.ts @@ -152,6 +152,12 @@ export class WDAHelper { } catch {} } + /** 激活任意 bundleId 的 app(用于切到系统设置 com.apple.Preferences 再切回)。 */ + async activateAppById(bundleId: string): Promise { + await this.request('POST', `/session/${this.sessionId}/wda/apps/activate`, { bundleId }); + await new Promise(r => setTimeout(r, 2500)); + } + async destroySession(): Promise { if (this.sessionId && !this.reusedSession) { await this.request('DELETE', `/session/${this.sessionId}`);