feat: 必测项自动化锚点 + 动态设备锚点 + 双协议网络切换 + ONES mutation 回写

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),换 <CAT>_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 <noreply@anthropic.com>
This commit is contained in:
woan 2026-06-01 16:10:57 +08:00
parent fda826446a
commit 373a8bd6fa
45 changed files with 1366 additions and 266 deletions

Binary file not shown.

View File

@ -106,7 +106,7 @@ add_table_with_header(doc, ['设备', '脚本文件数', '覆盖模块', '通过
doc.add_heading('1.4 目标', level=2) doc.add_heading('1.4 目标', level=2)
doc.add_paragraph('• P0 必测项:各单品添加 + 核心控制(双协议)准入闸门,来源 ONES 必测项-AI自动化') 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('• P2 全功能全维度回归card/control/setting/scene/logs覆盖深度')
doc.add_paragraph('• P3 平台:账号/房间/消息/家庭分享/场景等平台功能') doc.add_paragraph('• P3 平台:账号/房间/消息/家庭分享/场景等平台功能')
doc.add_paragraph('• 按优先级 backlog 依次交付,各层有独立退出标准(不绑定固定周次)') doc.add_paragraph('• 按优先级 backlog 依次交付,各层有独立退出标准(不绑定固定周次)')
@ -114,7 +114,7 @@ doc.add_paragraph('• 按优先级 backlog 依次交付,各层有独立退出
doc.add_page_break() doc.add_page_break()
# ======================== 2. 设备清单 ======================== # ======================== 2. 设备清单 ========================
doc.add_heading('2. 待覆盖设备清单(82款)', level=1) doc.add_heading('2. 待覆盖设备清单(78款', level=1)
device_categories = { device_categories = {
'窗帘 (9款)': [ '窗帘 (9款)': [
@ -214,11 +214,7 @@ device_categories = {
('AI PinNote', '需新写', '全新品类'), ('AI PinNote', '需新写', '全新品类'),
('Bot Rechargeable', '复用bot', '调试适配'), ('Bot Rechargeable', '复用bot', '调试适配'),
], ],
'机器人+联名+其他 (8款)': [ '机器人+联名+其他 (4款)': [
('OBBOTO 1.0', '需新写', '全新品类'),
('Robotic Actuator', '需新写', '全新品类'),
('Robotic Arm', '需新写', '全新品类'),
('Robotic Picker', '需新写', '全新品类'),
('SwitchBot KATAフレンズ KUMAMON ver.', '复用bot', '特殊UI定制'), ('SwitchBot KATAフレンズ KUMAMON ver.', '复用bot', '特殊UI定制'),
('KATA Friends 国行版', '复用bot', '特殊UI定制'), ('KATA Friends 国行版', '复用bot', '特殊UI定制'),
('Outdoor PTC', '需新写', '全新品类'), ('Outdoor PTC', '需新写', '全新品类'),
@ -257,7 +253,7 @@ add_table_with_header(doc,
) )
doc.add_paragraph() 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_paragraph('AI 提效口径:单维度脚本首台 0.2-1 人日、复用台 0.1-0.3 人日;串口添加框架为一次性投入(见 §5.2。下列各层人力预估均按此口径单位为人日1 人)。')
doc.add_page_break() doc.add_page_break()
@ -271,8 +267,8 @@ doc.add_paragraph(
rows = [ rows = [
('P0 必测项', '各单品添加 + 核心控制(双协议)', 'ONES 必测项-AI自动化 (CQz9YCNX)', '187', '每次提测/版本必跑', '通过率≥95%'), ('P0 必测项', '各单品添加 + 核心控制(双协议)', 'ONES 必测项-AI自动化 (CQz9YCNX)', '187', '每次提测/版本必跑', '通过率≥95%'),
('P1 单品探索', '每单品主流程冒烟card+核心control', '按单品模板', '82款各1遍', '每迭代', '全单品冒烟通过'), ('P1 单品探索', '每单品主流程冒烟card+核心control', '按单品模板', '78款各1遍', '每迭代', '全单品冒烟通过'),
('P2 全功能', '全维度回归(card/control/setting/scene/logs)', '各品类用例库', '~2366', '版本回归', '通过率≥85%'), ('P2 全功能', '全维度回归(card/control/setting/scene/logs)', '各品类用例库', '~2250', '版本回归', '通过率≥85%'),
('P3 平台', '账号/房间/消息/家庭分享/场景等平台功能', 'App平台用例库', '平台模块', '版本回归', '平台用例通过'), ('P3 平台', '账号/房间/消息/家庭分享/场景等平台功能', 'App平台用例库', '平台模块', '版本回归', '平台用例通过'),
] ]
add_table_with_header(doc, add_table_with_header(doc,
@ -350,14 +346,17 @@ doc.add_page_break()
# ======================== 6. P1 单品探索 ======================== # ======================== 6. P1 单品探索 ========================
doc.add_heading('6. P1 单品探索(覆盖广度)', level=1) doc.add_heading('6. P1 单品探索(覆盖广度)', level=1)
doc.add_paragraph( doc.add_paragraph(
'必测项达标后,为全部 82 款单品各跑通一遍主流程冒烟,确保每款设备最低限度可用。' '必测项达标后,为全部 78 款单品覆盖一遍探索测试card + 核心 control并扩展到'
'范围 = card首页卡片+ 核心 control开关/主模式),不含设置/场景/日志等深度回归' '全品类核心功能的「部分全功能用例」(每品类挑代表性主功能,不做完整深度回归)'
) )
doc.add_paragraph('• 目标82 款单品各 1 套冒烟,广度优先、深度浅。') doc.add_paragraph('• 目标78 款单品各 1 套探索集card + 核心控制 + 品类代表性主功能),广度优先。')
doc.add_paragraph('• 复用必测项已建的 control helper单品只补 card 与主流程断言。') doc.add_paragraph('• 已有/复用品类:复用 P0 已建 control helper补 card 与主功能断言,较快。')
doc.add_paragraph('• 退出标准每款单品冒烟用例通过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('• 运行频率:每迭代跑一遍,作为单品级回归基线。')
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() doc.add_page_break()
@ -368,11 +367,20 @@ doc.add_paragraph(
'同品类 UI 高度相似,调通首台后改设备名配置快速扩展,并沉淀通用 helper。' '同品类 UI 高度相似,调通首台后改设备名配置快速扩展,并沉淀通用 helper。'
'不绑定周次,按复用度从高到低排批次。' '不绑定周次,按复用度从高到低排批次。'
) )
doc.add_paragraph(
'脚本状态分三层成本(见第 2 节):「已有脚本」只需在真机上调试适配,最快;'
'「复用模板」改设备名/配置次之;「需新写」(全新 UI最贵。估算按此分层。'
)
doc.add_paragraph(
'进一步提效:功能页/设置页「入口」全单品通用(沉淀一个全局导航 helper不逐设备重写'
'同品类功能测试点高度雷同,首台调通后其余只调「差异功能」。'
'另有部分用例由旧代码转换而来、已含路径,只需定位元素即可执行。故 P2 实际只在每设备增量调试少量差异点。'
)
rows = [ rows = [
('批1·高复用', '窗帘9 / 锁12 / 插座开关6 / 灯光10', '37', '脚本×37 + curtain/lock/relay/light helper', '~25'), ('批1·高复用', '窗帘9 / 锁12 / 插座开关6 / 灯光10', '37', '差异功能 + 通用入口 helper', '~12'),
('批2·中复杂', '扫地机9 / 传感器温控7 / 风扇空净加湿6 / 摄像头门铃3', '25', '脚本×25 + robot/sensor/climate helper', '~27'), ('批2·中复杂', '扫地机9 / 传感器温控7 / 风扇空净加湿6 / 摄像头门铃3', '25', '差异功能 + robot/sensor/climate helper', '~14'),
('批3·新品特殊', 'Hub门控安防5 / AI产品5 / 机器人联名5', '15+', '脚本×15 + 全新UI框架', '~28'), ('批3·新品特殊', 'Hub门控安防 / AI产品 / 联名其他(KATA/FindCard/PTC)', '16', '深度回归(UI支持已在P1建好)', '~10'),
] ]
add_table_with_header(doc, add_table_with_header(doc,
['批次', '品类', '设备数', '交付物', 'AI预估(人日)'], ['批次', '品类', '设备数', '交付物', 'AI预估(人日)'],
@ -383,10 +391,10 @@ add_table_with_header(doc,
doc.add_paragraph() doc.add_paragraph()
p = doc.add_paragraph() p = doc.add_paragraph()
p.add_run('覆盖目标:').bold = True p.add_run('覆盖目标:').bold = True
p.add_run('全部 82 款单品全维度回归,~2366 条用例,单品功能覆盖率 100%') p.add_run('全部 78 款单品全维度回归,~2250 条用例,单品功能覆盖率 100%')
p = doc.add_paragraph() p = doc.add_paragraph()
p.add_run('P2 合计:').bold = True 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_heading('7.1 高复用品类要点', level=2)
doc.add_paragraph('• 窗帘: 位置控制、校准流程、定时器、群组控制 → curtain_helper') 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('• Hub 3 / AI Hub Show: 设备管理、红外学习、Matter、带屏交互')
doc.add_paragraph('• 门控安防: Garage Door、Safety Alarm、Radiator Thermostat') 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('• AI产品: Art Frame、AI Pet、AI PinNote全新UI需从Figma/UX重新分析')
doc.add_paragraph('机器人+联名: OBBOTO、Robotic Actuator/Arm/Picker、KATA FriendsBot复用+定制皮肤)') doc.add_paragraph('联名+其他: KATA FriendsBot复用+定制皮肤、FindCard已有脚本调试适配、Outdoor PTC需新写')
doc.add_page_break() doc.add_page_break()
@ -424,7 +432,7 @@ rows = [
add_table_with_header(doc, ['模块', '内容', '来源'], rows) add_table_with_header(doc, ['模块', '内容', '来源'], rows)
doc.add_heading('8.2 自动化/场景联动', level=2) 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('• 执行验证:触发条件满足后动作执行') doc.add_paragraph('• 执行验证:触发条件满足后动作执行')
doc.add_paragraph('• 编辑/删除:修改条件或动作、删除自动化') doc.add_paragraph('• 编辑/删除:修改条件或动作、删除自动化')
@ -452,7 +460,7 @@ doc.add_paragraph(
rows = [ rows = [
('P0 · 控制轨', '自动化(1人)', '', '先行:双协议控制 105 step不待串口'), ('P0 · 控制轨', '自动化(1人)', '', '先行:双协议控制 105 step不待串口'),
('P0 · 添加轨', '自动化(1人)', '嵌入式(串口协议+框架)', '串口就绪后插入73 添加 + 9 功能'), ('P0 · 添加轨', '自动化(1人)', '嵌入式(串口协议+框架)', '串口就绪后插入73 添加 + 9 功能'),
('P1 单品探索', '自动化(1人)', '', 'P0 达标后:82 款冒烟'), ('P1 单品探索', '自动化(1人)', '', 'P0 达标后:78 款冒烟'),
('P2 全功能', '自动化(1人)', '', '按品类批量,复用 helper'), ('P2 全功能', '自动化(1人)', '', '按品类批量,复用 helper'),
('P3 平台', '自动化(1人)', '', '最后:平台功能 + 场景联动'), ('P3 平台', '自动化(1人)', '', '最后:平台功能 + 场景联动'),
] ]
@ -475,12 +483,18 @@ add_table_with_header(doc, ['用途', '设备需求', '到位时间'], rows)
doc.add_heading('9.3 环境', level=2) doc.add_heading('9.3 环境', level=2)
rows = [ rows = [
('测试', 'Samsung (Android) + iPhone (iOS)', '已有'), ('测试', 'Mac×2双调试位+ Android×2Samsung 等)+ iOS×2iPhone', '已有'),
('Appium Server', 'v2.x + UIAutomator2', '已有'), ('Appium Server', 'v2.x + UIAutomator2 / WebDriverAgent', '已有'),
('网络环境', '稳定Wi-Fi (Deco)', '已有'), ('网络环境', '稳定Wi-Fi (Deco)', '已有'),
('CI/CD', 'Jenkins/GitHub Actions (可选)', '待搭建'), ('CI/CD', 'Jenkins/GitHub Actions (可选)', '待搭建'),
] ]
add_table_with_header(doc, ['项目', '说明', '状态'], rows) 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 agentClaude Code/Midscene并行“边跑边写”人只做 review——唯一能突破“1 人”瓶颈的方式。')
doc.add_paragraph('• 上限:仍 1 人手动交互调试不可一人并干两台Amdahl正确用法=「1 台主调 + 1 台挂 AI/跑回归」。')
doc.add_paragraph('• 净效果:双平台日历 ~90-130 → ~65-90 工作日(≈ 压回单平台水平),人日(工作量)不变;提速幅度取决于多少调试可交给 AI 自动跑。')
doc.add_page_break() doc.add_page_break()
@ -489,20 +503,53 @@ doc.add_heading('10. 优先级 backlog 一览', level=1)
rows = [ rows = [
('P0 必测项', '添加73 + 功能9 + 控制105step(双协议)', '187条', '~11-15', 'serial框架 + connect + 控制断言 + step回写', '通过率≥95%'), ('P0 必测项', '添加73 + 功能9 + 控制105step(双协议)', '187条', '~11-15', 'serial框架 + connect + 控制断言 + step回写', '通过率≥95%'),
('P1 单品探索', '82款单品主流程冒烟(card+核心control)', '82款×1', '~12-16', '每单品冒烟用例', '全单品冒烟通过'), ('P1 单品探索', '全品类 card+核心control + 代表性主功能(部分全功能);含新品UI首次支持', '78款各1套', '~16-22', '探索集脚本 + 新品UI支持', '全品类探索集通过'),
('P2 全功能', '全维度回归,按品类批量(高复用→新品)', '~2366条', '~75-90', '85+脚本 + 8个通用helper', '通过率≥85%、覆盖100%'), ('P2 全功能', '全维度深度回归(setting/scene/logs+完整功能点)', '~2250条', '~25-35', '深度回归(入口/路径多已就绪,补定位)', '通过率≥85%、覆盖100%'),
('P3 平台', '账号/房间/消息/家庭分享/场景等', '平台模块', '~12-15', '平台用例 + 场景联动', '平台用例通过'), ('P3 平台', '账号/房间/消息/家庭分享/场景等', '平台模块', '~10-13', '平台用例 + 场景联动(复用设备入口)', '平台用例通过'),
] ]
add_table_with_header(doc, add_table_with_header(doc,
['优先级', '范围', '用例量', 'AI预估(人日)', '交付物', '退出标准'], ['优先级', '范围', '用例量', 'AI预估(人日,单平台)', '交付物', '退出标准'],
rows, 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() doc.add_paragraph()
p = doc.add_paragraph() p = doc.add_paragraph()
p.add_run('人力总计AI 辅助口径):').bold = True p.add_run('人力总计AI 辅助口径,单平台):').bold = True
p.add_run('约 105-135 人日 ≈ 5-7 人月1 人,按 20 人日/月。P0 大幅复用既有 control、串口硬件联调由嵌入式主导不计入P2 “需新写”新品 UI 占比最高、最不可压缩。') 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('白天调 AndroidiOS 夜间挂测——让 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( doc.add_paragraph(
'人力构成说明:以上为 AI 辅助口径(较手工已下调约 2-3 倍)。其中“脚本编写”仅占约 1/3——AI 提效主要在此;' '人力构成说明:以上为 AI 辅助口径(较手工已下调约 2-3 倍)。其中“脚本编写”仅占约 1/3——AI 提效主要在此;'
@ -530,7 +577,7 @@ doc.add_paragraph('• 脚本可在 Android/iOS 双平台运行(通过平台
doc.add_heading('11.2 优先级层验收标准', level=2) doc.add_heading('11.2 优先级层验收标准', level=2)
rows = [ rows = [
('P0 必测项', '187条全部实现并跑通添加+控制双协议)', '通过率≥95%,作为提测门禁'), ('P0 必测项', '187条全部实现并跑通添加+控制双协议)', '通过率≥95%,作为提测门禁'),
('P1 单品探索', '82款单品冒烟全部通过', '广度100%覆盖'), ('P1 单品探索', '78款单品冒烟全部通过', '广度100%覆盖'),
('P2 全功能', '全维度回归通过率≥85%', '单品功能覆盖率100%'), ('P2 全功能', '全维度回归通过率≥85%', '单品功能覆盖率100%'),
('P3 平台', '平台功能 + 场景联动通过', '全流程覆盖'), ('P3 平台', '平台功能 + 场景联动通过', '全流程覆盖'),
] ]

View File

@ -57,6 +57,11 @@ export class AndroidDriver implements DeviceDriver {
this.sessionId = null; this.sessionId = null;
} }
async activateApp(appId: string): Promise<void> {
await this.request('POST', `/session/${this.sessionId}/appium/device/activate_app`, { appId });
await new Promise(r => setTimeout(r, 2000));
}
async findElement(locator: ElementLocator): Promise<string | null> { async findElement(locator: ElementLocator): Promise<string | null> {
if (!locator.android) return null; if (!locator.android) return null;
return this.findElementRaw(locator.android.using, locator.android.value); return this.findElementRaw(locator.android.using, locator.android.value);

View File

@ -62,6 +62,11 @@ export class HubShowDriver implements DeviceDriver {
this.sessionId = null; this.sessionId = null;
} }
// Hub Show 直连设备固件 UI不管理 AppactivateApp 为空实现以满足接口。
async activateApp(_appId: string): Promise<void> {
/* no-op */
}
async findElement(locator: ElementLocator): Promise<string | null> { async findElement(locator: ElementLocator): Promise<string | null> {
if (!locator.android) return null; if (!locator.android) return null;
return this.findElementRaw(locator.android.using, locator.android.value); return this.findElementRaw(locator.android.using, locator.android.value);

View File

@ -19,6 +19,9 @@ export interface DeviceDriver {
createSession(): Promise<void>; createSession(): Promise<void>;
destroySession(): Promise<void>; destroySession(): Promise<void>;
/** 激活/拉起指定 app(Android appPackage / iOS bundleId)。用于切到系统设置再切回。 */
activateApp(appId: string): Promise<void>;
findElement(locator: ElementLocator): Promise<string | null>; findElement(locator: ElementLocator): Promise<string | null>;
findElements(locator: ElementLocator): Promise<string[]>; findElements(locator: ElementLocator): Promise<string[]>;
findElementRaw(using: string, value: string): Promise<string | null>; findElementRaw(using: string, value: string): Promise<string | null>;

View File

@ -17,6 +17,10 @@ export class WDADriver implements DeviceDriver {
await this.wda.destroySession(); await this.wda.destroySession();
} }
async activateApp(appId: string): Promise<void> {
await this.wda.activateAppById(appId);
}
async findElement(locator: ElementLocator): Promise<string | null> { async findElement(locator: ElementLocator): Promise<string | null> {
if (!locator.ios) throw new Error(`Locator "${locator.name}" has no iOS strategy`); if (!locator.ios) throw new Error(`Locator "${locator.name}" has no iOS strategy`);
return this.wda.findElement(locator.ios.using, locator.ios.value); return this.wda.findElement(locator.ios.using, locator.ios.value);

View File

@ -43,7 +43,7 @@ from appium.options.ios import XCUITestOptions
caps = XCUITestOptions() caps = XCUITestOptions()
caps.platform_name = 'iOS' caps.platform_name = 'iOS'
caps.platform_version = '26.3.1' # 实际 iOS 版本 caps.platform_version = '26.5' # 实际 iOS 版本
caps.device_name = 'iPhone' caps.device_name = 'iPhone'
caps.udid = '00008110-001A34303AE9801E' # 设备 UDID caps.udid = '00008110-001A34303AE9801E' # 设备 UDID
caps.bundle_id = 'com.demo.wohand' 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 \ xcodebuild -project WebDriverAgent.xcodeproj \
-scheme WebDriverAgentRunner \ -scheme WebDriverAgentRunner \
-destination "id=YOUR_DEVICE_UDID" \ -destination "id=YOUR_DEVICE_UDID" \
IPHONEOS_DEPLOYMENT_TARGET=26.3.1 \ IPHONEOS_DEPLOYMENT_TARGET=26.5 \
-configuration Debug \ -configuration Debug \
build build
``` ```
@ -73,7 +73,7 @@ xcodebuild -project WebDriverAgent.xcodeproj \
**重要**:配置中的 platform_version 必须与设备实际版本一致 **重要**:配置中的 platform_version 必须与设备实际版本一致
- 查询设备版本:`ideviceinfo | grep ProductVersion` - 查询设备版本:`ideviceinfo | grep ProductVersion`
- 当前测试设备iOS 26.3.1iPhone 14 Pro - 当前测试设备iOS 26.5iPhone 14 Pro
- 设备 UDID00008110-001A34303AE9801E - 设备 UDID00008110-001A34303AE9801E
## 常见错误 ## 常见错误
@ -94,7 +94,7 @@ from appium import webdriver
from appium.options.ios import XCUITestOptions from appium.options.ios import XCUITestOptions
caps = XCUITestOptions() caps = XCUITestOptions()
caps.platform_name = 'iOS' caps.platform_name = 'iOS'
caps.platform_version = '26.3.1' caps.platform_version = '26.5'
caps.udid = '00008110-001A34303AE9801E' caps.udid = '00008110-001A34303AE9801E'
caps.bundle_id = 'com.demo.wohand' caps.bundle_id = 'com.demo.wohand'
caps.automation_name = 'XCUITest' caps.automation_name = 'XCUITest'

View File

@ -11,7 +11,8 @@
"test:card": "vitest run tests/bot/bot_card.test.ts", "test:card": "vitest run tests/bot/bot_card.test.ts",
"test:logs": "vitest run tests/bot/bot_logs.test.ts", "test:logs": "vitest run tests/bot/bot_logs.test.ts",
"test:scene": "vitest run tests/bot/bot_scene.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": [ "keywords": [
"ui-automation", "ui-automation",

View File

@ -106,18 +106,42 @@ export const MUST_TEST: MustTestItem[] = [
## 6. P0 标记约定(带 ONES 锚点) ## 6. P0 标记约定(带 ONES 锚点)
代码里用 `it` 名称打标,锚点指向 ONES,便于筛选与回写: **关键:锚点必须打在 `reporter.record(名称, ...)` 的名称里**——结果写入 `reports/.results.json` 用的是 record 名称,`buildAnchoredPayloads` 从中解析 `[ONES:号(#step)]` 做回写。`it()` 标题里也加同一锚点作**备注**(测试报告里可见、便于追溯),但回写不读 it 标题。
```ts ```ts
// 添加:锚点 = 用例号 // it 标题:加 [ONES:号] 作备注(可读/可追溯)
it(`[P0][ONES:91013] 通过BLE添加${deviceName}`, async () => { ... }); it(`[ONES:15968] 通过BLE添加${deviceName}设备`, async () => {
// ...
// reporter.record 名称:加 [P0][ONES:号] —— 这是回写真正依据
reporter.record(`[P0][ONES:15968] 添加${deviceName}`, 'PASS', dur, detail);
});
// 控制:锚点 = 用例号#step_uuid;协议标在中括号里(双协议则两条 it 或参数化) // 控制(协议相关):record 名称带 用例号#step_uuid + 协议;协议由 PROTO 环境变量切(见 §7)
it(`[P0][ONES:15975#${stepUuid}][ble] 开/关 ${deviceName}`, async () => { ... }); const CTRL = process.env.PROTO === 'wifi' ? '[P0][ONES:15974#<step>][wifi]' : '[P0][ONES:15975#<step>][ble]';
it(`[P0][ONES:15974#${stepUuid}][wifi] 开/关 ${deviceName}`, async () => { ... }); 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}` 维护;**新增型号补一行即可**。
- 跑法:**换 `<CAT>_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 节)。 **key 拼接规则(确定式)**
- **控制用例 15974 / 15975**:`uuid` = 该控制用例的 `testcaseCase.uuid`,`steps` 数组按 `step_uuid` 逐条填结果: - case: `testcase_plan_case-<planUUID>-<caseUUID>`
```json - step: `testcase_plan_case_step-<planUUID>-<caseUUID>-<stepUuid>`
{ "uuid":"Vp7vuhbu", "executor":"<user_id>", "result":"passed",
"steps":[ { "uuid":"<stepUuid>", "result":"passed", "actual_result":"开/关成功" }, ... ] } **写入 mutation**
``` ```bash
case 级 `result` 由其所有 step 聚合(全 pass→passed,有 fail→failed)。 # case 级
- 用例名只有 2 条、靠 LCS 匹配会误配 → 控制用例**改按 `[ONES:号#step]` 锚点精确匹配**,扩展 `utils/ones-sync.ts` / `scripts/sync-ones-results.ts` 支持 step 维度。 ones graphql 'mutation { updateTestcasePlanCase(key: "testcase_plan_case-CQz9YCNX-<caseUUID>", result: "passed") { key } }'
- 协议:`[ble]` 结果回 15975,`[wifi]` 结果回 15974。 # step 级result 字段名是 step_result不是 execute_result/result
ones graphql 'mutation { updateTestcasePlanCaseStep(key: "testcase_plan_case_step-CQz9YCNX-<caseUUID>-<stepUuid>", 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: ["<caseUUID>"] } }, limit: 60) { key stepResult actualResult } }'
```
(注意:查询用驼峰 `stepResult`/`actualResult`,filter 用 `testcasePlan`/`testcaseCase` 嵌套 `uuid_in`。)
--- ---

View File

@ -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 可用接口 ## DeviceDriver 可用接口
```typescript ```typescript
@ -840,43 +876,32 @@ ONES测试计划 → 读取用例列表 → 转换为自动化脚本 → 执行
| `'SKIP'` | `skipped` | | `'SKIP'` | `skipped` |
| 未执行 | `to_do` | | 未执行 | `to_do` |
### 3. 结果反写 (已确认可行) ### 3. 结果反写 (正确方法GraphQL mutation已验证)
API 端点: > ⚠️ **必须走 `ones graphql` mutation,不要用 curl 直连 REST `.../cases/update`。**
``` > `ones config show` 把 token 打码成 `***`,curl 直连必然 `401 AuthFailure.InvalidToken`;
POST /project/api/project/team/{team_uuid}/testcase/plan/{plan_uuid}/cases/update > `ones graphql` 复用 CLI 登录认证、无需 token/PAT,已在权限白名单。`utils/ones-sync.ts` 的 `postPayloads` 已按此实现。
```
请求体 (JSON 对象cases 数组包裹): **key 拼接(确定式):**
```json - case: `testcase_plan_case-<planUUID>-<caseUUID>`
{ - step: `testcase_plan_case_step-<planUUID>-<caseUUID>-<stepUuid>`
"cases": [
{
"uuid": "用例UUID (testcaseCase.uuid)",
"executor": "执行人UUID (user_id)",
"note": "",
"result": "passed|failed|skipped|to_do",
"steps": []
}
]
}
```
响应: **写入:**
```json ```bash
{ # case 级结果
"success_cases": ["BZfZGRcF"], ones graphql 'mutation { updateTestcasePlanCase(key: "testcase_plan_case-<plan>-<caseUUID>", result: "passed") { key } }'
"not_found_cases": [], # step 级结果(字段名 step_result可带 actual_result
"no_permission_cases": [], ones graphql 'mutation { updateTestcasePlanCaseStep(key: "testcase_plan_case_step-<plan>-<caseUUID>-<stepUuid>", step_result: "passed", actual_result: "...") { key } }'
"not_handle_cases": []
}
``` ```
取值 `passed|failed|skipped|to_do`PASS→passed、FAIL→failed、SKIP→skipped
说明: **读回校验:**
- `uuid`: 测试用例的 UUID不是 plan_case 的 key`testcaseCase.uuid` ```bash
- `executor`: 执行人 UUID`ones config show``user_id` 获取 ones graphql '{ testcasePlanCaseSteps(filter: { testcasePlan: { uuid_in: ["<plan>"] }, testcaseCase: { uuid_in: ["<caseUUID>"] } }, limit: 60) { key stepResult actualResult } }'
- `steps`: 步骤结果数组,无步骤时传空数组 `[]` ```
- 支持批量提交多条 (查询字段驼峰 `stepResult`/`actualResult`filter 用 `testcasePlan`/`testcaseCase` 嵌套 `uuid_in`。)
可用 mutation`updateTestcasePlanCase` / `updateTestcasePlanCaseStep`(经 `ones graphql '{ __schema { mutationType { fields { name } } } }'` 可查全部写操作)。
### 4. 完整同步命令 ### 4. 完整同步命令

View File

@ -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-<planUUID>-<caseUUID>
* step: testcase_plan_case_step-<planUUID>-<caseUUID>-<stepUuid>
*
* 用法: 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<string, any> = {};
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-<plan>-<caseUUID>; step=testcase_plan_case_step-<plan>-<caseUUID>-<stepUuid>。' +
'刷新: 重跑 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();

View File

@ -6,9 +6,9 @@
* *
* : * :
* 1. reports/.results.json () * 1. reports/.results.json ()
* 2. ONES * 2. test-plan/ones-writeback-params.json (uuid)+
* 3. [ONES:(#step)] ( step ) * ONES(, npm run gen:writeback-params)
* 退 LCS * 3. [ONES:(#step)] ( step );退 LCS
* 4. ONES (dry-run payload) * 4. ONES (dry-run payload)
*/ */
@ -16,17 +16,17 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { TestResult } from '../utils/test-reporter'; import { TestResult } from '../utils/test-reporter';
import { import {
fetchPlanCases,
matchResults, matchResults,
buildAnchoredPayloads, buildAnchoredPayloads,
postPayloads, postPayloads,
fetchCaseSteps, OnesPlanCase,
OnesUpdatePayload, OnesUpdatePayload,
} from '../utils/ones-sync'; } from '../utils/ones-sync';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
const ONES_CLI = '/Users/woan/local/bin/ones'; const ONES_CLI = '/Users/woan/local/bin/ones';
const RESULTS_FILE = path.resolve(__dirname, '../reports/.results.json'); const RESULTS_FILE = path.resolve(__dirname, '../reports/.results.json');
const PARAMS_FILE = path.resolve(__dirname, '../test-plan/ones-writeback-params.json');
function parseArgs() { function parseArgs() {
const args = process.argv.slice(2); const args = process.argv.slice(2);
@ -61,6 +61,28 @@ function loadResults(): TestResult[] {
return data.results || []; return data.results || [];
} }
/** 从本地参数文件取 用例(OnesPlanCase[]) + 控制用例步骤总数(号→step数)。不读 ONES。 */
function loadParams(): { planCases: OnesPlanCase[]; totalStepsByNumber: Map<number, number> } {
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<number, number>();
for (const [num, c] of Object.entries<any>(p.controlCases || {})) {
totalStepsByNumber.set(Number(num), (c.steps || []).length);
}
return { planCases, totalStepsByNumber };
}
function main() { function main() {
const { planUUID, dryRun } = parseArgs(); const { planUUID, dryRun } = parseArgs();
@ -82,27 +104,18 @@ function main() {
const skipped = testResults.filter(r => r.status === 'SKIP').length; const skipped = testResults.filter(r => r.status === 'SKIP').length;
console.log(` PASS: ${passed} | FAIL: ${failed} | SKIP: ${skipped}`); console.log(` PASS: ${passed} | FAIL: ${failed} | SKIP: ${skipped}`);
// 2. 从 ONES 拉取计划用例 // 2. 从本地参数文件取用例 + 步骤总数(不读 ONES
console.log(`\n[2/4] 从 ONES 拉取测试计划用例 ...`); console.log(`\n[2/4] 读取本地回写参数 ...`);
const planCases = fetchPlanCases(planUUID); const { planCases, totalStepsByNumber } = loadParams();
console.log(` 计划共 ${planCases.length} 用例`); console.log(` 参数: ${planCases.length} 用例, ${totalStepsByNumber.size} 个控制用例步骤数`);
// 3. 匹配:锚点优先(支持 step 级),无锚点回退 LCS // 3. 匹配:锚点优先(支持 step 级),无锚点回退 LCS
console.log(`\n[3/4] 匹配 (锚点优先 + LCS 兜底) ...`); console.log(`\n[3/4] 匹配 (锚点优先 + LCS 兜底) ...`);
// 含 step 锚点的用例需列全量 step → 先取其完整 step 列表
const stepNums = new Set<number>();
for (const tr of testResults) {
const m = /\[ONES:(\d+)#/.exec(tr.name);
if (m) stepNums.add(parseInt(m[1], 10));
}
const fullStepsByNumber = new Map<number, string[]>();
for (const num of stepNums) fullStepsByNumber.set(num, fetchCaseSteps(num));
const { payloads: anchored, unanchored } = buildAnchoredPayloads( const { payloads: anchored, unanchored } = buildAnchoredPayloads(
planCases, planCases,
testResults, testResults,
executor, executor,
{ fullStepsByNumber } { totalStepsByNumber }
); );
const anchoredUUIDs = new Set(anchored.map(p => p.uuid)); 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); const stepCount = anchored.reduce((n, p) => n + p.steps.filter(s => s.execute_result).length, 0);

View File

@ -0,0 +1,607 @@
{
"_note": "必测项回写参数。用例固定仅新增;回写时 plan UUID 为变量。key 规则: case=testcase_plan_case-<plan>-<caseUUID>; step=testcase_plan_case_step-<plan>-<caseUUID>-<stepUuid>。刷新: 重跑 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"
]
}
}
}

View File

@ -40,13 +40,13 @@ describe('Air Condition Connect - 添加红外空调设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('添加红外空调设备', async () => { it('[ONES:196802] 添加红外空调设备', async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record('添加红外空调设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record('[P0][ONES:196802] 添加红外空调设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -135,10 +135,10 @@ describe('Air Condition Connect - 添加红外空调设备', () => {
|| await waitForSource(driver, 'Air Conditioner', 5000); || await waitForSource(driver, 'Air Conditioner', 5000);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -6,6 +6,7 @@ import { BOT_LOCATORS } from '../../locators/bot-locators';
import { TestReporter } from '../../utils/test-reporter'; import { TestReporter } from '../../utils/test-reporter';
import { getDeviceName } from '../../config/device.config'; import { getDeviceName } from '../../config/device.config';
import { sleep } from '../../utils/common'; import { sleep } from '../../utils/common';
import { applyProtoNetwork } from '../../utils/common';
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv';
import * as path from 'path'; import * as path from 'path';
@ -13,6 +14,11 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('bot', 'BOT_DEVICE'); 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 - 首页卡片操作', () => { describe('Bot Card - 首页卡片操作', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let bot: BotHelper; let bot: BotHelper;
@ -23,6 +29,8 @@ describe('Bot Card - 首页卡片操作', () => {
await driver.createSession(); await driver.createSession();
bot = new BotHelper(driver); bot = new BotHelper(driver);
reporter = new TestReporter('Bot_Card', driver.platform.toUpperCase()); reporter = new TestReporter('Bot_Card', driver.platform.toUpperCase());
// 双协议前置:按 PROTO 切手机蓝牙/WiFi(ble→开蓝牙关WiFi / wifi→关蓝牙开WiFi),无人值守自动切
await applyProtoNetwork(driver, PROTO);
}); });
beforeEach(async () => { beforeEach(async () => {
@ -256,7 +264,7 @@ describe('Bot Card - 首页卡片操作', () => {
console.log('切换后状态:', statusBefore); console.log('切换后状态:', statusBefore);
if (statusBefore === 'unknown') { if (statusBefore === 'unknown') {
reporter.record('ON/OFF切换', 'SKIP', Date.now() - start, '切换Switch Mode后仍无法识别状态'); reporter.record(`${CTRL} 不加密开/关`, 'SKIP', Date.now() - start, '切换Switch Mode后仍无法识别状态');
return; return;
} }
} }
@ -284,7 +292,7 @@ describe('Bot Card - 首页卡片操作', () => {
expect(statusAfter).not.toBe(statusBefore); expect(statusAfter).not.toBe(statusBefore);
const detail = `${statusBefore}${statusAfter}`; const detail = `${statusBefore}${statusAfter}`;
reporter.record('ON/OFF切换', 'PASS', Date.now() - start, detail); reporter.record(`${CTRL} 不加密开/关`, 'PASS', Date.now() - start, detail);
// Restore state // Restore state
await tapBotAndWaitPopup(); await tapBotAndWaitPopup();
@ -298,7 +306,7 @@ describe('Bot Card - 首页卡片操作', () => {
await sleep(5000); await sleep(5000);
} catch (e: any) { } catch (e: any) {
const ss = await captureScreenshot(); 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; throw e;
} }
}); });
@ -532,10 +540,10 @@ describe('Bot Card - 首页卡片操作', () => {
const stillPress = source.includes('Press Mode'); const stillPress = source.includes('Press Mode');
console.log('按压后状态:', stillPress ? '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) { } catch (e: any) {
const ss = await captureScreenshot(); 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; throw e;
} }
}); });

View File

@ -31,13 +31,13 @@ describe('Bot Connect - 添加Bot设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it(`通过BLE添加${deviceName}设备`, async () => { it(`[ONES:15968] 通过BLE添加${deviceName}设备`, async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record(`添加${deviceName}`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record(`[P0][ONES:15968] 添加${deviceName}`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -53,10 +53,10 @@ describe('Bot Connect - 添加Bot设备', () => {
expect(result).toBe(true); expect(result).toBe(true);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -43,13 +43,13 @@ describe('CeilingLight Connect - 添加吸顶灯设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('通过BLE添加吸顶灯', async () => { it('[ONES:15950] 通过BLE添加吸顶灯', async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record('添加吸顶灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record('[P0][ONES:15950] 添加吸顶灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -65,10 +65,10 @@ describe('CeilingLight Connect - 添加吸顶灯设备', () => {
expect(result).toBe(true); expect(result).toBe(true);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -28,6 +28,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('ceilingLight', 'CEILING_LIGHT_DEVICE'); 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 - 吸顶灯功能页', () => { describe('CeilingLight Control - 吸顶灯功能页', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let reporter: TestReporter; let reporter: TestReporter;
@ -72,10 +76,10 @@ describe('CeilingLight Control - 吸顶灯功能页', () => {
const isOn = source.includes('ON') || source.includes('Night Light') || source.includes('Full'); const isOn = source.includes('ON') || source.includes('Night Light') || source.includes('Full');
expect(isOn).toBe(true); expect(isOn).toBe(true);
reporter.record('打开开关', 'PASS', Date.now() - start, `吸顶灯已开启`); reporter.record(`${CTRL_CEIL} 打开开关`, 'PASS', Date.now() - start, `吸顶灯已开启`);
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });
@ -254,10 +258,10 @@ describe('CeilingLight Control - 吸顶灯功能页', () => {
!source.includes('Night Light'); !source.includes('Night Light');
console.log('吸顶灯已关闭:', isOff); console.log('吸顶灯已关闭:', isOff);
reporter.record('关闭开关', 'PASS', Date.now() - start, `吸顶灯已关闭`); reporter.record(`${CTRL_CEIL} 关闭开关`, 'PASS', Date.now() - start, `吸顶灯已关闭`);
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -43,13 +43,13 @@ describe('ColorBulb Connect - 添加彩灯设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('通过BLE添加彩灯', async () => { it('[ONES:15960] 通过BLE添加彩灯', async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record('添加彩灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record('[P0][ONES:15960] 添加彩灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -65,10 +65,10 @@ describe('ColorBulb Connect - 添加彩灯设备', () => {
expect(result).toBe(true); expect(result).toBe(true);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -28,6 +28,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('colorBulb', 'COLOR_BULB_DEVICE'); 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 - 彩灯功能页', () => { describe('ColorBulb Control - 彩灯功能页', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let reporter: TestReporter; let reporter: TestReporter;
@ -72,10 +76,10 @@ describe('ColorBulb Control - 彩灯功能页', () => {
const isOn = source.includes('ON') || source.includes('Dynamic') || source.includes('Color'); const isOn = source.includes('ON') || source.includes('Dynamic') || source.includes('Color');
expect(isOn).toBe(true); expect(isOn).toBe(true);
reporter.record('打开开关', 'PASS', Date.now() - start, `彩灯已开启`); reporter.record(`${CTRL_BULB} 打开开关`, 'PASS', Date.now() - start, `彩灯已开启`);
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -15,6 +15,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('curtain', 'CURTAIN_DEVICE'); 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添加窗帘设备', () => { describe('Curtain Connect - 通过BLE添加窗帘设备', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let reporter: TestReporter; let reporter: TestReporter;
@ -30,13 +34,13 @@ describe('Curtain Connect - 通过BLE添加窗帘设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('通过BLE添加窗帘设备', async () => { it(`${ADD_ANCHOR} 通过BLE添加窗帘设备`, async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record('添加窗帘设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -53,10 +57,10 @@ describe('Curtain Connect - 通过BLE添加窗帘设备', () => {
expect(result).toBe(true); expect(result).toBe(true);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -19,6 +19,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('curtain', 'CURTAIN_DEVICE'); 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 - 窗帘控制功能', () => { describe('Curtain Control - 窗帘控制功能', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let reporter: TestReporter; let reporter: TestReporter;
@ -68,10 +72,10 @@ describe('Curtain Control - 窗帘控制功能', () => {
source.includes('Opening') || source.includes('Opened'); source.includes('Opening') || source.includes('Opened');
console.log('打开窗帘状态:', statusChanged); console.log('打开窗帘状态:', statusChanged);
reporter.record('打开窗帘', 'PASS', Date.now() - start, `Open按钮点击成功, 状态变化=${statusChanged}`); reporter.record(`${CTRL_CURTAIN} 打开窗帘`, 'PASS', Date.now() - start, `Open按钮点击成功, 状态变化=${statusChanged}`);
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });
@ -91,10 +95,10 @@ describe('Curtain Control - 窗帘控制功能', () => {
source.includes('Closing') || source.includes('Closed'); source.includes('Closing') || source.includes('Closed');
console.log('关闭窗帘状态:', statusChanged); console.log('关闭窗帘状态:', statusChanged);
reporter.record('关闭窗帘', 'PASS', Date.now() - start, `Close按钮点击成功, 状态变化=${statusChanged}`); reporter.record(`${CTRL_CURTAIN} 关闭窗帘`, 'PASS', Date.now() - start, `Close按钮点击成功, 状态变化=${statusChanged}`);
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -53,13 +53,13 @@ describe('Fan Connect - 添加风扇设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('通过BLE添加风扇', async () => { it('[ONES:122775] 通过BLE添加风扇', async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record('通过BLE添加风扇', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record('[P0][ONES:122775] 通过BLE添加风扇', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -75,10 +75,10 @@ describe('Fan Connect - 添加风扇设备', () => {
expect(result).toBe(true); expect(result).toBe(true);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -32,6 +32,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('fan', 'FAN_DEVICE'); 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 - 风扇控制页', () => { describe('Fan Control - 风扇控制页', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let reporter: TestReporter; let reporter: TestReporter;
@ -81,10 +85,10 @@ describe('Fan Control - 风扇控制页', () => {
const isOn = source.includes('ON') || source.includes('Normal') || source.includes('Speed'); const isOn = source.includes('ON') || source.includes('Normal') || source.includes('Speed');
console.log('风扇开启:', isOn); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });
@ -390,10 +394,10 @@ describe('Fan Control - 风扇控制页', () => {
const isOff = source.includes('OFF') || source.includes('off') || source.includes('Disconnected'); const isOff = source.includes('OFF') || source.includes('off') || source.includes('Disconnected');
console.log('风扇关闭:', isOff); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -14,6 +14,8 @@ import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') }); dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('hub', 'HUB_DEVICE'); const deviceName = getDeviceName('hub', 'HUB_DEVICE');
import { onesAdd } from "../../utils/common";
const ADD_ANCHOR = onesAdd("hub", deviceName);
describe('Hub Connect - 添加Hub设备', () => { describe('Hub Connect - 添加Hub设备', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
@ -30,13 +32,13 @@ describe('Hub Connect - 添加Hub设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('通过BLE添加Hub设备', async () => { it(`${ADD_ANCHOR} 通过BLE添加Hub设备`, async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record('添加Hub设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record(`${ADD_ANCHOR} 添加Hub设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -52,10 +54,10 @@ describe('Hub Connect - 添加Hub设备', () => {
expect(result).toBe(true); expect(result).toBe(true);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -14,6 +14,8 @@ import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') }); dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('humidifier', 'HUMIDIFIER_DEVICE'); const deviceName = getDeviceName('humidifier', 'HUMIDIFIER_DEVICE');
import { onesAdd } from "../../utils/common";
const ADD_ANCHOR = onesAdd("humidifier", deviceName);
describe('Humidifier Connect - 添加加湿器设备', () => { describe('Humidifier Connect - 添加加湿器设备', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
@ -30,13 +32,13 @@ describe('Humidifier Connect - 添加加湿器设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('通过BLE添加加湿器设备', async () => { it(`${ADD_ANCHOR} 通过BLE添加加湿器设备`, async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record('添加加湿器设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record(`${ADD_ANCHOR} 添加加湿器设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -52,10 +54,10 @@ describe('Humidifier Connect - 添加加湿器设备', () => {
expect(result).toBe(true); expect(result).toBe(true);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -17,6 +17,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('humidifier', 'HUMIDIFIER_DEVICE'); 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 - 功能页操作', () => { describe('Humidifier Control - 功能页操作', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let reporter: TestReporter; let reporter: TestReporter;
@ -61,10 +65,10 @@ describe('Humidifier Control - 功能页操作', () => {
const isOn = source.includes('ON') || source.includes('Auto') || source.includes('Manual'); const isOn = source.includes('ON') || source.includes('Auto') || source.includes('Manual');
expect(isOn).toBe(true); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });
@ -279,10 +283,10 @@ describe('Humidifier Control - 功能页操作', () => {
!source.includes('Auto'); !source.includes('Auto');
console.log('加湿器已关闭:', isOff); console.log('加湿器已关闭:', isOff);
reporter.record('功能页关闭开关', 'PASS', Date.now() - start, `加湿器已关闭`); reporter.record(`${CTRL_HUMID} 功能页关闭开关`, 'PASS', Date.now() - start, `加湿器已关闭`);
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -53,13 +53,13 @@ describe('Keypad Connect - 添加Keypad设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('通过BLE添加Keypad', async () => { it('[ONES:15949] 通过BLE添加Keypad', async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); 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; return;
} }
@ -75,10 +75,10 @@ describe('Keypad Connect - 添加Keypad设备', () => {
expect(result).toBe(true); expect(result).toBe(true);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -14,6 +14,8 @@ import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') }); dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('lock', 'LOCK_DEVICE'); const deviceName = getDeviceName('lock', 'LOCK_DEVICE');
import { onesAdd } from "../../utils/common";
const ADD_ANCHOR = onesAdd("lock", deviceName);
describe('Lock Connect - 添加Lock设备', () => { describe('Lock Connect - 添加Lock设备', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
@ -30,13 +32,13 @@ describe('Lock Connect - 添加Lock设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('通过BLE添加Lock设备', async () => { it(`${ADD_ANCHOR} 通过BLE添加Lock设备`, async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record('添加Lock设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record(`${ADD_ANCHOR} 添加Lock设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -53,10 +55,10 @@ describe('Lock Connect - 添加Lock设备', () => {
expect(result).toBe(true); expect(result).toBe(true);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -15,6 +15,11 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('lock', 'LOCK_DEVICE'); 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 - 功能页操作', () => { describe('Lock Control - 功能页操作', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let reporter: TestReporter; let reporter: TestReporter;
@ -68,10 +73,10 @@ describe('Lock Control - 功能页操作', () => {
console.log('上锁状态:', isLocked); console.log('上锁状态:', isLocked);
expect(isLocked).toBe(true); expect(isLocked).toBe(true);
reporter.record('功能页-上锁', 'PASS', Date.now() - start, '门锁已上锁'); reporter.record(`${CTRL_LOCK} 上锁`, 'PASS', Date.now() - start, '门锁已上锁');
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });
@ -105,10 +110,10 @@ describe('Lock Control - 功能页操作', () => {
await sleep(5000); await sleep(5000);
} }
reporter.record('功能页-解锁', 'PASS', Date.now() - start, '门锁已解锁'); reporter.record(`${CTRL_LOCK} 解锁`, 'PASS', Date.now() - start, '门锁已解锁');
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -14,6 +14,8 @@ import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') }); dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('meter', 'METER_DEVICE'); const deviceName = getDeviceName('meter', 'METER_DEVICE');
import { onesAdd } from "../../utils/common";
const ADD_ANCHOR = onesAdd("meter", deviceName);
describe('Meter Connect - 添加Meter设备', () => { describe('Meter Connect - 添加Meter设备', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
@ -30,13 +32,13 @@ describe('Meter Connect - 添加Meter设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('通过BLE添加Meter设备', async () => { it(`${ADD_ANCHOR} 通过BLE添加Meter设备`, async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record('添加Meter设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record(`${ADD_ANCHOR} 添加Meter设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -52,10 +54,10 @@ describe('Meter Connect - 添加Meter设备', () => {
expect(result).toBe(true); expect(result).toBe(true);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -14,6 +14,9 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('meter', 'METER_DEVICE'); const deviceName = getDeviceName('meter', 'METER_DEVICE');
// 必测项: meter 控制步(报警/校正)在此文件无对应用例,跳过;切换单位对应 feature 用例 74078(case 级,无协议)
const FEAT_UNIT = '[P0][ONES:74078]';
describe('Meter Control - 功能页操作', () => { describe('Meter Control - 功能页操作', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let reporter: TestReporter; let reporter: TestReporter;
@ -58,10 +61,10 @@ describe('Meter Control - 功能页操作', () => {
const hasCelsius = source.includes('°C') || source.includes('℃'); const hasCelsius = source.includes('°C') || source.includes('℃');
expect(hasCelsius).toBe(true); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });
@ -91,10 +94,10 @@ describe('Meter Control - 功能页操作', () => {
await sleep(1000); await sleep(1000);
} }
reporter.record('切换单位为°F', 'PASS', Date.now() - start, '温度单位已切换为°F并还原'); reporter.record(`${FEAT_UNIT} 切换单位为°F`, 'PASS', Date.now() - start, '温度单位已切换为°F并还原');
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -40,13 +40,13 @@ describe('OSC Connect - 通过BLE添加OSC', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('通过BLE添加OSC', async () => { it('[ONES:113942] 通过BLE添加OSC', async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); 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; return;
} }
@ -67,14 +67,14 @@ describe('OSC Connect - 通过BLE添加OSC', () => {
const elapsed = ((Date.now() - start) / 1000).toFixed(1); const elapsed = ((Date.now() - start) / 1000).toFixed(1);
if (success) { 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 { } 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添加失败'); throw new Error('OSC添加失败');
} }
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -14,6 +14,8 @@ import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') }); dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('plug', 'PLUG_DEVICE'); const deviceName = getDeviceName('plug', 'PLUG_DEVICE');
import { onesAdd } from "../../utils/common";
const ADD_ANCHOR = onesAdd("plug", deviceName);
describe('Plug Connect - 添加Plug设备', () => { describe('Plug Connect - 添加Plug设备', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
@ -30,13 +32,13 @@ describe('Plug Connect - 添加Plug设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('通过BLE添加Plug设备', async () => { it(`${ADD_ANCHOR} 通过BLE添加Plug设备`, async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record('添加Plug设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record(`${ADD_ANCHOR} 添加Plug设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -52,10 +54,10 @@ describe('Plug Connect - 添加Plug设备', () => {
expect(result).toBe(true); expect(result).toBe(true);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -19,6 +19,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('plug', 'PLUG_DEVICE'); 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 - 功能页操作', () => { describe('Plug Control - 功能页操作', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let reporter: TestReporter; let reporter: TestReporter;
@ -88,10 +92,10 @@ describe('Plug Control - 功能页操作', () => {
await sleep(5000); await sleep(5000);
expect(statusChanged).toBe(true); expect(statusChanged).toBe(true);
reporter.record('功能页打开/关闭开关', 'PASS', Date.now() - start, `控制页开关切换成功`); reporter.record(`${CTRL_PLUG}功能页打开/关闭开关`, 'PASS', Date.now() - start, `控制页开关切换成功`);
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -24,6 +24,8 @@ import * as path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') }); dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('robot', 'ROBOT_DEVICE'); const deviceName = getDeviceName('robot', 'ROBOT_DEVICE');
import { onesAdd } from "../../utils/common";
const ADD_ANCHOR = onesAdd("robot", deviceName);
describe('Robot Connect - 添加扫地机', () => { describe('Robot Connect - 添加扫地机', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
@ -40,13 +42,13 @@ describe('Robot Connect - 添加扫地机', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('添加扫地机', async () => { it(`${ADD_ANCHOR} 添加扫地机`, async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record('添加扫地机', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -67,14 +69,14 @@ describe('Robot Connect - 添加扫地机', () => {
const elapsed = ((Date.now() - start) / 1000).toFixed(1); const elapsed = ((Date.now() - start) / 1000).toFixed(1);
if (success) { if (success) {
reporter.record('添加扫地机', 'PASS', Date.now() - start, `扫地机添加成功, 耗时${elapsed}s`); reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'PASS', Date.now() - start, `扫地机添加成功, 耗时${elapsed}s`);
} else { } else {
reporter.record('添加扫地机', 'FAIL', Date.now() - start, `扫地机添加失败, 耗时${elapsed}s`); reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'FAIL', Date.now() - start, `扫地机添加失败, 耗时${elapsed}s`);
throw new Error('扫地机添加失败'); throw new Error('扫地机添加失败');
} }
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -15,6 +15,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('sensor', 'SENSOR_DEVICE'); 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 - 功能页操作', () => { describe('Sensor Control - 功能页操作', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let reporter: TestReporter; let reporter: TestReporter;
@ -64,10 +68,10 @@ describe('Sensor Control - 功能页操作', () => {
console.log(detail); console.log(detail);
expect(hasDetectionStatus || hasIndicatorLight).toBe(true); expect(hasDetectionStatus || hasIndicatorLight).toBe(true);
reporter.record('功能页显示', 'PASS', Date.now() - start, detail); reporter.record(`${CTRL_SENSOR} 功能页显示`, 'PASS', Date.now() - start, detail);
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -43,13 +43,13 @@ describe('StripLight Connect - 添加灯带设备', () => {
await driver.destroySession(); await driver.destroySession();
}); });
it('通过BLE添加灯带', async () => { it('[ONES:15959] 通过BLE添加灯带', async () => {
const start = Date.now(); const start = Date.now();
try { try {
const alreadyExists = await isDeviceOnHomepage(driver, deviceName); const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
if (alreadyExists) { if (alreadyExists) {
console.log(`${deviceName}已在首页,跳过重新添加`); console.log(`${deviceName}已在首页,跳过重新添加`);
reporter.record('添加灯带设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`); reporter.record('[P0][ONES:15959] 添加灯带设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
return; return;
} }
@ -65,10 +65,10 @@ describe('StripLight Connect - 添加灯带设备', () => {
expect(result).toBe(true); expect(result).toBe(true);
const elapsed = ((Date.now() - start) / 1000).toFixed(1); 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -28,6 +28,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('stripLight', 'STRIP_LIGHT_DEVICE'); 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 - 灯带功能页', () => { describe('StripLight Control - 灯带功能页', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let reporter: TestReporter; let reporter: TestReporter;
@ -72,10 +76,10 @@ describe('StripLight Control - 灯带功能页', () => {
const isOn = source.includes('ON') || source.includes('White') || source.includes('Scene'); const isOn = source.includes('ON') || source.includes('White') || source.includes('Scene');
expect(isOn).toBe(true); expect(isOn).toBe(true);
reporter.record('打开开关', 'PASS', Date.now() - start, `灯带已开启`); reporter.record(`${CTRL_STRIP} 打开开关`, 'PASS', Date.now() - start, `灯带已开启`);
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });
@ -245,10 +249,10 @@ describe('StripLight Control - 灯带功能页', () => {
!source.includes('Scene'); !source.includes('Scene');
console.log('灯带已关闭:', isOff); console.log('灯带已关闭:', isOff);
reporter.record('关闭开关', 'PASS', Date.now() - start, `灯带已关闭`); reporter.record(`${CTRL_STRIP} 关闭开关`, 'PASS', Date.now() - start, `灯带已关闭`);
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -25,6 +25,9 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const deviceName = getDeviceName('urc', 'URC_DEVICE'); const deviceName = getDeviceName('urc', 'URC_DEVICE');
// 必测项 feature: 同步URC设备、控制 → 用例 190802(case 级,无协议)
const FEAT_URC = '[P0][ONES:190802]';
describe('URC Control - 万能遥控器功能操作', () => { describe('URC Control - 万能遥控器功能操作', () => {
let driver: DeviceDriver; let driver: DeviceDriver;
let reporter: TestReporter; let reporter: TestReporter;
@ -159,10 +162,10 @@ describe('URC Control - 万能遥控器功能操作', () => {
|| source.includes('完成') || source.includes('Device Management'); || source.includes('完成') || source.includes('Device Management');
console.log('同步一个设备:', syncDone); console.log('同步一个设备:', syncDone);
reporter.record('设备管理-同步一个设备', 'PASS', Date.now() - start, `同步完成=${syncDone}`); reporter.record(`${FEAT_URC} 设备管理-同步一个设备`, 'PASS', Date.now() - start, `同步完成=${syncDone}`);
} catch (e: any) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });
@ -200,10 +203,10 @@ describe('URC Control - 万能遥控器功能操作', () => {
const syncDone = source.includes('Synced') || source.includes('Success') const syncDone = source.includes('Synced') || source.includes('Success')
|| source.includes('完成') || source.includes('Device Management'); || 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) { } catch (e: any) {
const ss = await driver.screenshot().catch(() => ''); 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; throw e;
} }
}); });

View File

@ -8,3 +8,5 @@ export * from './room.helper';
export * from './timer.helper'; export * from './timer.helper';
export * from './scene.helper'; export * from './scene.helper';
export * from './feedback.helper'; export * from './feedback.helper';
export * from './network.helper';
export * from './ones-anchor.helper';

View File

@ -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( ONBLE_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<void> {
adbShell(`svc wifi ${on ? 'enable' : 'disable'}`);
await sleep(2000);
}
async function androidSetBluetooth(on: boolean): Promise<void> {
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<void> {
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<void> {
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<void> {
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);
}

View File

@ -0,0 +1,74 @@
/**
* ONES 锚点解析:同品类不同型号(UI ),,
* <CAT>_DEVICE ONES
*
* key = DEVICE_CONFIG ; = / step uuid(ble/wifi)
* 控制步在两条协议超级用例里:ble15975 / wifi15974
* 新型号:在此补一行即可被脚本识别
*/
export interface DeviceAnchor {
add?: number; // 添加用例 ONES number
ctrlBle?: string; // 15975 下的 step uuid
ctrlWifi?: string; // 15974 下的 step uuid
}
const ANCHORS: Record<string, Record<string, DeviceAnchor>> = {
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]` : '';
}

View File

@ -141,36 +141,22 @@ function lcs(a: string, b: string): number {
return dp[m][n]; 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 * payload case step
* - [ONES:<num>] case * - [ONES:<num>] case
* - [ONES:<num>#<stepUuid>] step * - [ONES:<num>#<stepUuid>] step
* matchResults LCS * matchResults LCS
* *
* opts.fullStepsByNumber: 用例号 step uuid ONES step * step/ step
* execute_result uuid case to_do * opts.totalStepsByNumber: 用例号
* step < case to_do passed/failed
*
*/ */
export function buildAnchoredPayloads( export function buildAnchoredPayloads(
planCases: OnesPlanCase[], planCases: OnesPlanCase[],
testResults: TestResult[], testResults: TestResult[],
executor: string, executor: string,
opts: { fullStepsByNumber?: Map<number, string[]> } = {} opts: { totalStepsByNumber?: Map<number, number> } = {}
): { payloads: OnesUpdatePayload[]; unanchored: TestResult[] } { ): { payloads: OnesUpdatePayload[]; unanchored: TestResult[] } {
const byNumber = new Map<number, OnesPlanCase>(); const byNumber = new Map<number, OnesPlanCase>();
for (const pc of planCases) byNumber.set(pc.caseNumber, pc); for (const pc of planCases) byNumber.set(pc.caseNumber, pc);
@ -204,19 +190,12 @@ export function buildAnchoredPayloads(
const payloads: OnesUpdatePayload[] = []; const payloads: OnesUpdatePayload[] = [];
// step 级用例:列全量 step未跑的仅 uuid全部跑完才聚合 case 结果 // step 级用例:只写跑过的 step全跑完才聚合 case 结果,否则 to_do
for (const [caseUUID, stepMap] of runSteps) { for (const [caseUUID, stepMap] of runSteps) {
const num = numberByUUID.get(caseUUID)!; const num = numberByUUID.get(caseUUID)!;
const full = opts.fullStepsByNumber?.get(num); const steps = Array.from(stepMap.values());
let steps: OnesStepResult[]; const total = opts.totalStepsByNumber?.get(num);
let allRun: boolean; const allRun = total != null ? stepMap.size >= total : true;
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; // 无全量列表时,以已有结果为准
}
let agg: SyncStatus | undefined; let agg: SyncStatus | undefined;
for (const s of stepMap.values()) if (s.execute_result) agg = mergeStatus(agg, s.execute_result); 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'; const result: OnesUpdatePayload['result'] = allRun ? agg ?? 'to_do' : 'to_do';
@ -235,32 +214,62 @@ export function buildAnchoredPayloads(
/** /**
* POST payload ONES * 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 mutationupdateTestcasePlanCase /
* updateTestcasePlanCaseStep ones CLI token/PAT
*
* key
* case: testcase_plan_case-<planUUID>-<caseUUID>
* step: testcase_plan_case_step-<planUUID>-<caseUUID>-<stepUuid>
*/
export function postPayloads( export function postPayloads(
planUUID: string, planUUID: string,
payload: OnesUpdatePayload[] payload: OnesUpdatePayload[]
): { success: number; failed: number } { ): { success: number; failed: number } {
if (payload.length === 0) return { success: 0, failed: 0 };
const batchSize = 50;
let success = 0; let success = 0;
let failed = 0; let failed = 0;
for (let i = 0; i < payload.length; i += batchSize) { let stepsWritten = 0;
const batch = payload.slice(i, i + batchSize);
for (const p of payload) {
try { try {
execSync(buildCurlCommand(planUUID, batch), { encoding: 'utf-8', timeout: 30000 }); // 先写 step仅跑过的
success += batch.length; 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) { } catch (e: any) {
console.error(`ONES sync batch failed: ${e.message}`); console.error(`[ONES] 用例 ${p.uuid} 回写失败: ${e.message}`);
failed += batch.length; failed++;
} }
} }
if (stepsWritten) console.log(`[ONES] 写入 ${stepsWritten} 个 step 结果`);
return { success, failed }; return { success, failed };
} }
/** /**
* ONES * ONES case steps payload postPayloads
*
* API: POST /project/api/project/team/{team_uuid}/testcase/plan/{plan_uuid}/cases/update
* Body: { cases: [{ uuid, executor, note, result, steps: [{ uuid, execute_result }] }] }
*/ */
export function syncResultsToOnes( export function syncResultsToOnes(
planUUID: string, planUUID: string,
@ -274,17 +283,6 @@ export function syncResultsToOnes(
return postPayloads(planUUID, payload); 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}'`;
}
/** /**
* 一键同步: 读取计划用例 * 一键同步: 读取计划用例
*/ */

View File

@ -152,6 +152,12 @@ export class WDAHelper {
} catch {} } catch {}
} }
/** 激活任意 bundleId 的 app(用于切到系统设置 com.apple.Preferences 再切回)。 */
async activateAppById(bundleId: string): Promise<void> {
await this.request('POST', `/session/${this.sessionId}/wda/apps/activate`, { bundleId });
await new Promise(r => setTimeout(r, 2500));
}
async destroySession(): Promise<void> { async destroySession(): Promise<void> {
if (this.sessionId && !this.reusedSession) { if (this.sessionId && !this.reusedSession) {
await this.request('DELETE', `/session/${this.sessionId}`); await this.request('DELETE', `/session/${this.sessionId}`);