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