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:
parent
fda826446a
commit
373a8bd6fa
Binary file not shown.
|
|
@ -106,7 +106,7 @@ add_table_with_header(doc, ['设备', '脚本文件数', '覆盖模块', '通过
|
|||
|
||||
doc.add_heading('1.4 目标', level=2)
|
||||
doc.add_paragraph('• P0 必测项:各单品添加 + 核心控制(双协议)准入闸门,来源 ONES 必测项-AI自动化')
|
||||
doc.add_paragraph('• P1 单品探索:82 款单品主流程冒烟,覆盖广度')
|
||||
doc.add_paragraph('• P1 单品探索:78 款单品主流程冒烟,覆盖广度')
|
||||
doc.add_paragraph('• P2 全功能:全维度回归(card/control/setting/scene/logs),覆盖深度')
|
||||
doc.add_paragraph('• P3 平台:账号/房间/消息/家庭分享/场景等平台功能')
|
||||
doc.add_paragraph('• 按优先级 backlog 依次交付,各层有独立退出标准(不绑定固定周次)')
|
||||
|
|
@ -114,7 +114,7 @@ doc.add_paragraph('• 按优先级 backlog 依次交付,各层有独立退出
|
|||
doc.add_page_break()
|
||||
|
||||
# ======================== 2. 设备清单 ========================
|
||||
doc.add_heading('2. 待覆盖设备清单(82款)', level=1)
|
||||
doc.add_heading('2. 待覆盖设备清单(78款)', level=1)
|
||||
|
||||
device_categories = {
|
||||
'窗帘 (9款)': [
|
||||
|
|
@ -214,11 +214,7 @@ device_categories = {
|
|||
('AI PinNote', '需新写', '全新品类'),
|
||||
('Bot Rechargeable', '复用bot', '调试适配'),
|
||||
],
|
||||
'机器人+联名+其他 (8款)': [
|
||||
('OBBOTO 1.0', '需新写', '全新品类'),
|
||||
('Robotic Actuator', '需新写', '全新品类'),
|
||||
('Robotic Arm', '需新写', '全新品类'),
|
||||
('Robotic Picker', '需新写', '全新品类'),
|
||||
'机器人+联名+其他 (4款)': [
|
||||
('SwitchBot KATAフレンズ KUMAMON ver.', '复用bot', '特殊UI定制'),
|
||||
('KATA Friends 国行版', '复用bot', '特殊UI定制'),
|
||||
('Outdoor PTC', '需新写', '全新品类'),
|
||||
|
|
@ -257,7 +253,7 @@ add_table_with_header(doc,
|
|||
)
|
||||
|
||||
doc.add_paragraph()
|
||||
doc.add_paragraph('用例数量估算:每设备平均 25-40 条,82款设备总计约 2000-3000 条自动化用例。')
|
||||
doc.add_paragraph('用例数量估算:每设备平均 25-40 条,78款设备总计约 2000-3000 条自动化用例。')
|
||||
doc.add_paragraph('AI 提效口径:单维度脚本首台 0.2-1 人日、复用台 0.1-0.3 人日;串口添加框架为一次性投入(见 §5.2)。下列各层人力预估均按此口径,单位为人日(1 人)。')
|
||||
|
||||
doc.add_page_break()
|
||||
|
|
@ -271,8 +267,8 @@ doc.add_paragraph(
|
|||
|
||||
rows = [
|
||||
('P0 必测项', '各单品添加 + 核心控制(双协议)', 'ONES 必测项-AI自动化 (CQz9YCNX)', '187', '每次提测/版本必跑', '通过率≥95%'),
|
||||
('P1 单品探索', '每单品主流程冒烟(card+核心control)', '按单品模板', '82款各1遍', '每迭代', '全单品冒烟通过'),
|
||||
('P2 全功能', '全维度回归(card/control/setting/scene/logs)', '各品类用例库', '~2366', '版本回归', '通过率≥85%'),
|
||||
('P1 单品探索', '每单品主流程冒烟(card+核心control)', '按单品模板', '78款各1遍', '每迭代', '全单品冒烟通过'),
|
||||
('P2 全功能', '全维度回归(card/control/setting/scene/logs)', '各品类用例库', '~2250', '版本回归', '通过率≥85%'),
|
||||
('P3 平台', '账号/房间/消息/家庭分享/场景等平台功能', 'App平台用例库', '平台模块', '版本回归', '平台用例通过'),
|
||||
]
|
||||
add_table_with_header(doc,
|
||||
|
|
@ -350,14 +346,17 @@ doc.add_page_break()
|
|||
# ======================== 6. P1 单品探索 ========================
|
||||
doc.add_heading('6. P1 单品探索(覆盖广度)', level=1)
|
||||
doc.add_paragraph(
|
||||
'必测项达标后,为全部 82 款单品各跑通一遍主流程冒烟,确保每款设备最低限度可用。'
|
||||
'范围 = card(首页卡片)+ 核心 control(开关/主模式),不含设置/场景/日志等深度回归。'
|
||||
'必测项达标后,为全部 78 款单品覆盖一遍探索测试:card + 核心 control,并扩展到'
|
||||
'全品类核心功能的「部分全功能用例」(每品类挑代表性主功能,不做完整深度回归)。'
|
||||
)
|
||||
doc.add_paragraph('• 目标:82 款单品各 1 套冒烟,广度优先、深度浅。')
|
||||
doc.add_paragraph('• 复用必测项已建的 control helper,单品只补 card 与主流程断言。')
|
||||
doc.add_paragraph('• 退出标准:每款单品冒烟用例通过(card 可见 + 核心控制响应)。')
|
||||
doc.add_paragraph('• 目标:78 款单品各 1 套探索集(card + 核心控制 + 品类代表性主功能),广度优先。')
|
||||
doc.add_paragraph('• 已有/复用品类:复用 P0 已建 control helper,补 card 与主功能断言,较快。')
|
||||
doc.add_paragraph('• 新品未支持:扫地机新机 / Lock Ultra·Vision / AI产品 / Art Frame 等 ~26 款为全新 UI,'
|
||||
'P1 需先建该 UI 的探索脚本支持,这部分较贵、是 P1 的主要成本。')
|
||||
doc.add_paragraph('• 退出标准:每款单品探索集通过(card 可见 + 核心控制响应 + 代表性主功能可用)。')
|
||||
doc.add_paragraph('• 运行频率:每迭代跑一遍,作为单品级回归基线。')
|
||||
doc.add_paragraph('• 人力预估(AI 辅助):约 12-16 人日(建立在 P0 已建 control helper 之上,82 款多为同品类复用,首台 ~0.3、复用台 ~0.1 人日/款)。')
|
||||
doc.add_paragraph('• 旧代码转换提效:部分用例由以前旧代码转换而来,已含测试路径/步骤,只需定位元素位置使脚本可执行(Midscene AI 定位较快)——这类省去写用例的工作。新品无旧码、不受益。')
|
||||
doc.add_paragraph('• 人力预估(AI 辅助):约 16-22 人日(已有/复用品类 ~52 款较快,部分还有旧码转换的现成路径;新品 ~26 款需首次建新 UI 探索支持,为主要成本)。')
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
|
|
@ -368,11 +367,20 @@ doc.add_paragraph(
|
|||
'同品类 UI 高度相似,调通首台后改设备名配置快速扩展,并沉淀通用 helper。'
|
||||
'不绑定周次,按复用度从高到低排批次。'
|
||||
)
|
||||
doc.add_paragraph(
|
||||
'脚本状态分三层成本(见第 2 节):「已有脚本」只需在真机上调试适配,最快;'
|
||||
'「复用模板」改设备名/配置次之;「需新写」(全新 UI)最贵。估算按此分层。'
|
||||
)
|
||||
doc.add_paragraph(
|
||||
'进一步提效:功能页/设置页「入口」全单品通用(沉淀一个全局导航 helper,不逐设备重写);'
|
||||
'同品类功能测试点高度雷同,首台调通后其余只调「差异功能」。'
|
||||
'另有部分用例由旧代码转换而来、已含路径,只需定位元素即可执行。故 P2 实际只在每设备增量调试少量差异点。'
|
||||
)
|
||||
|
||||
rows = [
|
||||
('批1·高复用', '窗帘9 / 锁12 / 插座开关6 / 灯光10', '37', '脚本×37 + curtain/lock/relay/light helper', '~25'),
|
||||
('批2·中复杂', '扫地机9 / 传感器温控7 / 风扇空净加湿6 / 摄像头门铃3', '25', '脚本×25 + robot/sensor/climate helper', '~27'),
|
||||
('批3·新品特殊', 'Hub门控安防5 / AI产品5 / 机器人联名5', '15+', '脚本×15 + 全新UI框架', '~28'),
|
||||
('批1·高复用', '窗帘9 / 锁12 / 插座开关6 / 灯光10', '37', '差异功能 + 通用入口 helper', '~12'),
|
||||
('批2·中复杂', '扫地机9 / 传感器温控7 / 风扇空净加湿6 / 摄像头门铃3', '25', '差异功能 + robot/sensor/climate helper', '~14'),
|
||||
('批3·新品特殊', 'Hub门控安防 / AI产品 / 联名其他(KATA/FindCard/PTC)', '16', '深度回归(UI支持已在P1建好)', '~10'),
|
||||
]
|
||||
add_table_with_header(doc,
|
||||
['批次', '品类', '设备数', '交付物', 'AI预估(人日)'],
|
||||
|
|
@ -383,10 +391,10 @@ add_table_with_header(doc,
|
|||
doc.add_paragraph()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run('覆盖目标:').bold = True
|
||||
p.add_run('全部 82 款单品全维度回归,~2366 条用例,单品功能覆盖率 100%。')
|
||||
p.add_run('全部 78 款单品全维度回归,~2250 条用例,单品功能覆盖率 100%。')
|
||||
p = doc.add_paragraph()
|
||||
p.add_run('P2 合计:').bold = True
|
||||
p.add_run('约 75-90 人日(仅 card/control/setting 约 60-70,含 scene/logs 约 75-90)。约 50 款为“已有/复用”可低成本适配;真正“需新写”的新品 UI 约 30 款(扫地机新机/Lock Ultra·Vision/AI产品/机器人)需 Figma/UX 重新分析,AI 辅助有限、为主要成本;add/control 已被 P0/P1 覆盖可复用。')
|
||||
p.add_run('约 25-35 人日。入口通用、同品类只调差异功能、card/control 与新品 UI 已在 P0/P1 建好;且已有品类不少用例由旧代码转换、已含路径,只需定位元素(Midscene AI 定位)——P2 主要剩真机跑通+少量差异深度。已接近实际地板:每设备至少真机验一次 + flaky 复跑不可再压。注:P0+P1+P2 总量守恒。')
|
||||
|
||||
doc.add_heading('7.1 高复用品类要点', level=2)
|
||||
doc.add_paragraph('• 窗帘: 位置控制、校准流程、定时器、群组控制 → curtain_helper')
|
||||
|
|
@ -404,7 +412,7 @@ doc.add_heading('7.3 新品+特殊要点', level=2)
|
|||
doc.add_paragraph('• Hub 3 / AI Hub Show: 设备管理、红外学习、Matter、带屏交互')
|
||||
doc.add_paragraph('• 门控安防: Garage Door、Safety Alarm、Radiator Thermostat')
|
||||
doc.add_paragraph('• AI产品: Art Frame、AI Pet、AI PinNote(全新UI,需从Figma/UX重新分析)')
|
||||
doc.add_paragraph('• 机器人+联名: OBBOTO、Robotic Actuator/Arm/Picker、KATA Friends(Bot复用+定制皮肤)')
|
||||
doc.add_paragraph('• 联名+其他: KATA Friends(Bot复用+定制皮肤)、FindCard(已有脚本调试适配)、Outdoor PTC(需新写)')
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
|
|
@ -424,7 +432,7 @@ rows = [
|
|||
add_table_with_header(doc, ['模块', '内容', '来源'], rows)
|
||||
|
||||
doc.add_heading('8.2 自动化/场景联动', level=2)
|
||||
doc.add_paragraph('验证设备间联动可靠性(复用已有 automation 脚本基础):')
|
||||
doc.add_paragraph('验证设备间联动可靠性(复用已有 automation 脚本基础)。场景联动里的设备功能入口/控制 P0-P2 已调通,联动部分调试较快;账号/房间/消息/分享为纯平台功能,不复用设备脚本。')
|
||||
doc.add_paragraph('• 创建自动化:手动/条件触发/定时触发')
|
||||
doc.add_paragraph('• 执行验证:触发条件满足后动作执行')
|
||||
doc.add_paragraph('• 编辑/删除:修改条件或动作、删除自动化')
|
||||
|
|
@ -452,7 +460,7 @@ doc.add_paragraph(
|
|||
rows = [
|
||||
('P0 · 控制轨', '自动化(1人)', '—', '先行:双协议控制 105 step,不待串口'),
|
||||
('P0 · 添加轨', '自动化(1人)', '嵌入式(串口协议+框架)', '串口就绪后插入:73 添加 + 9 功能'),
|
||||
('P1 单品探索', '自动化(1人)', '—', 'P0 达标后:82 款冒烟'),
|
||||
('P1 单品探索', '自动化(1人)', '—', 'P0 达标后:78 款冒烟'),
|
||||
('P2 全功能', '自动化(1人)', '—', '按品类批量,复用 helper'),
|
||||
('P3 平台', '自动化(1人)', '—', '最后:平台功能 + 场景联动'),
|
||||
]
|
||||
|
|
@ -475,12 +483,18 @@ add_table_with_header(doc, ['用途', '设备需求', '到位时间'], rows)
|
|||
|
||||
doc.add_heading('9.3 环境', level=2)
|
||||
rows = [
|
||||
('测试手机', 'Samsung (Android) + iPhone (iOS)', '已有'),
|
||||
('Appium Server', 'v2.x + UIAutomator2', '已有'),
|
||||
('测试机', 'Mac×2(双调试位)+ Android×2(Samsung 等)+ iOS×2(iPhone)', '已有'),
|
||||
('Appium Server', 'v2.x + UIAutomator2 / WebDriverAgent', '已有'),
|
||||
('网络环境', '稳定Wi-Fi (Deco)', '已有'),
|
||||
('CI/CD', 'Jenkins/GitHub Actions (可选)', '待搭建'),
|
||||
]
|
||||
add_table_with_header(doc, ['项目', '说明', '状态'], rows)
|
||||
doc.add_paragraph('硬件并行(2 Mac + 2 Android + 2 iOS)压缩日历:')
|
||||
doc.add_paragraph('• Android∥iOS 并行:Mac1+Android、Mac2+iOS 同时推进,iOS 增量(25-45 人日)与 Android 时间线重叠,不再串行累加(最大杠杆)。')
|
||||
doc.add_paragraph('• 每平台 2 台:一台调试、另一台跑全量回归/flaky 复跑(白天+夜间),执行类 wall-clock 再砍。')
|
||||
doc.add_paragraph('• 双 Mac 各挂一个 AI agent(Claude Code/Midscene)并行“边跑边写”,人只做 review——唯一能突破“1 人”瓶颈的方式。')
|
||||
doc.add_paragraph('• 上限:仍 1 人,手动交互调试不可一人并干两台(Amdahl);正确用法=「1 台主调 + 1 台挂 AI/跑回归」。')
|
||||
doc.add_paragraph('• 净效果:双平台日历 ~90-130 → ~65-90 工作日(≈ 压回单平台水平),人日(工作量)不变;提速幅度取决于多少调试可交给 AI 自动跑。')
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
|
|
@ -489,20 +503,53 @@ doc.add_heading('10. 优先级 backlog 一览', level=1)
|
|||
|
||||
rows = [
|
||||
('P0 必测项', '添加73 + 功能9 + 控制105step(双协议)', '187条', '~11-15', 'serial框架 + connect + 控制断言 + step回写', '通过率≥95%'),
|
||||
('P1 单品探索', '82款单品主流程冒烟(card+核心control)', '82款×1', '~12-16', '每单品冒烟用例', '全单品冒烟通过'),
|
||||
('P2 全功能', '全维度回归,按品类批量(高复用→新品)', '~2366条', '~75-90', '85+脚本 + 8个通用helper', '通过率≥85%、覆盖100%'),
|
||||
('P3 平台', '账号/房间/消息/家庭分享/场景等', '平台模块', '~12-15', '平台用例 + 场景联动', '平台用例通过'),
|
||||
('P1 单品探索', '全品类 card+核心control + 代表性主功能(部分全功能);含新品UI首次支持', '78款各1套', '~16-22', '探索集脚本 + 新品UI支持', '全品类探索集通过'),
|
||||
('P2 全功能', '全维度深度回归(setting/scene/logs+完整功能点)', '~2250条', '~25-35', '深度回归(入口/路径多已就绪,补定位)', '通过率≥85%、覆盖100%'),
|
||||
('P3 平台', '账号/房间/消息/家庭分享/场景等', '平台模块', '~10-13', '平台用例 + 场景联动(复用设备入口)', '平台用例通过'),
|
||||
]
|
||||
add_table_with_header(doc,
|
||||
['优先级', '范围', '用例量', 'AI预估(人日)', '交付物', '退出标准'],
|
||||
['优先级', '范围', '用例量', 'AI预估(人日,单平台)', '交付物', '退出标准'],
|
||||
rows,
|
||||
col_widths=[2, 4.3, 1.5, 2, 4.2, 2.5]
|
||||
col_widths=[2, 4.3, 1.5, 2.2, 4, 2.5]
|
||||
)
|
||||
|
||||
doc.add_paragraph()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run('人力总计(AI 辅助口径):').bold = True
|
||||
p.add_run('约 105-135 人日 ≈ 5-7 人月(1 人,按 20 人日/月)。P0 大幅复用既有 control、串口硬件联调由嵌入式主导不计入;P2 “需新写”新品 UI 占比最高、最不可压缩。')
|
||||
p.add_run('人力总计(AI 辅助口径,单平台):').bold = True
|
||||
p.add_run('约 65-85 人日 ≈ 3.5-4.5 人月(1 人,按 20 人日/月)。P1 含新品(~26款)全新 UI 首次探索支持,是 P1 主成本;P2 因入口通用、card/control 与新品 UI 已在 P0-P1 建好、且不少用例由旧代码转换已含路径(只需定位元素),只做剩余深度回归。已接近地板:每设备至少真机验一次 + 新品 UI 分析 + flaky 复跑不可再压。P0+P1+P2 总量守恒。P3 纯平台功能不复用设备脚本。串口硬件联调由嵌入式主导、不计入。')
|
||||
|
||||
doc.add_paragraph()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run('双平台(Android + iOS):').bold = True
|
||||
p.add_run('以上为单平台口径(主调 Android)。iOS 增量约 +40-60%(非 ×2):用例逻辑/路径/helper 共享,'
|
||||
'Midscene AI 视觉定位跨平台大多可复用(省去两套选择器重写);增量主要是 iOS 真机执行+flaky 复跑、'
|
||||
'及平台差异(iOS 权限弹窗 / 返回交互 / 配网与添加流程 / 个别布局)。各层增量按其真机/差异占比分摊(P0 偏低,见下),分布如下:')
|
||||
|
||||
rows = [
|
||||
('P0 必测项', '11-15', '3-5', '14-20'),
|
||||
('P1 单品探索', '16-22', '6-13', '22-35'),
|
||||
('P2 全功能', '25-35', '10-21', '35-56'),
|
||||
('P3 平台', '10-13', '4-8', '14-21'),
|
||||
('合计', '65-85', '25-45', '90-130'),
|
||||
]
|
||||
add_table_with_header(doc, ['优先级', '单平台(人日)', 'iOS增量(人日)', '双平台合计(人日)'], rows)
|
||||
doc.add_paragraph('iOS 增量大头在 P2(深度回归真机执行最多)。P0 偏低:添加流程 app 侧通用(iOS 只适配一次 add 流程 + 配置驱动验证),'
|
||||
'串口框架(对硬件)与 step 回写(ONES API)与平台无关、iOS 零增量,故 P0 仅控制+少量添加差异有增量。'
|
||||
'P3 仅权限/登录小幅增量。')
|
||||
|
||||
doc.add_paragraph()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run('夜间并行 iOS 压缩日历(省周期,不省人力):').bold = True
|
||||
p.add_run('白天调 Android,iOS 夜间挂测——让 Midscene/AI 无人值守跑全量+flaky 复跑+自动改定位,次日早上 review 失败项。'
|
||||
'iOS 增量(~26-51 人日)中“机器执行类”可藏进夜间闲置时段(约占一半到三分之二);'
|
||||
'“人工判断类”(iOS 特有差异、AI 改不动的)仍占用同一人白天工时、省不掉。')
|
||||
rows = [
|
||||
('串行双平台', '~90-130 工作日', 'Android 调完再调 iOS'),
|
||||
('夜间并行 iOS', '~70-100 工作日(省约 15-35 天)', '理想可压回接近单平台'),
|
||||
]
|
||||
add_table_with_header(doc, ['方式', '日历(1人)', '说明'], rows)
|
||||
doc.add_paragraph('省多少取决于:① iOS 真机夜间在位、app 状态可自动复位;② iOS 平台差异多少;③ AI 自动修定位成功率。'
|
||||
'注意:压缩的是日历周期,人日(工作量)不变;夜测靠 AI 跑+早上 review,非人工加班,可持续。')
|
||||
|
||||
doc.add_paragraph(
|
||||
'人力构成说明:以上为 AI 辅助口径(较手工已下调约 2-3 倍)。其中“脚本编写”仅占约 1/3——AI 提效主要在此;'
|
||||
|
|
@ -530,7 +577,7 @@ doc.add_paragraph('• 脚本可在 Android/iOS 双平台运行(通过平台
|
|||
doc.add_heading('11.2 优先级层验收标准', level=2)
|
||||
rows = [
|
||||
('P0 必测项', '187条全部实现并跑通(添加+控制双协议)', '通过率≥95%,作为提测门禁'),
|
||||
('P1 单品探索', '82款单品冒烟全部通过', '广度100%覆盖'),
|
||||
('P1 单品探索', '78款单品冒烟全部通过', '广度100%覆盖'),
|
||||
('P2 全功能', '全维度回归,通过率≥85%', '单品功能覆盖率100%'),
|
||||
('P3 平台', '平台功能 + 场景联动通过', '全流程覆盖'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@ export class AndroidDriver implements DeviceDriver {
|
|||
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> {
|
||||
if (!locator.android) return null;
|
||||
return this.findElementRaw(locator.android.using, locator.android.value);
|
||||
|
|
|
|||
|
|
@ -62,6 +62,11 @@ export class HubShowDriver implements DeviceDriver {
|
|||
this.sessionId = null;
|
||||
}
|
||||
|
||||
// Hub Show 直连设备固件 UI,不管理 App;activateApp 为空实现以满足接口。
|
||||
async activateApp(_appId: string): Promise<void> {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
async findElement(locator: ElementLocator): Promise<string | null> {
|
||||
if (!locator.android) return null;
|
||||
return this.findElementRaw(locator.android.using, locator.android.value);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ export interface DeviceDriver {
|
|||
createSession(): Promise<void>;
|
||||
destroySession(): Promise<void>;
|
||||
|
||||
/** 激活/拉起指定 app(Android appPackage / iOS bundleId)。用于切到系统设置再切回。 */
|
||||
activateApp(appId: string): Promise<void>;
|
||||
|
||||
findElement(locator: ElementLocator): Promise<string | null>;
|
||||
findElements(locator: ElementLocator): Promise<string[]>;
|
||||
findElementRaw(using: string, value: string): Promise<string | null>;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ export class WDADriver implements DeviceDriver {
|
|||
await this.wda.destroySession();
|
||||
}
|
||||
|
||||
async activateApp(appId: string): Promise<void> {
|
||||
await this.wda.activateAppById(appId);
|
||||
}
|
||||
|
||||
async findElement(locator: ElementLocator): Promise<string | null> {
|
||||
if (!locator.ios) throw new Error(`Locator "${locator.name}" has no iOS strategy`);
|
||||
return this.wda.findElement(locator.ios.using, locator.ios.value);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ from appium.options.ios import XCUITestOptions
|
|||
|
||||
caps = XCUITestOptions()
|
||||
caps.platform_name = 'iOS'
|
||||
caps.platform_version = '26.3.1' # 实际 iOS 版本
|
||||
caps.platform_version = '26.5' # 实际 iOS 版本
|
||||
caps.device_name = 'iPhone'
|
||||
caps.udid = '00008110-001A34303AE9801E' # 设备 UDID
|
||||
caps.bundle_id = 'com.demo.wohand'
|
||||
|
|
@ -63,7 +63,7 @@ cd /Users/woan/.appium/node_modules/appium-xcuitest-driver/node_modules/appium-w
|
|||
xcodebuild -project WebDriverAgent.xcodeproj \
|
||||
-scheme WebDriverAgentRunner \
|
||||
-destination "id=YOUR_DEVICE_UDID" \
|
||||
IPHONEOS_DEPLOYMENT_TARGET=26.3.1 \
|
||||
IPHONEOS_DEPLOYMENT_TARGET=26.5 \
|
||||
-configuration Debug \
|
||||
build
|
||||
```
|
||||
|
|
@ -73,7 +73,7 @@ xcodebuild -project WebDriverAgent.xcodeproj \
|
|||
**重要**:配置中的 platform_version 必须与设备实际版本一致
|
||||
|
||||
- 查询设备版本:`ideviceinfo | grep ProductVersion`
|
||||
- 当前测试设备:iOS 26.3.1(iPhone 14 Pro)
|
||||
- 当前测试设备:iOS 26.5(iPhone 14 Pro)
|
||||
- 设备 UDID:00008110-001A34303AE9801E
|
||||
|
||||
## 常见错误
|
||||
|
|
@ -94,7 +94,7 @@ from appium import webdriver
|
|||
from appium.options.ios import XCUITestOptions
|
||||
caps = XCUITestOptions()
|
||||
caps.platform_name = 'iOS'
|
||||
caps.platform_version = '26.3.1'
|
||||
caps.platform_version = '26.5'
|
||||
caps.udid = '00008110-001A34303AE9801E'
|
||||
caps.bundle_id = 'com.demo.wohand'
|
||||
caps.automation_name = 'XCUITest'
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
"test:card": "vitest run tests/bot/bot_card.test.ts",
|
||||
"test:logs": "vitest run tests/bot/bot_logs.test.ts",
|
||||
"test:scene": "vitest run tests/bot/bot_scene.test.ts",
|
||||
"gen:must-test": "ts-node scripts/gen-must-test-manifest.ts"
|
||||
"gen:must-test": "ts-node scripts/gen-must-test-manifest.ts",
|
||||
"gen:writeback-params": "ts-node scripts/gen-writeback-params.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"ui-automation",
|
||||
|
|
|
|||
|
|
@ -106,18 +106,42 @@ export const MUST_TEST: MustTestItem[] = [
|
|||
|
||||
## 6. P0 标记约定(带 ONES 锚点)
|
||||
|
||||
代码里用 `it` 名称打标,锚点指向 ONES,便于筛选与回写:
|
||||
**关键:锚点必须打在 `reporter.record(名称, ...)` 的名称里**——结果写入 `reports/.results.json` 用的是 record 名称,`buildAnchoredPayloads` 从中解析 `[ONES:号(#step)]` 做回写。`it()` 标题里也加同一锚点作**备注**(测试报告里可见、便于追溯),但回写不读 it 标题。
|
||||
|
||||
```ts
|
||||
// 添加:锚点 = 用例号
|
||||
it(`[P0][ONES:91013] 通过BLE添加${deviceName}`, async () => { ... });
|
||||
// it 标题:加 [ONES:号] 作备注(可读/可追溯)
|
||||
it(`[ONES:15968] 通过BLE添加${deviceName}设备`, async () => {
|
||||
// ...
|
||||
// reporter.record 名称:加 [P0][ONES:号] —— 这是回写真正依据
|
||||
reporter.record(`[P0][ONES:15968] 添加${deviceName}`, 'PASS', dur, detail);
|
||||
});
|
||||
|
||||
// 控制:锚点 = 用例号#step_uuid;协议标在中括号里(双协议则两条 it 或参数化)
|
||||
it(`[P0][ONES:15975#${stepUuid}][ble] 开/关 ${deviceName}`, async () => { ... });
|
||||
it(`[P0][ONES:15974#${stepUuid}][wifi] 开/关 ${deviceName}`, async () => { ... });
|
||||
// 控制(协议相关):record 名称带 用例号#step_uuid + 协议;协议由 PROTO 环境变量切(见 §7)
|
||||
const CTRL = process.env.PROTO === 'wifi' ? '[P0][ONES:15974#<step>][wifi]' : '[P0][ONES:15975#<step>][ble]';
|
||||
reporter.record(`${CTRL} 开/关 ${deviceName}`, status, dur, detail);
|
||||
```
|
||||
|
||||
筛选:`vitest -t '\[P0\]'`(全量) / `-t '\[ble\]'` / `-t '\[wifi\]'`。
|
||||
- 一条 ONES step 可由多个用例覆盖 → 用同一 step 锚点,回写自动聚合(fail>skip>pass)。
|
||||
- 筛选:`vitest -t '\[P0\]'`(全量) / 结果文件里按 `[ble]`/`[wifi]` 区分协议。
|
||||
|
||||
### 同品类多型号:设备维度动态锚点
|
||||
|
||||
一个品类的脚本只测默认设备,但同品类多个 UI 相似型号(如 Curtain/Curtain3/BlindTilt)应共用脚本、各自回写。**不要写死 ONES 号**,改用 `utils/common/ones-anchor.helper.ts` 按当前设备动态解析:
|
||||
|
||||
```ts
|
||||
import { onesAdd, onesCtrl } from '../../utils/common';
|
||||
const ADD_ANCHOR = onesAdd('curtain', deviceName); // 添加: 按 CURTAIN_DEVICE 解析
|
||||
const CTRL_CURTAIN = onesCtrl('curtain', deviceName); // 控制: 按设备 + PROTO 解析
|
||||
reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, ...);
|
||||
```
|
||||
|
||||
- `ANCHORS` map(在 `ones-anchor.helper.ts`)按 `品类 → 设备名(DEVICE_CONFIG) → {add, ctrlBle, ctrlWifi}` 维护;**新增型号补一行即可**。
|
||||
- 跑法:**换 `<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 节)。
|
||||
- **控制用例 15974 / 15975**:`uuid` = 该控制用例的 `testcaseCase.uuid`,`steps` 数组按 `step_uuid` 逐条填结果:
|
||||
```json
|
||||
{ "uuid":"Vp7vuhbu", "executor":"<user_id>", "result":"passed",
|
||||
"steps":[ { "uuid":"<stepUuid>", "result":"passed", "actual_result":"开/关成功" }, ... ] }
|
||||
**key 拼接规则(确定式):**
|
||||
- case: `testcase_plan_case-<planUUID>-<caseUUID>`
|
||||
- step: `testcase_plan_case_step-<planUUID>-<caseUUID>-<stepUuid>`
|
||||
|
||||
**写入 mutation:**
|
||||
```bash
|
||||
# case 级
|
||||
ones graphql 'mutation { updateTestcasePlanCase(key: "testcase_plan_case-CQz9YCNX-<caseUUID>", result: "passed") { key } }'
|
||||
# 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 } }'
|
||||
```
|
||||
case 级 `result` 由其所有 step 聚合(全 pass→passed,有 fail→failed)。
|
||||
- 用例名只有 2 条、靠 LCS 匹配会误配 → 控制用例**改按 `[ONES:号#step]` 锚点精确匹配**,扩展 `utils/ones-sync.ts` / `scripts/sync-ones-results.ts` 支持 step 维度。
|
||||
- 协议:`[ble]` 结果回 15975,`[wifi]` 结果回 15974。
|
||||
- `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`。)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,42 @@
|
|||
|
||||
---
|
||||
|
||||
## iOS 真机快速连接
|
||||
|
||||
> 来源 `iOS连接修复指南.md`。iOS 跑测/双协议(iOS)前按此连。核心坑:**8100 端口被 iproxy 占用导致 WDA 起不来**。
|
||||
|
||||
**4 步快速连接:**
|
||||
```bash
|
||||
# 1. 取设备信息(UDID / iOS 版本)
|
||||
idevicepair pair # 手机弹窗点「信任」
|
||||
ideviceinfo | grep -E "UniqueDeviceID|ProductVersion"
|
||||
# 2. 清占用 8100 的进程(关键)
|
||||
pkill -f iproxy
|
||||
# 3. 确认 Appium 在跑
|
||||
curl -s http://localhost:4723/status
|
||||
```
|
||||
4. 用 **XCUITestOptions** 连接,`wdaLocalPort` 设为 **8200**(避开 8100);`platform_version` 必须与设备实际版本一致:
|
||||
```python
|
||||
from appium import webdriver
|
||||
from appium.options.ios import XCUITestOptions
|
||||
caps = XCUITestOptions()
|
||||
caps.platform_name = 'iOS'
|
||||
caps.platform_version = '26.5' # 与 ideviceinfo 实际版本一致
|
||||
caps.udid = '00008110-001A34303AE9801E' # ideviceinfo 取
|
||||
caps.bundle_id = 'com.wohand.wohand' # 项目 SwitchBot bundleId(指南示例 com.demo.wohand 为 demo 包)
|
||||
caps.automation_name = 'XCUITest'
|
||||
caps.set_capability('wdaLocalPort', 8200)
|
||||
driver = webdriver.Remote('http://127.0.0.1:4723', options=caps)
|
||||
```
|
||||
|
||||
**当前 iOS 真机**:iPhone 14 Pro,iOS 26.5,UDID `00008110-001A34303AE9801E`。
|
||||
|
||||
**常见错误**:`Port 8100 occupied`→`pkill -f iproxy` + 用 8200;`accept trust dialog`→手机点信任;`WDA build failed`→手动 `xcodebuild` 构建 WDA(见 `iOS连接修复指南.md` 第4节);`Connection refused`→确认 WDA 已在设备运行。
|
||||
|
||||
注:仓库自定义 `WDADriver` 走 8100(iproxy 8100:8100);若 8100 冲突,按上面切 8200 或先 `pkill -f iproxy` 再起。
|
||||
|
||||
---
|
||||
|
||||
## DeviceDriver 可用接口
|
||||
|
||||
```typescript
|
||||
|
|
@ -840,43 +876,32 @@ ONES测试计划 → 读取用例列表 → 转换为自动化脚本 → 执行
|
|||
| `'SKIP'` | `skipped` |
|
||||
| 未执行 | `to_do` |
|
||||
|
||||
### 3. 结果反写 (已确认可行)
|
||||
### 3. 结果反写 (正确方法:GraphQL mutation,已验证)
|
||||
|
||||
API 端点:
|
||||
```
|
||||
POST /project/api/project/team/{team_uuid}/testcase/plan/{plan_uuid}/cases/update
|
||||
```
|
||||
> ⚠️ **必须走 `ones graphql` mutation,不要用 curl 直连 REST `.../cases/update`。**
|
||||
> `ones config show` 把 token 打码成 `***`,curl 直连必然 `401 AuthFailure.InvalidToken`;
|
||||
> `ones graphql` 复用 CLI 登录认证、无需 token/PAT,已在权限白名单。`utils/ones-sync.ts` 的 `postPayloads` 已按此实现。
|
||||
|
||||
请求体 (JSON 对象,cases 数组包裹):
|
||||
```json
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"uuid": "用例UUID (testcaseCase.uuid)",
|
||||
"executor": "执行人UUID (user_id)",
|
||||
"note": "",
|
||||
"result": "passed|failed|skipped|to_do",
|
||||
"steps": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
**key 拼接(确定式):**
|
||||
- case: `testcase_plan_case-<planUUID>-<caseUUID>`
|
||||
- step: `testcase_plan_case_step-<planUUID>-<caseUUID>-<stepUuid>`
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"success_cases": ["BZfZGRcF"],
|
||||
"not_found_cases": [],
|
||||
"no_permission_cases": [],
|
||||
"not_handle_cases": []
|
||||
}
|
||||
**写入:**
|
||||
```bash
|
||||
# case 级结果
|
||||
ones graphql 'mutation { updateTestcasePlanCase(key: "testcase_plan_case-<plan>-<caseUUID>", result: "passed") { key } }'
|
||||
# step 级结果(字段名 step_result,可带 actual_result)
|
||||
ones graphql 'mutation { updateTestcasePlanCaseStep(key: "testcase_plan_case_step-<plan>-<caseUUID>-<stepUuid>", step_result: "passed", actual_result: "...") { key } }'
|
||||
```
|
||||
取值 `passed|failed|skipped|to_do`(PASS→passed、FAIL→failed、SKIP→skipped)。
|
||||
|
||||
说明:
|
||||
- `uuid`: 测试用例的 UUID(不是 plan_case 的 key,是 `testcaseCase.uuid`)
|
||||
- `executor`: 执行人 UUID,从 `ones config show` 的 `user_id` 获取
|
||||
- `steps`: 步骤结果数组,无步骤时传空数组 `[]`
|
||||
- 支持批量提交多条
|
||||
**读回校验:**
|
||||
```bash
|
||||
ones graphql '{ testcasePlanCaseSteps(filter: { testcasePlan: { uuid_in: ["<plan>"] }, testcaseCase: { uuid_in: ["<caseUUID>"] } }, limit: 60) { key stepResult actualResult } }'
|
||||
```
|
||||
(查询字段驼峰 `stepResult`/`actualResult`;filter 用 `testcasePlan`/`testcaseCase` 嵌套 `uuid_in`。)
|
||||
|
||||
可用 mutation:`updateTestcasePlanCase` / `updateTestcasePlanCaseStep`(经 `ones graphql '{ __schema { mutationType { fields { name } } } }'` 可查全部写操作)。
|
||||
|
||||
### 4. 完整同步命令
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -6,9 +6,9 @@
|
|||
*
|
||||
* 流程:
|
||||
* 1. 读取 reports/.results.json (自动化执行后的结果)
|
||||
* 2. 从 ONES 拉取测试计划用例列表
|
||||
* 3. 优先按测试名锚点 [ONES:号(#step)] 精确匹配(支持 step 级回写),
|
||||
* 无锚点的回退到用例名 LCS 模糊匹配
|
||||
* 2. 从本地参数文件 test-plan/ones-writeback-params.json 取用例(号→uuid)+ 步骤总数
|
||||
* —— 不再读 ONES(用例固定仅新增,刷新参数用 npm run gen:writeback-params)
|
||||
* 3. 按测试名锚点 [ONES:号(#step)] 精确匹配(支持 step 级);无锚点回退用例名 LCS
|
||||
* 4. 反写结果到 ONES (dry-run 仅打印 payload)
|
||||
*/
|
||||
|
||||
|
|
@ -16,17 +16,17 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import { TestResult } from '../utils/test-reporter';
|
||||
import {
|
||||
fetchPlanCases,
|
||||
matchResults,
|
||||
buildAnchoredPayloads,
|
||||
postPayloads,
|
||||
fetchCaseSteps,
|
||||
OnesPlanCase,
|
||||
OnesUpdatePayload,
|
||||
} from '../utils/ones-sync';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const ONES_CLI = '/Users/woan/local/bin/ones';
|
||||
const RESULTS_FILE = path.resolve(__dirname, '../reports/.results.json');
|
||||
const PARAMS_FILE = path.resolve(__dirname, '../test-plan/ones-writeback-params.json');
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
|
|
@ -61,6 +61,28 @@ function loadResults(): TestResult[] {
|
|||
return data.results || [];
|
||||
}
|
||||
|
||||
/** 从本地参数文件取 用例(OnesPlanCase[]) + 控制用例步骤总数(号→step数)。不读 ONES。 */
|
||||
function loadParams(): { planCases: OnesPlanCase[]; totalStepsByNumber: Map<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() {
|
||||
const { planUUID, dryRun } = parseArgs();
|
||||
|
||||
|
|
@ -82,27 +104,18 @@ function main() {
|
|||
const skipped = testResults.filter(r => r.status === 'SKIP').length;
|
||||
console.log(` PASS: ${passed} | FAIL: ${failed} | SKIP: ${skipped}`);
|
||||
|
||||
// 2. 从 ONES 拉取计划用例
|
||||
console.log(`\n[2/4] 从 ONES 拉取测试计划用例 ...`);
|
||||
const planCases = fetchPlanCases(planUUID);
|
||||
console.log(` 计划共 ${planCases.length} 条用例`);
|
||||
// 2. 从本地参数文件取用例 + 步骤总数(不读 ONES)
|
||||
console.log(`\n[2/4] 读取本地回写参数 ...`);
|
||||
const { planCases, totalStepsByNumber } = loadParams();
|
||||
console.log(` 参数: ${planCases.length} 用例, ${totalStepsByNumber.size} 个控制用例步骤数`);
|
||||
|
||||
// 3. 匹配:锚点优先(支持 step 级),无锚点回退 LCS
|
||||
console.log(`\n[3/4] 匹配 (锚点优先 + LCS 兜底) ...`);
|
||||
// 含 step 锚点的用例需列全量 step → 先取其完整 step 列表
|
||||
const stepNums = new Set<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(
|
||||
planCases,
|
||||
testResults,
|
||||
executor,
|
||||
{ fullStepsByNumber }
|
||||
{ totalStepsByNumber }
|
||||
);
|
||||
const anchoredUUIDs = new Set(anchored.map(p => p.uuid));
|
||||
const stepCount = anchored.reduce((n, p) => n + p.steps.filter(s => s.execute_result).length, 0);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,13 +40,13 @@ describe('Air Condition Connect - 添加红外空调设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('添加红外空调设备', async () => {
|
||||
it('[ONES:196802] 添加红外空调设备', async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('添加红外空调设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record('[P0][ONES:196802] 添加红外空调设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -135,10 +135,10 @@ describe('Air Condition Connect - 添加红外空调设备', () => {
|
|||
|| await waitForSource(driver, 'Air Conditioner', 5000);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('添加红外空调设备', 'PASS', Date.now() - start, `红外空调添加完成, 耗时${elapsed}s, 首页可见=${found}`);
|
||||
reporter.record('[P0][ONES:196802] 添加红外空调设备', 'PASS', Date.now() - start, `红外空调添加完成, 耗时${elapsed}s, 首页可见=${found}`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('添加红外空调设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record('[P0][ONES:196802] 添加红外空调设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { BOT_LOCATORS } from '../../locators/bot-locators';
|
|||
import { TestReporter } from '../../utils/test-reporter';
|
||||
import { getDeviceName } from '../../config/device.config';
|
||||
import { sleep } from '../../utils/common';
|
||||
import { applyProtoNetwork } from '../../utils/common';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
|
|
@ -13,6 +14,11 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('bot', 'BOT_DEVICE');
|
||||
|
||||
// 必测项控制步骤锚点(协议相关,由 PROTO 环境变量切换): BLE→15975#6e4uZSVe / WiFi→15974#Qt4hcgB4
|
||||
// 该 step「点击控制Bot 不加密开&不加密关&加密按压」由 ON/OFF切换 + 加密按压 两个用例共同覆盖,回写时按 step 聚合
|
||||
const PROTO = process.env.PROTO === 'wifi' ? 'wifi' : 'ble';
|
||||
const CTRL = PROTO === 'wifi' ? '[P0][ONES:15974#Qt4hcgB4][wifi]' : '[P0][ONES:15975#6e4uZSVe][ble]';
|
||||
|
||||
describe('Bot Card - 首页卡片操作', () => {
|
||||
let driver: DeviceDriver;
|
||||
let bot: BotHelper;
|
||||
|
|
@ -23,6 +29,8 @@ describe('Bot Card - 首页卡片操作', () => {
|
|||
await driver.createSession();
|
||||
bot = new BotHelper(driver);
|
||||
reporter = new TestReporter('Bot_Card', driver.platform.toUpperCase());
|
||||
// 双协议前置:按 PROTO 切手机蓝牙/WiFi(ble→开蓝牙关WiFi / wifi→关蓝牙开WiFi),无人值守自动切
|
||||
await applyProtoNetwork(driver, PROTO);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
@ -256,7 +264,7 @@ describe('Bot Card - 首页卡片操作', () => {
|
|||
console.log('切换后状态:', statusBefore);
|
||||
|
||||
if (statusBefore === 'unknown') {
|
||||
reporter.record('ON/OFF切换', 'SKIP', Date.now() - start, '切换Switch Mode后仍无法识别状态');
|
||||
reporter.record(`${CTRL} 不加密开/关`, 'SKIP', Date.now() - start, '切换Switch Mode后仍无法识别状态');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -284,7 +292,7 @@ describe('Bot Card - 首页卡片操作', () => {
|
|||
expect(statusAfter).not.toBe(statusBefore);
|
||||
|
||||
const detail = `${statusBefore} → ${statusAfter}`;
|
||||
reporter.record('ON/OFF切换', 'PASS', Date.now() - start, detail);
|
||||
reporter.record(`${CTRL} 不加密开/关`, 'PASS', Date.now() - start, detail);
|
||||
|
||||
// Restore state
|
||||
await tapBotAndWaitPopup();
|
||||
|
|
@ -298,7 +306,7 @@ describe('Bot Card - 首页卡片操作', () => {
|
|||
await sleep(5000);
|
||||
} catch (e: any) {
|
||||
const ss = await captureScreenshot();
|
||||
reporter.record('ON/OFF切换', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL} 不加密开/关`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
@ -532,10 +540,10 @@ describe('Bot Card - 首页卡片操作', () => {
|
|||
const stillPress = source.includes('Press Mode');
|
||||
console.log('按压后状态:', stillPress ? 'Press Mode' : '已执行');
|
||||
|
||||
reporter.record('加密-按压操作', 'PASS', Date.now() - start, `加密按压完成, Press=${stillPress}`);
|
||||
reporter.record(`${CTRL} 加密按压`, 'PASS', Date.now() - start, `加密按压完成, Press=${stillPress}`);
|
||||
} catch (e: any) {
|
||||
const ss = await captureScreenshot();
|
||||
reporter.record('加密-按压操作', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL} 加密按压`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ describe('Bot Connect - 添加Bot设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it(`通过BLE添加${deviceName}设备`, async () => {
|
||||
it(`[ONES:15968] 通过BLE添加${deviceName}设备`, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record(`添加${deviceName}`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`[P0][ONES:15968] 添加${deviceName}`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -53,10 +53,10 @@ describe('Bot Connect - 添加Bot设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record(`添加${deviceName}`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record(`[P0][ONES:15968] 添加${deviceName}`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record(`添加${deviceName}`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`[P0][ONES:15968] 添加${deviceName}`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,13 +43,13 @@ describe('CeilingLight Connect - 添加吸顶灯设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('通过BLE添加吸顶灯', async () => {
|
||||
it('[ONES:15950] 通过BLE添加吸顶灯', async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('添加吸顶灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record('[P0][ONES:15950] 添加吸顶灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -65,10 +65,10 @@ describe('CeilingLight Connect - 添加吸顶灯设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('添加吸顶灯设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record('[P0][ONES:15950] 添加吸顶灯设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('添加吸顶灯设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record('[P0][ONES:15950] 添加吸顶灯设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('ceilingLight', 'CEILING_LIGHT_DEVICE');
|
||||
|
||||
// 必测项控制锚点: 点击控制吸顶灯pro开/关灯 BLE 15975#2528azD6 / WiFi 15974#TcnL3gFM
|
||||
const PROTO = process.env.PROTO === 'wifi' ? 'wifi' : 'ble';
|
||||
const CTRL_CEIL = PROTO === 'wifi' ? '[P0][ONES:15974#TcnL3gFM][wifi]' : '[P0][ONES:15975#2528azD6][ble]';
|
||||
|
||||
describe('CeilingLight Control - 吸顶灯功能页', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
|
@ -72,10 +76,10 @@ describe('CeilingLight Control - 吸顶灯功能页', () => {
|
|||
const isOn = source.includes('ON') || source.includes('Night Light') || source.includes('Full');
|
||||
expect(isOn).toBe(true);
|
||||
|
||||
reporter.record('打开开关', 'PASS', Date.now() - start, `吸顶灯已开启`);
|
||||
reporter.record(`${CTRL_CEIL} 打开开关`, 'PASS', Date.now() - start, `吸顶灯已开启`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('打开开关', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_CEIL} 打开开关`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
@ -254,10 +258,10 @@ describe('CeilingLight Control - 吸顶灯功能页', () => {
|
|||
!source.includes('Night Light');
|
||||
console.log('吸顶灯已关闭:', isOff);
|
||||
|
||||
reporter.record('关闭开关', 'PASS', Date.now() - start, `吸顶灯已关闭`);
|
||||
reporter.record(`${CTRL_CEIL} 关闭开关`, 'PASS', Date.now() - start, `吸顶灯已关闭`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('关闭开关', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_CEIL} 关闭开关`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,13 +43,13 @@ describe('ColorBulb Connect - 添加彩灯设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('通过BLE添加彩灯', async () => {
|
||||
it('[ONES:15960] 通过BLE添加彩灯', async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('添加彩灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record('[P0][ONES:15960] 添加彩灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -65,10 +65,10 @@ describe('ColorBulb Connect - 添加彩灯设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('添加彩灯设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record('[P0][ONES:15960] 添加彩灯设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('添加彩灯设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record('[P0][ONES:15960] 添加彩灯设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('colorBulb', 'COLOR_BULB_DEVICE');
|
||||
|
||||
// 必测项控制锚点(协议相关): 点击控制Bulb 开/关 BLE 15975#TNhSAuUC / WiFi 15974#Kaj2UdKf
|
||||
const PROTO = process.env.PROTO === 'wifi' ? 'wifi' : 'ble';
|
||||
const CTRL_BULB = PROTO === 'wifi' ? '[P0][ONES:15974#Kaj2UdKf][wifi]' : '[P0][ONES:15975#TNhSAuUC][ble]';
|
||||
|
||||
describe('ColorBulb Control - 彩灯功能页', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
|
@ -72,10 +76,10 @@ describe('ColorBulb Control - 彩灯功能页', () => {
|
|||
const isOn = source.includes('ON') || source.includes('Dynamic') || source.includes('Color');
|
||||
expect(isOn).toBe(true);
|
||||
|
||||
reporter.record('打开开关', 'PASS', Date.now() - start, `彩灯已开启`);
|
||||
reporter.record(`${CTRL_BULB} 打开开关`, 'PASS', Date.now() - start, `彩灯已开启`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('打开开关', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_BULB} 打开开关`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('curtain', 'CURTAIN_DEVICE');
|
||||
|
||||
// 设备维度动态添加锚点:按当前 CURTAIN_DEVICE 解析(Curtain/Curtain3/BlindTilt 各自的 ONES 号)
|
||||
import { onesAdd } from '../../utils/common';
|
||||
const ADD_ANCHOR = onesAdd('curtain', deviceName);
|
||||
|
||||
describe('Curtain Connect - 通过BLE添加窗帘设备', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
|
@ -30,13 +34,13 @@ describe('Curtain Connect - 通过BLE添加窗帘设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('通过BLE添加窗帘设备', async () => {
|
||||
it(`${ADD_ANCHOR} 通过BLE添加窗帘设备`, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('添加窗帘设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -53,10 +57,10 @@ describe('Curtain Connect - 通过BLE添加窗帘设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('添加窗帘设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('添加窗帘设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('curtain', 'CURTAIN_DEVICE');
|
||||
|
||||
// 设备维度动态控制锚点:按当前 CURTAIN_DEVICE + PROTO 解析(Curtain/Curtain3/BlindTilt 各自的 step)
|
||||
import { onesCtrl } from '../../utils/common';
|
||||
const CTRL_CURTAIN = onesCtrl('curtain', deviceName);
|
||||
|
||||
describe('Curtain Control - 窗帘控制功能', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
|
@ -68,10 +72,10 @@ describe('Curtain Control - 窗帘控制功能', () => {
|
|||
source.includes('Opening') || source.includes('Opened');
|
||||
console.log('打开窗帘状态:', statusChanged);
|
||||
|
||||
reporter.record('打开窗帘', 'PASS', Date.now() - start, `Open按钮点击成功, 状态变化=${statusChanged}`);
|
||||
reporter.record(`${CTRL_CURTAIN} 打开窗帘`, 'PASS', Date.now() - start, `Open按钮点击成功, 状态变化=${statusChanged}`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('打开窗帘', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_CURTAIN} 打开窗帘`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
@ -91,10 +95,10 @@ describe('Curtain Control - 窗帘控制功能', () => {
|
|||
source.includes('Closing') || source.includes('Closed');
|
||||
console.log('关闭窗帘状态:', statusChanged);
|
||||
|
||||
reporter.record('关闭窗帘', 'PASS', Date.now() - start, `Close按钮点击成功, 状态变化=${statusChanged}`);
|
||||
reporter.record(`${CTRL_CURTAIN} 关闭窗帘`, 'PASS', Date.now() - start, `Close按钮点击成功, 状态变化=${statusChanged}`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('关闭窗帘', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_CURTAIN} 关闭窗帘`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,13 +53,13 @@ describe('Fan Connect - 添加风扇设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('通过BLE添加风扇', async () => {
|
||||
it('[ONES:122775] 通过BLE添加风扇', async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('通过BLE添加风扇', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record('[P0][ONES:122775] 通过BLE添加风扇', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -75,10 +75,10 @@ describe('Fan Connect - 添加风扇设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('通过BLE添加风扇', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record('[P0][ONES:122775] 通过BLE添加风扇', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('通过BLE添加风扇', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record('[P0][ONES:122775] 通过BLE添加风扇', 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('fan', 'FAN_DEVICE');
|
||||
|
||||
// 必测项控制锚点: 无电池款 Circulator Fan 首页控制开关 BLE 15975#JBszgxRS / WiFi 15974#KNyzKhn6
|
||||
const PROTO = process.env.PROTO === 'wifi' ? 'wifi' : 'ble';
|
||||
const CTRL_FAN = PROTO === 'wifi' ? '[P0][ONES:15974#KNyzKhn6][wifi]' : '[P0][ONES:15975#JBszgxRS][ble]';
|
||||
|
||||
describe('Fan Control - 风扇控制页', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
|
@ -81,10 +85,10 @@ describe('Fan Control - 风扇控制页', () => {
|
|||
const isOn = source.includes('ON') || source.includes('Normal') || source.includes('Speed');
|
||||
console.log('风扇开启:', isOn);
|
||||
|
||||
reporter.record('打开开关', 'PASS', Date.now() - start, `Power已点击, ON=${isOn}`);
|
||||
reporter.record(`${CTRL_FAN} 打开开关`, 'PASS', Date.now() - start, `Power已点击, ON=${isOn}`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('打开开关', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_FAN} 打开开关`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
@ -390,10 +394,10 @@ describe('Fan Control - 风扇控制页', () => {
|
|||
const isOff = source.includes('OFF') || source.includes('off') || source.includes('Disconnected');
|
||||
console.log('风扇关闭:', isOff);
|
||||
|
||||
reporter.record('关闭开关', 'PASS', Date.now() - start, `Power OFF=${isOff}`);
|
||||
reporter.record(`${CTRL_FAN} 关闭开关`, 'PASS', Date.now() - start, `Power OFF=${isOff}`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('关闭开关', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_FAN} 关闭开关`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import * as path from 'path';
|
|||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('hub', 'HUB_DEVICE');
|
||||
import { onesAdd } from "../../utils/common";
|
||||
const ADD_ANCHOR = onesAdd("hub", deviceName);
|
||||
|
||||
describe('Hub Connect - 添加Hub设备', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
@ -30,13 +32,13 @@ describe('Hub Connect - 添加Hub设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('通过BLE添加Hub设备', async () => {
|
||||
it(`${ADD_ANCHOR} 通过BLE添加Hub设备`, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('添加Hub设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Hub设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -52,10 +54,10 @@ describe('Hub Connect - 添加Hub设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('添加Hub设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Hub设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('添加Hub设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Hub设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import * as path from 'path';
|
|||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('humidifier', 'HUMIDIFIER_DEVICE');
|
||||
import { onesAdd } from "../../utils/common";
|
||||
const ADD_ANCHOR = onesAdd("humidifier", deviceName);
|
||||
|
||||
describe('Humidifier Connect - 添加加湿器设备', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
@ -30,13 +32,13 @@ describe('Humidifier Connect - 添加加湿器设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('通过BLE添加加湿器设备', async () => {
|
||||
it(`${ADD_ANCHOR} 通过BLE添加加湿器设备`, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('添加加湿器设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加加湿器设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -52,10 +54,10 @@ describe('Humidifier Connect - 添加加湿器设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('添加加湿器设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加加湿器设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('添加加湿器设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${ADD_ANCHOR} 添加加湿器设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('humidifier', 'HUMIDIFIER_DEVICE');
|
||||
|
||||
// 必测项控制锚点: Humidifier 快捷弹窗开启加湿器(自动/低/中/高/关) BLE 15975#GkkrZpaC / WiFi 15974#8tx3Rg76
|
||||
import { onesCtrl } from "../../utils/common";
|
||||
const CTRL_HUMID = onesCtrl("humidifier", deviceName);
|
||||
|
||||
describe('Humidifier Control - 功能页操作', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
|
@ -61,10 +65,10 @@ describe('Humidifier Control - 功能页操作', () => {
|
|||
const isOn = source.includes('ON') || source.includes('Auto') || source.includes('Manual');
|
||||
expect(isOn).toBe(true);
|
||||
|
||||
reporter.record('功能页打开开关', 'PASS', Date.now() - start, `加湿器已开启, ON=${isOn}`);
|
||||
reporter.record(`${CTRL_HUMID} 功能页打开开关`, 'PASS', Date.now() - start, `加湿器已开启, ON=${isOn}`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('功能页打开开关', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_HUMID} 功能页打开开关`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
@ -279,10 +283,10 @@ describe('Humidifier Control - 功能页操作', () => {
|
|||
!source.includes('Auto');
|
||||
console.log('加湿器已关闭:', isOff);
|
||||
|
||||
reporter.record('功能页关闭开关', 'PASS', Date.now() - start, `加湿器已关闭`);
|
||||
reporter.record(`${CTRL_HUMID} 功能页关闭开关`, 'PASS', Date.now() - start, `加湿器已关闭`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('功能页关闭开关', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_HUMID} 功能页关闭开关`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,13 +53,13 @@ describe('Keypad Connect - 添加Keypad设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('通过BLE添加Keypad', async () => {
|
||||
it('[ONES:15949] 通过BLE添加Keypad', async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('通过BLE添加Keypad', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record('[P0][ONES:15949] 通过BLE添加Keypad', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -75,10 +75,10 @@ describe('Keypad Connect - 添加Keypad设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('通过BLE添加Keypad', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record('[P0][ONES:15949] 通过BLE添加Keypad', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('通过BLE添加Keypad', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record('[P0][ONES:15949] 通过BLE添加Keypad', 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import * as path from 'path';
|
|||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('lock', 'LOCK_DEVICE');
|
||||
import { onesAdd } from "../../utils/common";
|
||||
const ADD_ANCHOR = onesAdd("lock", deviceName);
|
||||
|
||||
describe('Lock Connect - 添加Lock设备', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
@ -30,13 +32,13 @@ describe('Lock Connect - 添加Lock设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('通过BLE添加Lock设备', async () => {
|
||||
it(`${ADD_ANCHOR} 通过BLE添加Lock设备`, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('添加Lock设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Lock设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -53,10 +55,10 @@ describe('Lock Connect - 添加Lock设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('添加Lock设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Lock设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('添加Lock设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Lock设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('lock', 'LOCK_DEVICE');
|
||||
|
||||
// 必测项控制锚点(协议相关): 点击控制Lock 开锁/解锁 BLE 15975#5YEfmpJz / WiFi 15974#7cEkbh4Y
|
||||
// 上锁+解锁两个用例同覆盖此 step,回写按 step 聚合
|
||||
import { onesCtrl } from "../../utils/common";
|
||||
const CTRL_LOCK = onesCtrl("lock", deviceName);
|
||||
|
||||
describe('Lock Control - 功能页操作', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
|
@ -68,10 +73,10 @@ describe('Lock Control - 功能页操作', () => {
|
|||
console.log('上锁状态:', isLocked);
|
||||
expect(isLocked).toBe(true);
|
||||
|
||||
reporter.record('功能页-上锁', 'PASS', Date.now() - start, '门锁已上锁');
|
||||
reporter.record(`${CTRL_LOCK} 上锁`, 'PASS', Date.now() - start, '门锁已上锁');
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('功能页-上锁', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_LOCK} 上锁`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
@ -105,10 +110,10 @@ describe('Lock Control - 功能页操作', () => {
|
|||
await sleep(5000);
|
||||
}
|
||||
|
||||
reporter.record('功能页-解锁', 'PASS', Date.now() - start, '门锁已解锁');
|
||||
reporter.record(`${CTRL_LOCK} 解锁`, 'PASS', Date.now() - start, '门锁已解锁');
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('功能页-解锁', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_LOCK} 解锁`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import * as path from 'path';
|
|||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('meter', 'METER_DEVICE');
|
||||
import { onesAdd } from "../../utils/common";
|
||||
const ADD_ANCHOR = onesAdd("meter", deviceName);
|
||||
|
||||
describe('Meter Connect - 添加Meter设备', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
@ -30,13 +32,13 @@ describe('Meter Connect - 添加Meter设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('通过BLE添加Meter设备', async () => {
|
||||
it(`${ADD_ANCHOR} 通过BLE添加Meter设备`, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('添加Meter设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Meter设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -52,10 +54,10 @@ describe('Meter Connect - 添加Meter设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('添加Meter设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Meter设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('添加Meter设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Meter设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('meter', 'METER_DEVICE');
|
||||
|
||||
// 必测项: meter 控制步(报警/校正)在此文件无对应用例,跳过;切换单位对应 feature 用例 74078(case 级,无协议)
|
||||
const FEAT_UNIT = '[P0][ONES:74078]';
|
||||
|
||||
describe('Meter Control - 功能页操作', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
|
@ -58,10 +61,10 @@ describe('Meter Control - 功能页操作', () => {
|
|||
const hasCelsius = source.includes('°C') || source.includes('℃');
|
||||
expect(hasCelsius).toBe(true);
|
||||
|
||||
reporter.record('切换单位为°C', 'PASS', Date.now() - start, '温度单位已切换为°C');
|
||||
reporter.record(`${FEAT_UNIT} 切换单位为°C`, 'PASS', Date.now() - start, '温度单位已切换为°C');
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('切换单位为°C', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${FEAT_UNIT} 切换单位为°C`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
@ -91,10 +94,10 @@ describe('Meter Control - 功能页操作', () => {
|
|||
await sleep(1000);
|
||||
}
|
||||
|
||||
reporter.record('切换单位为°F', 'PASS', Date.now() - start, '温度单位已切换为°F并还原');
|
||||
reporter.record(`${FEAT_UNIT} 切换单位为°F`, 'PASS', Date.now() - start, '温度单位已切换为°F并还原');
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('切换单位为°F', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${FEAT_UNIT} 切换单位为°F`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,13 +40,13 @@ describe('OSC Connect - 通过BLE添加OSC', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('通过BLE添加OSC', async () => {
|
||||
it('[ONES:113942] 通过BLE添加OSC', async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('通过BLE添加OSC', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record('[P0][ONES:113942] 通过BLE添加OSC', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -67,14 +67,14 @@ describe('OSC Connect - 通过BLE添加OSC', () => {
|
|||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
if (success) {
|
||||
reporter.record('通过BLE添加OSC', 'PASS', Date.now() - start, `OSC添加成功, 耗时${elapsed}s`);
|
||||
reporter.record('[P0][ONES:113942] 通过BLE添加OSC', 'PASS', Date.now() - start, `OSC添加成功, 耗时${elapsed}s`);
|
||||
} else {
|
||||
reporter.record('通过BLE添加OSC', 'FAIL', Date.now() - start, `OSC添加失败, 耗时${elapsed}s`);
|
||||
reporter.record('[P0][ONES:113942] 通过BLE添加OSC', 'FAIL', Date.now() - start, `OSC添加失败, 耗时${elapsed}s`);
|
||||
throw new Error('OSC添加失败');
|
||||
}
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('通过BLE添加OSC', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record('[P0][ONES:113942] 通过BLE添加OSC', 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import * as path from 'path';
|
|||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('plug', 'PLUG_DEVICE');
|
||||
import { onesAdd } from "../../utils/common";
|
||||
const ADD_ANCHOR = onesAdd("plug", deviceName);
|
||||
|
||||
describe('Plug Connect - 添加Plug设备', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
@ -30,13 +32,13 @@ describe('Plug Connect - 添加Plug设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('通过BLE添加Plug设备', async () => {
|
||||
it(`${ADD_ANCHOR} 通过BLE添加Plug设备`, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('添加Plug设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Plug设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -52,10 +54,10 @@ describe('Plug Connect - 添加Plug设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('添加Plug设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Plug设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('添加Plug设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Plug设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('plug', 'PLUG_DEVICE');
|
||||
|
||||
// 必测项控制锚点: 点击控制Plug 开/关 仅 WiFi 必测(15974#Ay79Pj23);BLE 无 base Plug 必测步 → ble 模式不打锚点
|
||||
import { onesCtrl } from "../../utils/common";
|
||||
const CTRL_PLUG = onesCtrl("plug", deviceName);
|
||||
|
||||
describe('Plug Control - 功能页操作', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
|
@ -88,10 +92,10 @@ describe('Plug Control - 功能页操作', () => {
|
|||
await sleep(5000);
|
||||
|
||||
expect(statusChanged).toBe(true);
|
||||
reporter.record('功能页打开/关闭开关', 'PASS', Date.now() - start, `控制页开关切换成功`);
|
||||
reporter.record(`${CTRL_PLUG}功能页打开/关闭开关`, 'PASS', Date.now() - start, `控制页开关切换成功`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('功能页打开/关闭开关', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_PLUG}功能页打开/关闭开关`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import * as path from 'path';
|
|||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('robot', 'ROBOT_DEVICE');
|
||||
import { onesAdd } from "../../utils/common";
|
||||
const ADD_ANCHOR = onesAdd("robot", deviceName);
|
||||
|
||||
describe('Robot Connect - 添加扫地机', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
@ -40,13 +42,13 @@ describe('Robot Connect - 添加扫地机', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('添加扫地机', async () => {
|
||||
it(`${ADD_ANCHOR} 添加扫地机`, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('添加扫地机', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -67,14 +69,14 @@ describe('Robot Connect - 添加扫地机', () => {
|
|||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
if (success) {
|
||||
reporter.record('添加扫地机', 'PASS', Date.now() - start, `扫地机添加成功, 耗时${elapsed}s`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'PASS', Date.now() - start, `扫地机添加成功, 耗时${elapsed}s`);
|
||||
} else {
|
||||
reporter.record('添加扫地机', 'FAIL', Date.now() - start, `扫地机添加失败, 耗时${elapsed}s`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'FAIL', Date.now() - start, `扫地机添加失败, 耗时${elapsed}s`);
|
||||
throw new Error('扫地机添加失败');
|
||||
}
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('添加扫地机', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('sensor', 'SENSOR_DEVICE');
|
||||
|
||||
// 必测项控制锚点: Contact Sensor 功能页显示开关状态 BLE 15975#A8C4BsCm / WiFi 15974#JKSgHHbC
|
||||
import { onesCtrl } from "../../utils/common";
|
||||
const CTRL_SENSOR = onesCtrl("sensor", deviceName);
|
||||
|
||||
describe('Sensor Control - 功能页操作', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
|
@ -64,10 +68,10 @@ describe('Sensor Control - 功能页操作', () => {
|
|||
console.log(detail);
|
||||
expect(hasDetectionStatus || hasIndicatorLight).toBe(true);
|
||||
|
||||
reporter.record('功能页显示', 'PASS', Date.now() - start, detail);
|
||||
reporter.record(`${CTRL_SENSOR} 功能页显示`, 'PASS', Date.now() - start, detail);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('功能页显示', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_SENSOR} 功能页显示`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,13 +43,13 @@ describe('StripLight Connect - 添加灯带设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('通过BLE添加灯带', async () => {
|
||||
it('[ONES:15959] 通过BLE添加灯带', async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('添加灯带设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record('[P0][ONES:15959] 添加灯带设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -65,10 +65,10 @@ describe('StripLight Connect - 添加灯带设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('添加灯带设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record('[P0][ONES:15959] 添加灯带设备', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('添加灯带设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record('[P0][ONES:15959] 添加灯带设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('stripLight', 'STRIP_LIGHT_DEVICE');
|
||||
|
||||
// 必测项控制锚点: 点击控制Strip Light开/关 BLE 15975#4poyiVwD / WiFi 15974#QgX4dX7X
|
||||
const PROTO = process.env.PROTO === 'wifi' ? 'wifi' : 'ble';
|
||||
const CTRL_STRIP = PROTO === 'wifi' ? '[P0][ONES:15974#QgX4dX7X][wifi]' : '[P0][ONES:15975#4poyiVwD][ble]';
|
||||
|
||||
describe('StripLight Control - 灯带功能页', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
|
@ -72,10 +76,10 @@ describe('StripLight Control - 灯带功能页', () => {
|
|||
const isOn = source.includes('ON') || source.includes('White') || source.includes('Scene');
|
||||
expect(isOn).toBe(true);
|
||||
|
||||
reporter.record('打开开关', 'PASS', Date.now() - start, `灯带已开启`);
|
||||
reporter.record(`${CTRL_STRIP} 打开开关`, 'PASS', Date.now() - start, `灯带已开启`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('打开开关', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_STRIP} 打开开关`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
@ -245,10 +249,10 @@ describe('StripLight Control - 灯带功能页', () => {
|
|||
!source.includes('Scene');
|
||||
console.log('灯带已关闭:', isOff);
|
||||
|
||||
reporter.record('关闭开关', 'PASS', Date.now() - start, `灯带已关闭`);
|
||||
reporter.record(`${CTRL_STRIP} 关闭开关`, 'PASS', Date.now() - start, `灯带已关闭`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('关闭开关', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${CTRL_STRIP} 关闭开关`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||
|
||||
const deviceName = getDeviceName('urc', 'URC_DEVICE');
|
||||
|
||||
// 必测项 feature: 同步URC设备、控制 → 用例 190802(case 级,无协议)
|
||||
const FEAT_URC = '[P0][ONES:190802]';
|
||||
|
||||
describe('URC Control - 万能遥控器功能操作', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
|
@ -159,10 +162,10 @@ describe('URC Control - 万能遥控器功能操作', () => {
|
|||
|| source.includes('完成') || source.includes('Device Management');
|
||||
console.log('同步一个设备:', syncDone);
|
||||
|
||||
reporter.record('设备管理-同步一个设备', 'PASS', Date.now() - start, `同步完成=${syncDone}`);
|
||||
reporter.record(`${FEAT_URC} 设备管理-同步一个设备`, 'PASS', Date.now() - start, `同步完成=${syncDone}`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('设备管理-同步一个设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${FEAT_URC} 设备管理-同步一个设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
@ -200,10 +203,10 @@ describe('URC Control - 万能遥控器功能操作', () => {
|
|||
const syncDone = source.includes('Synced') || source.includes('Success')
|
||||
|| source.includes('完成') || source.includes('Device Management');
|
||||
|
||||
reporter.record('设备管理-同步多个设备', 'PASS', Date.now() - start, `同步${selectCount}个设备完成=${syncDone}`);
|
||||
reporter.record(`${FEAT_URC} 设备管理-同步多个设备`, 'PASS', Date.now() - start, `同步${selectCount}个设备完成=${syncDone}`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('设备管理-同步多个设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${FEAT_URC} 设备管理-同步多个设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,3 +8,5 @@ export * from './room.helper';
|
|||
export * from './timer.helper';
|
||||
export * from './scene.helper';
|
||||
export * from './feedback.helper';
|
||||
export * from './network.helper';
|
||||
export * from './ones-anchor.helper';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* 双协议网络前置:按 PROTO 切手机蓝牙/WiFi,用于必测项控制用例的 BLE/WiFi 两种模式。
|
||||
*
|
||||
* - ble 模式: 开蓝牙、关 WiFi → app 走 BLE 直连
|
||||
* - wifi 模式: 关蓝牙、开 WiFi → app 走 WiFi/云
|
||||
*
|
||||
* 平台能力(已在三星真机实测):
|
||||
* - Android WiFi : adb `svc wifi enable/disable` 可行(dumpsys 实测真关/开)
|
||||
* - Android 蓝牙 : adb `svc bluetooth` 被新系统禁用(exit 137)、`cmd bluetooth_manager` 无实现、
|
||||
* `settings put bluetooth_on` 不动 radio → 改为 `am start 蓝牙设置` + 点 switch_widget(实测可切 ON↔BLE_ON)
|
||||
* - iOS : 无公开 API,走系统设置(com.apple.Preferences)UI;locator 需 iOS 真机校准
|
||||
*/
|
||||
import { execSync } from 'child_process';
|
||||
import { DeviceDriver } from '../../drivers/types';
|
||||
import { APP_CONFIG } from '../../config/app.config';
|
||||
import { sleep } from './element.helper';
|
||||
|
||||
function adbShell(cmd: string): string {
|
||||
return execSync(`adb shell ${cmd}`, { encoding: 'utf-8', timeout: 20000 });
|
||||
}
|
||||
|
||||
// ---------- Android(纯 adb,已实测) ----------
|
||||
async function androidSetWifi(on: boolean): Promise<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);
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* 设备维度的 ONES 锚点解析:同品类不同型号(UI 相似)用同一脚本,按当前设备动态解析锚点,
|
||||
* 换 <CAT>_DEVICE 环境变量重跑即可分别回写各型号的 ONES 用例。
|
||||
*
|
||||
* 映射 key = DEVICE_CONFIG 里的设备名;值 = 该型号的 添加用例号 / 控制 step uuid(ble/wifi)。
|
||||
* 控制步在两条协议超级用例里:ble→15975 / wifi→15974。
|
||||
* 新型号:在此补一行即可被脚本识别。
|
||||
*/
|
||||
export interface DeviceAnchor {
|
||||
add?: number; // 添加用例 ONES number
|
||||
ctrlBle?: string; // 15975 下的 step uuid
|
||||
ctrlWifi?: string; // 15974 下的 step uuid
|
||||
}
|
||||
|
||||
const ANCHORS: Record<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]` : '';
|
||||
}
|
||||
|
|
@ -141,36 +141,22 @@ function lcs(a: string, b: string): number {
|
|||
return dp[m][n];
|
||||
}
|
||||
|
||||
/**
|
||||
* 取某用例的全部 step uuid(按定义顺序),用于回写时列全量 step。
|
||||
*/
|
||||
export function fetchCaseSteps(caseNumber: number): string[] {
|
||||
try {
|
||||
const out = execSync(`${ONES_CLI} testcase case search --key ${caseNumber}`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 30000,
|
||||
});
|
||||
const c = JSON.parse(out).cases?.[0];
|
||||
return (c?.steps || []).map((s: any) => s.uuid);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按测试名锚点构建回写 payload(支持 case 级与 step 级)。
|
||||
* - [ONES:<num>] → case 级结果
|
||||
* - [ONES:<num>#<stepUuid>] → step 级结果
|
||||
* 无锚点的结果不在此处理(交由 matchResults LCS 兜底)。
|
||||
*
|
||||
* opts.fullStepsByNumber: 用例号 → 全部 step uuid。提供时按 ONES 要求列全量 step,
|
||||
* 只给跑过的填 execute_result,未跑的仅列 uuid;全部跑完才聚合 case 结果,否则保持 to_do。
|
||||
* 只写「跑过的」step(不读取/不列出原有 step,提高效率)。
|
||||
* opts.totalStepsByNumber: 用例号 → 该用例步骤总数(来自本地参数文件)。
|
||||
* 提供时:跑过的 step 数 < 总数 → case 保持 to_do(部分执行);全跑完才聚合 passed/failed。
|
||||
* 不提供时:按已有结果聚合。
|
||||
*/
|
||||
export function buildAnchoredPayloads(
|
||||
planCases: OnesPlanCase[],
|
||||
testResults: TestResult[],
|
||||
executor: string,
|
||||
opts: { fullStepsByNumber?: Map<number, string[]> } = {}
|
||||
opts: { totalStepsByNumber?: Map<number, number> } = {}
|
||||
): { payloads: OnesUpdatePayload[]; unanchored: TestResult[] } {
|
||||
const byNumber = new Map<number, OnesPlanCase>();
|
||||
for (const pc of planCases) byNumber.set(pc.caseNumber, pc);
|
||||
|
|
@ -204,19 +190,12 @@ export function buildAnchoredPayloads(
|
|||
|
||||
const payloads: OnesUpdatePayload[] = [];
|
||||
|
||||
// step 级用例:列全量 step(未跑的仅 uuid),全部跑完才聚合 case 结果
|
||||
// step 级用例:只写跑过的 step;全跑完才聚合 case 结果,否则 to_do
|
||||
for (const [caseUUID, stepMap] of runSteps) {
|
||||
const num = numberByUUID.get(caseUUID)!;
|
||||
const full = opts.fullStepsByNumber?.get(num);
|
||||
let steps: OnesStepResult[];
|
||||
let allRun: boolean;
|
||||
if (full && full.length) {
|
||||
steps = full.map((uuid) => stepMap.get(uuid) ?? { uuid });
|
||||
allRun = full.every((uuid) => stepMap.has(uuid));
|
||||
} else {
|
||||
steps = Array.from(stepMap.values());
|
||||
allRun = true; // 无全量列表时,以已有结果为准
|
||||
}
|
||||
const steps = Array.from(stepMap.values());
|
||||
const total = opts.totalStepsByNumber?.get(num);
|
||||
const allRun = total != null ? stepMap.size >= total : true;
|
||||
let agg: SyncStatus | undefined;
|
||||
for (const s of stepMap.values()) if (s.execute_result) agg = mergeStatus(agg, s.execute_result);
|
||||
const result: OnesUpdatePayload['result'] = allRun ? agg ?? 'to_do' : 'to_do';
|
||||
|
|
@ -235,32 +214,62 @@ export function buildAnchoredPayloads(
|
|||
/**
|
||||
* 分批 POST 回写 payload 到 ONES 测试计划。
|
||||
*/
|
||||
/**
|
||||
* 执行一个 GraphQL mutation(经 ones CLI,复用登录认证)。失败抛错。
|
||||
*/
|
||||
function runOnesMutation(body: string): void {
|
||||
const resp = runOnesGraphQL(`mutation { ${body} }`);
|
||||
if (!resp || !resp.data) {
|
||||
throw new Error(JSON.stringify(resp?.detail || resp || {}).slice(0, 300));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回写 payload 到 ONES 测试计划,走 GraphQL mutation(updateTestcasePlanCase /
|
||||
* updateTestcasePlanCaseStep),用 ones CLI 的登录认证,无需 token/PAT。
|
||||
*
|
||||
* key 规则:
|
||||
* case: testcase_plan_case-<planUUID>-<caseUUID>
|
||||
* step: testcase_plan_case_step-<planUUID>-<caseUUID>-<stepUuid>
|
||||
*/
|
||||
export function postPayloads(
|
||||
planUUID: string,
|
||||
payload: OnesUpdatePayload[]
|
||||
): { success: number; failed: number } {
|
||||
if (payload.length === 0) return { success: 0, failed: 0 };
|
||||
const batchSize = 50;
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
for (let i = 0; i < payload.length; i += batchSize) {
|
||||
const batch = payload.slice(i, i + batchSize);
|
||||
let stepsWritten = 0;
|
||||
|
||||
for (const p of payload) {
|
||||
try {
|
||||
execSync(buildCurlCommand(planUUID, batch), { encoding: 'utf-8', timeout: 30000 });
|
||||
success += batch.length;
|
||||
// 先写 step(仅跑过的)
|
||||
for (const s of p.steps) {
|
||||
if (!s.execute_result) continue;
|
||||
const sk = `testcase_plan_case_step-${planUUID}-${p.uuid}-${s.uuid}`;
|
||||
const ar = s.actual_result ? `, actual_result: ${JSON.stringify(s.actual_result)}` : '';
|
||||
runOnesMutation(
|
||||
`updateTestcasePlanCaseStep(key: ${JSON.stringify(sk)}, step_result: ${JSON.stringify(s.execute_result)}${ar}) { key }`
|
||||
);
|
||||
stepsWritten++;
|
||||
}
|
||||
// 再写 case 级结果
|
||||
const ck = `testcase_plan_case-${planUUID}-${p.uuid}`;
|
||||
runOnesMutation(
|
||||
`updateTestcasePlanCase(key: ${JSON.stringify(ck)}, result: ${JSON.stringify(p.result)}) { key }`
|
||||
);
|
||||
success++;
|
||||
} catch (e: any) {
|
||||
console.error(`ONES sync batch failed: ${e.message}`);
|
||||
failed += batch.length;
|
||||
console.error(`[ONES] 用例 ${p.uuid} 回写失败: ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (stepsWritten) console.log(`[ONES] 写入 ${stepsWritten} 个 step 结果`);
|
||||
return { success, failed };
|
||||
}
|
||||
|
||||
/**
|
||||
* 反写结果到 ONES 测试计划
|
||||
*
|
||||
* API: POST /project/api/project/team/{team_uuid}/testcase/plan/{plan_uuid}/cases/update
|
||||
* Body: { cases: [{ uuid, executor, note, result, steps: [{ uuid, execute_result }] }] }
|
||||
* 反写结果到 ONES 测试计划(case 级)。步骤级请用带 steps 的 payload 走 postPayloads。
|
||||
*/
|
||||
export function syncResultsToOnes(
|
||||
planUUID: string,
|
||||
|
|
@ -274,17 +283,6 @@ export function syncResultsToOnes(
|
|||
return postPayloads(planUUID, payload);
|
||||
}
|
||||
|
||||
function buildCurlCommand(planUUID: string, payload: OnesUpdatePayload[]): string {
|
||||
const configStr = execSync(`${ONES_CLI} config show`, { encoding: 'utf-8' });
|
||||
const config = JSON.parse(configStr);
|
||||
const { base_url, team_uuid, token } = config;
|
||||
|
||||
const url = `${base_url}/project/api/project/team/${team_uuid}/testcase/plan/${planUUID}/cases/update`;
|
||||
const body = JSON.stringify({ cases: payload }).replace(/'/g, "'\\''");
|
||||
|
||||
return `curl -s -X POST '${url}' -H 'Authorization: Bearer ${token}' -H 'Content-Type: application/json' -d '${body}'`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键同步: 读取计划用例 → 匹配自动化结果 → 反写
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -152,6 +152,12 @@ export class WDAHelper {
|
|||
} 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> {
|
||||
if (this.sessionId && !this.reusedSession) {
|
||||
await this.request('DELETE', `/session/${this.sessionId}`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue