feat: 串口配对添加链路 + 多设备连接用例 + soak/继电器工具
- 设备添加: device-add.helper 串口配对主链路(addDeviceWithSerialPairing), 按平台设备名注册表(device-registry.helper),hub/curtain/qr 专用 helper - 新增多设备连接用例: remote、outdoor_meter、rn_plugin,刷新各品类 connect 用例 - 配置: relay.config(继电器通道/按键映射)、wifi.config(分平台配网凭据) - 脚本: 夜间 soak(android/ios)、reset-devices、relay 控制与报告、调试脚本 - 工具: serial_controller 串口协议、zendesk/feedback/ones-anchor helper、test-reporter - 杂项: 撤出追踪 .DS_Store / .claude 本地设置;.gitignore 忽略 docs/ test-plan/ test_must_case/(生成产物/单独维护) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
373a8bd6fa
commit
d5c81826fb
|
|
@ -1,49 +0,0 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(curl -s http://localhost:8100/status)",
|
||||
"Bash(python3 -m json.tool)",
|
||||
"Bash(xcrun simctl *)",
|
||||
"Bash(idevice_id -l)",
|
||||
"Bash(system_profiler SPUSBDataType)",
|
||||
"Bash(xcodebuild -version)",
|
||||
"Bash(tidevice list *)",
|
||||
"Bash(curl -s -X POST http://localhost:8100/session -H 'Content-Type: application/json' -d '{\"capabilities\":{\"alwaysMatch\":{\"platformName\":\"iOS\",\"shouldUseSingletonTestManager\":false}}}')",
|
||||
"Bash(python3 -c ' *)",
|
||||
"Bash(curl -s -X POST http://localhost:8100/session/01F3CC63-AC4B-49B7-9CEC-DCA8495C861A/elements -H 'Content-Type: application/json' -d '{\"using\":\"predicate string\",\"value\":\"name CONTAINS \\\\\"摄像机Plus 3K\\\\\" AND type == \\\\\"XCUIElementTypeCell\\\\\"\"}')",
|
||||
"Bash(curl -s http://localhost:8100/session/01F3CC63-AC4B-49B7-9CEC-DCA8495C861A/source)",
|
||||
"Bash(curl -s -X POST http://localhost:8100/session/01F3CC63-AC4B-49B7-9CEC-DCA8495C861A/element/87000000-0000-0000-430D-000000000000/click -H 'Content-Type: application/json' -d '{}')",
|
||||
"Bash(curl -s -X POST http://localhost:8100/session/01F3CC63-AC4B-49B7-9CEC-DCA8495C861A/wda/tap -H 'Content-Type: application/json' -d '{\"x\":195,\"y\":280}')",
|
||||
"Bash(curl -s -X POST http://localhost:8100/session/01F3CC63-AC4B-49B7-9CEC-DCA8495C861A/elements -H 'Content-Type: application/json' -d '{\"using\":\"name\",\"value\":\"Features\"}')",
|
||||
"Bash(curl -s -X POST http://localhost:8100/session/E0B85237-9A02-4880-AC53-249942845E57/wda/tap -H 'Content-Type: application/json' -d '{\"x\":195,\"y\":200}')",
|
||||
"Bash(curl -s http://localhost:8100/session/E0B85237-9A02-4880-AC53-249942845E57/screenshot)",
|
||||
"Bash(curl -s -X DELETE \"http://localhost:8100/session/E0B85237-9A02-4880-AC53-249942845E57\")",
|
||||
"Bash(curl -s -X GET http://localhost:4723/sessions)",
|
||||
"Bash(curl -s http://localhost:8100/session/C0178F7D-492D-4E16-9E50-A6B9AFC34937/source)",
|
||||
"Bash(curl -s http://localhost:8100/session/__TRACKED_VAR__/source)",
|
||||
"Bash(curl -s -X POST http://localhost:8100/session/FF243246-C365-4F90-8590-E42A5D724D45/element -H 'Content-Type: application/json' -d '{\"using\":\"name\",\"value\":\"More\"}')",
|
||||
"Bash(curl -s -X GET https://sz.ones.cn/openapi/v1/team/98Q19ZsW/testcase/plans -H 'Content-Type: application/json' -H 'Ones-Auth-Token: CN-MGM2MmM4NmFjYjU0NDliZDU2MjlhN2YyZTEwM2ViMWM-pat_f8dc5cd57c9f450975d7c0e6147e9fd5' -w '\\\\nHTTP_CODE: %{http_code}\\\\n')",
|
||||
"WebFetch(domain:developer.ones.cn)",
|
||||
"Bash(curl -s 'https://sz.ones.cn/openapi/v2/testcase/libraries/cases?teamID=98Q19ZsW&testCaseLibraryID=HFcbrnoL' -H 'Authorization: Bearer CN-MjFiZTI1YmUwNWQ1NGYyZjU4YzMzZmNlNDEwNGY2OGQ-pat_914ebfdbd0ad400c6e7f610093b3d1e4')",
|
||||
"Bash(curl -s 'https://sz.ones.cn/openapi/v2/testcase/libraries/cases?teamID=98Q19ZsW&testCaseLibraryID=8WUGnd2a' -H 'Authorization: Bearer CN-MjFiZTI1YmUwNWQ1NGYyZjU4YzMzZmNlNDEwNGY2OGQ-pat_914ebfdbd0ad400c6e7f610093b3d1e4')",
|
||||
"Bash(curl -s 'https://sz.ones.cn/openapi/v2/testcase/libraries/cases?teamID=98Q19ZsW&library=HFcbrnoL' -H 'Authorization: Bearer CN-MjFiZTI1YmUwNWQ1NGYyZjU4YzMzZmNlNDEwNGY2OGQ-pat_914ebfdbd0ad400c6e7f610093b3d1e4')",
|
||||
"Bash(curl -s 'https://sz.ones.cn/openapi/v2/testcase/libraries?teamID=98Q19ZsW&pageSize=50&cursor=8M5Xs9OUjhIxAAAAdGVzdGNhc2VfbGlicmFyeS1MRHJKbVZ5Sw==' -H 'Authorization: Bearer CN-MjFiZTI1YmUwNWQ1NGYyZjU4YzMzZmNlNDEwNGY2OGQ-pat_914ebfdbd0ad400c6e7f610093b3d1e4')",
|
||||
"Bash(chmod +x /Users/woan/local/bin/ones)",
|
||||
"Bash(/Users/woan/local/bin/ones config *)",
|
||||
"Bash(/Users/woan/local/bin/ones graphql *)",
|
||||
"mcp__figma__get_screenshot",
|
||||
"Bash(/Users/woan/.local/bin/ones config *)",
|
||||
"Bash(/Users/woan/local/bin/ones testcase *)",
|
||||
"Bash(npx tsc *)",
|
||||
"Bash(npx vitest *)",
|
||||
"Bash(appium --version)",
|
||||
"Bash(appium --port 4723 --allow-insecure chromedriver_autodownload)",
|
||||
"Bash(echo \"Appium PID: $!\")",
|
||||
"Bash(curl -s http://localhost:4723/status)",
|
||||
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\('Appium ready:', d.get\\('value',{}\\).get\\('ready',False\\)\\)\")",
|
||||
"Bash(adb devices *)",
|
||||
"Bash(xcrun xctrace *)",
|
||||
"Bash(PLATFORM=android npx vitest run tests/aihub/aihub_daily_report.test.ts)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -6,3 +6,10 @@ midscene_run/
|
|||
.idea/
|
||||
.DS_Store
|
||||
.claude/
|
||||
|
||||
# 旧 Appium/pytest 必测用例 + allure 报告产物(不入库)
|
||||
test_must_case/
|
||||
# 测试计划/清单(本地生成,机器相关)
|
||||
test-plan/
|
||||
# 文档(单独维护,不随代码提交)
|
||||
docs/
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { getAddedDevice } from '../utils/common/device-registry.helper';
|
||||
|
||||
export interface DeviceCategoryConfig {
|
||||
devices: string[];
|
||||
defaultDevice: string;
|
||||
|
|
@ -9,11 +11,11 @@ export const DEVICE_CONFIG: Record<string, DeviceCategoryConfig> = {
|
|||
defaultDevice: 'Bot 0F',
|
||||
},
|
||||
camera: {
|
||||
devices: ['摄像机Plus 3K'],
|
||||
devices: ['摄像机Plus 3K', 'Pan Tilt', 'Pan Tilt Cam 2K', 'Pan Tilt Plus 2K', 'Pan Tilt Plus 3K', 'Indoor Cam'],
|
||||
defaultDevice: '摄像机Plus 3K',
|
||||
},
|
||||
osc: {
|
||||
devices: ['OSC 2K'],
|
||||
devices: ['OSC 2K', 'Outdoor'],
|
||||
defaultDevice: 'OSC 2K',
|
||||
},
|
||||
curtain: {
|
||||
|
|
@ -21,7 +23,7 @@ export const DEVICE_CONFIG: Record<string, DeviceCategoryConfig> = {
|
|||
defaultDevice: 'Curtain 1A',
|
||||
},
|
||||
plug: {
|
||||
devices: ['Plug 4D', 'PlugMini 5E'],
|
||||
devices: ['Plug 4D', 'PlugMini 5E', 'Plug Mini (JP)'],
|
||||
defaultDevice: 'Plug 4D',
|
||||
},
|
||||
lock: {
|
||||
|
|
@ -29,7 +31,7 @@ export const DEVICE_CONFIG: Record<string, DeviceCategoryConfig> = {
|
|||
defaultDevice: 'Lock 6F',
|
||||
},
|
||||
hub: {
|
||||
devices: ['Hub2 8H', 'HubMini 9I', 'HubMiniMatter 0J'],
|
||||
devices: ['Hub2 8H', 'HubMini 9I', 'HubMiniMatter 0J', 'Hub Plus'],
|
||||
defaultDevice: 'Hub2 8H',
|
||||
},
|
||||
humidifier: {
|
||||
|
|
@ -37,7 +39,7 @@ export const DEVICE_CONFIG: Record<string, DeviceCategoryConfig> = {
|
|||
defaultDevice: 'Humidifier 1K',
|
||||
},
|
||||
meter: {
|
||||
devices: ['Meter 3M', 'Outdoor Meter 4N'],
|
||||
devices: ['Meter 3M', 'Meter Plus', 'Outdoor Meter 4N'],
|
||||
defaultDevice: 'Meter 3M',
|
||||
},
|
||||
sensor: {
|
||||
|
|
@ -45,7 +47,7 @@ export const DEVICE_CONFIG: Record<string, DeviceCategoryConfig> = {
|
|||
defaultDevice: 'Contact Sensor 5O',
|
||||
},
|
||||
ceilingLight: {
|
||||
devices: ['CeilingLight 7Q'],
|
||||
devices: ['CeilingLight 7Q', 'Ceiling Light pro'],
|
||||
defaultDevice: 'CeilingLight 7Q',
|
||||
},
|
||||
colorBulb: {
|
||||
|
|
@ -65,7 +67,7 @@ export const DEVICE_CONFIG: Record<string, DeviceCategoryConfig> = {
|
|||
defaultDevice: 'WaterDetector 1U',
|
||||
},
|
||||
keypad: {
|
||||
devices: ['Keypad 2V'],
|
||||
devices: ['Keypad 2V', 'Keypad Touch'],
|
||||
defaultDevice: 'Keypad 2V',
|
||||
},
|
||||
remote: {
|
||||
|
|
@ -99,6 +101,21 @@ export const DEVICE_CONFIG: Record<string, DeviceCategoryConfig> = {
|
|||
};
|
||||
|
||||
export function getDeviceName(category: string, envVar?: string): string {
|
||||
if (envVar && process.env[envVar]) {
|
||||
return process.env[envVar]!;
|
||||
}
|
||||
// 添加成功后写入的实际设备名(进入功能页入口),优先于配置默认值
|
||||
const added = getAddedDevice(category);
|
||||
if (added) return added;
|
||||
return DEVICE_CONFIG[category]?.defaultDevice || category;
|
||||
}
|
||||
|
||||
/**
|
||||
* 型号名(用于 ONES 锚点解析等"按型号"的场景):env 覆盖 > 配置默认值。
|
||||
* **不读注册表**——注册表是真机实际显示名(如 "Meter 0B"),而锚点表按型号名(如 "Meter 3M")建,
|
||||
* 两者需区分:操作入口用 getDeviceName(实际名),锚点/型号用 getModelName(型号名)。
|
||||
*/
|
||||
export function getModelName(category: string, envVar?: string): string {
|
||||
if (envVar && process.env[envVar]) {
|
||||
return process.env[envVar]!;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* 继电器板通道映射 + 物理按键动作配置(参数化,勿在脚本里写死通道号/时长)。
|
||||
*
|
||||
* 每路继电器接到某设备的某个物理按键或电源。这里把「设备 → 按键名 → 通道号」以及
|
||||
* 「进入配对模式要按哪些键、按多久」集中配置,脚本只引用语义名(如 'remote' 的 'pairing')。
|
||||
*
|
||||
* **按平台区分通道**:Android 与 iOS 是两套真机,同一设备的按键接在继电器板的不同通道上。
|
||||
* 通道号写成 { android, ios };动作逻辑(按哪些键/按多久)两端共用。平台取 process.env.PLATFORM
|
||||
* (与 config/app.config、设备名注册表一致,默认 ios)。
|
||||
*
|
||||
* 实测接线(Android,2026-06):ch33=meter 主键、ch34=remote 凸键、ch35=remote 凹键。
|
||||
* iOS 通道待接线后填入(留空则在 iOS 下调用会报清晰错误)。
|
||||
* 时长可被 env 覆盖:RELAY_HOLD_MS_<DEVICE>_<ACTION>(全大写)。
|
||||
*/
|
||||
|
||||
/** 通道号:两端相同写数字;两端不同写 { android, ios }。 */
|
||||
export type ChannelSpec = number | { android?: number; ios?: number };
|
||||
|
||||
export interface RelayAction {
|
||||
buttons: string[]; // 同时按下的按键名(引用本设备 buttons 的 key);单键即数组里一个
|
||||
holdMs: number; // 按住毫秒数
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
export interface RelayDeviceConfig {
|
||||
buttons: Record<string, ChannelSpec>; // 按键名 → 通道(可按平台)
|
||||
actions?: Record<string, RelayAction>;
|
||||
}
|
||||
|
||||
export const RELAY_MAP: Record<string, RelayDeviceConfig> = {
|
||||
meter: {
|
||||
buttons: { main: { android: 33 /*, ios: <填iOS通道> */ } },
|
||||
actions: {
|
||||
pairing: { buttons: ['main'], holdMs: 2000, desc: '按住主键进入添加模式' },
|
||||
},
|
||||
},
|
||||
remote: {
|
||||
buttons: {
|
||||
convex: { android: 34 /*, ios: <填> */ }, // 凸键
|
||||
concave: { android: 35 /*, ios: <填> */ }, // 凹键
|
||||
},
|
||||
actions: {
|
||||
pairing: { buttons: ['convex', 'concave'], holdMs: 2000, desc: '同时按凸键+凹键 2s 进入添加模式(切换式:按一次进、再按退,勿重按)' },
|
||||
},
|
||||
},
|
||||
outdoor_meter: {
|
||||
buttons: { main: { android: 36 /*, ios: <填> */ } },
|
||||
actions: {
|
||||
pairing: { buttons: ['main'], holdMs: 2000, desc: '按住主键 2s 进入添加模式' },
|
||||
},
|
||||
},
|
||||
curtain3: {
|
||||
buttons: {
|
||||
power: { android: 37 /*, ios: <填> */ }, // 开关按钮
|
||||
track: { android: 38 /*, ios: <填> */ }, // 控制轨道按钮(控制用,不参与配对)
|
||||
},
|
||||
actions: {
|
||||
pairing: { buttons: ['power'], holdMs: 2000, desc: '按住开关按钮 2s 进入添加模式' },
|
||||
},
|
||||
},
|
||||
curtain: {
|
||||
buttons: { power: { android: 39 /*, ios: <填> */ } }, // 开关按钮
|
||||
actions: {
|
||||
pairing: { buttons: ['power'], holdMs: 2000, desc: '按住开关按钮 2s 进入添加模式' },
|
||||
},
|
||||
},
|
||||
hub: {
|
||||
buttons: { main: { android: 43 /*, ios: <填> */ } }, // Hub Mini 主键
|
||||
actions: {
|
||||
pairing: { buttons: ['main'], holdMs: 4000, desc: '按住主键 4s 进入添加模式(Hub Mini;指引页写3s,实测需4s)' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function currentPlatform(): 'android' | 'ios' {
|
||||
return (process.env.PLATFORM || 'ios').toLowerCase() === 'android' ? 'android' : 'ios';
|
||||
}
|
||||
|
||||
/** 解析某按键在当前平台的通道号。iOS 未单独配通道时,回退到 android(两端复用同一套设备/通道时用)。 */
|
||||
function channelOf(device: string, button: string): number {
|
||||
const spec = RELAY_MAP[device]?.buttons?.[button];
|
||||
if (spec == null) throw new Error(`relay 配置无 '${device}.${button}'(config/relay.config.ts)`);
|
||||
if (typeof spec === 'number') return spec;
|
||||
const plat = currentPlatform();
|
||||
const ch = spec[plat] ?? spec.android; // iOS 缺省回退 android(先复用 Android 配置)
|
||||
if (ch == null) throw new Error(`'${device}.${button}' 未配置 ${plat}(且无 android 回退)的通道(config/relay.config.ts)`);
|
||||
return ch;
|
||||
}
|
||||
|
||||
/** 取设备某动作要触发的通道列表 + 时长(按当前平台解析通道;env 可覆盖时长)。 */
|
||||
export function resolveRelayAction(
|
||||
device: string,
|
||||
action: string
|
||||
): { channels: number[]; holdMs: number; desc?: string } {
|
||||
const dev = RELAY_MAP[device];
|
||||
if (!dev) throw new Error(`relay 配置无设备 '${device}'(config/relay.config.ts)`);
|
||||
const act = dev.actions?.[action];
|
||||
if (!act) throw new Error(`设备 '${device}' 无动作 '${action}'`);
|
||||
const channels = act.buttons.map((b) => channelOf(device, b));
|
||||
const envKey = `RELAY_HOLD_MS_${device.toUpperCase()}_${action.toUpperCase()}`;
|
||||
const holdMs = process.env[envKey] ? parseInt(process.env[envKey] as string, 10) : act.holdMs;
|
||||
return { channels, holdMs, desc: act.desc };
|
||||
}
|
||||
|
||||
/** 取设备某按键在当前平台的通道号。 */
|
||||
export function resolveRelayButton(device: string, button: string): number {
|
||||
return channelOf(device, button);
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* WiFi 配网凭据 —— 用于 Hub Mini 等需要连 WiFi 的设备添加流程。
|
||||
*
|
||||
* 分平台配置(Android / iOS 可用不同 WiFi)。取值优先级:
|
||||
* 平台专用 env(WIFI_SSID_ANDROID / WIFI_SSID_IOS) > 通用 env(WIFI_SSID) > 默认(woanmarket/woankeji)
|
||||
* 密码同理(WIFI_PASSWORD_ANDROID / WIFI_PASSWORD_IOS > WIFI_PASSWORD > woankeji)。
|
||||
*
|
||||
* 示例(.env 或 shell):
|
||||
* WIFI_SSID_ANDROID=woanmarket WIFI_PASSWORD_ANDROID=woankeji
|
||||
* WIFI_SSID_IOS=woanmarket_5g WIFI_PASSWORD_IOS=xxxx
|
||||
* 当前两端默认相同(woanmarket/woankeji);后续要分开只需在 .env 设平台专用变量。
|
||||
*/
|
||||
export interface WifiCredentials {
|
||||
ssid: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SSID = 'woanmarket';
|
||||
const DEFAULT_PASSWORD = 'woankeji';
|
||||
|
||||
function pick(platformVar: string, genericVar: string, def: string): string {
|
||||
return process.env[platformVar] || process.env[genericVar] || def;
|
||||
}
|
||||
|
||||
/** 取当前(或指定)平台的 WiFi 凭据。platform 缺省读 PLATFORM env(android/ios)。 */
|
||||
export function getWifiCredentials(platform?: 'android' | 'ios'): WifiCredentials {
|
||||
const isIos = (platform || process.env.PLATFORM || 'android').toLowerCase() === 'ios';
|
||||
const suffix = isIos ? 'IOS' : 'ANDROID';
|
||||
return {
|
||||
ssid: pick(`WIFI_SSID_${suffix}`, 'WIFI_SSID', DEFAULT_SSID),
|
||||
password: pick(`WIFI_PASSWORD_${suffix}`, 'WIFI_PASSWORD', DEFAULT_PASSWORD),
|
||||
};
|
||||
}
|
||||
Binary file not shown.
|
|
@ -1,618 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""生成 UI自动化测试计划.docx"""
|
||||
|
||||
from docx import Document
|
||||
from docx.shared import Inches, Pt, Cm, RGBColor
|
||||
from docx.enum.table import WD_TABLE_ALIGNMENT
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.enum.section import WD_ORIENT
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
import datetime
|
||||
|
||||
doc = Document()
|
||||
|
||||
# ======================== 样式设置 ========================
|
||||
style = doc.styles['Normal']
|
||||
style.font.name = '微软雅黑'
|
||||
style.font.size = Pt(10.5)
|
||||
style.element.rPr.rFonts.set(qn('w:eastAsia'), '微软雅黑')
|
||||
|
||||
|
||||
def set_cell_shading(cell, color):
|
||||
shading_elm = OxmlElement('w:shd')
|
||||
shading_elm.set(qn('w:fill'), color)
|
||||
shading_elm.set(qn('w:val'), 'clear')
|
||||
cell._tc.get_or_add_tcPr().append(shading_elm)
|
||||
|
||||
|
||||
def add_table_with_header(doc, headers, rows, col_widths=None):
|
||||
table = doc.add_table(rows=1 + len(rows), cols=len(headers))
|
||||
table.style = 'Table Grid'
|
||||
# Header
|
||||
for i, h in enumerate(headers):
|
||||
cell = table.rows[0].cells[i]
|
||||
cell.text = h
|
||||
cell.paragraphs[0].runs[0].bold = True
|
||||
set_cell_shading(cell, '4472C4')
|
||||
cell.paragraphs[0].runs[0].font.color.rgb = RGBColor(255, 255, 255)
|
||||
# Data
|
||||
for r_idx, row_data in enumerate(rows):
|
||||
for c_idx, val in enumerate(row_data):
|
||||
table.rows[r_idx + 1].cells[c_idx].text = str(val)
|
||||
if col_widths:
|
||||
for i, w in enumerate(col_widths):
|
||||
for row in table.rows:
|
||||
row.cells[i].width = Cm(w)
|
||||
return table
|
||||
|
||||
|
||||
# ======================== 封面 ========================
|
||||
doc.add_paragraph()
|
||||
doc.add_paragraph()
|
||||
title = doc.add_paragraph()
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = title.add_run('SwitchBot App UI自动化测试计划')
|
||||
run.font.size = Pt(26)
|
||||
run.bold = True
|
||||
|
||||
doc.add_paragraph()
|
||||
subtitle = doc.add_paragraph()
|
||||
subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = subtitle.add_run('单品设备功能测试 + 设备添加 + 自动化场景')
|
||||
run.font.size = Pt(14)
|
||||
run.font.color.rgb = RGBColor(68, 114, 196)
|
||||
|
||||
doc.add_paragraph()
|
||||
doc.add_paragraph()
|
||||
info = doc.add_paragraph()
|
||||
info.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
info.add_run(f'版本: v1.0\n').font.size = Pt(11)
|
||||
info.add_run(f'日期: {datetime.date.today().strftime("%Y-%m-%d")}\n').font.size = Pt(11)
|
||||
info.add_run('部门: 测试部\n').font.size = Pt(11)
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
# ======================== 1. 项目概述 ========================
|
||||
doc.add_heading('1. 项目概述', level=1)
|
||||
|
||||
doc.add_heading('1.1 背景', level=2)
|
||||
doc.add_paragraph(
|
||||
'SwitchBot App 涵盖 100+ 款 IoT 设备,需要对每款设备的 App 功能进行自动化回归测试。'
|
||||
'本计划基于已验证的 Vitest + Appium 技术框架,按优先级分层推进:'
|
||||
'必测项 → 单品探索 → 全功能 → 平台,高优先级层先达标再投入下一层。'
|
||||
)
|
||||
|
||||
doc.add_heading('1.2 技术栈', level=2)
|
||||
rows = [
|
||||
('框架', 'Vitest (TypeScript) + Appium'),
|
||||
('驱动', 'UIAutomator2 (Android) / WebDriverAgent (iOS)'),
|
||||
('AI辅助', 'Claude Code 边跑边写生成 & 调试脚本'),
|
||||
('测试设备', 'Samsung 1080x2280 (Android) / iPhone 390x844pt (iOS)'),
|
||||
('App', 'SwitchBot (com.theswitchbot.switchbot)'),
|
||||
('报告', '自定义 HTML 报告 + JSON 结果'),
|
||||
('用例管理', 'ONES 平台同步'),
|
||||
]
|
||||
add_table_with_header(doc, ['项目', '说明'], rows)
|
||||
|
||||
doc.add_heading('1.3 当前进度', level=2)
|
||||
doc.add_paragraph('已调试完成的设备(不在本计划范围内):')
|
||||
rows = [
|
||||
('Bot', '5', '卡片/控制/设置/场景/日志', '100%'),
|
||||
('Camera (Pan/Tilt Plus 3K)', '6', '卡片/控制/全屏/事件/录像/设置', '100%'),
|
||||
('AI Hub', '11', '设置/投屏/侦测/勿扰/存储/回放/AI事件/日报/功能页/连接/绑定', '86%'),
|
||||
]
|
||||
add_table_with_header(doc, ['设备', '脚本文件数', '覆盖模块', '通过率'], rows)
|
||||
|
||||
doc.add_heading('1.4 目标', level=2)
|
||||
doc.add_paragraph('• P0 必测项:各单品添加 + 核心控制(双协议)准入闸门,来源 ONES 必测项-AI自动化')
|
||||
doc.add_paragraph('• P1 单品探索:78 款单品主流程冒烟,覆盖广度')
|
||||
doc.add_paragraph('• P2 全功能:全维度回归(card/control/setting/scene/logs),覆盖深度')
|
||||
doc.add_paragraph('• P3 平台:账号/房间/消息/家庭分享/场景等平台功能')
|
||||
doc.add_paragraph('• 按优先级 backlog 依次交付,各层有独立退出标准(不绑定固定周次)')
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
# ======================== 2. 设备清单 ========================
|
||||
doc.add_heading('2. 待覆盖设备清单(78款)', level=1)
|
||||
|
||||
device_categories = {
|
||||
'窗帘 (9款)': [
|
||||
('Curtain Rod', '已有脚本', '调试适配'),
|
||||
('Curtain Rod 2', '已有脚本', '调试适配'),
|
||||
('Curtain U Rail', '已有脚本', '调试适配'),
|
||||
('Curtain U Rail 2', '已有脚本', '调试适配'),
|
||||
('Curtain U Rail 2.3', '复用模板', '新增配置'),
|
||||
('Curtain 3 U Rail', '复用模板', '新增配置'),
|
||||
('Curtain 3 Rod 2025', '复用模板', '新增配置'),
|
||||
('Curtain 4 Urail', '复用模板', '新增配置'),
|
||||
('Roller Shade', '复用模板', '新增配置'),
|
||||
],
|
||||
'锁 (12款)': [
|
||||
('Lock (JP)', '已有脚本', '调试适配'),
|
||||
('Lock (US)', '已有脚本', '调试适配'),
|
||||
('Lock (EU)', '复用模板', '新增配置'),
|
||||
('Lock Pro (EU)', '复用模板', '新增配置'),
|
||||
('Lock Pro (US)', '复用模板', '新增配置'),
|
||||
('Lock Lite', '复用模板', '新增配置'),
|
||||
('Lock Ultra (JP)', '需新写', 'UI差异大'),
|
||||
('Lock Ultra (US)', '需新写', 'UI差异大'),
|
||||
('Lock Ultra (EU)', '需新写', 'UI差异大'),
|
||||
('Lock Pro Matter Enabled', '复用模板', '新增配置'),
|
||||
('US Vision Deadbolt Pro', '需新写', '全新UI'),
|
||||
('US Vision Deadbolt', '需新写', '全新UI'),
|
||||
],
|
||||
'灯光 (10款)': [
|
||||
('LED Strip Light 2', '已有脚本', '调试适配'),
|
||||
('Strip Light 3', '已有脚本', '调试适配'),
|
||||
('RGBICWW Strip Light', '复用模板', '新增配置'),
|
||||
('RGBICWW Floor Lamp', '复用模板', '新增配置'),
|
||||
('RGBIC Neon Rope Light', '复用模板', '新增配置'),
|
||||
('RGBIC Wire Neon Rope Light', '复用模板', '新增配置'),
|
||||
('RGBICWW Ceiling Light', '已有脚本', '调试适配'),
|
||||
('Floor Lamp', '复用模板', '新增配置'),
|
||||
('Candle Lamp', '复用模板', '新增配置'),
|
||||
('Permanent Outdoor Lights', '需新写', '户外新品'),
|
||||
],
|
||||
'插座+开关 (6款)': [
|
||||
('Plug Mini (JP) HomeKit Enabled', '已有脚本', '调试适配'),
|
||||
('Plug Mini (US) HomeKit Enabled', '已有脚本', '调试适配'),
|
||||
('Plug Mini (EU)', '复用模板', '新增配置'),
|
||||
('Relay Switch 1', '已有脚本', '调试适配'),
|
||||
('Relay Switch 1PM', '复用模板', '新增配置'),
|
||||
('Relay Switch 2PM', '复用模板', '新增配置'),
|
||||
],
|
||||
'扫地机 (9款)': [
|
||||
('Robot Vacuum Cleaner S1', '已有脚本', '调试适配'),
|
||||
('Robot Vacuum Cleaner S1 Plus (W)', '已有脚本', '调试适配'),
|
||||
('Mini Robot Vacuum K10+Pro', '复用模板', '新增配置'),
|
||||
('Robot Vacuum K20+ Pro', '需新写', '新一代UI'),
|
||||
('Robot Vacuum K10+ Pro Combo', '复用模板', '新增配置'),
|
||||
('Robot Vacuum K11+', '需新写', '新一代UI'),
|
||||
('Robot Vacuum K11+ Pro', '需新写', '新一代UI'),
|
||||
('Floor Cleaning Robot S20', '需新写', '全新UI'),
|
||||
('Floor Cleaning Robot S20 Mini', '需新写', '全新UI'),
|
||||
],
|
||||
'传感器+温控 (7款)': [
|
||||
('Indoor/Outdoor Thermo-Hygrometer', '已有脚本', '调试适配'),
|
||||
('Meter Pro', '已有脚本', '调试适配'),
|
||||
('Meter Pro (CO2 Monitor)', '复用模板', '新增CO2功能'),
|
||||
('Water Leak Detector with Sensor Cable', '已有脚本', '调试适配'),
|
||||
('Presence Sensor', '需新写', '全新UI'),
|
||||
('Home Climate Panel', '需新写', '全新UI'),
|
||||
('Weather Station', '需新写', '全新UI'),
|
||||
],
|
||||
'风扇+空净+加湿器 (6款)': [
|
||||
('Circulator Fan', '已有脚本', '调试适配'),
|
||||
('Standing Circulator Fan', '复用模板', '新增配置'),
|
||||
('Circulator Fan Pro', '复用模板', '新增配置'),
|
||||
('Air Purifier', '需新写', '全新品类'),
|
||||
('Air Purifier Table', '需新写', '全新品类'),
|
||||
('Evaporative Humidifier (Auto-refill)', '已有脚本', '调试适配'),
|
||||
],
|
||||
'摄像头+门铃 (3款)': [
|
||||
('Pan/Tilt Cam 3MP猫狗定制款', '复用camera', '调试适配'),
|
||||
('Video Doorbell', '需新写', '门铃UI差异'),
|
||||
('OSC KVS', '已有脚本', '调试适配'),
|
||||
],
|
||||
'网关Hub (3款)': [
|
||||
('Hub 3', '已有脚本', '调试适配'),
|
||||
('AI Hub', '已完成', '—'),
|
||||
('AI Hub Show', '需新写', '带屏新品'),
|
||||
],
|
||||
'门控+安防+配件 (5款)': [
|
||||
('Keypad Vision', '已有脚本', '调试适配'),
|
||||
('Keypad Vision Pro', '复用模板', '新增配置'),
|
||||
('Garage Door Opener', '需新写', '全新品类'),
|
||||
('Safety Alarm', '需新写', '全新品类'),
|
||||
('Smart Radiator Thermostat', '需新写', '全新品类'),
|
||||
],
|
||||
'AI产品 (5款)': [
|
||||
('Art Frame', '需新写', '全新UI框架'),
|
||||
('Art Frame luma', '需新写', '全新UI框架'),
|
||||
('AI Pet', '需新写', '全新品类'),
|
||||
('AI PinNote', '需新写', '全新品类'),
|
||||
('Bot Rechargeable', '复用bot', '调试适配'),
|
||||
],
|
||||
'机器人+联名+其他 (4款)': [
|
||||
('SwitchBot KATAフレンズ KUMAMON ver.', '复用bot', '特殊UI定制'),
|
||||
('KATA Friends 国行版', '复用bot', '特殊UI定制'),
|
||||
('Outdoor PTC', '需新写', '全新品类'),
|
||||
('FindCard', '已有脚本', '调试适配'),
|
||||
],
|
||||
}
|
||||
|
||||
for cat_name, devices in device_categories.items():
|
||||
doc.add_heading(cat_name, level=2)
|
||||
add_table_with_header(doc,
|
||||
['设备名称', '脚本状态', '备注'],
|
||||
devices,
|
||||
col_widths=[6, 3, 4]
|
||||
)
|
||||
doc.add_paragraph()
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
# ======================== 3. 测试维度说明 ========================
|
||||
doc.add_heading('3. 测试维度说明', level=1)
|
||||
|
||||
doc.add_paragraph('每款设备根据功能复杂度,覆盖以下测试维度。人力预估基于 AI 辅助(Claude Code 边跑边写):首台需建模分析,同品类复用台仅改配置,工时约减半或更低。')
|
||||
|
||||
rows = [
|
||||
('card', '{device}_card.test.ts', '首页卡片展示与交互', '3-5', 'P1 / P2', '0.3 / 0.1'),
|
||||
('control', '{device}_control.test.ts', '功能页核心操作(开关/模式/参数调节)', '8-20', 'P0 / P1 / P2', '1.0 / 0.3'),
|
||||
('setting', '{device}_setting.test.ts', '设备设置页(名称/房间/固件/信息)', '5-8', 'P2', '0.5 / 0.2'),
|
||||
('connect', '{device}_connect.test.ts', '设备添加/配对流程', '3-5', 'P0 / P2', '0.5 / 0.2'),
|
||||
('scene', '{device}_scene.test.ts', '自动化/场景联动', '3-8', 'P3', '0.5 / 0.2'),
|
||||
('logs', '{device}_logs.test.ts', '操作日志查看', '2-4', 'P2(可选)', '0.2 / 0.1'),
|
||||
]
|
||||
add_table_with_header(doc,
|
||||
['维度', '文件命名', '内容', '用例数', '优先级层', 'AI预估(人日:首台/复用)'],
|
||||
rows,
|
||||
col_widths=[1.3, 3.8, 4.2, 1.2, 2, 3]
|
||||
)
|
||||
|
||||
doc.add_paragraph()
|
||||
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()
|
||||
|
||||
# ======================== 4. 测试优先级分层 ========================
|
||||
doc.add_heading('4. 测试优先级分层', level=1)
|
||||
doc.add_paragraph(
|
||||
'本计划按优先级分四层推进,依次为:必测项 → 单品探索 → 全功能 → 平台。'
|
||||
'高优先级层先达标再投入下一层;不绑定固定周次,按相对工作量排 backlog。'
|
||||
)
|
||||
|
||||
rows = [
|
||||
('P0 必测项', '各单品添加 + 核心控制(双协议)', 'ONES 必测项-AI自动化 (CQz9YCNX)', '187', '每次提测/版本必跑', '通过率≥95%'),
|
||||
('P1 单品探索', '每单品主流程冒烟(card+核心control)', '按单品模板', '78款各1遍', '每迭代', '全单品冒烟通过'),
|
||||
('P2 全功能', '全维度回归(card/control/setting/scene/logs)', '各品类用例库', '~2250', '版本回归', '通过率≥85%'),
|
||||
('P3 平台', '账号/房间/消息/家庭分享/场景等平台功能', 'App平台用例库', '平台模块', '版本回归', '平台用例通过'),
|
||||
]
|
||||
add_table_with_header(doc,
|
||||
['优先级', '范围', '来源', '用例量', '运行频率', '退出标准'],
|
||||
rows,
|
||||
col_widths=[2.2, 4, 3.5, 1.3, 2.5, 2.5]
|
||||
)
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
# ======================== 5. P0 必测项 ========================
|
||||
doc.add_heading('5. P0 必测项(最高优先)', level=1)
|
||||
doc.add_paragraph(
|
||||
'必测项是提测/版本更新的准入闸门,来源 ONES 测试计划「必测项-AI自动化」(plan CQz9YCNX / lib EPfZfC9Y),'
|
||||
'映射清单见 test-plan/must-test.manifest.ts,转换规则见 prompts/must_test_conversion.md。'
|
||||
)
|
||||
|
||||
doc.add_heading('5.1 必测项构成', level=2)
|
||||
rows = [
|
||||
('添加 (connect)', '每单品型号一条「添加X验证」', '73', 'connect 维度,需串口配合'),
|
||||
('单品功能 (feature)', '绑定手持/学习遥控器/keypad密码指纹/温湿度单位等', '9', 'control/setting 维度'),
|
||||
('控制 (control)', '2条协议超级用例逐 step:BLE 49 + WiFi 56', '105', 'control 维度,双协议各跑一遍'),
|
||||
]
|
||||
add_table_with_header(doc, ['类型', '说明', '条数', '落点/备注'], rows)
|
||||
doc.add_paragraph()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run('合计:').bold = True
|
||||
p.add_run('187 条(另 2 条 na:safety alarm 无目录、强绑定解绑归平台)。控制粒度在 step,按 (用例号#step)+协议精确回写 ONES。')
|
||||
|
||||
doc.add_heading('5.2 前置:设备添加自动化(关键路径,最先启动)', level=2)
|
||||
doc.add_paragraph(
|
||||
'必测含 73 条添加,添加流程需硬件配合(串口控制设备进入配对模式),分 BLE 直连与 Wi-Fi 配网两种模式。'
|
||||
'此为 P0 第一阻塞项,须最先启动;控制部分(无需串口)可并行先行。'
|
||||
)
|
||||
rows = [
|
||||
('串口命令协议文档', '统一所有设备“进入配对模式”的串口指令', '嵌入式+测试'),
|
||||
('串口控制模块', 'serial_controller.ts (基于 serialport库)', '测试'),
|
||||
('添加流程通用框架', 'connect_base_helper.ts', '测试'),
|
||||
('Wi-Fi 配网模块', 'wifi_connect_helper.ts', '测试'),
|
||||
('硬件环境', 'USB Hub + USB转TTL × 5-10', '测试'),
|
||||
]
|
||||
add_table_with_header(doc, ['任务', '输出', '负责'], rows)
|
||||
doc.add_paragraph()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run('框架一次性投入:').bold = True
|
||||
p.add_run('serial_controller + connect_base_helper + wifi_connect_helper ≈ 1-1.5 人日纯代码(serial 基于现成桌面 serialport 工具薄封装、add 流程复用现有 addDeviceViaBLE,仅 wifi 配网 helper 需新写)。硬件联调(USB-TTL 接线/端口/时序)由嵌入式主导,为 wall-clock 不确定项,不计入纯人日。')
|
||||
doc.add_paragraph(
|
||||
'关键:添加流程 app 侧通用,逐设备差异仅为“开发版上进入配对模式的按键/串口指令”,由嵌入式提供的「设备→串口键」配置表驱动,逐设备近乎零编码。'
|
||||
'app 侧只需按类别区分设置步骤(BLE 直连 / Wi-Fi 配网 / 带屏 / 锁含校准),约 4-5 类,而非 73 份脚本。'
|
||||
)
|
||||
|
||||
doc.add_heading('5.3 添加 backlog(通用流程 + 配置表驱动)', level=2)
|
||||
rows = [
|
||||
('首验', 'BLE直连(Bot/Meter/Sensor) + Wi-Fi配网框架打通', '端到端 demo', '0.5'),
|
||||
('BLE 直连类', '窗帘/传感器/风扇/遥控器/插座开关/灯 等', '设备→串口键配置 + 验证', '1'),
|
||||
('Wi-Fi 配网类', 'Hub/摄像头/扫地机 等(额外配网步骤)', '配网流程 + 配置', '1.5'),
|
||||
('带屏 / 锁类', 'AI Hub Show / Lock 系列(含校准)', '类别专属设置步骤', '1.5'),
|
||||
]
|
||||
add_table_with_header(doc, ['批次', '范围', '交付物', 'AI预估(人日)'], rows)
|
||||
doc.add_paragraph('添加小计:约 4-6 人日(流程通用、按 4-5 类区分;逐设备仅配置串口键,成本主要在逐台硬件验证的 wall-clock,非人日)。')
|
||||
|
||||
doc.add_heading('5.4 控制 backlog(双协议,105 step)', level=2)
|
||||
doc.add_paragraph('• 协议为运行模式:PROTO=ble(关WiFi/热点、开蓝牙)/ PROTO=wifi(开WiFi/热点、关蓝牙),各跑一遍。')
|
||||
doc.add_paragraph('• camera/robot/osc 仅 WiFi;Bot/Lock/Curtain/Meter 等两协议都覆盖。')
|
||||
doc.add_paragraph('• 控制内容含开关/开锁/百分比/meter温湿度校正/camera出流3min/robot清扫暂停回充/Humidifier2绑温湿度计。')
|
||||
doc.add_paragraph('• 结果按 [ONES:号#step][协议] 锚点 step 级回写 plan CQz9YCNX。')
|
||||
doc.add_paragraph('控制小计:约 3-4 人日(大部分品类已有 _control 用例 → P0 主要是给既有断言打 [P0][#step][协议] 锚点 + 双协议切换 harness ~1 人日 + 补少量缺口;双协议第二遍仅切前置、增量低)。')
|
||||
doc.add_paragraph()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run('P0 合计:').bold = True
|
||||
p.add_run('约 11-15 人日(串口框架~1-1.5 + 添加 4-6 + 控制 3-4 + 功能9条~3 + step回写工具~0.5)。控制大部分品类已有 _control 用例可复用,主要是打锚点 + 双协议 harness;功能 9 条为 keypad 密码指纹/学习遥控/绑定手持等较复杂项;硬件联调由嵌入式主导,单列不计入。')
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
# ======================== 6. P1 单品探索 ========================
|
||||
doc.add_heading('6. P1 单品探索(覆盖广度)', level=1)
|
||||
doc.add_paragraph(
|
||||
'必测项达标后,为全部 78 款单品覆盖一遍探索测试:card + 核心 control,并扩展到'
|
||||
'全品类核心功能的「部分全功能用例」(每品类挑代表性主功能,不做完整深度回归)。'
|
||||
)
|
||||
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('• 旧代码转换提效:部分用例由以前旧代码转换而来,已含测试路径/步骤,只需定位元素位置使脚本可执行(Midscene AI 定位较快)——这类省去写用例的工作。新品无旧码、不受益。')
|
||||
doc.add_paragraph('• 人力预估(AI 辅助):约 16-22 人日(已有/复用品类 ~52 款较快,部分还有旧码转换的现成路径;新品 ~26 款需首次建新 UI 探索支持,为主要成本)。')
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
# ======================== 7. P2 全功能 ========================
|
||||
doc.add_heading('7. P2 全功能(深度回归)', level=1)
|
||||
doc.add_paragraph(
|
||||
'对每款单品做全维度回归(card/control/setting/scene/logs)。按品类批量推进:'
|
||||
'同品类 UI 高度相似,调通首台后改设备名配置快速扩展,并沉淀通用 helper。'
|
||||
'不绑定周次,按复用度从高到低排批次。'
|
||||
)
|
||||
doc.add_paragraph(
|
||||
'脚本状态分三层成本(见第 2 节):「已有脚本」只需在真机上调试适配,最快;'
|
||||
'「复用模板」改设备名/配置次之;「需新写」(全新 UI)最贵。估算按此分层。'
|
||||
)
|
||||
doc.add_paragraph(
|
||||
'进一步提效:功能页/设置页「入口」全单品通用(沉淀一个全局导航 helper,不逐设备重写);'
|
||||
'同品类功能测试点高度雷同,首台调通后其余只调「差异功能」。'
|
||||
'另有部分用例由旧代码转换而来、已含路径,只需定位元素即可执行。故 P2 实际只在每设备增量调试少量差异点。'
|
||||
)
|
||||
|
||||
rows = [
|
||||
('批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预估(人日)'],
|
||||
rows,
|
||||
col_widths=[2.3, 5, 1.3, 5, 2.2]
|
||||
)
|
||||
|
||||
doc.add_paragraph()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run('覆盖目标:').bold = True
|
||||
p.add_run('全部 78 款单品全维度回归,~2250 条用例,单品功能覆盖率 100%。')
|
||||
p = doc.add_paragraph()
|
||||
p.add_run('P2 合计:').bold = True
|
||||
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')
|
||||
doc.add_paragraph('• 锁: 密码管理、开锁方式切换、自动锁定、电量监控 → lock_helper')
|
||||
doc.add_paragraph('• 插座开关: 开关、定时器、功率/电量统计、过载保护 → relay_helper')
|
||||
doc.add_paragraph('• 灯光: RGB颜色、亮度、模式切换、渐变 → light_helper')
|
||||
|
||||
doc.add_heading('7.2 中复杂品类要点', level=2)
|
||||
doc.add_paragraph('• 扫地机: 地图管理/禁区/多楼层、清扫模式、自清洁基站(地图画布操作为难点)')
|
||||
doc.add_paragraph('• 传感器+温控: 温湿度展示、历史图表、告警、灵敏度(图表验证需截图/文本提取)')
|
||||
doc.add_paragraph('• 风扇+空净+加湿: 风速/模式/摇头/定时、空气质量/滤网、湿度目标/水箱')
|
||||
doc.add_paragraph('• 摄像头+门铃: 出流/录像/事件、门铃呼叫/访客/对讲、OSC 复用')
|
||||
|
||||
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('• 联名+其他: KATA Friends(Bot复用+定制皮肤)、FindCard(已有脚本调试适配)、Outdoor PTC(需新写)')
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
# ======================== 8. P3 平台 ========================
|
||||
doc.add_heading('8. P3 平台功能', level=1)
|
||||
doc.add_paragraph(
|
||||
'单品三层达标后,覆盖跨单品的平台级功能(必测项中约 12 条平台项 + 平台用例库)。'
|
||||
)
|
||||
|
||||
doc.add_heading('8.1 平台功能范围', level=2)
|
||||
rows = [
|
||||
('账号', '注册/登录/登出/忘记密码/第三方登录/欧区账号', '账号2.0'),
|
||||
('家庭与房间', '创建房间、家庭分享3.0、家庭分组', '分享/分组用例库'),
|
||||
('消息与商城', '消息中心3.0、商城、我的、小组件', '对应平台库'),
|
||||
('其他', '覆盖安装、插件热更、第三方服务接入', 'App必测项平台模块'),
|
||||
]
|
||||
add_table_with_header(doc, ['模块', '内容', '来源'], rows)
|
||||
|
||||
doc.add_heading('8.2 自动化/场景联动', level=2)
|
||||
doc.add_paragraph('验证设备间联动可靠性(复用已有 automation 脚本基础)。场景联动里的设备功能入口/控制 P0-P2 已调通,联动部分调试较快;账号/房间/消息/分享为纯平台功能,不复用设备脚本。')
|
||||
doc.add_paragraph('• 创建自动化:手动/条件触发/定时触发')
|
||||
doc.add_paragraph('• 执行验证:触发条件满足后动作执行')
|
||||
doc.add_paragraph('• 编辑/删除:修改条件或动作、删除自动化')
|
||||
doc.add_paragraph('• 多设备联动:A设备触发 → B设备执行')
|
||||
doc.add_paragraph('• 异常处理:设备离线时的自动化表现')
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
# ======================== 8. 资源需求 ========================
|
||||
doc.add_heading('9. 资源需求汇总', level=1)
|
||||
|
||||
doc.add_heading('9.1 人力与分工', level=2)
|
||||
rows = [
|
||||
('自动化测试工程师', '1人', '全职、全程,承担全部脚本编写与调试'),
|
||||
('嵌入式工程师(配合)', '按需', '提供串口协议与配对框架支持(P0 添加阻塞项),最优先'),
|
||||
('AI辅助 (Claude Code)', '—', '边跑边写生成+调试脚本,贯穿全程提效'),
|
||||
]
|
||||
add_table_with_header(doc, ['角色', '人数', '说明'], rows)
|
||||
|
||||
doc.add_paragraph()
|
||||
doc.add_paragraph(
|
||||
'人力安排:仅 1 名自动化工程师,四层按优先级串行推进,AI 辅助提效。'
|
||||
'P0 内部拆两条轨——「控制轨」不依赖串口可先行,「添加轨」待嵌入式串口框架就绪后插入,两轨由同一人分时推进。'
|
||||
)
|
||||
rows = [
|
||||
('P0 · 控制轨', '自动化(1人)', '—', '先行:双协议控制 105 step,不待串口'),
|
||||
('P0 · 添加轨', '自动化(1人)', '嵌入式(串口协议+框架)', '串口就绪后插入:73 添加 + 9 功能'),
|
||||
('P1 单品探索', '自动化(1人)', '—', 'P0 达标后:78 款冒烟'),
|
||||
('P2 全功能', '自动化(1人)', '—', '按品类批量,复用 helper'),
|
||||
('P3 平台', '自动化(1人)', '—', '最后:平台功能 + 场景联动'),
|
||||
]
|
||||
add_table_with_header(doc,
|
||||
['优先级层', '主负责', '配合', '安排说明'],
|
||||
rows,
|
||||
col_widths=[2.5, 2.5, 3.5, 6]
|
||||
)
|
||||
|
||||
doc.add_heading('9.2 硬件设备', level=2)
|
||||
doc.add_paragraph('每品类至少需要1台实体设备用于调试和验证:')
|
||||
rows = [
|
||||
('P0 串口硬件', 'USB Hub + USB转TTL×5-10、开发版设备', '最先到位(添加为关键路径)'),
|
||||
('P0/P1 必测+冒烟', '必测涉及的各品类代表设备各1台', '随 P0 启动'),
|
||||
('P2 高复用', '窗帘×1, 锁×2, 插座×1, 开关×1, 灯×3', '全功能批1前到位'),
|
||||
('P2 中复杂', '扫地机×2, 传感器×3, 风扇×1, 空净×1, 加湿器×1, 门铃×1', '全功能批2前到位'),
|
||||
('P2 新品特殊', 'Hub 3, AI Hub Show, Art Frame, AI Pet等', '全功能批3前到位'),
|
||||
]
|
||||
add_table_with_header(doc, ['用途', '设备需求', '到位时间'], rows)
|
||||
|
||||
doc.add_heading('9.3 环境', level=2)
|
||||
rows = [
|
||||
('测试机', '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()
|
||||
|
||||
# ======================== 10. 优先级 backlog 一览 ========================
|
||||
doc.add_heading('10. 优先级 backlog 一览', level=1)
|
||||
|
||||
rows = [
|
||||
('P0 必测项', '添加73 + 功能9 + 控制105step(双协议)', '187条', '~11-15', 'serial框架 + connect + 控制断言 + step回写', '通过率≥95%'),
|
||||
('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预估(人日,单平台)', '交付物', '退出标准'],
|
||||
rows,
|
||||
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('约 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 提效主要在此;'
|
||||
'其余约 2/3 是真机调试(元素核对、加载等待、flaky 复跑)、设备/硬件配置与新品 UI 分析,受真机往返与 UI 稳定性约束,AI 难以进一步压缩。'
|
||||
'项目使用 Midscene(AI 元素定位)已减少传统选择器维护成本,故未再按“零调试”乐观值估算。'
|
||||
'若需再压缩周期,杠杆在于:多台真机并行、收窄 P2 范围(只保高频设备)、放宽 flaky 容忍度,而非提高编写速度。'
|
||||
)
|
||||
|
||||
doc.add_paragraph()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run('说明:').bold = True
|
||||
p.add_run('按优先级依次交付,高层达标后投入下一层;不绑定固定周次。P0 控制部分可不待串口先行,添加随串口框架就绪推进。')
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
# ======================== 10. 质量标准 ========================
|
||||
doc.add_heading('11. 质量标准与验收', level=1)
|
||||
|
||||
doc.add_heading('11.1 单设备验收标准', level=2)
|
||||
doc.add_paragraph('• 核心用例通过率 ≥ 85%(首轮调试)')
|
||||
doc.add_paragraph('• 单次全量执行时间 < 10分钟/设备')
|
||||
doc.add_paragraph('• 用例覆盖:card + control + setting 三个维度必须覆盖')
|
||||
doc.add_paragraph('• 脚本可在 Android/iOS 双平台运行(通过平台适配层)')
|
||||
|
||||
doc.add_heading('11.2 优先级层验收标准', level=2)
|
||||
rows = [
|
||||
('P0 必测项', '187条全部实现并跑通(添加+控制双协议)', '通过率≥95%,作为提测门禁'),
|
||||
('P1 单品探索', '78款单品冒烟全部通过', '广度100%覆盖'),
|
||||
('P2 全功能', '全维度回归,通过率≥85%', '单品功能覆盖率100%'),
|
||||
('P3 平台', '平台功能 + 场景联动通过', '全流程覆盖'),
|
||||
]
|
||||
add_table_with_header(doc, ['优先级', '验收标准', '覆盖目标'], rows)
|
||||
|
||||
doc.add_heading('11.3 回归策略', level=2)
|
||||
doc.add_paragraph('• P0 必测项:每次提测/版本更新必跑(双协议),作为准入门禁')
|
||||
doc.add_paragraph('• P1 单品探索:每迭代执行一遍全单品冒烟')
|
||||
doc.add_paragraph('• P2 全功能:版本回归执行累计设备全量')
|
||||
doc.add_paragraph('• P3 平台:版本回归执行平台功能与场景联动')
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
# ======================== 11. 风险与依赖 ========================
|
||||
doc.add_heading('12. 风险与依赖', level=1)
|
||||
|
||||
doc.add_heading('12.1 关键依赖', level=2)
|
||||
rows = [
|
||||
('串口/添加为关键路径(前移)', '高', '硬件或协议不就绪将阻塞 P0 整体', '控制部分(无需串口)先行,串口框架并行启动'),
|
||||
('实体设备到位', '高', '无设备无法调试', 'P0 涉及的代表设备最先准备'),
|
||||
('嵌入式串口协议', '高', '直接影响 P0 添加', '最优先启动协议定义'),
|
||||
('同品类UI一致性', '中', 'UI差异大则无法复用', '差异设备单独排期'),
|
||||
('新品UI稳定', '高', 'UI仍在迭代会导致返工', '等UI冻结后再开始(主要影响P2新品)'),
|
||||
('App版本稳定', '中', '频繁改版影响脚本维护', '锁定测试版本'),
|
||||
]
|
||||
add_table_with_header(doc, ['依赖项', '风险等级', '影响', '缓解措施'], rows)
|
||||
|
||||
doc.add_heading('12.2 待讨论问题', level=2)
|
||||
doc.add_paragraph('1. 设备添加的串口开发版/协议时间线?(决定 P0 添加何时能启动)')
|
||||
doc.add_paragraph('2. 必测项控制双协议环境切换(开关WiFi/蓝牙)能否用 adb/串口自动化,还是需人工配合?')
|
||||
doc.add_paragraph('3. 已下市产品(Hub Plus、老灯带、加湿器1等)是否纳入 P2 全功能?')
|
||||
doc.add_paragraph('4. 同一设备的不同区域版本(JP/US/EU)UI差异程度?是否需逐个调试?')
|
||||
doc.add_paragraph('5. CI/CD 持续集成环境是否需要在 P0 之前搭建?')
|
||||
|
||||
# ======================== 保存 ========================
|
||||
output_path = '/Users/woan/Desktop/AI_UIAutomation/docs/UI自动化测试计划.docx'
|
||||
doc.save(output_path)
|
||||
print(f'✓ 文档已生成: {output_path}')
|
||||
|
|
@ -47,6 +47,12 @@ export class AndroidDriver implements DeviceDriver {
|
|||
};
|
||||
const result = await this.request('POST', '/session', { capabilities });
|
||||
this.sessionId = result.sessionId;
|
||||
// 首页有实时读数(温湿度)持续刷新,UI 永不 idle → 默认 waitForIdleTimeout(10s)会让
|
||||
// 每次 getSource/查找都白等满。但设为 0 又会在添加流程的连续跳转页抓到"未渲染完"的页面
|
||||
// (品类网格没加载完就误判设备缺失→狂滑)。折中 1s:既不死等 10s,也给跳转页一点渲染时间。
|
||||
await this.request('POST', `/session/${this.sessionId}/appium/settings`, {
|
||||
settings: { waitForIdleTimeout: 1000 },
|
||||
}).catch(() => { /* 老版本 driver 不支持则忽略 */ });
|
||||
await this.request('POST', `/session/${this.sessionId}/appium/device/activate_app`, { appId: APP_CONFIG.android.appPackage });
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
}
|
||||
|
|
@ -198,30 +204,39 @@ export class AndroidDriver implements DeviceDriver {
|
|||
|
||||
async swipe(fromX: number, fromY: number, toX: number, toY: number, duration = 0.5): Promise<void> {
|
||||
const durationMs = Math.round(duration * 1000);
|
||||
await this.request('POST', `/session/${this.sessionId}/actions`, {
|
||||
const body = {
|
||||
actions: [{
|
||||
type: 'pointer',
|
||||
id: 'finger1',
|
||||
parameters: { pointerType: 'touch' },
|
||||
actions: [
|
||||
{ type: 'pointerMove', duration: 0, x: Math.round(fromX), y: Math.round(fromY) },
|
||||
{ type: 'pointerMove', duration: 0, origin: 'viewport', x: Math.round(fromX), y: Math.round(fromY) },
|
||||
{ type: 'pointerDown', button: 0 },
|
||||
{ type: 'pointerMove', duration: durationMs, x: Math.round(toX), y: Math.round(toY) },
|
||||
{ type: 'pointerMove', duration: durationMs, origin: 'viewport', x: Math.round(toX), y: Math.round(toY) },
|
||||
{ type: 'pointerUp', button: 0 },
|
||||
],
|
||||
}],
|
||||
});
|
||||
};
|
||||
// UIA2 偶发 "Unable to perform W3C actions" 瞬时抖动 → 清理手势链后重试一次
|
||||
try {
|
||||
await this.request('POST', `/session/${this.sessionId}/actions`, body);
|
||||
} catch (e: any) {
|
||||
if (!/W3C actions/i.test(e.message || '')) throw e;
|
||||
await this.request('DELETE', `/session/${this.sessionId}/actions`).catch(() => {});
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
await this.request('POST', `/session/${this.sessionId}/actions`, body);
|
||||
}
|
||||
}
|
||||
|
||||
async scrollDown(distance = 300): Promise<void> {
|
||||
const size = await this.getWindowSize();
|
||||
const size = await this.getWindowSizeCached();
|
||||
const startX = size.width / 2;
|
||||
const startY = size.height / 2;
|
||||
await this.swipe(startX, startY, startX, startY - distance, 0.5);
|
||||
}
|
||||
|
||||
async scrollUp(distance = 300): Promise<void> {
|
||||
const size = await this.getWindowSize();
|
||||
const size = await this.getWindowSizeCached();
|
||||
const startX = size.width / 2;
|
||||
const startY = size.height / 2;
|
||||
await this.swipe(startX, startY, startX, startY + distance, 0.5);
|
||||
|
|
@ -240,6 +255,13 @@ export class AndroidDriver implements DeviceDriver {
|
|||
return { width: result.width, height: result.height };
|
||||
}
|
||||
|
||||
private _winSize: { width: number; height: number } | null = null;
|
||||
/** 窗口尺寸缓存:滚动等高频调用不必每次往返查询(屏幕尺寸一个会话内不变)。 */
|
||||
async getWindowSizeCached(): Promise<{ width: number; height: number }> {
|
||||
if (!this._winSize) this._winSize = await this.getWindowSize();
|
||||
return this._winSize;
|
||||
}
|
||||
|
||||
async screenshot(): Promise<string> {
|
||||
return await this.request('GET', `/session/${this.sessionId}/screenshot`);
|
||||
}
|
||||
|
|
@ -301,22 +323,36 @@ export class AndroidDriver implements DeviceDriver {
|
|||
}
|
||||
|
||||
async dismissPopupIfPresent(): Promise<boolean> {
|
||||
const source = await this.getSource();
|
||||
if (source.includes('android.app.Dialog') || source.includes('permission') ||
|
||||
source.includes('Upgrade') || source.includes("What's New") ||
|
||||
source.includes('AlertDialog') || source.includes('Please Note') ||
|
||||
source.includes('Restart') || source.includes('Got it')) {
|
||||
const dismissTexts = ['OK', 'Got it', 'Close', 'Cancel', 'Confirm', 'Allow',
|
||||
'Later', 'Skip', 'Done', 'ALLOW', 'DENY', 'Not Now'];
|
||||
// 关闭按钮(只点"否定/关闭"类,避免误点"更新/同意"推进);中英都覆盖
|
||||
const dismissTexts = [
|
||||
'Cancel', 'Close', 'Later', 'Maybe Later', 'Not Now', 'No Thanks', 'No, Thanks', 'Dismiss', 'Skip', 'Ignore',
|
||||
'Got it', 'OK', 'I know', 'I Know', 'Done', 'Allow', 'ALLOW', 'DENY', 'While using the app',
|
||||
'取消', '关闭', '稍后', '以后再说', '暂不', '暂不更新', '忽略', '跳过', '我知道了', '知道了', '好的', '允许', '不用了',
|
||||
];
|
||||
// 触发:对话框/常见弹框关键字(扩充:更新/评分/新功能/通知/试用/优惠/连接失败 等)
|
||||
const triggers = [
|
||||
'android.app.Dialog', 'AlertDialog', 'permission', 'Upgrade', 'Update', "What's New", 'New Feature',
|
||||
'Please Note', 'Restart', 'Got it', 'Rate', 'Review', 'Reminder', 'Notification', 'Subscribe', 'Trial', 'Welcome', 'Tips',
|
||||
'Failed to connect', 'connect to your device', 'Connection failed', 'Try Again',
|
||||
'更新', '升级', '新功能', '评分', '评价', '提示', '通知', '订阅', '试用', '体验', '优惠', '连接失败', '请注意', '温馨提示',
|
||||
];
|
||||
let dismissed = false;
|
||||
// 循环清掉叠加的多个弹框(最多 4 个)
|
||||
for (let round = 0; round < 4; round++) {
|
||||
const source = await this.getSource();
|
||||
if (!triggers.some(t => source.includes(t))) break;
|
||||
let hit = false;
|
||||
for (const text of dismissTexts) {
|
||||
const el = await this.findElementRaw('-android uiautomator', `new UiSelector().text("${text}")`);
|
||||
if (el) {
|
||||
await this.tapElement(el);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
return true;
|
||||
dismissed = true; hit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hit) break; // 有触发词但没找到可点按钮,停止避免空转
|
||||
}
|
||||
return false;
|
||||
return dismissed;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,11 @@ export class WDADriver implements DeviceDriver {
|
|||
return this.wda.typeText(elementId, text);
|
||||
}
|
||||
|
||||
/** 向聚焦输入框发送键盘按键(iOS 自绘输入框用)。 */
|
||||
async sendKeys(keys: string[]): Promise<void> {
|
||||
return this.wda.sendKeys(keys);
|
||||
}
|
||||
|
||||
async clearText(elementId: string): Promise<void> {
|
||||
return this.wda.clearText(elementId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ function checkAppium(): Promise<boolean> {
|
|||
}
|
||||
|
||||
export async function setup() {
|
||||
TestReporter.clearSharedResults();
|
||||
if (!process.env.SOAK_MODE) TestReporter.clearSharedResults(); // soak 模式跨多次 run 累积,不每次清空
|
||||
console.log(`[global-setup] Platform: ${platform}`);
|
||||
if (platform === 'android') {
|
||||
const ready = await checkAppium();
|
||||
|
|
@ -82,6 +82,7 @@ export async function setup() {
|
|||
}
|
||||
|
||||
export function teardown() {
|
||||
if (process.env.SOAK_MODE) return; // soak 模式:由 soak 脚本在全程结束后统一出一份报告
|
||||
const p = process.env.PLATFORM || 'ios';
|
||||
TestReporter.generateCombinedReport(`AIHub_${p.toUpperCase()}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,16 @@
|
|||
"@midscene/ios": "^1.8.1",
|
||||
"@types/node": "^25.8.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"jsqr": "^1.4.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"serialport": "^13.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@midscene/web": "^1.8.1",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"qrcode": "^1.5.4",
|
||||
"vitest": "^4.1.6"
|
||||
}
|
||||
},
|
||||
|
|
@ -1730,6 +1735,211 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@serialport/binding-mock": {
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz",
|
||||
"integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/bindings-interface": "^1.2.1",
|
||||
"debug": "^4.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-13.0.0.tgz",
|
||||
"integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/bindings-interface": "1.2.2",
|
||||
"@serialport/parser-readline": "12.0.0",
|
||||
"debug": "4.4.0",
|
||||
"node-addon-api": "8.3.0",
|
||||
"node-gyp-build": "4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz",
|
||||
"integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz",
|
||||
"integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/parser-delimiter": "12.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-interface": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz",
|
||||
"integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.22 || ^14.13 || >=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-byte-length": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz",
|
||||
"integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-cctalk": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz",
|
||||
"integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-delimiter": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz",
|
||||
"integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-inter-byte-timeout": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz",
|
||||
"integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-packet-length": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz",
|
||||
"integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-readline": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-13.0.0.tgz",
|
||||
"integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/parser-delimiter": "13.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-ready": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-13.0.0.tgz",
|
||||
"integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-regex": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-13.0.0.tgz",
|
||||
"integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-slip-encoder": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz",
|
||||
"integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-spacepacket": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz",
|
||||
"integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/stream": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-13.0.0.tgz",
|
||||
"integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/bindings-interface": "1.2.2",
|
||||
"debug": "4.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@silvia-odwyer/photon": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@silvia-odwyer/photon/-/photon-0.3.3.tgz",
|
||||
|
|
@ -1842,6 +2052,16 @@
|
|||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pngjs": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
|
||||
"integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
|
|
@ -2392,6 +2612,16 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
|
|
@ -2621,6 +2851,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/default-browser": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
|
||||
|
|
@ -2720,6 +2960,13 @@
|
|||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.4.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||
|
|
@ -3286,6 +3533,20 @@
|
|||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
|
|
@ -3746,6 +4007,12 @@
|
|||
"jsonrepair": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsqr": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
|
||||
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
|
|
@ -4019,6 +4286,19 @@
|
|||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
|
|
@ -4192,6 +4472,15 @@
|
|||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz",
|
||||
"integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
|
|
@ -4230,6 +4519,17 @@
|
|||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
|
@ -4332,6 +4632,45 @@
|
|||
"opener": "bin/opener-bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pac-proxy-agent": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
|
||||
|
|
@ -4375,6 +4714,16 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
|
|
@ -4467,6 +4816,15 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/portfinder": {
|
||||
"version": "1.0.38",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz",
|
||||
|
|
@ -4599,6 +4957,90 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/qrcode/node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
|
|
@ -4648,6 +5090,13 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
|
|
@ -4814,6 +5263,34 @@
|
|||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serialport": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz",
|
||||
"integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/binding-mock": "10.2.2",
|
||||
"@serialport/bindings-cpp": "13.0.0",
|
||||
"@serialport/parser-byte-length": "13.0.0",
|
||||
"@serialport/parser-cctalk": "13.0.0",
|
||||
"@serialport/parser-delimiter": "13.0.0",
|
||||
"@serialport/parser-inter-byte-timeout": "13.0.0",
|
||||
"@serialport/parser-packet-length": "13.0.0",
|
||||
"@serialport/parser-readline": "13.0.0",
|
||||
"@serialport/parser-ready": "13.0.0",
|
||||
"@serialport/parser-regex": "13.0.0",
|
||||
"@serialport/parser-slip-encoder": "13.0.0",
|
||||
"@serialport/parser-spacepacket": "13.0.0",
|
||||
"@serialport/stream": "13.0.0",
|
||||
"debug": "4.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||
|
|
@ -4829,6 +5306,13 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
|
|
@ -5792,6 +6276,13 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
|
|
|
|||
|
|
@ -29,11 +29,16 @@
|
|||
"@midscene/ios": "^1.8.1",
|
||||
"@types/node": "^25.8.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"jsqr": "^1.4.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"serialport": "^13.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@midscene/web": "^1.8.1",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"qrcode": "^1.5.4",
|
||||
"vitest": "^4.1.6"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,24 @@ reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, ...);
|
|||
|
||||
---
|
||||
|
||||
## 8.1 特殊用例:插件热更 / 获取RN插件(平台 · Zendesk,工单号为变量)
|
||||
|
||||
ONES **`48734 插件热更`**(库 `EPfZfC9Y` / 模块 其他功能 `Eeme1qVk`)。验证 app 上报的各 RN 插件包版本(Bundle_Version)。数据**不在** app 内,而在 **Zendesk 工单的评论**里——app 提交 feedback 后,工单评论会自动带一段环境信息(`App Version` / `JS_Version` / `Bundle_Version`)。
|
||||
|
||||
- 实现:`utils/common/zendesk.helper.ts`,测试 `tests/automation/rn_plugin.test.ts`(纯 API,无 driver,锚点 `[P0][ONES:48734]`)。
|
||||
- **工单号是变量,绝不写死**。三级解析(`resolveTicketId`):入参 > `env ZENDESK_TICKET_ID` > 上游用例写的共享文件 `reports/.zendesk-ticket.json`。
|
||||
- **上游传值链(确定方案)**:feedback 提交**成功页带工单号** → `captureFeedbackTicketId(driver)`(在 `feedback.helper.ts`)用 `extractTicketId()` 从页面源码抓号、`setSharedTicketId()` 落共享文件 → 插件热更用例 `getTicketRnPlugins()` 自动取用。这就是"app 提交 feedback 后保留工单号传给插件热更"。
|
||||
- 解析:`parseBundleVersion()` 把首尾相连的 `模块:版本` 串(如 `Home:default 1.0.49.0Scene:...`)拆成 `{模块: 版本}`;`parseDeviceInfo()` 同时抽 App/JS 版本;`extractTicketId()` 兼容「工单号/Ticket/Feedback ID/#数字/兜底长数字」多种页面文案。
|
||||
- 连接配置在 `.env`(gitignore):`ZENDESK_DOMAIN` / `ZENDESK_EMAIL` / `ZENDESK_TOKEN`(Basic 认证 `email/token:TOKEN`)。
|
||||
- 无工单号或未配 Zendesk → 用例 **SKIP**(无人值守不阻塞,不静默)。
|
||||
|
||||
```bash
|
||||
# 手动验证(工单号 env 传入,不写死)
|
||||
ZENDESK_TICKET_ID=<工单号> npx vitest run tests/automation/rn_plugin.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. step 级结果回写 ONES(正确方法:GraphQL mutation)
|
||||
|
||||
> ⚠️ **必须走 `ones graphql` mutation,不要用 curl 直连 REST `.../cases/update`。**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
import { createDriver } from '../drivers/factory';
|
||||
import { navigateToAddPage } from '../utils/common/navigation.helper';
|
||||
import { sleep } from '../utils/common/element.helper';
|
||||
import { enterPairingMode } from '../utils/common/relay.helper';
|
||||
|
||||
// Meter add:选品类 → 过 Disclaimers → Add Meter 页按继电器 → 向下滑动找设备入口
|
||||
async function main() {
|
||||
const device = process.argv[2] || 'meter';
|
||||
const category = process.argv[3] || 'Meter';
|
||||
const keyword = process.argv[4] || 'Meter 3M';
|
||||
const driver = createDriver();
|
||||
await driver.createSession();
|
||||
try {
|
||||
await navigateToAddPage(driver, 'Device');
|
||||
await sleep(2500);
|
||||
const dump = async (label: string) => {
|
||||
const src = await driver.getSource();
|
||||
const t = Array.from(src.matchAll(/text="([^"]{1,50})"/g)).map((m) => m[1]).filter(Boolean);
|
||||
const d = Array.from(src.matchAll(/content-desc="([^"]{1,50})"/g)).map((m) => m[1]).filter(Boolean);
|
||||
console.log(`\n=== ${label} ===\n text:`, Array.from(new Set(t)).join(' | '), '\n desc:', Array.from(new Set(d)).join(' | '));
|
||||
return src;
|
||||
};
|
||||
|
||||
const find = () => driver.findElementRaw('-android uiautomator', `new UiSelector().text("${category}")`);
|
||||
let el = await find();
|
||||
for (let i = 0; i < 6 && !el; i++) { await sleep(800); el = await find(); }
|
||||
for (let i = 0; i < 8 && !el; i++) { await driver.scrollDown(600); await sleep(700); el = await find(); }
|
||||
console.log(`找到品类 ${category}:`, !!el);
|
||||
if (!el) return;
|
||||
await driver.tapElement(el);
|
||||
await sleep(3000);
|
||||
|
||||
// 过一次 Disclaimers 的 Next
|
||||
let src = await dump('选品类后');
|
||||
if (src.includes('Disclaimer')) {
|
||||
const next = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Next")');
|
||||
if (next) { await driver.tapElement(next); await sleep(2500); }
|
||||
}
|
||||
await dump('Add 页(滑动前)');
|
||||
|
||||
// 按继电器配对,设备开始广播
|
||||
console.log(`\n>>> 按 ${device} 配对...`);
|
||||
await enterPairingMode(device).catch((e) => console.log('relay 失败:', e.message));
|
||||
await sleep(3000);
|
||||
|
||||
// 向下滑动查找设备入口
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
src = await dump(i === 0 ? '配对后(顶部)' : `下滑#${i}`);
|
||||
if (src.includes(keyword)) { console.log(`\n>>> 找到设备 "${keyword}"`); break; }
|
||||
await driver.scrollDown(500);
|
||||
await sleep(1500);
|
||||
}
|
||||
} finally {
|
||||
await driver.destroySession();
|
||||
}
|
||||
}
|
||||
main().catch((e) => { console.error('ERR:', e.message); process.exit(1); });
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
import { createDriver } from '../drivers/factory';
|
||||
import { navigateToAddPage } from '../utils/common/navigation.helper';
|
||||
import { sleep } from '../utils/common/element.helper';
|
||||
|
||||
// 导航到 Add <Category> 页并逐页截图(过 Disclaimers),便于人工确认实际 UI
|
||||
async function main() {
|
||||
const category = process.argv[2] || 'Meter';
|
||||
const steps = parseInt(process.argv[3] || '3', 10); // 截多少页(每页点一次 Next)
|
||||
const driver = createDriver();
|
||||
await driver.createSession();
|
||||
const outDir = path.resolve(__dirname, '../reports');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
try {
|
||||
await navigateToAddPage(driver, 'Device');
|
||||
await sleep(2500);
|
||||
const find = () => driver.findElementRaw('-android uiautomator', `new UiSelector().text("${category}")`);
|
||||
let el = await find();
|
||||
for (let i = 0; i < 6 && !el; i++) { await sleep(800); el = await find(); }
|
||||
for (let i = 0; i < 8 && !el; i++) { await driver.scrollDown(600); await sleep(700); el = await find(); }
|
||||
if (!el) { console.log('未找到品类', category); return; }
|
||||
await driver.tapElement(el);
|
||||
await sleep(3000);
|
||||
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const b64 = await driver.screenshot();
|
||||
const f = path.join(outDir, `add_${category}_step${i}.png`);
|
||||
fs.writeFileSync(f, Buffer.from(b64, 'base64'));
|
||||
const src = await driver.getSource();
|
||||
const t = Array.from(src.matchAll(/text="([^"]{1,50})"/g)).map((m) => m[1]).filter(Boolean);
|
||||
console.log(`step${i} 截图 ${f} | text: ${Array.from(new Set(t)).join(' | ')}`);
|
||||
const next = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Next")');
|
||||
if (!next) { console.log(' 无 Next,停止'); break; }
|
||||
await driver.tapElement(next);
|
||||
await sleep(2500);
|
||||
}
|
||||
} finally {
|
||||
await driver.destroySession();
|
||||
}
|
||||
}
|
||||
main().catch((e) => { console.error('ERR:', e.message); process.exit(1); });
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
import { createDriver } from '../drivers/factory';
|
||||
import { navigateToAddPage } from '../utils/common/navigation.helper';
|
||||
import { selectDeviceCategory } from '../utils/common/device-add.helper';
|
||||
import { sleep } from '../utils/common/element.helper';
|
||||
|
||||
// 进 Bot 添加扫描页,dump 扫描列表里多个 Bot 的真实条目(看是否带唯一后缀/MAC)
|
||||
async function main() {
|
||||
const category = process.argv[2] || 'Bot';
|
||||
const driver = createDriver();
|
||||
await driver.createSession();
|
||||
try {
|
||||
await navigateToAddPage(driver, 'Device');
|
||||
await sleep(2500);
|
||||
const ok = await selectDeviceCategory(driver, category);
|
||||
console.log(`选品类 ${category}:`, ok);
|
||||
// 扫描页多次采样,dump 所有含 Bot 的条目(text + content-desc)
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
await sleep(2500);
|
||||
const src = await driver.getSource();
|
||||
const t = Array.from(src.matchAll(/text="([^"]{1,40})"/g)).map((m) => m[1]).filter(Boolean);
|
||||
const d = Array.from(src.matchAll(/content-desc="([^"]{1,40})"/g)).map((m) => m[1]).filter(Boolean);
|
||||
const all = Array.from(new Set([...t, ...d]));
|
||||
const bots = all.filter((x) => /bot|[0-9A-Fa-f]{2}$|:|MAC/i.test(x));
|
||||
console.log(`\n采样#${i} 全部:`, all.join(' | '));
|
||||
console.log(`采样#${i} 疑似设备/ID:`, bots.join(' | '));
|
||||
}
|
||||
} finally {
|
||||
await driver.destroySession();
|
||||
}
|
||||
}
|
||||
main().catch((e) => { console.error('ERR:', e.message); process.exit(1); });
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
import { createDriver } from '../drivers/factory';
|
||||
import { navigateToAddPage } from '../utils/common/navigation.helper';
|
||||
import { selectDeviceCategory } from '../utils/common/device-add.helper';
|
||||
import { enterPairingMode } from '../utils/common/relay.helper';
|
||||
import { sleep } from '../utils/common/element.helper';
|
||||
|
||||
// 走 curtain 添加,过引导按继电器配对,然后逐页 dump,定位"行程校验"页及其交互
|
||||
async function main() {
|
||||
const category = process.argv[2] || 'Curtain';
|
||||
const relayDevice = process.argv[3] || 'curtain';
|
||||
const driver = createDriver();
|
||||
await driver.createSession();
|
||||
try {
|
||||
const dump = async (label: string) => {
|
||||
const src = await driver.getSource();
|
||||
const t = Array.from(src.matchAll(/text="([^"]{1,50})"/g)).map((m) => m[1]).filter(Boolean);
|
||||
const d = Array.from(src.matchAll(/content-desc="([^"]{1,50})"/g)).map((m) => m[1]).filter(Boolean);
|
||||
console.log(`\n=== ${label} ===\n text:`, Array.from(new Set(t)).join(' | '), '\n desc:', Array.from(new Set(d)).join(' | '));
|
||||
return src;
|
||||
};
|
||||
|
||||
await navigateToAddPage(driver, 'Device');
|
||||
await sleep(2500);
|
||||
const ok = await selectDeviceCategory(driver, category);
|
||||
console.log(`选品类 ${category}:`, ok);
|
||||
if (!ok) return;
|
||||
|
||||
// 过 Disclaimers
|
||||
let src = await dump('选品类后');
|
||||
if (src.includes('Disclaimer')) {
|
||||
const next = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Next")');
|
||||
if (next) { await driver.tapElement(next); await sleep(2500); }
|
||||
}
|
||||
await dump('引导页(按键前)');
|
||||
|
||||
// 继电器按开关键 2s 进配对
|
||||
console.log(`\n>>> 继电器配对 ${relayDevice}...`);
|
||||
await enterPairingMode(relayDevice).catch((e) => console.log('relay 失败:', e.message));
|
||||
await sleep(2000);
|
||||
// 点 Next 连接
|
||||
const n = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Next")');
|
||||
if (n) { await driver.tapElement(n); console.log('点了 Next 连接'); }
|
||||
|
||||
// 逐页观察(含连接、选模式、行程校验、命名…),关键字命中即重点标注
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
await sleep(3000);
|
||||
src = await dump(`连接后 第${i}页`);
|
||||
if (/Calibrat|行程|Stroke|stroke|校准|校正|Set.*Position|Travel|Adjust/i.test(src)) {
|
||||
console.log(`\n>>> 第${i}页 疑似【行程校验】,停下详查`);
|
||||
break;
|
||||
}
|
||||
// 选模式页:选"单边开合"再 Confirm
|
||||
if (src.includes('Select Mode') || src.includes('Open from one side')) {
|
||||
const one = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Open from one side")')
|
||||
|| await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Open from one side")');
|
||||
if (one) { await driver.tapElement(one); await sleep(1000); console.log(' → 选 Open from one side'); }
|
||||
const conf = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||||
if (conf) { await driver.tapElement(conf); console.log(' → Confirm'); }
|
||||
continue;
|
||||
}
|
||||
const nx = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Next")')
|
||||
|| await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")')
|
||||
|| await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Start")')
|
||||
|| await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Continue")')
|
||||
|| await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")');
|
||||
if (nx) { await driver.tapElement(nx); console.log(' → 点击推进'); }
|
||||
else console.log(' (无按钮,等待…可能在连接中)');
|
||||
}
|
||||
} finally {
|
||||
await driver.destroySession();
|
||||
}
|
||||
}
|
||||
main().catch((e) => { console.error('ERR:', e.message); process.exit(1); });
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
import { createDriver } from '../drivers/factory';
|
||||
import { navigateToAddPage } from '../utils/common/navigation.helper';
|
||||
import { selectDeviceCategory } from '../utils/common/device-add.helper';
|
||||
import { enterPairingMode } from '../utils/common/relay.helper';
|
||||
import { sleep, waitForSource } from '../utils/common/element.helper';
|
||||
|
||||
// 走 curtain3 全程,到 Auto-Calibrate Close 后逐页 dump,看真实收尾流程
|
||||
async function main() {
|
||||
const d = createDriver();
|
||||
await d.createSession();
|
||||
try {
|
||||
const dump = async (l: string) => { const s = await d.getSource(); const t = Array.from(s.matchAll(/text="([^"]{1,60})"/g)).map(m => m[1]).filter(Boolean); console.log(`\n=== ${l} ===\n`, Array.from(new Set(t)).join(' | ')); return s; };
|
||||
const tapText = async (txt: string) => { const e = await d.findElementRaw('-android uiautomator', `new UiSelector().text("${txt}")`); if (e) { await d.tapElement(e); console.log(' 点', txt); return true; } return false; };
|
||||
const tapDesc = async (desc: string) => { const e = await d.findElementRaw('-android uiautomator', `new UiSelector().descriptionContains("${desc}")`); if (e) { await d.tapElement(e); console.log(' 勾', desc); return true; } return false; };
|
||||
|
||||
await navigateToAddPage(d, 'Device'); await sleep(2500);
|
||||
await selectDeviceCategory(d, 'Curtain');
|
||||
let s = await d.getSource();
|
||||
if (s.includes('Disclaimer')) { await tapText('Next'); await sleep(2500); }
|
||||
await enterPairingMode('curtain3').catch(() => {});
|
||||
await sleep(1500); await tapText('Next');
|
||||
|
||||
await waitForSource(d, 'Select Mode', 60000).catch(() => {});
|
||||
if (!(await tapDesc('Open from one side'))) await tapText('Open from one side');
|
||||
await sleep(1000); await tapText('Confirm');
|
||||
await waitForSource(d, 'Install', 30000).catch(() => {});
|
||||
await tapText('Install');
|
||||
await waitForSource(d, 'rail type', 15000).catch(() => {}); await tapText('Skip');
|
||||
await waitForSource(d, 'Custom Calibration', 15000).catch(() => {}); await tapText('Custom Calibration');
|
||||
await waitForSource(d, 'Confirm Orientation', 15000).catch(() => {}); await tapDesc('facing inwards'); await sleep(800); await tapText('Next');
|
||||
await waitForSource(d, 'fully closed', 15000).catch(() => {}); await tapDesc('fully closed'); await sleep(800); await tapText('Start Calibration');
|
||||
await waitForSource(d, 'Magnet Detection', 15000).catch(() => {}); await tapText('Skip magnet detection'); await sleep(1500); await tapText('Skip');
|
||||
await waitForSource(d, 'Move left', 20000).catch(() => {});
|
||||
await tapText('Move left'); console.log('等15s...'); await sleep(15000);
|
||||
await tapText('Auto-Calibrate Close');
|
||||
// ↓↓↓ 重点:Auto-Calibrate Close 之后逐页观察
|
||||
for (let i = 1; i <= 12; i++) { await sleep(3000); s = await dump(`Auto-Close 后 第${i}页`); const left = await d.findElementsRaw('id', 'com.theswitchbot.switchbot:id/leftCard'); if (left.length) { console.log(`>>> 回首页 卡片=${left.length}`); break; } }
|
||||
} finally { await d.destroySession(); }
|
||||
}
|
||||
main().catch((e) => { console.error('ERR:', e.message); process.exit(1); });
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
import { createDriver } from '../drivers/factory';
|
||||
import { navigateToAddPage } from '../utils/common/navigation.helper';
|
||||
import { sleep } from '../utils/common/element.helper';
|
||||
|
||||
// 下滑添加页,逐屏找含 "Remote" 的文案,定位标准 Remote 入口的确切文字与位置
|
||||
async function main() {
|
||||
const needle = process.argv[2] || 'Remote';
|
||||
const driver = createDriver();
|
||||
await driver.createSession();
|
||||
try {
|
||||
await navigateToAddPage(driver, 'Device');
|
||||
await sleep(2500);
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
const src = await driver.getSource();
|
||||
const hits = Array.from(src.matchAll(/text="([^"]*)"/g)).map((m) => m[1])
|
||||
.filter((t) => t && t.toLowerCase().includes(needle.toLowerCase()));
|
||||
console.log(`屏#${i} 命中"${needle}":`, hits.length ? Array.from(new Set(hits)).join(' | ') : '(无)');
|
||||
// 命中精确 needle 就停
|
||||
const exact = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${needle}")`);
|
||||
if (exact) { console.log(`>>> 屏#${i} 找到精确 text("${needle}")`); break; }
|
||||
await driver.scrollDown(700);
|
||||
await sleep(1200);
|
||||
}
|
||||
} finally {
|
||||
await driver.destroySession();
|
||||
}
|
||||
}
|
||||
main().catch((e) => { console.error('ERR:', e.message); process.exit(1); });
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
import { createDriver } from '../drivers/factory';
|
||||
import { ensureHomeTab } from '../utils/common/navigation.helper';
|
||||
import { sleep } from '../utils/common/element.helper';
|
||||
|
||||
// 探索:首页房间标签 + 长按设备卡进入删除模式后出现什么
|
||||
async function main() {
|
||||
const cardText = process.argv[2] || 'Meter';
|
||||
const driver = createDriver();
|
||||
await driver.createSession();
|
||||
try {
|
||||
await ensureHomeTab(driver);
|
||||
await driver.dismissPopupIfPresent();
|
||||
await sleep(1500);
|
||||
|
||||
const dump = async (label: string) => {
|
||||
const src = await driver.getSource();
|
||||
const t = Array.from(src.matchAll(/text="([^"]{1,40})"/g)).map((m) => m[1]).filter(Boolean);
|
||||
const d = Array.from(src.matchAll(/content-desc="([^"]{1,40})"/g)).map((m) => m[1]).filter(Boolean);
|
||||
console.log(`\n=== ${label} ===\n text:`, Array.from(new Set(t)).join(' | '), '\n desc:', Array.from(new Set(d)).join(' | '));
|
||||
};
|
||||
// 房间标签
|
||||
const rooms = await driver.findElementsRaw('id', 'com.theswitchbot.switchbot:id/clSelectRoom');
|
||||
console.log('房间标签数:', rooms.length);
|
||||
for (const r of rooms) { const tx = await driver.getElementAttribute(r, 'text').catch(() => ''); if (tx) console.log(' 房间:', tx); }
|
||||
|
||||
await dump('首页(长按前)');
|
||||
|
||||
// 找设备卡 → 长按其中心
|
||||
let card = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${cardText}")`);
|
||||
if (!card) { console.log(`未找到含"${cardText}"的卡片`); return; }
|
||||
const rect = await driver.getElementRect(card);
|
||||
console.log(`长按 "${cardText}" 卡片中心 (${Math.round(rect.x + rect.width / 2)}, ${Math.round(rect.y + rect.height / 2)})`);
|
||||
await driver.longPress(Math.round(rect.x + rect.width / 2), Math.round(rect.y + rect.height / 2), 1.2);
|
||||
await sleep(2500);
|
||||
await dump('长按后(删除/管理模式?)');
|
||||
} finally {
|
||||
await driver.destroySession();
|
||||
}
|
||||
}
|
||||
main().catch((e) => { console.error('ERR:', e.message); process.exit(1); });
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
import { createDriver } from '../drivers/factory';
|
||||
import { navigateToAddPage } from '../utils/common/navigation.helper';
|
||||
import { sleep } from '../utils/common/element.helper';
|
||||
|
||||
// 抓取 App「添加设备」完整产品目录(产品看板),输出全部单品设备名
|
||||
async function main() {
|
||||
const driver = createDriver();
|
||||
await driver.createSession();
|
||||
try {
|
||||
await navigateToAddPage(driver, 'Device');
|
||||
await sleep(2500);
|
||||
// 点 Show More 展开全部品类
|
||||
const more = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Show More")');
|
||||
if (more) { await driver.tapElement(more); await sleep(1500); }
|
||||
|
||||
const all = new Set<string>();
|
||||
// 非产品的 UI 文案/区块标题,过滤掉
|
||||
const NON_PRODUCT = new Set([
|
||||
'Add Device', 'Scanning for Bluetooth devices nearby...', 'Show More', 'Show less',
|
||||
'Add Manually', 'Home Automation', 'Smart Home Appliances', 'Sensor', 'Camera',
|
||||
'Lock', 'Light', 'Curtain', 'Hub', 'Robot Vacuum', 'Others', 'Smart Switch',
|
||||
'Climate', 'Security', 'Lighting', 'Cleaning',
|
||||
]);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const src = await driver.getSource();
|
||||
for (const m of src.matchAll(/text="([^"]{2,45})"/g)) {
|
||||
const t = m[1].trim();
|
||||
if (!t) continue;
|
||||
if (/^\d+$/.test(t)) continue; // 纯数字(角标计数)
|
||||
if (NON_PRODUCT.has(t)) continue;
|
||||
all.add(t);
|
||||
}
|
||||
await driver.scrollDown(650);
|
||||
await sleep(900);
|
||||
}
|
||||
const list = Array.from(all).sort();
|
||||
console.log('=== 产品目录(共 ' + list.length + ' 项,已过滤UI/区块标题)===');
|
||||
list.forEach((x, i) => console.log(`${String(i + 1).padStart(3)}. ${x}`));
|
||||
} finally {
|
||||
await driver.destroySession();
|
||||
}
|
||||
}
|
||||
main().catch((e) => { console.error('ERR:', e.message); process.exit(1); });
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* 从累积的 reports/.results.json 生成一份合并报告(逐用例 + 失败截图 + 结论/问题/解决方案)。
|
||||
* 供 soak 全程结束后调用(SOAK_MODE 下 .results.json 跨多次 vitest run 累积)。
|
||||
* npx ts-node scripts/gen-combined.ts <报告名前缀>
|
||||
*/
|
||||
import { TestReporter } from '../utils/test-reporter';
|
||||
const name = process.argv[2] || 'Combined';
|
||||
const out = TestReporter.generateCombinedReport(name);
|
||||
console.log('合并报告:', out || '(无结果)');
|
||||
|
|
@ -42,6 +42,13 @@ const CATEGORY_MODULES = new Set([
|
|||
'Yb6hgZ9T', // 扫地机类
|
||||
]);
|
||||
|
||||
// 平台级模块(账号/房间/消息/场景/安装等)—— 同属 P0 必测,非设备品类
|
||||
const PLATFORM_MODULES = new Set([
|
||||
'Czs8zN3v', // 我的(账号/房间/feedback)
|
||||
'Eeme1qVk', // 其他功能(消息中心/插件热更/覆盖安装/场景操作)
|
||||
'PJpV3Qz2', // 平台功能(第三方操作)
|
||||
]);
|
||||
|
||||
// 关键字 -> 品类(= 测试目录名, snake_case)。按最长命中优先匹配。
|
||||
const KEYWORD_TO_CATEGORY: [string, string][] = [
|
||||
['relay switch', 'plug'], ['garage', 'plug'], ['plug mini', 'plug'], ['plugmini', 'plug'], ['eu plug', 'plug'], ['plug', 'plug'],
|
||||
|
|
@ -101,7 +108,7 @@ function shortDesc(s: string): string {
|
|||
}
|
||||
|
||||
interface AddRow {
|
||||
kind: 'add' | 'feature';
|
||||
kind: 'add' | 'feature' | 'platform';
|
||||
ones: number;
|
||||
name: string;
|
||||
cat: string;
|
||||
|
|
@ -166,6 +173,22 @@ function main() {
|
|||
});
|
||||
}
|
||||
|
||||
// 1b) platform: 平台模块内、在计划内的 case(P0,非设备)
|
||||
for (const c of libCases) {
|
||||
if (!PLATFORM_MODULES.has(c.module_uuid)) continue;
|
||||
if (!planNums.has(c.number)) continue;
|
||||
rows.push({
|
||||
kind: 'platform',
|
||||
ones: c.number,
|
||||
name: c.name,
|
||||
cat: 'platform',
|
||||
device: '',
|
||||
file: '', // 多为新建脚本;场景操作可指向 tests/automation
|
||||
testName: `[P0][ONES:${c.number}] ${c.name}`,
|
||||
status: 'todo',
|
||||
});
|
||||
}
|
||||
|
||||
// 2) ctrl: 两条协议超级用例的每个 step
|
||||
for (const { number, proto } of CTRL_CASES) {
|
||||
const found = runOnes(`testcase case search --key ${number}`).cases || [];
|
||||
|
|
@ -216,9 +239,11 @@ function argVal(argv: string[], flag: string): string | undefined {
|
|||
function printSummary(rows: Row[], na: string[], plan: string) {
|
||||
const by = (k: string) => rows.filter((r) => r.kind === k);
|
||||
const add = by('add'), feat = by('feature'), ctrl = by('ctrl') as CtrlRow[];
|
||||
const platform = by('platform');
|
||||
console.log(`\n=== 必测项覆盖摘要 (plan ${plan}) ===`);
|
||||
console.log(`add(添加) : ${add.length}`);
|
||||
console.log(`feature(功能): ${feat.length}`);
|
||||
console.log(`platform(平台): ${platform.length}`);
|
||||
console.log(`ctrl(控制step): ${ctrl.length} (ble ${ctrl.filter((r) => r.proto === 'ble').length} / wifi ${ctrl.filter((r) => r.proto === 'wifi').length})`);
|
||||
const catCount: Record<string, number> = {};
|
||||
for (const r of rows) catCount[r.cat || '∅'] = (catCount[r.cat || '∅'] || 0) + 1;
|
||||
|
|
@ -234,7 +259,7 @@ function renderManifest(rows: Row[], plan: string): string {
|
|||
|
||||
export type MustTestItem =
|
||||
| {
|
||||
kind: 'add' | 'feature';
|
||||
kind: 'add' | 'feature' | 'platform';
|
||||
ones: number;
|
||||
name: string;
|
||||
cat: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
#!/bin/bash
|
||||
# iOS 夜间添加稳定性 soak:循环【清空设备 → 逐个添加】到次日 09:00。
|
||||
# 关键:iOS 走 WDA(8100),WDA 易掉 → 每轮检查并自动重起;每个设备添加前 terminate/launch 把 app 复位到首页
|
||||
# (iOS navigateToAddPage 无回首页恢复)。计数沿用 SKIP 护栏(SKIP 不计 PASS)。
|
||||
# nohup caffeinate -dimsu bash scripts/overnight-soak-ios.sh > reports/soak-ios.out 2>&1 &
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
export PLATFORM=ios
|
||||
export SOAK_MODE=1
|
||||
UDID=00008110-001A34303AE9801E
|
||||
BUNDLE=com.wohand.wohand
|
||||
|
||||
DEADLINE=$(date -v+1d -v9H -v0M -v0S +%s 2>/dev/null) || DEADLINE=$(($(date +%s) + 50400))
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
mkdir -p reports
|
||||
LOG="reports/soak-ios-$TS.log"
|
||||
DETAIL="reports/soak-ios-$TS.detail.log"
|
||||
|
||||
DEVICES=(
|
||||
"meter|tests/meter/meter_connect.test.ts|"
|
||||
"remote|tests/remote/remote_connect.test.ts|"
|
||||
"hub|tests/hub/hub_connect.test.ts|export HUB_DEVICE='HubMini 9I'"
|
||||
)
|
||||
|
||||
PASS=(); FAIL=(); SKIP=()
|
||||
for i in "${!DEVICES[@]}"; do PASS[$i]=0; FAIL[$i]=0; SKIP[$i]=0; done
|
||||
log(){ echo "[$(date '+%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
|
||||
|
||||
wda_sid(){ curl -s --max-time 5 http://localhost:8100/status 2>/dev/null | grep -oE '[0-9A-F]{8}-[0-9A-F-]{27,}' | head -1; }
|
||||
wda_ready(){ curl -s --max-time 5 http://localhost:8100/status 2>/dev/null | grep -q '"ready" : true'; }
|
||||
ensure_wda(){
|
||||
if wda_ready; then return 0; fi
|
||||
log " WDA 不在,重起 WDA..."
|
||||
pkill -f iproxy 2>/dev/null; sleep 1
|
||||
curl -s --max-time 280 -X POST http://127.0.0.1:4723/session -H 'Content-Type: application/json' \
|
||||
-d "{\"capabilities\":{\"alwaysMatch\":{\"platformName\":\"iOS\",\"appium:automationName\":\"XCUITest\",\"appium:udid\":\"$UDID\",\"appium:platformVersion\":\"26.5\",\"appium:deviceName\":\"iPhone\",\"appium:bundleId\":\"$BUNDLE\",\"appium:wdaLocalPort\":8100,\"appium:noReset\":true,\"appium:newCommandTimeout\":3600}}}" >>"$DETAIL" 2>&1
|
||||
sleep 3
|
||||
wda_ready && log " WDA 已就绪" || log " WDA 重起失败(本轮可能全失败)"
|
||||
}
|
||||
app_home(){ local sid; sid=$(wda_sid); [ -z "$sid" ] && return 0
|
||||
curl -s --max-time 15 -X POST "http://localhost:8100/session/$sid/wda/apps/terminate" -H 'Content-Type: application/json' -d "{\"bundleId\":\"$BUNDLE\"}" >/dev/null 2>&1
|
||||
sleep 1
|
||||
curl -s --max-time 15 -X POST "http://localhost:8100/session/$sid/wda/apps/launch" -H 'Content-Type: application/json' -d "{\"bundleId\":\"$BUNDLE\"}" >/dev/null 2>&1
|
||||
sleep 2
|
||||
}
|
||||
|
||||
names=""; for spec in "${DEVICES[@]}"; do names+="${spec%%|*} "; done
|
||||
log "===== iOS SOAK 开始,截止 $(date -r $DEADLINE '+%m-%d %H:%M' 2>/dev/null) ; 设备: $names====="
|
||||
rm -f reports/.results.json
|
||||
ensure_wda
|
||||
|
||||
cycle=0
|
||||
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||
cycle=$((cycle+1))
|
||||
log "----- Cycle $cycle -----"
|
||||
ensure_wda
|
||||
app_home
|
||||
if RESET_DEVICES=1 npx ts-node scripts/reset-devices.ts >>"$DETAIL" 2>&1; then log " reset 完成"; else log " reset 异常(继续)"; fi
|
||||
|
||||
for i in "${!DEVICES[@]}"; do
|
||||
spec="${DEVICES[$i]}"; name="${spec%%|*}"; rest="${spec#*|}"; file="${rest%%|*}"; envs="${rest#*|}"
|
||||
echo "[$(date '+%H:%M:%S')] >>> Cycle $cycle $name" >>"$DETAIL"
|
||||
ensure_wda; app_home # 每个设备添加前复位到首页(iOS 导航无回首页恢复)
|
||||
runlog=$(mktemp)
|
||||
( eval "$envs"; npx vitest run "$file" ) >"$runlog" 2>&1; rc=$?
|
||||
cat "$runlog" >>"$DETAIL"
|
||||
if grep -q "\[SKIP\]" "$runlog"; then
|
||||
SKIP[$i]=$(( SKIP[$i] + 1 )); log " $name SKIP(设备已存在 → 不计通过)"
|
||||
elif [ $rc -eq 0 ] && grep -q "\[PASS\]" "$runlog"; then
|
||||
PASS[$i]=$(( PASS[$i] + 1 )); log " $name PASS"
|
||||
else
|
||||
FAIL[$i]=$(( FAIL[$i] + 1 )); log " $name FAIL (见 $DETAIL)"
|
||||
fi
|
||||
rm -f "$runlog"
|
||||
done
|
||||
|
||||
tally=""; for i in "${!DEVICES[@]}"; do n="${DEVICES[$i]%%|*}"; tot=$(( PASS[$i] + FAIL[$i] + SKIP[$i] )); tally+="$n ${PASS[$i]}/${tot}(skip${SKIP[$i]}) "; done
|
||||
log " 累计通过率(pass/total,skip另计): $tally"
|
||||
done
|
||||
|
||||
log "===== iOS SOAK 结束,共 $cycle 轮 ====="
|
||||
final=""; for i in "${!DEVICES[@]}"; do n="${DEVICES[$i]%%|*}"; tot=$(( PASS[$i] + FAIL[$i] + SKIP[$i] )); final+="$n=${PASS[$i]}/${tot}(skip${SKIP[$i]}) "; done
|
||||
log "最终: $final"
|
||||
log "生成汇总报告..."
|
||||
npx ts-node scripts/soak-report.ts "ios-$TS" >>"$DETAIL" 2>&1 && log "时间线汇总: reports/soak-report-ios-$TS.*" || log "时间线汇总生成失败"
|
||||
npx ts-node scripts/gen-combined.ts "SoakIOS_$TS" >>"$DETAIL" 2>&1 && log "逐用例报告: reports/SoakIOS_${TS}_*.html" || log "逐用例报告生成失败"
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#!/bin/bash
|
||||
# 夜间添加稳定性 soak:循环【清空所有设备 → 逐个添加】直到次日 09:00,记录每设备 通过/失败 计数。
|
||||
# 用法(后台不挂断运行):
|
||||
# nohup bash scripts/overnight-soak.sh > reports/soak.out 2>&1 &
|
||||
# 设备清单见 DEVICES;每项 "名称|测试文件|额外ENV(eval形式export语句,可含空格值)"。
|
||||
# 兼容 macOS 自带 bash 3.2(用索引数组,不用关联数组)。
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
export PLATFORM=android
|
||||
export SOAK_MODE=1 # 让各次 vitest run 累积结果到 reports/.results.json,全程结束统一出一份逐用例+截图报告
|
||||
|
||||
# 截止时间 = 次日 09:00(macOS date 语法)
|
||||
DEADLINE=$(date -v+1d -v9H -v0M -v0S +%s 2>/dev/null) || DEADLINE=$(($(date +%s) + 50400))
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
mkdir -p reports
|
||||
LOG="reports/soak-$TS.log"
|
||||
DETAIL="reports/soak-$TS.detail.log"
|
||||
|
||||
DEVICES=(
|
||||
"meter|tests/meter/meter_connect.test.ts|"
|
||||
"outdoor_meter|tests/meter/outdoor_meter_connect.test.ts|"
|
||||
"remote|tests/remote/remote_connect.test.ts|"
|
||||
"curtain3|tests/curtain/curtain_connect.test.ts|export CURTAIN_DEVICE='Curtain3 2B'; export CURTAIN_CALIBRATE=1"
|
||||
)
|
||||
|
||||
# 索引数组(bash 3.2 兼容)
|
||||
PASS=(); FAIL=(); SKIP=()
|
||||
for i in "${!DEVICES[@]}"; do PASS[$i]=0; FAIL[$i]=0; SKIP[$i]=0; done
|
||||
|
||||
log(){ echo "[$(date '+%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
|
||||
|
||||
names=""; for spec in "${DEVICES[@]}"; do names+="${spec%%|*} "; done
|
||||
log "===== SOAK 开始,截止 $(date -r $DEADLINE '+%m-%d %H:%M' 2>/dev/null) ; 设备: $names====="
|
||||
rm -f reports/.results.json # 全程开始前清一次,之后累积所有轮次的逐用例结果
|
||||
|
||||
cycle=0
|
||||
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||
cycle=$((cycle+1))
|
||||
log "----- Cycle $cycle -----"
|
||||
|
||||
if RESET_DEVICES=1 npx ts-node scripts/reset-devices.ts >>"$DETAIL" 2>&1; then
|
||||
log " reset 完成"
|
||||
else
|
||||
log " reset 异常(继续)"
|
||||
fi
|
||||
|
||||
for i in "${!DEVICES[@]}"; do
|
||||
spec="${DEVICES[$i]}"
|
||||
name="${spec%%|*}"; rest="${spec#*|}"; file="${rest%%|*}"; envs="${rest#*|}"
|
||||
echo "[$(date '+%H:%M:%S')] >>> Cycle $cycle $name" >>"$DETAIL"
|
||||
runlog=$(mktemp)
|
||||
( eval "$envs"; npx vitest run "$file" ) >"$runlog" 2>&1; rc=$?
|
||||
cat "$runlog" >>"$DETAIL"
|
||||
# 按实际结果计数:SKIP(设备已存在=reset 未清干净)单列,绝不计入 PASS,以免通过率被灌水
|
||||
if grep -q "\[SKIP\]" "$runlog"; then
|
||||
SKIP[$i]=$(( SKIP[$i] + 1 )); log " $name SKIP(设备已存在,reset 未清干净 → 不计通过)"
|
||||
elif [ $rc -eq 0 ] && grep -q "\[PASS\]" "$runlog"; then
|
||||
PASS[$i]=$(( PASS[$i] + 1 )); log " $name PASS"
|
||||
else
|
||||
FAIL[$i]=$(( FAIL[$i] + 1 )); log " $name FAIL (见 $DETAIL)"
|
||||
fi
|
||||
rm -f "$runlog"
|
||||
done
|
||||
|
||||
tally=""
|
||||
for i in "${!DEVICES[@]}"; do n="${DEVICES[$i]%%|*}"; tot=$(( PASS[$i] + FAIL[$i] + SKIP[$i] )); tally+="$n ${PASS[$i]}/${tot}(skip${SKIP[$i]}) "; done
|
||||
log " 累计通过率(pass/total,skip另计): $tally"
|
||||
done
|
||||
|
||||
log "===== SOAK 结束,共 $cycle 轮 ====="
|
||||
final=""; for i in "${!DEVICES[@]}"; do n="${DEVICES[$i]%%|*}"; tot=$(( PASS[$i] + FAIL[$i] + SKIP[$i] )); final+="$n=${PASS[$i]}/${tot}(skip${SKIP[$i]}) "; done
|
||||
log "最终: $final"
|
||||
|
||||
# 生成单份汇总报告(结论/问题/解决方案)
|
||||
log "生成汇总报告..."
|
||||
npx ts-node scripts/soak-report.ts "$TS" >>"$DETAIL" 2>&1 && log "时间线汇总: reports/soak-report-$TS.md / .html" || log "时间线汇总生成失败(见 $DETAIL)"
|
||||
# 逐用例 + 失败截图的整体报告(参照 combined 报告格式)
|
||||
npx ts-node scripts/gen-combined.ts "Soak_$TS" >>"$DETAIL" 2>&1 && log "逐用例报告: reports/Soak_${TS}_*.html" || log "逐用例报告生成失败(见 $DETAIL)"
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* 继电器开发板按钮点击次数报告 —— 统计每块板、每个通道(=设备按键)的累计点击,
|
||||
* 对照继电器约 20 万次寿命,显示占比/剩余/告警。多块板按串口序列号自动分别统计。
|
||||
* npx ts-node scripts/relay-press-report.ts
|
||||
* 数据来源:test-plan/relay-press-counts.json(由 SerialController 每次通道吸合时累加)。
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { RELAY_MAP } from '../config/relay.config';
|
||||
|
||||
const FILE = path.resolve(__dirname, '../test-plan/relay-press-counts.json');
|
||||
const LIFE = 200000;
|
||||
|
||||
// 反查:通道号 → "设备.按键"(取 android 通道;两端不同时也显示 ios)
|
||||
const chLabel: Record<number, string> = {};
|
||||
for (const [dev, cfg] of Object.entries(RELAY_MAP)) {
|
||||
for (const [btn, spec] of Object.entries(cfg.buttons)) {
|
||||
const chs = typeof spec === 'number' ? [spec] : [spec.android, spec.ios].filter((x): x is number => x != null);
|
||||
for (const ch of chs) chLabel[ch] = `${dev}.${btn}`;
|
||||
}
|
||||
}
|
||||
|
||||
const data: Record<string, Record<string, number>> = fs.existsSync(FILE)
|
||||
? JSON.parse(fs.readFileSync(FILE, 'utf-8')) : {};
|
||||
|
||||
if (!Object.keys(data).length) {
|
||||
console.log('(暂无点击计数;跑过添加/继电器操作后才会有。文件:' + FILE + ')');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
for (const [board, chs] of Object.entries(data)) {
|
||||
console.log(`\n=== 开发板 ${board} ===`);
|
||||
console.log('通道'.padEnd(6) + '设备.按键'.padEnd(22) + '点击次数'.padEnd(12) + '寿命占比'.padEnd(10) + '剩余');
|
||||
const entries = Object.entries(chs).map(([ch, c]) => [Number(ch), c] as [number, number]).sort((a, b) => b[1] - a[1]);
|
||||
let total = 0;
|
||||
for (const [ch, cnt] of entries) {
|
||||
total += cnt;
|
||||
const pct = (cnt / LIFE * 100).toFixed(2) + '%';
|
||||
const warn = cnt >= LIFE * 0.9 ? ' ⚠️已超90%寿命' : cnt >= LIFE * 0.8 ? ' ⚠接近寿命' : '';
|
||||
console.log(
|
||||
`ch${ch}`.padEnd(6) +
|
||||
(chLabel[ch] || '-').padEnd(22) +
|
||||
String(cnt).padEnd(12) +
|
||||
pct.padEnd(10) +
|
||||
String(LIFE - cnt) + warn
|
||||
);
|
||||
}
|
||||
console.log(`板内累计点击: ${total}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* 继电器命令行调试工具 —— 参数化触发按键/配对/原始命令,方便接线调试。
|
||||
*
|
||||
* 用法(端口/波特率走 .env 的 SERIAL_PORT/SERIAL_BAUD):
|
||||
* npx ts-node scripts/relay.ts probe # 查版本(只读握手)
|
||||
* npx ts-node scripts/relay.ts status # 读 64 路状态
|
||||
* npx ts-node scripts/relay.ts pair remote # 进配对(按 config 默认)
|
||||
* npx ts-node scripts/relay.ts pair meter --hold 2500 # 进配对,覆盖时长
|
||||
* npx ts-node scripts/relay.ts press remote convex --hold 800 # 点按命名键
|
||||
* npx ts-node scripts/relay.ts pulse 34,35 --hold 3000 # 同时脉冲指定通道
|
||||
* npx ts-node scripts/relay.ts on 34 / off 34 # 单路通/断
|
||||
* npx ts-node scripts/relay.ts raw 55C8210155 # 发原始 16 进制帧
|
||||
*/
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
|
||||
import { SerialController } from '../utils/common/serial_controller';
|
||||
import { triggerRelayAction, pressButton } from '../utils/common/relay.helper';
|
||||
|
||||
function flag(name: string): string | undefined {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
return i >= 0 ? process.argv[i + 1] : undefined;
|
||||
}
|
||||
|
||||
/** 取位置参数(剔除 --flag 及其紧随的值)。 */
|
||||
function positionals(): string[] {
|
||||
const argv = process.argv.slice(2);
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
if (argv[i].startsWith('--')) {
|
||||
i++; // 跳过该 flag 的值
|
||||
continue;
|
||||
}
|
||||
out.push(argv[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [cmd, a, b] = positionals();
|
||||
const hold = flag('hold') ? parseInt(flag('hold') as string, 10) : undefined;
|
||||
|
||||
switch (cmd) {
|
||||
case 'probe': {
|
||||
const c = new SerialController();
|
||||
await c.open();
|
||||
console.log(await c.queryVersion());
|
||||
await c.close();
|
||||
break;
|
||||
}
|
||||
case 'status': {
|
||||
const c = new SerialController();
|
||||
await c.open();
|
||||
console.log('status:', (await c.readStatus()).toString('hex').toUpperCase());
|
||||
await c.close();
|
||||
break;
|
||||
}
|
||||
case 'pair': {
|
||||
if (!a) throw new Error('用法: pair <device> [--hold ms]');
|
||||
await triggerRelayAction(a, 'pairing', hold ? { holdMs: hold } : {});
|
||||
break;
|
||||
}
|
||||
case 'press': {
|
||||
if (!a || !b) throw new Error('用法: press <device> <button> [--hold ms]');
|
||||
await pressButton(a, b, hold ?? 500);
|
||||
break;
|
||||
}
|
||||
case 'pulse': {
|
||||
if (!a) throw new Error('用法: pulse <ch1,ch2,...> [--hold ms]');
|
||||
const chs = a.split(',').map((s) => parseInt(s, 10));
|
||||
const c = new SerialController();
|
||||
await c.open();
|
||||
await c.pulseChannels(chs, hold ?? 500);
|
||||
console.log(`脉冲通道[${chs.join(',')}] ${hold ?? 500}ms 完成`);
|
||||
await c.close();
|
||||
break;
|
||||
}
|
||||
case 'on':
|
||||
case 'off': {
|
||||
if (!a) throw new Error(`用法: ${cmd} <ch>`);
|
||||
const c = new SerialController();
|
||||
await c.open();
|
||||
const resp = cmd === 'on' ? await c.relayOn(parseInt(a, 10)) : await c.relayOff(parseInt(a, 10));
|
||||
console.log(`${cmd} ${a}:`, resp.toString('hex').toUpperCase());
|
||||
await c.close();
|
||||
break;
|
||||
}
|
||||
case 'raw': {
|
||||
if (!a) throw new Error('用法: raw <hex,如 55C8210155>');
|
||||
const bytes = (a.match(/.{1,2}/g) || []).map((h) => parseInt(h, 16));
|
||||
const c = new SerialController();
|
||||
await c.open();
|
||||
console.log('resp:', (await c.send(bytes)).toString('hex').toUpperCase());
|
||||
await c.close();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.log('命令: probe | status | pair <dev> | press <dev> <btn> | pulse <chs> | on <ch> | off <ch> | raw <hex> [--hold ms]');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error('错误:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* 必测执行前置:清空当前平台账号下**所有**设备,确保 connect 用例真正走添加流程。
|
||||
* 删除方式:首页长按卡片 → 多选模式 → Delete → 确认(逐个,跨房间不能一起删)。删完清空注册表。
|
||||
*
|
||||
* 安全开关:必须 RESET_DEVICES=1 才执行(防误删)。按平台:PLATFORM=android/ios。
|
||||
* RESET_DEVICES=1 PLATFORM=android npx ts-node scripts/reset-devices.ts
|
||||
*/
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
import { createDriver } from '../drivers/factory';
|
||||
import { removeAllDevices, homepageHasDeviceCards } from '../utils/common';
|
||||
|
||||
async function main() {
|
||||
if (process.env.RESET_DEVICES !== '1') {
|
||||
console.error('已阻止:这是删除所有设备的操作。确认后用 RESET_DEVICES=1 重跑。');
|
||||
process.exit(2);
|
||||
}
|
||||
const driver = createDriver();
|
||||
await driver.createSession();
|
||||
try {
|
||||
const removed = await removeAllDevices(driver);
|
||||
console.log(`\n[reset] 平台=${process.env.PLATFORM || 'ios'} 删除 ${removed.length} 个设备:`, removed.join(', ') || '(无)');
|
||||
// 删后校验:首页若仍有设备卡 → reset 没清干净(如批量删卡失败),exit 1 让上层(soak)记为"reset 异常",
|
||||
// 避免后续 connect 走 SKIP 把通过率灌水。
|
||||
if (await homepageHasDeviceCards(driver)) {
|
||||
console.error('[reset] 警告:首页仍有设备卡,未清空干净!');
|
||||
process.exitCode = 1;
|
||||
}
|
||||
} finally {
|
||||
await driver.destroySession();
|
||||
}
|
||||
}
|
||||
main().catch((e) => { console.error('ERR:', e.message); process.exit(1); });
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
/**
|
||||
* Soak 汇总报告生成器 —— 把一整轮夜间 soak 的所有结果聚合成**一份**报告(Markdown)。
|
||||
* 含:概览、各设备成功率、失败模式分析、失败时间线(死区)、结论、存在的问题、解决方案。
|
||||
*
|
||||
* 用法:
|
||||
* npx ts-node scripts/soak-report.ts [soak日志时间戳, 如 20260602-191820] # 缺省取最新
|
||||
* 输出:reports/soak-report-<TS>.md
|
||||
* (overnight-soak.sh 结束时会自动调用本脚本。)
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const reportsDir = path.resolve(__dirname, '../reports');
|
||||
|
||||
// 失败模式 → 说明 + 解决方案(知识库)
|
||||
const FAILURE_KB: Record<string, { label: string; solution: string }> = {
|
||||
navFail: {
|
||||
label: '导航到添加页失败(找不到首页"+")',
|
||||
solution: '残留弹框/卡死页挡住"+"。已修:navigateToAddPage 找不到"+"时 force-stop 重启 App 兜底(最多3次);dismissPopupIfPresent 识别"连接失败"弹框;removeAllDevices 开头重启清场。',
|
||||
},
|
||||
pairNotConnected: {
|
||||
label: '按了继电器但未连上(配对未触发)',
|
||||
solution: '物理配对触发不稳(如 remote 凸+凹同时按 3s,~29% 漏)。已修:addDeviceWithSerialPairing 配对未发现设备就重按重试(pairRetries=3,判据=连接关键字或设备名命中)。',
|
||||
},
|
||||
connTimeout: { label: '连接超时', solution: '检查蓝牙/设备环境;必要时延长 waitForConnection 或重按重试。' },
|
||||
noNext: { label: '引导页未找到 Next', solution: '核对引导页文案/时序;tapNext 增加等待或重试。' },
|
||||
catFail: { label: '品类选择失败', solution: '已用 UiScrollable.scrollIntoView 直达深层品类 + 重启兜底。' },
|
||||
calibFail: { label: '行程校验异常(Something went wrong)', solution: 'curtain3 校准需设备装真实导轨;Auto-Calibrate Close 后重试点 Finish 已加。' },
|
||||
other: { label: '其它/未分类失败', solution: '查 detail 日志对应 Cycle 段落定位。' },
|
||||
};
|
||||
|
||||
interface DevStat { pass: number; total: number; modes: Record<string, number>; }
|
||||
|
||||
function classifySegment(seg: string): string {
|
||||
if (/cannot navigate to Add Device|addBto not found/.test(seg)) return 'navFail';
|
||||
if (/cannot select category|no .* option/.test(seg)) return 'catFail';
|
||||
if (/connection timeout/.test(seg)) return 'connTimeout';
|
||||
if (/引导页未找到 Next|未找到 Next/.test(seg)) return 'noNext';
|
||||
if (/Something went wrong/.test(seg)) return 'calibFail';
|
||||
// 跑了继电器但最终失败且没连上
|
||||
if (/relay: .*pairing/.test(seg) && /✗|expected false to be true/.test(seg)) return 'pairNotConnected';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function main() {
|
||||
const ts = process.argv[2] || latestTs();
|
||||
if (!ts) { console.error('未找到 soak 日志'); process.exit(1); }
|
||||
const mainLog = path.join(reportsDir, `soak-${ts}.log`);
|
||||
const detailLog = path.join(reportsDir, `soak-${ts}.detail.log`);
|
||||
if (!fs.existsSync(mainLog)) { console.error('找不到', mainLog); process.exit(1); }
|
||||
|
||||
const mainText = fs.readFileSync(mainLog, 'utf-8');
|
||||
const detailText = fs.existsSync(detailLog) ? fs.readFileSync(detailLog, 'utf-8') : '';
|
||||
|
||||
// 1) 主日志:时间范围、轮次、每设备 PASS/FAIL 序列
|
||||
const lines = mainText.split('\n');
|
||||
const stamps = lines.map((l) => (l.match(/^\[(\d\d-\d\d \d\d:\d\d:\d\d)\]/) || [])[1]).filter(Boolean) as string[];
|
||||
const startT = stamps[0] || '?', endT = stamps[stamps.length - 1] || '?';
|
||||
const cycles = (mainText.match(/----- Cycle \d+ -----/g) || []).length;
|
||||
const devOrder: string[] = [];
|
||||
const stats: Record<string, DevStat> = {};
|
||||
// 每轮结果(用于死区检测):cycle -> {dev: pass/fail}
|
||||
const perCycle: Record<number, Record<string, boolean>> = {};
|
||||
let curCycle = 0;
|
||||
for (const l of lines) {
|
||||
const cm = l.match(/----- Cycle (\d+) -----/);
|
||||
if (cm) { curCycle = +cm[1]; perCycle[curCycle] = {}; continue; }
|
||||
const rm = l.match(/^\[.*\] (\w+) (PASS|FAIL)/);
|
||||
if (rm) {
|
||||
const [, dev, res] = rm;
|
||||
if (!stats[dev]) { stats[dev] = { pass: 0, total: 0, modes: {} }; devOrder.push(dev); }
|
||||
stats[dev].total++; if (res === 'PASS') stats[dev].pass++;
|
||||
if (perCycle[curCycle]) perCycle[curCycle][dev] = res === 'PASS';
|
||||
}
|
||||
}
|
||||
|
||||
// 2) detail 日志:按 Cycle/device 段落归类失败模式
|
||||
if (detailText) {
|
||||
const segRe = />>> Cycle \d+\s+(\w+)/g;
|
||||
const idxs: { dev: string; pos: number }[] = [];
|
||||
let m;
|
||||
while ((m = segRe.exec(detailText)) !== null) idxs.push({ dev: m[1], pos: m.index });
|
||||
for (let i = 0; i < idxs.length; i++) {
|
||||
const seg = detailText.slice(idxs[i].pos, i + 1 < idxs.length ? idxs[i + 1].pos : undefined);
|
||||
if (/✗|FAIL tests\/|expected false to be true/.test(seg) && !/✓ \[PASS\]/.test(seg.slice(0, 400))) {
|
||||
const dev = idxs[i].dev;
|
||||
if (!stats[dev]) { stats[dev] = { pass: 0, total: 0, modes: {} }; devOrder.push(dev); }
|
||||
const mode = classifySegment(seg);
|
||||
stats[dev].modes[mode] = (stats[dev].modes[mode] || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 死区:连续"全设备 FAIL"的轮次
|
||||
const deadCycles: number[] = [];
|
||||
for (const c of Object.keys(perCycle).map(Number).sort((a, b) => a - b)) {
|
||||
const r = perCycle[c]; const vals = Object.values(r);
|
||||
if (vals.length && vals.every((v) => v === false)) deadCycles.push(c);
|
||||
}
|
||||
|
||||
// 4) 生成报告
|
||||
// 剔除死区后的"健康期"成功率(更反映设备本身可靠性)
|
||||
const deadSet = new Set(deadCycles);
|
||||
const healthy: Record<string, { pass: number; total: number }> = {};
|
||||
for (const dev of devOrder) healthy[dev] = { pass: 0, total: 0 };
|
||||
for (const c of Object.keys(perCycle).map(Number)) {
|
||||
if (deadSet.has(c)) continue;
|
||||
for (const dev of Object.keys(perCycle[c])) {
|
||||
if (!healthy[dev]) healthy[dev] = { pass: 0, total: 0 };
|
||||
healthy[dev].total++;
|
||||
if (perCycle[c][dev]) healthy[dev].pass++;
|
||||
}
|
||||
}
|
||||
const report = render({ ts, startT, endT, cycles, devOrder, stats, healthy, deadCycles, perCycleCount: Object.keys(perCycle).length });
|
||||
const out = path.join(reportsDir, `soak-report-${ts}.md`);
|
||||
fs.writeFileSync(out, report, 'utf-8');
|
||||
|
||||
const html = renderHtml({ ts, startT, endT, cycles, devOrder, stats, healthy, deadCycles });
|
||||
const outHtml = path.join(reportsDir, `soak-report-${ts}.html`);
|
||||
fs.writeFileSync(outHtml, html, 'utf-8');
|
||||
|
||||
console.log('已生成汇总报告:');
|
||||
console.log(' Markdown:', out);
|
||||
console.log(' HTML :', outHtml);
|
||||
console.log('\n' + report);
|
||||
}
|
||||
|
||||
function pct(p: number, t: number) { return t ? ((p / t) * 100).toFixed(1) + '%' : '-'; }
|
||||
|
||||
function render(d: any): string {
|
||||
const { ts, startT, endT, cycles, devOrder, stats, healthy, deadCycles } = d;
|
||||
let r = `# 夜间添加稳定性 Soak 报告\n\n`;
|
||||
r += `- 运行:${startT} → ${endT} | 轮次:${cycles} | 日志:soak-${ts}\n\n`;
|
||||
|
||||
r += `## 一、各设备成功率\n\n| 设备 | 通过/总数 | 成功率 | 剔除死区后 |\n|---|---|---|---|\n`;
|
||||
for (const dev of devOrder) {
|
||||
const s = stats[dev]; const h = healthy[dev] || { pass: 0, total: 0 };
|
||||
r += `| ${dev} | ${s.pass}/${s.total} | ${pct(s.pass, s.total)} | ${pct(h.pass, h.total)} (${h.pass}/${h.total}) |\n`;
|
||||
}
|
||||
r += `\n> "剔除死区后"= 排除整轮全失败的卡死轮次后,设备本身的真实可靠性。\n`;
|
||||
|
||||
r += `\n## 二、失败模式分布\n\n| 设备 | 失败模式 | 次数 |\n|---|---|---|\n`;
|
||||
for (const dev of devOrder) {
|
||||
const modes = stats[dev].modes; const keys = Object.keys(modes).sort((a, b) => modes[b] - modes[a]);
|
||||
if (!keys.length) { r += `| ${dev} | (无失败明细) | - |\n`; continue; }
|
||||
for (const k of keys) r += `| ${dev} | ${FAILURE_KB[k]?.label || k} | ${modes[k]} |\n`;
|
||||
}
|
||||
|
||||
// 死区
|
||||
r += `\n## 三、失败时间线(死区检测)\n\n`;
|
||||
if (deadCycles.length) {
|
||||
r += `检测到 **${deadCycles.length}** 个"整轮全失败"轮次(全设备同时失败,通常是 App 卡死未恢复):Cycle ${deadCycles[0]} ~ ${deadCycles[deadCycles.length - 1]}。\n`;
|
||||
r += `> 这类连续死区会严重拉低整体成功率,且多为单点卡死(如未处理的弹框)引发的级联。\n`;
|
||||
} else { r += `未检测到连续整轮死区。\n`; }
|
||||
|
||||
// 结论
|
||||
r += `\n## 四、结论\n\n`;
|
||||
const hrate = (dv: string) => { const h = healthy[dv] || { pass: 0, total: 0 }; return h.total ? h.pass / h.total : 0; };
|
||||
const stableDevs = devOrder.filter((dv: string) => hrate(dv) >= 0.95);
|
||||
const weak = devOrder.filter((dv: string) => (healthy[dv]?.total || 0) > 0 && hrate(dv) < 0.85);
|
||||
if (deadCycles.length) r += `- 本轮存在 ${deadCycles.length} 个整轮死区,是整体成功率偏低的**主因**;按"剔除死区后"看设备真实可靠性更准。\n`;
|
||||
if (stableDevs.length) r += `- 稳定设备(剔除死区后 ≥95%):${stableDevs.join('、')}。\n`;
|
||||
if (weak.length) r += `- 偏脆设备(剔除死区后 <85%):${weak.join('、')} —— 需针对性优化其配对/连接。\n`;
|
||||
if (!weak.length && !deadCycles.length) r += `- 各设备成功率均良好,添加流程稳定。\n`;
|
||||
|
||||
// 问题 + 解决方案(基于实际命中的失败模式)
|
||||
const hitModes = new Set<string>();
|
||||
for (const dev of devOrder) Object.keys(stats[dev].modes).forEach((k) => hitModes.add(k));
|
||||
r += `\n## 五、存在的问题\n\n`;
|
||||
if (deadCycles.length) r += `1. **整轮死区**:连续 ${deadCycles.length} 轮全失败,单点卡死引发级联(无人值守缺重启级恢复时尤甚)。\n`;
|
||||
let i = deadCycles.length ? 2 : 1;
|
||||
for (const k of hitModes) { if (k === 'navFail' && deadCycles.length) continue; r += `${i++}. **${FAILURE_KB[k]?.label || k}**\n`; }
|
||||
if (i === 1) r += `(无显著问题)\n`;
|
||||
|
||||
r += `\n## 六、解决方案\n\n`;
|
||||
let j = 1;
|
||||
if (deadCycles.length || hitModes.has('navFail')) r += `${j++}. ${FAILURE_KB.navFail.solution}\n`;
|
||||
for (const k of hitModes) { if (k === 'navFail') continue; r += `${j++}. ${FAILURE_KB[k]?.solution || ''}\n`; }
|
||||
if (j === 1) r += `保持现状,继续监控。\n`;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
function latestTs(): string | null {
|
||||
if (!fs.existsSync(reportsDir)) return null;
|
||||
const f = fs.readdirSync(reportsDir).filter((x) => /^soak-\d+-\d+\.log$/.test(x)).sort().pop();
|
||||
return f ? (f.match(/soak-(\d+-\d+)\.log/) || [])[1] : null;
|
||||
}
|
||||
|
||||
// ---- HTML 报告 ----
|
||||
function rateColor(rate: number): string {
|
||||
if (rate >= 0.95) return '#1a9850'; // 绿
|
||||
if (rate >= 0.85) return '#f5a623'; // 橙
|
||||
return '#d73027'; // 红
|
||||
}
|
||||
function esc(s: string): string {
|
||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function renderHtml(d: any): string {
|
||||
const { ts, startT, endT, cycles, devOrder, stats, healthy, deadCycles } = d;
|
||||
const hrate = (dv: string) => { const h = healthy[dv] || { pass: 0, total: 0 }; return h.total ? h.pass / h.total : 0; };
|
||||
|
||||
// 成功率表
|
||||
let rateRows = '';
|
||||
for (const dev of devOrder) {
|
||||
const s = stats[dev]; const h = healthy[dev] || { pass: 0, total: 0 };
|
||||
const raw = s.total ? s.pass / s.total : 0; const hh = h.total ? h.pass / h.total : 0;
|
||||
rateRows += `<tr><td>${esc(dev)}</td><td>${s.pass}/${s.total}</td>` +
|
||||
`<td><b style="color:${rateColor(raw)}">${(raw * 100).toFixed(1)}%</b></td>` +
|
||||
`<td><b style="color:${rateColor(hh)}">${(hh * 100).toFixed(1)}%</b> <span class="dim">(${h.pass}/${h.total})</span></td></tr>`;
|
||||
}
|
||||
// 失败模式表
|
||||
let modeRows = '';
|
||||
for (const dev of devOrder) {
|
||||
const modes = stats[dev].modes; const keys = Object.keys(modes).sort((a, b) => modes[b] - modes[a]);
|
||||
if (!keys.length) { modeRows += `<tr><td>${esc(dev)}</td><td class="dim">(无失败明细)</td><td>-</td></tr>`; continue; }
|
||||
for (const k of keys) modeRows += `<tr><td>${esc(dev)}</td><td>${esc(FAILURE_KB[k]?.label || k)}</td><td>${modes[k]}</td></tr>`;
|
||||
}
|
||||
// 结论/问题/解决方案
|
||||
const stableDevs = devOrder.filter((dv: string) => hrate(dv) >= 0.95);
|
||||
const weak = devOrder.filter((dv: string) => (healthy[dv]?.total || 0) > 0 && hrate(dv) < 0.85);
|
||||
const concl: string[] = [];
|
||||
if (deadCycles.length) concl.push(`本轮存在 <b>${deadCycles.length}</b> 个整轮死区,是整体成功率偏低的<b>主因</b>;按"剔除死区后"看设备真实可靠性更准。`);
|
||||
if (stableDevs.length) concl.push(`稳定设备(剔除死区后 ≥95%):<b>${stableDevs.map(esc).join('、')}</b>。`);
|
||||
if (weak.length) concl.push(`偏脆设备(剔除死区后 <85%):<b style="color:#d73027">${weak.map(esc).join('、')}</b> —— 需针对性优化其配对/连接。`);
|
||||
if (!weak.length && !deadCycles.length) concl.push('各设备成功率均良好,添加流程稳定。');
|
||||
|
||||
const hitModes = new Set<string>();
|
||||
for (const dev of devOrder) Object.keys(stats[dev].modes).forEach((k) => hitModes.add(k));
|
||||
const probs: string[] = [];
|
||||
if (deadCycles.length) probs.push(`<b>整轮死区</b>:连续 ${deadCycles.length} 轮全失败,单点卡死引发级联(无人值守缺重启级恢复时尤甚)。`);
|
||||
for (const k of hitModes) { if (k === 'navFail' && deadCycles.length) continue; probs.push(`<b>${esc(FAILURE_KB[k]?.label || k)}</b>`); }
|
||||
if (!probs.length) probs.push('无显著问题。');
|
||||
|
||||
const sols: string[] = [];
|
||||
if (deadCycles.length || hitModes.has('navFail')) sols.push(esc(FAILURE_KB.navFail.solution));
|
||||
for (const k of hitModes) { if (k === 'navFail') continue; if (FAILURE_KB[k]) sols.push(esc(FAILURE_KB[k].solution)); }
|
||||
if (!sols.length) sols.push('保持现状,继续监控。');
|
||||
|
||||
const deadNote = deadCycles.length
|
||||
? `<p class="warn">检测到 <b>${deadCycles.length}</b> 个"整轮全失败"轮次:Cycle ${deadCycles[0]} ~ ${deadCycles[deadCycles.length - 1]}(全设备同时失败,通常 App 卡死未恢复 → 级联)。</p>`
|
||||
: `<p>未检测到连续整轮死区。</p>`;
|
||||
|
||||
return `<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8">
|
||||
<title>Soak 报告 ${ts}</title><style>
|
||||
body{font-family:-apple-system,"PingFang SC",Segoe UI,sans-serif;margin:32px;color:#222;background:#fafafa;line-height:1.6}
|
||||
h1{font-size:22px}h2{font-size:17px;border-left:4px solid #4a90d9;padding-left:10px;margin-top:28px}
|
||||
table{border-collapse:collapse;margin:10px 0;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.08)}
|
||||
th,td{border:1px solid #e2e2e2;padding:7px 14px;text-align:left;font-size:14px}
|
||||
th{background:#f0f4f8}.dim{color:#999;font-size:12px}
|
||||
.meta{color:#555;font-size:13px}.warn{background:#fff3cd;border-left:4px solid #f5a623;padding:8px 12px}
|
||||
ol{padding-left:22px}ol li{margin:5px 0}
|
||||
</style></head><body>
|
||||
<h1>夜间添加稳定性 Soak 报告</h1>
|
||||
<p class="meta">运行:${esc(startT)} → ${esc(endT)} | 轮次:${cycles} | 日志:soak-${ts}</p>
|
||||
<h2>一、各设备成功率</h2>
|
||||
<table><tr><th>设备</th><th>通过/总数</th><th>成功率</th><th>剔除死区后</th></tr>${rateRows}</table>
|
||||
<p class="dim">"剔除死区后"= 排除整轮全失败的卡死轮次后,设备本身的真实可靠性。</p>
|
||||
<h2>二、失败模式分布</h2>
|
||||
<table><tr><th>设备</th><th>失败模式</th><th>次数</th></tr>${modeRows}</table>
|
||||
<h2>三、失败时间线(死区检测)</h2>${deadNote}
|
||||
<h2>四、结论</h2><ol>${concl.map((c) => `<li>${c}</li>`).join('')}</ol>
|
||||
<h2>五、存在的问题</h2><ol>${probs.map((c) => `<li>${c}</li>`).join('')}</ol>
|
||||
<h2>六、解决方案</h2><ol>${sols.map((c) => `<li>${c}</li>`).join('')}</ol>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
// 兼容:healthy 以设备名为 key
|
||||
main();
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
// AUTO-GENERATED by scripts/gen-must-test-manifest.ts — do not edit by hand.
|
||||
// 源: ONES plan CQz9YCNX / lib EPfZfC9Y (必测项-AI自动化)。重新生成: npm run gen:must-test
|
||||
// 详见 prompts/must_test_conversion.md
|
||||
|
||||
export type MustTestItem =
|
||||
| {
|
||||
kind: 'add' | 'feature';
|
||||
ones: number;
|
||||
name: string;
|
||||
cat: string;
|
||||
device: string;
|
||||
file: string;
|
||||
testName: string;
|
||||
status: 'todo' | 'na' | 'done';
|
||||
}
|
||||
| {
|
||||
kind: 'ctrl';
|
||||
ones: number;
|
||||
step: string;
|
||||
proto: 'ble' | 'wifi';
|
||||
name: string;
|
||||
result: string;
|
||||
cat: string;
|
||||
device: string;
|
||||
action: string;
|
||||
file: string;
|
||||
testName: string;
|
||||
status: 'todo' | 'na' | 'done';
|
||||
};
|
||||
|
||||
export const MUST_TEST: MustTestItem[] = [
|
||||
{"kind":"add","ones":393236,"name":"添加Candle lamp(融蜡灯)验证(融蜡灯需要插110V电压)","cat":"ceiling_light","device":"CeilingLight 7Q","file":"tests/ceiling_light/ceiling_light_connect.test.ts","testName":"[P0][ONES:393236] 添加Candle lamp(融蜡灯)验证(融蜡灯需要插110V电压)","status":"todo"},
|
||||
{"kind":"add","ones":393235,"name":"添加RGBIC Neon Rope Light(炫彩霓虹灯)验证","cat":"strip_light","device":"StripLight 9S","file":"tests/strip_light/strip_light_connect.test.ts","testName":"[P0][ONES:393235] 添加RGBIC Neon Rope Light(炫彩霓虹灯)验证","status":"todo"},
|
||||
{"kind":"add","ones":393234,"name":"添加Standing Circulator fan验证","cat":"fan","device":"Fan 0T","file":"tests/fan/fan_connect.test.ts","testName":"[P0][ONES:393234] 添加Standing Circulator fan验证","status":"todo"},
|
||||
{"kind":"add","ones":332919,"name":"添加safety alarm","cat":"safety_alarm","device":"","file":"","testName":"","status":"na"},
|
||||
{"kind":"add","ones":332918,"name":"添加eu plug","cat":"plug","device":"Plug 4D","file":"tests/plug/plug_connect.test.ts","testName":"[P0][ONES:332918] 添加eu plug","status":"todo"},
|
||||
{"kind":"add","ones":332917,"name":"添加人体存在传感器","cat":"sensor","device":"Contact Sensor 5O","file":"tests/sensor/sensor_connect.test.ts","testName":"[P0][ONES:332917] 添加人体存在传感器","status":"todo"},
|
||||
{"kind":"add","ones":319305,"name":"添加Hub Mini S3验证","cat":"hub","device":"Hub2 8H","file":"tests/hub/hub_connect.test.ts","testName":"[P0][ONES:319305] 添加Hub Mini S3验证","status":"todo"},
|
||||
{"kind":"add","ones":319304,"name":"添加Hub2 S3验证","cat":"hub","device":"Hub2 8H","file":"tests/hub/hub_connect.test.ts","testName":"[P0][ONES:319304] 添加Hub2 S3验证","status":"todo"},
|
||||
{"kind":"add","ones":319303,"name":"添加Hub Mini Matter S3验证","cat":"hub","device":"Hub2 8H","file":"tests/hub/hub_connect.test.ts","testName":"[P0][ONES:319303] 添加Hub Mini Matter S3验证","status":"todo"},
|
||||
{"kind":"add","ones":311020,"name":"添加RGBIC Neon Wire Rope Light(炫彩钢丝霓虹灯)验证","cat":"strip_light","device":"StripLight 9S","file":"tests/strip_light/strip_light_connect.test.ts","testName":"[P0][ONES:311020] 添加RGBIC Neon Wire Rope Light(炫彩钢丝霓虹灯)验证","status":"todo"},
|
||||
{"kind":"add","ones":311019,"name":"添加RGBICWW Floor Lamp(炫彩落地灯)验证","cat":"ceiling_light","device":"CeilingLight 7Q","file":"tests/ceiling_light/ceiling_light_connect.test.ts","testName":"[P0][ONES:311019] 添加RGBICWW Floor Lamp(炫彩落地灯)验证","status":"todo"},
|
||||
{"kind":"add","ones":311018,"name":"添加RGBICWW Strip Light(炫彩灯带) 验证","cat":"strip_light","device":"StripLight 9S","file":"tests/strip_light/strip_light_connect.test.ts","testName":"[P0][ONES:311018] 添加RGBICWW Strip Light(炫彩灯带) 验证","status":"todo"},
|
||||
{"kind":"add","ones":310704,"name":"添加暖气阀Smart Radiator Thermostat验证","cat":"meter","device":"Meter 3M","file":"tests/meter/meter_connect.test.ts","testName":"[P0][ONES:310704] 添加暖气阀Smart Radiator Thermostat验证","status":"todo"},
|
||||
{"kind":"add","ones":310703,"name":"添加Climate Panel验证","cat":"meter","device":"Meter 3M","file":"tests/meter/meter_connect.test.ts","testName":"[P0][ONES:310703] 添加Climate Panel验证","status":"todo"},
|
||||
{"kind":"add","ones":304133,"name":"添加Lock Pro WiFi验证","cat":"lock","device":"Lock 6F","file":"tests/lock/lock_connect.test.ts","testName":"[P0][ONES:304133] 添加Lock Pro WiFi验证","status":"todo"},
|
||||
{"kind":"add","ones":304132,"name":"添加Lock Lite验证","cat":"lock","device":"Lock 6F","file":"tests/lock/lock_connect.test.ts","testName":"[P0][ONES:304132] 添加Lock Lite验证","status":"todo"},
|
||||
{"kind":"add","ones":304131,"name":"添加Keypad Vision验证","cat":"keypad","device":"Keypad 2V","file":"tests/keypad/keypad_connect.test.ts","testName":"[P0][ONES:304131] 添加Keypad Vision验证","status":"todo"},
|
||||
{"kind":"add","ones":304130,"name":"添加Lock Ultra验证","cat":"lock","device":"Lock 6F","file":"tests/lock/lock_connect.test.ts","testName":"[P0][ONES:304130] 添加Lock Ultra验证","status":"todo"},
|
||||
{"kind":"add","ones":302289,"name":"添加Garage Door Opener验证","cat":"plug","device":"Plug 4D","file":"tests/plug/plug_connect.test.ts","testName":"[P0][ONES:302289] 添加Garage Door Opener验证","status":"todo"},
|
||||
{"kind":"add","ones":302288,"name":"添加Relay Switch验证","cat":"plug","device":"Plug 4D","file":"tests/plug/plug_connect.test.ts","testName":"[P0][ONES:302288] 添加Relay Switch验证","status":"todo"},
|
||||
{"kind":"add","ones":301586,"name":"添加RGBWW Floor Lamp (落地灯)验证","cat":"ceiling_light","device":"CeilingLight 7Q","file":"tests/ceiling_light/ceiling_light_connect.test.ts","testName":"[P0][ONES:301586] 添加RGBWW Floor Lamp (落地灯)验证","status":"todo"},
|
||||
{"kind":"add","ones":301585,"name":"添加RGBWW Strip light3(彩色灯带3) 验证 ","cat":"strip_light","device":"StripLight 9S","file":"tests/strip_light/strip_light_connect.test.ts","testName":"[P0][ONES:301585] 添加RGBWW Strip light3(彩色灯带3) 验证 ","status":"todo"},
|
||||
{"kind":"add","ones":273787,"name":"添加K11+扫地机","cat":"robot","device":"Robot S1","file":"tests/robot/robot_connect.test.ts","testName":"[P0][ONES:273787] 添加K11+扫地机","status":"todo"},
|
||||
{"kind":"add","ones":267367,"name":"添加Meter pro验证","cat":"meter","device":"Meter 3M","file":"tests/meter/meter_connect.test.ts","testName":"[P0][ONES:267367] 添加Meter pro验证","status":"todo"},
|
||||
{"kind":"add","ones":267366,"name":"添加Meter pro co2验证","cat":"meter","device":"Meter 3M","file":"tests/meter/meter_connect.test.ts","testName":"[P0][ONES:267366] 添加Meter pro co2验证","status":"todo"},
|
||||
{"kind":"add","ones":265933,"name":"添加curtain 3 2025验证","cat":"curtain","device":"Curtain 1A","file":"tests/curtain/curtain_connect.test.ts","testName":"[P0][ONES:265933] 添加curtain 3 2025验证","status":"todo"},
|
||||
{"kind":"add","ones":205248,"name":"添加S20扫地机","cat":"robot","device":"Robot S1","file":"tests/robot/robot_connect.test.ts","testName":"[P0][ONES:205248] 添加S20扫地机","status":"todo"},
|
||||
{"kind":"add","ones":197389,"name":"添加Doorbell","cat":"camera","device":"摄像机Plus 3K","file":"tests/camera/camera_connect.test.ts","testName":"[P0][ONES:197389] 添加Doorbell","status":"todo"},
|
||||
{"kind":"add","ones":196802,"name":"添加空气净化器验证(基础款和table款)","cat":"air_condition","device":"AirCondition IR","file":"tests/air_condition/air_condition_connect.test.ts","testName":"[P0][ONES:196802] 添加空气净化器验证(基础款和table款)","status":"todo"},
|
||||
{"kind":"add","ones":191100,"name":"添加Roller Shade验证","cat":"curtain","device":"Curtain 1A","file":"tests/curtain/curtain_connect.test.ts","testName":"[P0][ONES:191100] 添加Roller Shade验证","status":"todo"},
|
||||
{"kind":"feature","ones":190802,"name":"同步URC设备、控制","cat":"urc","device":"URC 5Y","file":"tests/urc/urc_control.test.ts","testName":"[P0][ONES:190802] 同步URC设备、控制","status":"todo"},
|
||||
{"kind":"add","ones":190801,"name":"添加URC验证","cat":"urc","device":"URC 5Y","file":"tests/urc/urc_connect.test.ts","testName":"[P0][ONES:190801] 添加URC验证","status":"todo"},
|
||||
{"kind":"feature","ones":162264,"name":"K10+ Pro Combo解除绑定手持","cat":"robot","device":"Robot S1","file":"tests/robot/robot_control.test.ts","testName":"[P0][ONES:162264] K10+ Pro Combo解除绑定手持","status":"todo"},
|
||||
{"kind":"feature","ones":162263,"name":"K10+ Pro Combo绑定手持吸尘器","cat":"robot","device":"Robot S1","file":"tests/robot/robot_control.test.ts","testName":"[P0][ONES:162263] K10+ Pro Combo绑定手持吸尘器","status":"todo"},
|
||||
{"kind":"add","ones":162261,"name":"添加K10+ Pro Combo","cat":"robot","device":"Robot S1","file":"tests/robot/robot_connect.test.ts","testName":"[P0][ONES:162261] 添加K10+ Pro Combo","status":"todo"},
|
||||
{"kind":"add","ones":122775,"name":"添加 Circulator Fan验证(无电池款)","cat":"fan","device":"Fan 0T","file":"tests/fan/fan_connect.test.ts","testName":"[P0][ONES:122775] 添加 Circulator Fan验证(无电池款)","status":"todo"},
|
||||
{"kind":"add","ones":122737,"name":"添加K10+Pro扫地机","cat":"robot","device":"Robot S1","file":"tests/robot/robot_connect.test.ts","testName":"[P0][ONES:122737] 添加K10+Pro扫地机","status":"todo"},
|
||||
{"kind":"add","ones":113943,"name":"添加PTC Plus 5MP","cat":"camera","device":"摄像机Plus 3K","file":"tests/camera/camera_connect.test.ts","testName":"[P0][ONES:113943] 添加PTC Plus 5MP","status":"todo"},
|
||||
{"kind":"add","ones":113942,"name":"添加OSC 2K验证","cat":"osc","device":"OSC 2K","file":"tests/osc/osc_connect.test.ts","testName":"[P0][ONES:113942] 添加OSC 2K验证","status":"todo"},
|
||||
{"kind":"add","ones":113941,"name":"添加OSC验证","cat":"osc","device":"OSC 2K","file":"tests/osc/osc_connect.test.ts","testName":"[P0][ONES:113941] 添加OSC验证","status":"todo"},
|
||||
{"kind":"add","ones":105436,"name":"添加lock Pro&kit验证","cat":"lock","device":"Lock 6F","file":"tests/lock/lock_connect.test.ts","testName":"[P0][ONES:105436] 添加lock Pro&kit验证","status":"todo"},
|
||||
{"kind":"add","ones":78369,"name":"添加S10扫地机","cat":"robot","device":"Robot S1","file":"tests/robot/robot_connect.test.ts","testName":"[P0][ONES:78369] 添加S10扫地机","status":"todo"},
|
||||
{"kind":"feature","ones":74105,"name":"强绑定设备的自动解绑申请","cat":"","device":"","file":"","testName":"","status":"na"},
|
||||
{"kind":"add","ones":74104,"name":"添加Humidifier2验证","cat":"humidifier","device":"Humidifier 1K","file":"tests/humidifier/humidifier_connect.test.ts","testName":"[P0][ONES:74104] 添加Humidifier2验证","status":"todo"},
|
||||
{"kind":"feature","ones":74078,"name":"APP切换温湿度单位显示","cat":"meter","device":"Meter 3M","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:74078] APP切换温湿度单位显示","status":"todo"},
|
||||
{"kind":"add","ones":66694,"name":"添加Lock Pro验证","cat":"lock","device":"Lock 6F","file":"tests/lock/lock_connect.test.ts","testName":"[P0][ONES:66694] 添加Lock Pro验证","status":"todo"},
|
||||
{"kind":"add","ones":65336,"name":"添加Hub Mini Matter验证","cat":"hub","device":"Hub2 8H","file":"tests/hub/hub_connect.test.ts","testName":"[P0][ONES:65336] 添加Hub Mini Matter验证","status":"todo"},
|
||||
{"kind":"add","ones":51440,"name":"添加lock&kit验证","cat":"lock","device":"Lock 6F","file":"tests/lock/lock_connect.test.ts","testName":"[P0][ONES:51440] 添加lock&kit验证","status":"todo"},
|
||||
{"kind":"add","ones":48159,"name":"添加Battery Circulator Fan验证","cat":"fan","device":"Fan 0T","file":"tests/fan/fan_connect.test.ts","testName":"[P0][ONES:48159] 添加Battery Circulator Fan验证","status":"todo"},
|
||||
{"kind":"add","ones":47214,"name":"添加PTC Plus 3MP","cat":"camera","device":"摄像机Plus 3K","file":"tests/camera/camera_connect.test.ts","testName":"[P0][ONES:47214] 添加PTC Plus 3MP","status":"todo"},
|
||||
{"kind":"add","ones":40955,"name":"添加curtain 3验证","cat":"curtain","device":"Curtain 1A","file":"tests/curtain/curtain_connect.test.ts","testName":"[P0][ONES:40955] 添加curtain 3验证","status":"todo"},
|
||||
{"kind":"add","ones":40954,"name":"添加K10+扫地机","cat":"robot","device":"Robot S1","file":"tests/robot/robot_connect.test.ts","testName":"[P0][ONES:40954] 添加K10+扫地机","status":"todo"},
|
||||
{"kind":"add","ones":35871,"name":"添加IOSensor验证","cat":"sensor","device":"Contact Sensor 5O","file":"tests/sensor/sensor_connect.test.ts","testName":"[P0][ONES:35871] 添加IOSensor验证","status":"todo"},
|
||||
{"kind":"add","ones":21787,"name":"添加Hub2 验证","cat":"hub","device":"Hub2 8H","file":"tests/hub/hub_connect.test.ts","testName":"[P0][ONES:21787] 添加Hub2 验证","status":"todo"},
|
||||
{"kind":"feature","ones":15973,"name":"keypad、keypad touch添加密码、指纹、卡片验证","cat":"keypad","device":"Keypad 2V","file":"tests/keypad/keypad_control.test.ts","testName":"[P0][ONES:15973] keypad、keypad touch添加密码、指纹、卡片验证","status":"todo"},
|
||||
{"kind":"add","ones":15972,"name":"添加Hub Mini验证","cat":"hub","device":"Hub2 8H","file":"tests/hub/hub_connect.test.ts","testName":"[P0][ONES:15972] 添加Hub Mini验证","status":"todo"},
|
||||
{"kind":"add","ones":15971,"name":"添加Hub Plus验证","cat":"hub","device":"Hub2 8H","file":"tests/hub/hub_connect.test.ts","testName":"[P0][ONES:15971] 添加Hub Plus验证","status":"todo"},
|
||||
{"kind":"add","ones":15970,"name":"添加Lock验证","cat":"lock","device":"Lock 6F","file":"tests/lock/lock_connect.test.ts","testName":"[P0][ONES:15970] 添加Lock验证","status":"todo"},
|
||||
{"kind":"add","ones":15969,"name":"添加Curtain验证","cat":"curtain","device":"Curtain 1A","file":"tests/curtain/curtain_connect.test.ts","testName":"[P0][ONES:15969] 添加Curtain验证","status":"todo"},
|
||||
{"kind":"add","ones":15968,"name":"添加Bot验证","cat":"bot","device":"Bot 0F","file":"tests/bot/bot_connect.test.ts","testName":"[P0][ONES:15968] 添加Bot验证","status":"todo"},
|
||||
{"kind":"add","ones":15967,"name":"添加Remote验证","cat":"remote","device":"Remote 3W","file":"tests/remote/remote_connect.test.ts","testName":"[P0][ONES:15967] 添加Remote验证","status":"todo"},
|
||||
{"kind":"add","ones":15966,"name":"添加Plug验证","cat":"plug","device":"Plug 4D","file":"tests/plug/plug_connect.test.ts","testName":"[P0][ONES:15966] 添加Plug验证","status":"todo"},
|
||||
{"kind":"add","ones":15965,"name":"添加Meter验证","cat":"meter","device":"Meter 3M","file":"tests/meter/meter_connect.test.ts","testName":"[P0][ONES:15965] 添加Meter验证","status":"todo"},
|
||||
{"kind":"add","ones":15964,"name":"添加Humidifier验证","cat":"humidifier","device":"Humidifier 1K","file":"tests/humidifier/humidifier_connect.test.ts","testName":"[P0][ONES:15964] 添加Humidifier验证","status":"todo"},
|
||||
{"kind":"add","ones":15963,"name":"添加Motion Sensor验证","cat":"sensor","device":"Contact Sensor 5O","file":"tests/sensor/sensor_connect.test.ts","testName":"[P0][ONES:15963] 添加Motion Sensor验证","status":"todo"},
|
||||
{"kind":"add","ones":15962,"name":"添加Contact Sensor验证","cat":"sensor","device":"Contact Sensor 5O","file":"tests/sensor/sensor_connect.test.ts","testName":"[P0][ONES:15962] 添加Contact Sensor验证","status":"todo"},
|
||||
{"kind":"add","ones":15961,"name":"添加Indoor Cam验证","cat":"camera","device":"摄像机Plus 3K","file":"tests/camera/camera_connect.test.ts","testName":"[P0][ONES:15961] 添加Indoor Cam验证","status":"todo"},
|
||||
{"kind":"add","ones":15960,"name":"添加Color Bulb验证","cat":"color_bulb","device":"ColorBulb 8R","file":"tests/color_bulb/color_bulb_connect.test.ts","testName":"[P0][ONES:15960] 添加Color Bulb验证","status":"todo"},
|
||||
{"kind":"add","ones":15959,"name":"添加Strip Light验证","cat":"strip_light","device":"StripLight 9S","file":"tests/strip_light/strip_light_connect.test.ts","testName":"[P0][ONES:15959] 添加Strip Light验证","status":"todo"},
|
||||
{"kind":"add","ones":15958,"name":"添加Plug Mini验证(JP和US)","cat":"plug","device":"Plug 4D","file":"tests/plug/plug_connect.test.ts","testName":"[P0][ONES:15958] 添加Plug Mini验证(JP和US)","status":"todo"},
|
||||
{"kind":"add","ones":15957,"name":"添加Pan/Tilt Cam验证","cat":"camera","device":"摄像机Plus 3K","file":"tests/camera/camera_connect.test.ts","testName":"[P0][ONES:15957] 添加Pan/Tilt Cam验证","status":"todo"},
|
||||
{"kind":"add","ones":15956,"name":"添加Meter Plus验证","cat":"meter","device":"Meter 3M","file":"tests/meter/meter_connect.test.ts","testName":"[P0][ONES:15956] 添加Meter Plus验证","status":"todo"},
|
||||
{"kind":"feature","ones":15955,"name":"智能匹配遥控器","cat":"remote","device":"Remote 3W","file":"tests/remote/remote_control.test.ts","testName":"[P0][ONES:15955] 智能匹配遥控器","status":"todo"},
|
||||
{"kind":"feature","ones":15954,"name":"学习自定义遥控器","cat":"remote","device":"Remote 3W","file":"tests/remote/remote_control.test.ts","testName":"[P0][ONES:15954] 学习自定义遥控器","status":"todo"},
|
||||
{"kind":"feature","ones":15953,"name":"学习others遥控器","cat":"remote","device":"Remote 3W","file":"tests/remote/remote_control.test.ts","testName":"[P0][ONES:15953] 学习others遥控器","status":"todo"},
|
||||
{"kind":"add","ones":15952,"name":"添加Robot Vacuum S1验证","cat":"robot","device":"Robot S1","file":"tests/robot/robot_connect.test.ts","testName":"[P0][ONES:15952] 添加Robot Vacuum S1验证","status":"todo"},
|
||||
{"kind":"add","ones":15951,"name":"添加Robot Vacuum S1 Plus验证","cat":"robot","device":"Robot S1","file":"tests/robot/robot_connect.test.ts","testName":"[P0][ONES:15951] 添加Robot Vacuum S1 Plus验证","status":"todo"},
|
||||
{"kind":"add","ones":15950,"name":"添加吸顶灯验证(Pro和lte)","cat":"ceiling_light","device":"CeilingLight 7Q","file":"tests/ceiling_light/ceiling_light_connect.test.ts","testName":"[P0][ONES:15950] 添加吸顶灯验证(Pro和lte)","status":"todo"},
|
||||
{"kind":"add","ones":15949,"name":"添加keypad验证","cat":"keypad","device":"Keypad 2V","file":"tests/keypad/keypad_connect.test.ts","testName":"[P0][ONES:15949] 添加keypad验证","status":"todo"},
|
||||
{"kind":"add","ones":15948,"name":"添加keypad touch验证","cat":"keypad","device":"Keypad 2V","file":"tests/keypad/keypad_connect.test.ts","testName":"[P0][ONES:15948] 添加keypad touch验证","status":"todo"},
|
||||
{"kind":"add","ones":15947,"name":"添加Pan/Tilt Cam 2K验证","cat":"camera","device":"摄像机Plus 3K","file":"tests/camera/camera_connect.test.ts","testName":"[P0][ONES:15947] 添加Pan/Tilt Cam 2K验证","status":"todo"},
|
||||
{"kind":"add","ones":15946,"name":"添加Blind Tilt验证","cat":"curtain","device":"Curtain 1A","file":"tests/curtain/curtain_connect.test.ts","testName":"[P0][ONES:15946] 添加Blind Tilt验证","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"6e4uZSVe","proto":"ble","name":"点击控制Bot 不加密开&不加密关&加密按压","result":"对应Bot固件响应动作","cat":"bot","device":"Bot 0F","action":"点击控制Bot 不加密开&不加密关&加密按压","file":"tests/bot/bot_control.test.ts","testName":"[P0][ONES:15975#6e4uZSVe][ble] 点击控制Bot 不加密开&不加密关&加密按压 Bot 0F","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"5YEfmpJz","proto":"ble","name":"点击控制Lock 开锁/解锁","result":"对应Lock固件响应动作开锁/解锁","cat":"lock","device":"Lock 6F","action":"点击控制Lock 开锁/解锁","file":"tests/lock/lock_control.test.ts","testName":"[P0][ONES:15975#5YEfmpJz][ble] 点击控制Lock 开锁/解锁 Lock 6F","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"774V8bND","proto":"ble","name":"Curtain---快捷弹窗---开/关","result":"对应Curtain固件响应动作开/关","cat":"curtain","device":"Curtain 1A","action":"Curtain---快捷弹窗---开/关","file":"tests/curtain/curtain_control.test.ts","testName":"[P0][ONES:15975#774V8bND][ble] Curtain---快捷弹窗---开/关 Curtain 1A","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"NvpUv9Tt","proto":"ble","name":"Meter---历史数据---右上角设置---报警条件,开启温度和湿度报警,拉动滑条使当前温湿度在报警范围内","result":"历史数据能够加载,不会有特别异常的数据图表,主页对应Meter温湿度字体变红色","cat":"meter","device":"Meter 3M","action":"Meter---历史数据---右上角设置---报","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:15975#NvpUv9Tt][ble] Meter---历史数据---右上角设置---报 Meter 3M","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"3Jm5FAdW","proto":"ble","name":"Meter---历史数据---右上角设置---校正---校准温度---下一步,调整温度范围增加温度","result":"温度校正过程中对应Meter固件的温度值也相应变化","cat":"meter","device":"Meter 3M","action":"Meter---历史数据---右上角设置---校","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:15975#3Jm5FAdW][ble] Meter---历史数据---右上角设置---校 Meter 3M","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"QfYLdj8s","proto":"ble","name":"执行步骤5至校正---校正湿度---下一步,调整湿度范围增加湿度","result":"湿度校正过程中对应Meter固件的湿度值也相应变化","cat":"humidifier","device":"Humidifier 1K","action":"执行步骤5至校正---校正湿度---下一步,调整","file":"tests/humidifier/humidifier_control.test.ts","testName":"[P0][ONES:15975#QfYLdj8s][ble] 执行步骤5至校正---校正湿度---下一步,调整 Humidifier 1K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"GkkrZpaC","proto":"ble","name":"Humidifier---快捷弹窗---开启加湿器自动/低/中/高档/关闭","result":"对应Humidifier固件响应动作开启加湿器自动/低/中/高档/关闭","cat":"humidifier","device":"Humidifier 1K","action":"Humidifier---快捷弹窗---开启加湿","file":"tests/humidifier/humidifier_control.test.ts","testName":"[P0][ONES:15975#GkkrZpaC][ble] Humidifier---快捷弹窗---开启加湿 Humidifier 1K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"TNhSAuUC","proto":"ble","name":"点击控制Bulb 开/关","result":"对应Bulb固件响应动作开/关","cat":"color_bulb","device":"ColorBulb 8R","action":"点击控制Bulb 开/关","file":"tests/color_bulb/color_bulb_control.test.ts","testName":"[P0][ONES:15975#TNhSAuUC][ble] 点击控制Bulb 开/关 ColorBulb 8R","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"4poyiVwD","proto":"ble","name":"点击控制Strip Light开/关","result":"对应Strip Light固件响应动作开/关","cat":"strip_light","device":"StripLight 9S","action":"点击控制Strip Light开/关","file":"tests/strip_light/strip_light_control.test.ts","testName":"[P0][ONES:15975#4poyiVwD][ble] 点击控制Strip Light开/关 StripLight 9S","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"2528azD6","proto":"ble","name":"点击控制吸顶灯pro开/关灯","result":"对应吸顶灯pro固件响应动作开/关灯","cat":"ceiling_light","device":"CeilingLight 7Q","action":"点击控制吸顶灯pro开/关灯","file":"tests/ceiling_light/ceiling_light_control.test.ts","testName":"[P0][ONES:15975#2528azD6][ble] 点击控制吸顶灯pro开/关灯 CeilingLight 7Q","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"V15M64cP","proto":"ble","name":"点击控制吸顶灯lte开/关灯","result":"对应吸顶灯lte固件响应动作开/关灯","cat":"ceiling_light","device":"CeilingLight 7Q","action":"点击控制吸顶灯lte开/关灯","file":"tests/ceiling_light/ceiling_light_control.test.ts","testName":"[P0][ONES:15975#V15M64cP][ble] 点击控制吸顶灯lte开/关灯 CeilingLight 7Q","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"2jSFfY4k","proto":"ble","name":"点击控制Plug Mini开/关","result":"对应Plug Mini固件响应动作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Plug Mini开/关","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15975#2jSFfY4k][ble] 点击控制Plug Mini开/关 Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"UZCtZRPy","proto":"ble","name":"Meter Plus---历史数据---右上角设置---报警条件,开启温度和湿度报警,拉动滑条使当前温湿度在报警范围内","result":"历史数据能够加载,不会有特别异常的数据图表,主页对应Meter Plus温湿度字体变红色","cat":"meter","device":"Meter 3M","action":"Meter Plus---历史数据---右上角设","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:15975#UZCtZRPy][ble] Meter Plus---历史数据---右上角设 Meter 3M","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"9mheLMDJ","proto":"ble","name":"Meter Plus---历史数据---右上角设置---校正---校准温度---下一步,调整温度范围增加温度","result":"温度校正过程中对应Meter Plus固件的温度值也相应变化","cat":"meter","device":"Meter 3M","action":"Meter Plus---历史数据---右上角设","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:15975#9mheLMDJ][ble] Meter Plus---历史数据---右上角设 Meter 3M","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"GSZv8BYm","proto":"ble","name":"执行步骤14至校正---校正湿度---下一步,调整湿度范围增加湿度","result":"湿度校正过程中对应Meter Plus固件的湿度值也相应变化","cat":"humidifier","device":"Humidifier 1K","action":"执行步骤14至校正---校正湿度---下一步,调","file":"tests/humidifier/humidifier_control.test.ts","testName":"[P0][ONES:15975#GSZv8BYm][ble] 执行步骤14至校正---校正湿度---下一步,调 Humidifier 1K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"HrigrD2X","proto":"ble","name":"Motion Sensor---功能页面,在Motion Sensor固件前移动","result":"功能页面正常显示有人移动","cat":"sensor","device":"Contact Sensor 5O","action":"Motion Sensor---功能页面,在Mo","file":"tests/sensor/sensor_control.test.ts","testName":"[P0][ONES:15975#HrigrD2X][ble] Motion Sensor---功能页面,在Mo Contact Sensor 5O","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"A8C4BsCm","proto":"ble","name":"Contact Sensor---功能页面,开关Contact Sensor固件","result":"功能页面正常显示Contact Sensor的开关状态","cat":"sensor","device":"Contact Sensor 5O","action":"Contact Sensor---功能页面,开关","file":"tests/sensor/sensor_control.test.ts","testName":"[P0][ONES:15975#A8C4BsCm][ble] Contact Sensor---功能页面,开关 Contact Sensor 5O","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"3frMRDZj","proto":"ble","name":"点击控制固件V20版本BlindTilt首页按钮","result":"响应到正确开合状态","cat":"curtain","device":"Curtain 1A","action":"点击控制固件V20版本BlindTilt首页按钮","file":"tests/curtain/curtain_control.test.ts","testName":"[P0][ONES:15975#3frMRDZj][ble] 点击控制固件V20版本BlindTilt首页按钮 Curtain 1A","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"Qbm9VB72","proto":"ble","name":"点击控制固件V12版本BlindTil功能页按钮(验证兼容旧版本)","result":"响应到正确开合状态","cat":"curtain","device":"Curtain 1A","action":"点击控制固件V12版本BlindTil功能页按钮","file":"tests/curtain/curtain_control.test.ts","testName":"[P0][ONES:15975#Qbm9VB72][ble] 点击控制固件V12版本BlindTil功能页按钮 Curtain 1A","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"QwnZJARA","proto":"ble","name":"IOSensor---历史数据---右上角设置---报警条件,开启温度和湿度报警,拉动滑条使当前温湿度在报警范围内","result":"历史数据能够加载,不会有特别异常的数据图表,主页对应IOSensor温湿度字体变红色","cat":"sensor","device":"Contact Sensor 5O","action":"IOSensor---历史数据---右上角设置-","file":"tests/sensor/sensor_control.test.ts","testName":"[P0][ONES:15975#QwnZJARA][ble] IOSensor---历史数据---右上角设置- Contact Sensor 5O","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"Ncg7BDeR","proto":"ble","name":"IOSensor---历史数据---右上角设置---校正---校准温度---下一步,调整温度范围增加温度","result":"温度校正过程中对应IOSensor固件的温度值也相应变化","cat":"sensor","device":"Contact Sensor 5O","action":"IOSensor---历史数据---右上角设置-","file":"tests/sensor/sensor_control.test.ts","testName":"[P0][ONES:15975#Ncg7BDeR][ble] IOSensor---历史数据---右上角设置- Contact Sensor 5O","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"k6cqKqtZ","proto":"ble","name":"执行步骤21至校正---校正湿度---下一步,调整湿度范围增加湿度","result":"湿度校正过程中对应IOSensor固件的湿度值也相应变化","cat":"humidifier","device":"Humidifier 1K","action":"执行步骤21至校正---校正湿度---下一步,调","file":"tests/humidifier/humidifier_control.test.ts","testName":"[P0][ONES:15975#k6cqKqtZ][ble] 执行步骤21至校正---校正湿度---下一步,调 Humidifier 1K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"QqsnPMc3","proto":"ble","name":"Curtain 3---快捷弹窗---开/关","result":"对应Curtain 3固件响应动作开/关","cat":"curtain","device":"Curtain 1A","action":"Curtain 3---快捷弹窗---开/关","file":"tests/curtain/curtain_control.test.ts","testName":"[P0][ONES:15975#QqsnPMc3][ble] Curtain 3---快捷弹窗---开/关 Curtain 1A","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"CiFPEbNS","proto":"ble","name":"带电池款Battery Circulator Fan---首页控制开关","result":"对应循环扇开关","cat":"fan","device":"Fan 0T","action":"带电池款Battery Circulator F","file":"tests/fan/fan_control.test.ts","testName":"[P0][ONES:15975#CiFPEbNS][ble] 带电池款Battery Circulator F Fan 0T","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"LnPNEJmS","proto":"ble","name":"点击控制Lock Pro开锁/解锁","result":"对应Lock Pro固件响应动作开锁/解锁","cat":"lock","device":"Lock 6F","action":"点击控制Lock Pro开锁/解锁","file":"tests/lock/lock_control.test.ts","testName":"[P0][ONES:15975#LnPNEJmS][ble] 点击控制Lock Pro开锁/解锁 Lock 6F","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"3PmZMk1s","proto":"ble","name":"进入Humidifier2设置页,绑定温湿度计,点击切换档位、模式","result":"绑定成功,功能页显示对应湿度值;切换档位、模式、目标湿度正常。","cat":"humidifier","device":"Humidifier 1K","action":"进入Humidifier2设置页,绑定温湿度计,","file":"tests/humidifier/humidifier_control.test.ts","testName":"[P0][ONES:15975#3PmZMk1s][ble] 进入Humidifier2设置页,绑定温湿度计, Humidifier 1K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"JBszgxRS","proto":"ble","name":"无电池款Circulator Fan---首页控制开关","result":"对应循环扇开关","cat":"fan","device":"Fan 0T","action":"无电池款Circulator Fan---首页控","file":"tests/fan/fan_control.test.ts","testName":"[P0][ONES:15975#JBszgxRS][ble] 无电池款Circulator Fan---首页控 Fan 0T","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"SmzuHtWi","proto":"ble","name":"Roller Shade功能页控制设备全开、暂停、全关、任意百分比控制","result":"Roller Shade正常响应所有控制指令","cat":"curtain","device":"Curtain 1A","action":"Roller Shade功能页控制设备全开、暂停","file":"tests/curtain/curtain_control.test.ts","testName":"[P0][ONES:15975#SmzuHtWi][ble] Roller Shade功能页控制设备全开、暂停 Curtain 1A","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"7qxA1ajE","proto":"ble","name":"点击控制美规基础款空气净化器--开/关","result":"对应美规基础款空气净化器开关","cat":"air_condition","device":"AirCondition IR","action":"点击控制美规基础款空气净化器--开/关","file":"tests/air_condition/air_condition_control.test.ts","testName":"[P0][ONES:15975#7qxA1ajE][ble] 点击控制美规基础款空气净化器--开/关 AirCondition IR","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"R5PDp8BR","proto":"ble","name":"点击控制日规Table空气净化器--开/关","result":"对应日规Table空气净化器开/关","cat":"air_condition","device":"AirCondition IR","action":"点击控制日规Table空气净化器--开/关","file":"tests/air_condition/air_condition_control.test.ts","testName":"[P0][ONES:15975#R5PDp8BR][ble] 点击控制日规Table空气净化器--开/关 AirCondition IR","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"3wM6fW8W","proto":"ble","name":"Curtain 3 2025---快捷弹窗---开/关","result":"对应Curtain 3 2025固件响应动作开/关","cat":"curtain","device":"Curtain 1A","action":"Curtain 3 2025---快捷弹窗---","file":"tests/curtain/curtain_control.test.ts","testName":"[P0][ONES:15975#3wM6fW8W][ble] Curtain 3 2025---快捷弹窗--- Curtain 1A","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"JggzNXDo","proto":"ble","name":"Meter pro---图表数据---右上角设置---报警条件,开启温度和湿度报警,拉动滑条使当前温湿度在报警范围内","result":"图表数据能够加载,不会有特别异常的数据图表,主页对应Meter Pro温湿度字体变红色","cat":"meter","device":"Meter 3M","action":"Meter pro---图表数据---右上角设置","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:15975#JggzNXDo][ble] Meter pro---图表数据---右上角设置 Meter 3M","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"L967RGHa","proto":"ble","name":"Meter pro---设置页--校正---校准温度---下一步,调整温度范围增加温度","result":"温度校正过程中对应Meter Pro固件的温度值也相应变化","cat":"meter","device":"Meter 3M","action":"Meter pro---设置页--校正---校准","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:15975#L967RGHa][ble] Meter pro---设置页--校正---校准 Meter 3M","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"8Wzaj5pB","proto":"ble","name":"校正---校正湿度---下一步,调整湿度范围增加湿度","result":"湿度校正过程中对应Meter Pro固件的湿度值也相应变化","cat":"humidifier","device":"Humidifier 1K","action":"校正---校正湿度---下一步,调整湿度范围增加","file":"tests/humidifier/humidifier_control.test.ts","testName":"[P0][ONES:15975#8Wzaj5pB][ble] 校正---校正湿度---下一步,调整湿度范围增加 Humidifier 1K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"3bYsmiZG","proto":"ble","name":"Meter pro co2---图表数据---右上角设置---报警条件,开启温度和湿度报警,拉动滑条使当前温湿度在报警范围内","result":"图表数据能够加载,不会有特别异常的数据图表,主页对应Meter Pro co2温湿度字体变红色","cat":"meter","device":"Meter 3M","action":"Meter pro co2---图表数据---右","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:15975#3bYsmiZG][ble] Meter pro co2---图表数据---右 Meter 3M","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"NDqRQxVx","proto":"ble","name":"Meter pro co2---设置页--校正---校准温度---下一步,调整温度范围增加温度","result":"温度校正过程中对应Meter Pro co2固件的温度值也相应变化","cat":"meter","device":"Meter 3M","action":"Meter pro co2---设置页--校正-","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:15975#NDqRQxVx][ble] Meter pro co2---设置页--校正- Meter 3M","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"DxKG9Acs","proto":"ble","name":"校正---校正湿度---下一步,调整湿度范围增加湿度","result":"湿度校正过程中对应Meter Pro co2固件的湿度值也相应变化","cat":"humidifier","device":"Humidifier 1K","action":"校正---校正湿度---下一步,调整湿度范围增加","file":"tests/humidifier/humidifier_control.test.ts","testName":"[P0][ONES:15975#DxKG9Acs][ble] 校正---校正湿度---下一步,调整湿度范围增加 Humidifier 1K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"3ob75cjf","proto":"ble","name":"点击控制RGBWW 彩色灯带3 --开/关","result":"对应RGBWW 彩色灯带3 --固件响应动作开/关","cat":"strip_light","device":"StripLight 9S","action":"点击控制RGBWW 彩色灯带3 --开/关","file":"tests/strip_light/strip_light_control.test.ts","testName":"[P0][ONES:15975#3ob75cjf][ble] 点击控制RGBWW 彩色灯带3 --开/关 StripLight 9S","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"JVnRz7xY","proto":"ble","name":"点击控制RGBWW 落地灯 --开/关","result":"对应RGBWW 落地灯 --固件响应动作开/关","cat":"ceiling_light","device":"CeilingLight 7Q","action":"点击控制RGBWW 落地灯 --开/关","file":"tests/ceiling_light/ceiling_light_control.test.ts","testName":"[P0][ONES:15975#JVnRz7xY][ble] 点击控制RGBWW 落地灯 --开/关 CeilingLight 7Q","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"MKgLq1oy","proto":"ble","name":"点击控制Relay switch 1pm","result":"对应Relay Switch 1pm -固件响应动作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Relay switch 1pm","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15975#MKgLq1oy][ble] 点击控制Relay switch 1pm Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"A1TLxsFw","proto":"ble","name":"点击控制Relay switch 1","result":"对应Relay Switch 1-固件响应动作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Relay switch 1","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15975#A1TLxsFw][ble] 点击控制Relay switch 1 Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"PG62s725","proto":"ble","name":"点击控制Relay switch 2pm-开关控制器","result":"对应Relay Switch 2pm开关控制器-固件响应动作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Relay switch 2pm-开关控","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15975#PG62s725][ble] 点击控制Relay switch 2pm-开关控 Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"AafJ5tj5","proto":"ble","name":"点击控制Relay switch 2pm-覆盖控制器","result":"对应Relay Switch 2pm覆盖控制器-固件响应动作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Relay switch 2pm-覆盖控","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15975#AafJ5tj5][ble] 点击控制Relay switch 2pm-覆盖控 Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"E5YRm4Pg","proto":"ble","name":"点击控制Garage Door Opener","result":"对应Garage Door Opener-固件响应动作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Garage Door Opener","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15975#E5YRm4Pg][ble] 点击控制Garage Door Opener Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"QAHNrXrm","proto":"ble","name":"点击控制RGBICWW Strip Light (炫彩灯带)--开/关","result":"对应RGBICWW Strip Light (炫彩灯带)--固件响应动作开/关","cat":"strip_light","device":"StripLight 9S","action":"点击控制RGBICWW Strip Light ","file":"tests/strip_light/strip_light_control.test.ts","testName":"[P0][ONES:15975#QAHNrXrm][ble] 点击控制RGBICWW Strip Light StripLight 9S","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"AfxmoAEq","proto":"ble","name":"点击控制RGBICWW Floor Lamp(炫彩落地灯)--开/关","result":"对应RGBICWW Floor Lamp(炫彩落地灯)--固件响应动作开/关","cat":"ceiling_light","device":"CeilingLight 7Q","action":"点击控制RGBICWW Floor Lamp(炫","file":"tests/ceiling_light/ceiling_light_control.test.ts","testName":"[P0][ONES:15975#AfxmoAEq][ble] 点击控制RGBICWW Floor Lamp(炫 CeilingLight 7Q","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"E2QCS8xy","proto":"ble","name":"点击控制RGBIC Neon Wire Rope Light(炫彩钢丝霓虹灯)--开/关","result":"对应RGBIC Neon Wire Rope Light(炫彩钢丝霓虹灯)--固件响应动作开/关","cat":"strip_light","device":"StripLight 9S","action":"点击控制RGBIC Neon Wire Rope","file":"tests/strip_light/strip_light_control.test.ts","testName":"[P0][ONES:15975#E2QCS8xy][ble] 点击控制RGBIC Neon Wire Rope StripLight 9S","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"TSVKWcJ3","proto":"ble","name":"点击控制eu plug--开/关","result":"对应设备响应开/关","cat":"plug","device":"Plug 4D","action":"点击控制eu plug--开/关","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15975#TSVKWcJ3][ble] 点击控制eu plug--开/关 Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15975,"step":"XWmNA9UL","proto":"ble","name":"Standing Circulator fan--首页控制开关","result":"对应落地扇响应开/关","cat":"fan","device":"Fan 0T","action":"Standing Circulator fan-","file":"tests/fan/fan_control.test.ts","testName":"[P0][ONES:15975#XWmNA9UL][ble] Standing Circulator fan- Fan 0T","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"Qt4hcgB4","proto":"wifi","name":"点击控制Bot 不加密开&不加密关&加密按压","result":"对应Bot固件响应动作","cat":"bot","device":"Bot 0F","action":"点击控制Bot 不加密开&不加密关&加密按压","file":"tests/bot/bot_control.test.ts","testName":"[P0][ONES:15974#Qt4hcgB4][wifi] 点击控制Bot 不加密开&不加密关&加密按压 Bot 0F","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"7cEkbh4Y","proto":"wifi","name":"点击控制Lock 开锁/解锁","result":"对应Lock固件响应动作开锁/解锁","cat":"lock","device":"Lock 6F","action":"点击控制Lock 开锁/解锁","file":"tests/lock/lock_control.test.ts","testName":"[P0][ONES:15974#7cEkbh4Y][wifi] 点击控制Lock 开锁/解锁 Lock 6F","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"GPT3wJd9","proto":"wifi","name":"Curtain---快捷弹窗---开/关","result":"对应Curtain固件响应动作开/关","cat":"curtain","device":"Curtain 1A","action":"Curtain---快捷弹窗---开/关","file":"tests/curtain/curtain_control.test.ts","testName":"[P0][ONES:15974#GPT3wJd9][wifi] Curtain---快捷弹窗---开/关 Curtain 1A","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"63kDVNx1","proto":"wifi","name":"Meter---历史数据---右上角设置---报警条件,开启温度和湿度报警,拉动滑条使当前温湿度在报警范围内","result":"历史数据能够加载,不会有特别异常的数据图表,主页对应Meter温湿度字体变红色","cat":"meter","device":"Meter 3M","action":"Meter---历史数据---右上角设置---报","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:15974#63kDVNx1][wifi] Meter---历史数据---右上角设置---报 Meter 3M","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"8yGTCPwY","proto":"wifi","name":"Meter Plus---历史数据---右上角设置---报警条件,开启温度和湿度报警,拉动滑条使当前温湿度在报警范围内","result":"历史数据能够加载,不会有特别异常的数据图表,主页对应Meter Plus温湿度字体变红色","cat":"meter","device":"Meter 3M","action":"Meter Plus---历史数据---右上角设","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:15974#8yGTCPwY][wifi] Meter Plus---历史数据---右上角设 Meter 3M","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"Ay79Pj23","proto":"wifi","name":"点击控制Plug 开/关","result":"对应Plug固件响应动作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Plug 开/关","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15974#Ay79Pj23][wifi] 点击控制Plug 开/关 Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"8tx3Rg76","proto":"wifi","name":"Humidifier---快捷弹窗---开启加湿器自动/低/中/高档/关闭","result":"对应Humidifier固件响应动作开启加湿器自动/低/中/高档/关闭","cat":"humidifier","device":"Humidifier 1K","action":"Humidifier---快捷弹窗---开启加湿","file":"tests/humidifier/humidifier_control.test.ts","testName":"[P0][ONES:15974#8tx3Rg76][wifi] Humidifier---快捷弹窗---开启加湿 Humidifier 1K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"Kaj2UdKf","proto":"wifi","name":"点击控制Bulb 开/关","result":"对应Bulb固件响应动作开/关","cat":"color_bulb","device":"ColorBulb 8R","action":"点击控制Bulb 开/关","file":"tests/color_bulb/color_bulb_control.test.ts","testName":"[P0][ONES:15974#Kaj2UdKf][wifi] 点击控制Bulb 开/关 ColorBulb 8R","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"QgX4dX7X","proto":"wifi","name":"点击控制Strip Light开/关","result":"对应Strip Light固件响应动作开/关","cat":"strip_light","device":"StripLight 9S","action":"点击控制Strip Light开/关","file":"tests/strip_light/strip_light_control.test.ts","testName":"[P0][ONES:15974#QgX4dX7X][wifi] 点击控制Strip Light开/关 StripLight 9S","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"3nXsFW9n","proto":"wifi","name":"点击控制Plug Mini开/关","result":"对应Plug Mini固件响应动作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Plug Mini开/关","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15974#3nXsFW9n][wifi] 点击控制Plug Mini开/关 Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"N7fSHmH3","proto":"wifi","name":"Motion Sensor---功能页面,在Motion Sensor固件前移动","result":"功能页面正常显示有人移动","cat":"sensor","device":"Contact Sensor 5O","action":"Motion Sensor---功能页面,在Mo","file":"tests/sensor/sensor_control.test.ts","testName":"[P0][ONES:15974#N7fSHmH3][wifi] Motion Sensor---功能页面,在Mo Contact Sensor 5O","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"JKSgHHbC","proto":"wifi","name":"Contact Sensor---功能页面,开关Contact Sensor固件","result":"功能页面正常显示Contact Sensor的开关状态","cat":"sensor","device":"Contact Sensor 5O","action":"Contact Sensor---功能页面,开关","file":"tests/sensor/sensor_control.test.ts","testName":"[P0][ONES:15974#JKSgHHbC][wifi] Contact Sensor---功能页面,开关 Contact Sensor 5O","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"4oeeJEV8","proto":"wifi","name":"点击Indoor Cam,出流成功后在页面停留3min","result":"跳转功能页面,能够正常加载视频流","cat":"camera","device":"摄像机Plus 3K","action":"点击Indoor Cam,出流成功后在页面停留3","file":"tests/camera/camera_control.test.ts","testName":"[P0][ONES:15974#4oeeJEV8][wifi] 点击Indoor Cam,出流成功后在页面停留3 摄像机Plus 3K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"16szaUdD","proto":"wifi","name":"点击Pan/Tilt Cam,出流成功后在页面停留3min","result":"跳转功能页面,能够正常加载视频流,画面显示正常,时间水印正常跳动","cat":"camera","device":"摄像机Plus 3K","action":"点击Pan/Tilt Cam,出流成功后在页面停","file":"tests/camera/camera_control.test.ts","testName":"[P0][ONES:15974#16szaUdD][wifi] 点击Pan/Tilt Cam,出流成功后在页面停 摄像机Plus 3K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"Hj5Z9jSf","proto":"wifi","name":"点击Pan/Tilt Cam 2K,出流成功后在页面停留3min","result":"跳转功能页面,能够正常加载视频流,画面显示正常,时间水印正常跳动","cat":"camera","device":"摄像机Plus 3K","action":"点击Pan/Tilt Cam 2K,出流成功后在","file":"tests/camera/camera_control.test.ts","testName":"[P0][ONES:15974#Hj5Z9jSf][wifi] 点击Pan/Tilt Cam 2K,出流成功后在 摄像机Plus 3K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"SaKmFWLw","proto":"wifi","name":"点击Pan/Tilt Cam Plus 3MP,出流成功后在页面停留3min","result":"跳转功能页面,能够正常加载视频流,画面显示正常,时间水印正常跳动","cat":"camera","device":"摄像机Plus 3K","action":"点击Pan/Tilt Cam Plus 3MP,","file":"tests/camera/camera_control.test.ts","testName":"[P0][ONES:15974#SaKmFWLw][wifi] 点击Pan/Tilt Cam Plus 3MP, 摄像机Plus 3K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"LDqSDPPT","proto":"wifi","name":"点击Pan/Tilt Cam Plus 5MP,出流成功后在页面停留3min","result":"跳转功能页面,能够正常加载视频流,画面显示正常,时间水印正常跳动","cat":"camera","device":"摄像机Plus 3K","action":"点击Pan/Tilt Cam Plus 5MP,","file":"tests/camera/camera_control.test.ts","testName":"[P0][ONES:15974#LDqSDPPT][wifi] 点击Pan/Tilt Cam Plus 5MP, 摄像机Plus 3K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"HoDdVUm1","proto":"wifi","name":"点击OSC,出流成功后在页面停留3min","result":"跳转功能页面,能够正常加载视频流,画面显示正常,时间水印正常跳动","cat":"osc","device":"OSC 2K","action":"点击OSC,出流成功后在页面停留3min","file":"tests/osc/osc_control.test.ts","testName":"[P0][ONES:15974#HoDdVUm1][wifi] 点击OSC,出流成功后在页面停留3min OSC 2K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"WH7kyypF","proto":"wifi","name":"点击OSC 2K,出流成功后在页面停留3min","result":"跳转功能页面,能够正常加载视频流,画面显示正常,时间水印正常跳动","cat":"osc","device":"OSC 2K","action":"点击OSC 2K,出流成功后在页面停留3min","file":"tests/osc/osc_control.test.ts","testName":"[P0][ONES:15974#WH7kyypF][wifi] 点击OSC 2K,出流成功后在页面停留3min OSC 2K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"J8BQ1p9G","proto":"wifi","name":"点击Doorbell,出流成功后在页面停留3min","result":"跳转功能页面,能够正常加载视频流,画面显示正常,时间水印正常跳动","cat":"camera","device":"摄像机Plus 3K","action":"点击Doorbell,出流成功后在页面停留3mi","file":"tests/camera/camera_control.test.ts","testName":"[P0][ONES:15974#J8BQ1p9G][wifi] 点击Doorbell,出流成功后在页面停留3mi 摄像机Plus 3K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"KNk1HaB2","proto":"wifi","name":"点击Outdoor PTC,出流成功后在页面停留3min","result":"跳转功能页面,能够正常加载视频流,画面显示正常,时间水印正常跳动","cat":"camera","device":"摄像机Plus 3K","action":"点击Outdoor PTC,出流成功后在页面停留","file":"tests/camera/camera_control.test.ts","testName":"[P0][ONES:15974#KNk1HaB2][wifi] 点击Outdoor PTC,出流成功后在页面停留 摄像机Plus 3K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"67oRHoYJ","proto":"wifi","name":"点击Robot Vacuum S1控制清扫、暂停、回充","result":"对应Robot Vacuum S1固件执行清扫、暂停、回充","cat":"robot","device":"Robot S1","action":"点击Robot Vacuum S1控制清扫、暂停","file":"tests/robot/robot_control.test.ts","testName":"[P0][ONES:15974#67oRHoYJ][wifi] 点击Robot Vacuum S1控制清扫、暂停 Robot S1","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"YAeiQE6n","proto":"wifi","name":"点击Robot Vacuum S1 Plus控制清扫、暂停、回充","result":"对应Robot Vacuum S1 Plus固件执行清扫、暂停、回充","cat":"robot","device":"Robot S1","action":"点击Robot Vacuum S1 Plus控制","file":"tests/robot/robot_control.test.ts","testName":"[P0][ONES:15974#YAeiQE6n][wifi] 点击Robot Vacuum S1 Plus控制 Robot S1","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"TcnL3gFM","proto":"wifi","name":"点击控制吸顶灯pro开/关灯","result":"对应吸顶灯pro响应动作开/关灯","cat":"ceiling_light","device":"CeilingLight 7Q","action":"点击控制吸顶灯pro开/关灯","file":"tests/ceiling_light/ceiling_light_control.test.ts","testName":"[P0][ONES:15974#TcnL3gFM][wifi] 点击控制吸顶灯pro开/关灯 CeilingLight 7Q","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"WL8KSQLb","proto":"wifi","name":"点击控制吸顶灯lte开/关灯","result":"对应吸顶灯lte响应动作开/关灯","cat":"ceiling_light","device":"CeilingLight 7Q","action":"点击控制吸顶灯lte开/关灯","file":"tests/ceiling_light/ceiling_light_control.test.ts","testName":"[P0][ONES:15974#WL8KSQLb][wifi] 点击控制吸顶灯lte开/关灯 CeilingLight 7Q","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"6gv1WLKE","proto":"wifi","name":"首页点击控制固件V20版本BlindTilt","result":"对应BlindTilt固件响应","cat":"curtain","device":"Curtain 1A","action":"首页点击控制固件V20版本BlindTilt","file":"tests/curtain/curtain_control.test.ts","testName":"[P0][ONES:15974#6gv1WLKE][wifi] 首页点击控制固件V20版本BlindTilt Curtain 1A","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"B5N4u7Fj","proto":"wifi","name":"首页点击控制固件V12版本BlindTil功能页按钮(验证兼容旧版本)","result":"对应BlindTilt固件响应","cat":"curtain","device":"Curtain 1A","action":"首页点击控制固件V12版本BlindTil功能页","file":"tests/curtain/curtain_control.test.ts","testName":"[P0][ONES:15974#B5N4u7Fj][wifi] 首页点击控制固件V12版本BlindTil功能页 Curtain 1A","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"5yYJmwLL","proto":"wifi","name":"IOSensor---历史数据---右上角设置---报警条件,开启温度和湿度报警,拉动滑条使当前温湿度在报警范围内","result":"历史数据能够加载,不会有特别异常的数据图表,主页对应IOSensor温湿度字体变红色","cat":"sensor","device":"Contact Sensor 5O","action":"IOSensor---历史数据---右上角设置-","file":"tests/sensor/sensor_control.test.ts","testName":"[P0][ONES:15974#5yYJmwLL][wifi] IOSensor---历史数据---右上角设置- Contact Sensor 5O","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"GYtvdMTW","proto":"wifi","name":"Curtain 3---快捷弹窗---开/关","result":"对应Curtain 3固件响应动作开/关","cat":"curtain","device":"Curtain 1A","action":"Curtain 3---快捷弹窗---开/关","file":"tests/curtain/curtain_control.test.ts","testName":"[P0][ONES:15974#GYtvdMTW][wifi] Curtain 3---快捷弹窗---开/关 Curtain 1A","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"44FEY1z2","proto":"wifi","name":"点击K10+ 扫地机控制清扫、暂停、回充","result":"对应扫地机执行清扫、暂停、回充","cat":"robot","device":"Robot S1","action":"点击K10+ 扫地机控制清扫、暂停、回充","file":"tests/robot/robot_control.test.ts","testName":"[P0][ONES:15974#44FEY1z2][wifi] 点击K10+ 扫地机控制清扫、暂停、回充 Robot S1","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"6RC22BGo","proto":"wifi","name":"带电池款Battery Circulator Fan---首页控制开关","result":"对应循环扇开关","cat":"fan","device":"Fan 0T","action":"带电池款Battery Circulator F","file":"tests/fan/fan_control.test.ts","testName":"[P0][ONES:15974#6RC22BGo][wifi] 带电池款Battery Circulator F Fan 0T","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"Virku9zd","proto":"wifi","name":"点击控制Lock Pro开锁/解锁","result":"对应Lock Pro固件响应动作开锁/解锁","cat":"lock","device":"Lock 6F","action":"点击控制Lock Pro开锁/解锁","file":"tests/lock/lock_control.test.ts","testName":"[P0][ONES:15974#Virku9zd][wifi] 点击控制Lock Pro开锁/解锁 Lock 6F","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"BhNuXgg7","proto":"wifi","name":"进入Humidifier2设置页,远程绑定温湿度计,点击切换档位、模式","result":"绑定成功,功能页显示对应湿度值;切换档位、模式、目标湿度正常。","cat":"humidifier","device":"Humidifier 1K","action":"进入Humidifier2设置页,远程绑定温湿度","file":"tests/humidifier/humidifier_control.test.ts","testName":"[P0][ONES:15974#BhNuXgg7][wifi] 进入Humidifier2设置页,远程绑定温湿度 Humidifier 1K","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"9bcyjgJD","proto":"wifi","name":"点击S10控制清扫、暂停、回充","result":"对应S10固件执行清扫、暂停、回充","cat":"robot","device":"Robot S1","action":"点击S10控制清扫、暂停、回充","file":"tests/robot/robot_control.test.ts","testName":"[P0][ONES:15974#9bcyjgJD][wifi] 点击S10控制清扫、暂停、回充 Robot S1","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"KNyzKhn6","proto":"wifi","name":"无电池款 Circulator Fan---首页控制开关","result":"对应循环扇开关","cat":"fan","device":"Fan 0T","action":"无电池款 Circulator Fan---首页","file":"tests/fan/fan_control.test.ts","testName":"[P0][ONES:15974#KNyzKhn6][wifi] 无电池款 Circulator Fan---首页 Fan 0T","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"W7nK4VEB","proto":"wifi","name":"点击K10+Pro控制清扫、暂停、回充","result":"K10+ Pro正常响应清扫、暂停、回充","cat":"robot","device":"Robot S1","action":"点击K10+Pro控制清扫、暂停、回充","file":"tests/robot/robot_control.test.ts","testName":"[P0][ONES:15974#W7nK4VEB][wifi] 点击K10+Pro控制清扫、暂停、回充 Robot S1","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"GWAbic5S","proto":"wifi","name":"点击K10+ProCombo控制清扫、暂停、回充","result":"K10+ ProCombo正常响应清扫、暂停、回充","cat":"robot","device":"Robot S1","action":"点击K10+ProCombo控制清扫、暂停、回充","file":"tests/robot/robot_control.test.ts","testName":"[P0][ONES:15974#GWAbic5S][wifi] 点击K10+ProCombo控制清扫、暂停、回充 Robot S1","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"ScBpcSfJ","proto":"wifi","name":"Roller Shade功能页控制设备全开、暂停、全关、任意百分比控制","result":"Roller Shade正常响应所有控制指令","cat":"curtain","device":"Curtain 1A","action":"Roller Shade功能页控制设备全开、暂停","file":"tests/curtain/curtain_control.test.ts","testName":"[P0][ONES:15974#ScBpcSfJ][wifi] Roller Shade功能页控制设备全开、暂停 Curtain 1A","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"AWVhA6za","proto":"wifi","name":"点击控制美规基础款空气净化器--开/关","result":"对应美规基础款空气净化器--开关","cat":"air_condition","device":"AirCondition IR","action":"点击控制美规基础款空气净化器--开/关","file":"tests/air_condition/air_condition_control.test.ts","testName":"[P0][ONES:15974#AWVhA6za][wifi] 点击控制美规基础款空气净化器--开/关 AirCondition IR","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"MmodMTvh","proto":"wifi","name":"点击控制日规Table空气净化器--开/关","result":"对应日规Table空气净化器--开/关","cat":"air_condition","device":"AirCondition IR","action":"点击控制日规Table空气净化器--开/关","file":"tests/air_condition/air_condition_control.test.ts","testName":"[P0][ONES:15974#MmodMTvh][wifi] 点击控制日规Table空气净化器--开/关 AirCondition IR","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"VbqWg59G","proto":"wifi","name":"Curtain 3 2025---快捷弹窗---开/关","result":"对应Curtain 3 2025固件响应动作开/关","cat":"curtain","device":"Curtain 1A","action":"Curtain 3 2025---快捷弹窗---","file":"tests/curtain/curtain_control.test.ts","testName":"[P0][ONES:15974#VbqWg59G][wifi] Curtain 3 2025---快捷弹窗--- Curtain 1A","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"N5i3oQP2","proto":"wifi","name":"Meter Pro---图表数据---右上角设置---报警条件,开启温度和湿度报警,拉动滑条使当前温湿度在报警范围内","result":"图表数据能够加载,不会有特别异常的数据图表,主页对应Meter Pro温湿度字体变红色","cat":"meter","device":"Meter 3M","action":"Meter Pro---图表数据---右上角设置","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:15974#N5i3oQP2][wifi] Meter Pro---图表数据---右上角设置 Meter 3M","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"LAtdvNBV","proto":"wifi","name":"Meter Pro co2\n---图表数据---右上角设置---报警条件,开启温度和湿度报警,拉动滑条使当前温湿度在报警范围内","result":"图表数据能够加载,不会有特别异常的数据图表,主页对应Meter Pro co2温湿度字体变红色","cat":"meter","device":"Meter 3M","action":"Meter Pro co2 ---图表数据---","file":"tests/meter/meter_control.test.ts","testName":"[P0][ONES:15974#LAtdvNBV][wifi] Meter Pro co2 ---图表数据--- Meter 3M","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"N1kzcb7S","proto":"wifi","name":"点击控制RGBWW 彩色灯带3 --开/关","result":"对应RGBWW 彩色灯带3 --固件响应动作开/关","cat":"strip_light","device":"StripLight 9S","action":"点击控制RGBWW 彩色灯带3 --开/关","file":"tests/strip_light/strip_light_control.test.ts","testName":"[P0][ONES:15974#N1kzcb7S][wifi] 点击控制RGBWW 彩色灯带3 --开/关 StripLight 9S","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"2hm6sXrk","proto":"wifi","name":"点击控制RGBWW 落地灯 --开/关","result":"对应RGBWW 落地灯 --固件响应动作开/关","cat":"ceiling_light","device":"CeilingLight 7Q","action":"点击控制RGBWW 落地灯 --开/关","file":"tests/ceiling_light/ceiling_light_control.test.ts","testName":"[P0][ONES:15974#2hm6sXrk][wifi] 点击控制RGBWW 落地灯 --开/关 CeilingLight 7Q","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"XvFF8AK7","proto":"wifi","name":"点击控制Relay Switch 1pm--开/关","result":"对应Relay Switch 1pm --固件响应动作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Relay Switch 1pm--开/","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15974#XvFF8AK7][wifi] 点击控制Relay Switch 1pm--开/ Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"Y9gosqbQ","proto":"wifi","name":"点击控制Relay Switch 1--开/关","result":"对应Relay Switch 1 --固件响应动作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Relay Switch 1--开/关","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15974#Y9gosqbQ][wifi] 点击控制Relay Switch 1--开/关 Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"Vro1QBrg","proto":"wifi","name":"点击控制Relay Switch 2pm开关控制器--开/关","result":"对应 --固件响应Relay Switch 2pm开关控制器响作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Relay Switch 2pm开关控制","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15974#Vro1QBrg][wifi] 点击控制Relay Switch 2pm开关控制 Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"c34Tk92u","proto":"wifi","name":"点击控制Relay Switch 2pm覆盖控制器--开/关","result":"对应 --固件响应动Relay Switch 2pm覆盖控制器响作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Relay Switch 2pm覆盖控制","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15974#c34Tk92u][wifi] 点击控制Relay Switch 2pm覆盖控制 Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"2nRaC87F","proto":"wifi","name":"点击控制Garage Door Opener--开/关","result":"对应 --固件响应Garage Door Opener动响作开/关","cat":"plug","device":"Plug 4D","action":"点击控制Garage Door Opener--","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15974#2nRaC87F][wifi] 点击控制Garage Door Opener-- Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"MAwYyCaC","proto":"wifi","name":"点击控制RGBICWW Strip Light (炫彩灯带)--开/关","result":"对应RGBICWW Strip Light (炫彩灯带)--固件响应动作开/关","cat":"strip_light","device":"StripLight 9S","action":"点击控制RGBICWW Strip Light ","file":"tests/strip_light/strip_light_control.test.ts","testName":"[P0][ONES:15974#MAwYyCaC][wifi] 点击控制RGBICWW Strip Light StripLight 9S","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"ML8CSkWX","proto":"wifi","name":"点击控制RGBICWW Floor Lamp(炫彩落地灯)--开/关","result":"对应RGBICWW Floor Lamp(炫彩落地灯)--固件响应动作开/关","cat":"ceiling_light","device":"CeilingLight 7Q","action":"点击控制RGBICWW Floor Lamp(炫","file":"tests/ceiling_light/ceiling_light_control.test.ts","testName":"[P0][ONES:15974#ML8CSkWX][wifi] 点击控制RGBICWW Floor Lamp(炫 CeilingLight 7Q","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"5sJcsTgi","proto":"wifi","name":"点击控制RGBIC Neon Wire Rope Light(炫彩钢丝霓虹灯)--开/关","result":"对应RGBIC Neon Wire Rope Light(炫彩钢丝霓虹灯)--固件响应动作开/关","cat":"strip_light","device":"StripLight 9S","action":"点击控制RGBIC Neon Wire Rope","file":"tests/strip_light/strip_light_control.test.ts","testName":"[P0][ONES:15974#5sJcsTgi][wifi] 点击控制RGBIC Neon Wire Rope StripLight 9S","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"U5z9wocr","proto":"wifi","name":"点击控制eu plug--开/关","result":"对应设备响应开/关","cat":"plug","device":"Plug 4D","action":"点击控制eu plug--开/关","file":"tests/plug/plug_control.test.ts","testName":"[P0][ONES:15974#U5z9wocr][wifi] 点击控制eu plug--开/关 Plug 4D","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"BiEXxZDr","proto":"wifi","name":"点击控制RGBIC Neon Rope Light(炫彩霓虹灯)--开/关","result":"对应RGBIC Neon Rope Light(炫彩霓虹灯)--固件响应动作开/关","cat":"strip_light","device":"StripLight 9S","action":"点击控制RGBIC Neon Rope Ligh","file":"tests/strip_light/strip_light_control.test.ts","testName":"[P0][ONES:15974#BiEXxZDr][wifi] 点击控制RGBIC Neon Rope Ligh StripLight 9S","status":"todo"},
|
||||
{"kind":"ctrl","ones":15974,"step":"RXm3iNLR","proto":"wifi","name":"点击控制Candle lamp(融蜡灯)--开/关(融蜡灯需要插110V电压)","result":"对应Candle lamp(融蜡灯)--固件响应动作开/关","cat":"ceiling_light","device":"CeilingLight 7Q","action":"点击控制Candle lamp(融蜡灯)--开/","file":"tests/ceiling_light/ceiling_light_control.test.ts","testName":"[P0][ONES:15974#RXm3iNLR][wifi] 点击控制Candle lamp(融蜡灯)--开/ CeilingLight 7Q","status":"todo"},
|
||||
];
|
||||
|
|
@ -1,607 +0,0 @@
|
|||
{
|
||||
"_note": "必测项回写参数。用例固定仅新增;回写时 plan UUID 为变量。key 规则: case=testcase_plan_case-<plan>-<caseUUID>; step=testcase_plan_case_step-<plan>-<caseUUID>-<stepUuid>。刷新: 重跑 scripts/gen-writeback-params.ts。",
|
||||
"defaultPlan": "CQz9YCNX",
|
||||
"cases": [
|
||||
{
|
||||
"number": 15940,
|
||||
"uuid": "6yxCKHLc",
|
||||
"name": "账户注册验证"
|
||||
},
|
||||
{
|
||||
"number": 15941,
|
||||
"uuid": "9TYHacGi",
|
||||
"name": "忘记密码验证"
|
||||
},
|
||||
{
|
||||
"number": 15942,
|
||||
"uuid": "79wW7aGj",
|
||||
"name": "第三方登录"
|
||||
},
|
||||
{
|
||||
"number": 15943,
|
||||
"uuid": "61CCdeGk",
|
||||
"name": "账户登出验证"
|
||||
},
|
||||
{
|
||||
"number": 15944,
|
||||
"uuid": "6KfLxzGc",
|
||||
"name": "创建房间验证"
|
||||
},
|
||||
{
|
||||
"number": 15945,
|
||||
"uuid": "SuNW9m9B",
|
||||
"name": "feedback提交验证"
|
||||
},
|
||||
{
|
||||
"number": 15946,
|
||||
"uuid": "3eHivSnk",
|
||||
"name": "添加Blind Tilt验证"
|
||||
},
|
||||
{
|
||||
"number": 15947,
|
||||
"uuid": "2s4GbZAh",
|
||||
"name": "添加Pan/Tilt Cam 2K验证"
|
||||
},
|
||||
{
|
||||
"number": 15948,
|
||||
"uuid": "YCHSGAPP",
|
||||
"name": "添加keypad touch验证"
|
||||
},
|
||||
{
|
||||
"number": 15949,
|
||||
"uuid": "KDcB3xju",
|
||||
"name": "添加keypad验证"
|
||||
},
|
||||
{
|
||||
"number": 15950,
|
||||
"uuid": "PhJTMDYV",
|
||||
"name": "添加吸顶灯验证(Pro和lte)"
|
||||
},
|
||||
{
|
||||
"number": 15951,
|
||||
"uuid": "TBp2WxHN",
|
||||
"name": "添加Robot Vacuum S1 Plus验证"
|
||||
},
|
||||
{
|
||||
"number": 15952,
|
||||
"uuid": "EGzyeS4Z",
|
||||
"name": "添加Robot Vacuum S1验证"
|
||||
},
|
||||
{
|
||||
"number": 15953,
|
||||
"uuid": "4zJjZbQr",
|
||||
"name": "学习others遥控器"
|
||||
},
|
||||
{
|
||||
"number": 15954,
|
||||
"uuid": "KHUtwWFg",
|
||||
"name": "学习自定义遥控器"
|
||||
},
|
||||
{
|
||||
"number": 15955,
|
||||
"uuid": "ASgigVHm",
|
||||
"name": "智能匹配遥控器"
|
||||
},
|
||||
{
|
||||
"number": 15956,
|
||||
"uuid": "5ujCPVBF",
|
||||
"name": "添加Meter Plus验证"
|
||||
},
|
||||
{
|
||||
"number": 15957,
|
||||
"uuid": "7KAwsNmC",
|
||||
"name": "添加Pan/Tilt Cam验证"
|
||||
},
|
||||
{
|
||||
"number": 15958,
|
||||
"uuid": "GdqgjWmu",
|
||||
"name": "添加Plug Mini验证(JP和US)"
|
||||
},
|
||||
{
|
||||
"number": 15959,
|
||||
"uuid": "PgeMXwx6",
|
||||
"name": "添加Strip Light验证"
|
||||
},
|
||||
{
|
||||
"number": 15960,
|
||||
"uuid": "XQGshXkv",
|
||||
"name": "添加Color Bulb验证"
|
||||
},
|
||||
{
|
||||
"number": 15961,
|
||||
"uuid": "VaxZgabA",
|
||||
"name": "添加Indoor Cam验证"
|
||||
},
|
||||
{
|
||||
"number": 15962,
|
||||
"uuid": "VVTCNWiX",
|
||||
"name": "添加Contact Sensor验证"
|
||||
},
|
||||
{
|
||||
"number": 15963,
|
||||
"uuid": "U45e5n66",
|
||||
"name": "添加Motion Sensor验证"
|
||||
},
|
||||
{
|
||||
"number": 15964,
|
||||
"uuid": "JjvbRHYU",
|
||||
"name": "添加Humidifier验证"
|
||||
},
|
||||
{
|
||||
"number": 15965,
|
||||
"uuid": "8UFaR2qu",
|
||||
"name": "添加Meter验证"
|
||||
},
|
||||
{
|
||||
"number": 15966,
|
||||
"uuid": "36y6sC3z",
|
||||
"name": "添加Plug验证"
|
||||
},
|
||||
{
|
||||
"number": 15967,
|
||||
"uuid": "L4C4AVQe",
|
||||
"name": "添加Remote验证"
|
||||
},
|
||||
{
|
||||
"number": 15968,
|
||||
"uuid": "K6LFdz81",
|
||||
"name": "添加Bot验证"
|
||||
},
|
||||
{
|
||||
"number": 15969,
|
||||
"uuid": "3XahnWS4",
|
||||
"name": "添加Curtain验证"
|
||||
},
|
||||
{
|
||||
"number": 15970,
|
||||
"uuid": "SKAj9t7t",
|
||||
"name": "添加Lock验证"
|
||||
},
|
||||
{
|
||||
"number": 15971,
|
||||
"uuid": "NSQT1PGJ",
|
||||
"name": "添加Hub Plus验证"
|
||||
},
|
||||
{
|
||||
"number": 15972,
|
||||
"uuid": "Fi2V2XPY",
|
||||
"name": "添加Hub Mini验证"
|
||||
},
|
||||
{
|
||||
"number": 15973,
|
||||
"uuid": "SP75BSp2",
|
||||
"name": "keypad、keypad touch添加密码、指纹、卡片验证"
|
||||
},
|
||||
{
|
||||
"number": 15974,
|
||||
"uuid": "Vp7vuhbu",
|
||||
"name": "WiFi控制设备"
|
||||
},
|
||||
{
|
||||
"number": 15975,
|
||||
"uuid": "Lqpkx6mp",
|
||||
"name": "蓝牙控制设备"
|
||||
},
|
||||
{
|
||||
"number": 15976,
|
||||
"uuid": "6qQ6Pmm2",
|
||||
"name": "场景操作验证"
|
||||
},
|
||||
{
|
||||
"number": 15977,
|
||||
"uuid": "Be91sD22",
|
||||
"name": "第三方操作验证"
|
||||
},
|
||||
{
|
||||
"number": 15978,
|
||||
"uuid": "76VscU5y",
|
||||
"name": "覆盖安装测试"
|
||||
},
|
||||
{
|
||||
"number": 21787,
|
||||
"uuid": "SF9cqxki",
|
||||
"name": "添加Hub2 验证"
|
||||
},
|
||||
{
|
||||
"number": 35871,
|
||||
"uuid": "PRvriHQj",
|
||||
"name": "添加IOSensor验证"
|
||||
},
|
||||
{
|
||||
"number": 40954,
|
||||
"uuid": "8GpJwfuo",
|
||||
"name": "添加K10+扫地机"
|
||||
},
|
||||
{
|
||||
"number": 40955,
|
||||
"uuid": "y9ksiDTo",
|
||||
"name": "添加curtain 3验证"
|
||||
},
|
||||
{
|
||||
"number": 47214,
|
||||
"uuid": "L3dTTpJU",
|
||||
"name": "添加PTC Plus 3MP"
|
||||
},
|
||||
{
|
||||
"number": 48159,
|
||||
"uuid": "Dh4gScBV",
|
||||
"name": "添加Battery Circulator Fan验证"
|
||||
},
|
||||
{
|
||||
"number": 48734,
|
||||
"uuid": "Cvu7v7dt",
|
||||
"name": "插件热更"
|
||||
},
|
||||
{
|
||||
"number": 51440,
|
||||
"uuid": "5F6H4XW4",
|
||||
"name": "添加lock&kit验证"
|
||||
},
|
||||
{
|
||||
"number": 65336,
|
||||
"uuid": "9CT8TcY4",
|
||||
"name": "添加Hub Mini Matter验证"
|
||||
},
|
||||
{
|
||||
"number": 66694,
|
||||
"uuid": "CNK9w5gf",
|
||||
"name": "添加Lock Pro验证"
|
||||
},
|
||||
{
|
||||
"number": 74078,
|
||||
"uuid": "g7y2EGXF",
|
||||
"name": "APP切换温湿度单位显示"
|
||||
},
|
||||
{
|
||||
"number": 74104,
|
||||
"uuid": "PJ3bJLTg",
|
||||
"name": "添加Humidifier2验证"
|
||||
},
|
||||
{
|
||||
"number": 74105,
|
||||
"uuid": "XTNTvNyF",
|
||||
"name": "强绑定设备的自动解绑申请"
|
||||
},
|
||||
{
|
||||
"number": 78369,
|
||||
"uuid": "7mrfzZr1",
|
||||
"name": "添加S10扫地机"
|
||||
},
|
||||
{
|
||||
"number": 105436,
|
||||
"uuid": "D2c4eLE5",
|
||||
"name": "添加lock Pro&kit验证"
|
||||
},
|
||||
{
|
||||
"number": 113941,
|
||||
"uuid": "B7CRfHh4",
|
||||
"name": "添加OSC验证"
|
||||
},
|
||||
{
|
||||
"number": 113942,
|
||||
"uuid": "5JYiBYff",
|
||||
"name": "添加OSC 2K验证"
|
||||
},
|
||||
{
|
||||
"number": 113943,
|
||||
"uuid": "S4f2GnKM",
|
||||
"name": "添加PTC Plus 5MP"
|
||||
},
|
||||
{
|
||||
"number": 122737,
|
||||
"uuid": "9aCBMCZM",
|
||||
"name": "添加K10+Pro扫地机"
|
||||
},
|
||||
{
|
||||
"number": 122775,
|
||||
"uuid": "svdK6AdD",
|
||||
"name": "添加 Circulator Fan验证(无电池款)"
|
||||
},
|
||||
{
|
||||
"number": 162261,
|
||||
"uuid": "2E5kn5TV",
|
||||
"name": "添加K10+ Pro Combo"
|
||||
},
|
||||
{
|
||||
"number": 162263,
|
||||
"uuid": "JCqQmM1y",
|
||||
"name": "K10+ Pro Combo绑定手持吸尘器"
|
||||
},
|
||||
{
|
||||
"number": 162264,
|
||||
"uuid": "UvH2Zoiw",
|
||||
"name": "K10+ Pro Combo解除绑定手持"
|
||||
},
|
||||
{
|
||||
"number": 190801,
|
||||
"uuid": "XeoBqvEX",
|
||||
"name": "添加URC验证"
|
||||
},
|
||||
{
|
||||
"number": 190802,
|
||||
"uuid": "WcEHdNnS",
|
||||
"name": "同步URC设备、控制"
|
||||
},
|
||||
{
|
||||
"number": 191100,
|
||||
"uuid": "LiFS1jPW",
|
||||
"name": "添加Roller Shade验证"
|
||||
},
|
||||
{
|
||||
"number": 196802,
|
||||
"uuid": "HhrjuktG",
|
||||
"name": "添加空气净化器验证(基础款和table款)"
|
||||
},
|
||||
{
|
||||
"number": 197389,
|
||||
"uuid": "SZZq5NDW",
|
||||
"name": "添加Doorbell"
|
||||
},
|
||||
{
|
||||
"number": 205248,
|
||||
"uuid": "4jt45mtk",
|
||||
"name": "添加S20扫地机"
|
||||
},
|
||||
{
|
||||
"number": 227290,
|
||||
"uuid": "XY3yL3yL",
|
||||
"name": "欧区账号登录"
|
||||
},
|
||||
{
|
||||
"number": 265933,
|
||||
"uuid": "T4C5Bc4h",
|
||||
"name": "添加curtain 3 2025验证"
|
||||
},
|
||||
{
|
||||
"number": 267366,
|
||||
"uuid": "8h8m2oMK",
|
||||
"name": "添加Meter pro co2验证"
|
||||
},
|
||||
{
|
||||
"number": 267367,
|
||||
"uuid": "AdAxoQss",
|
||||
"name": "添加Meter pro验证"
|
||||
},
|
||||
{
|
||||
"number": 273787,
|
||||
"uuid": "4qEc6uDh",
|
||||
"name": "添加K11+扫地机"
|
||||
},
|
||||
{
|
||||
"number": 298968,
|
||||
"uuid": "YSyKzd5b",
|
||||
"name": "消息中心"
|
||||
},
|
||||
{
|
||||
"number": 301585,
|
||||
"uuid": "MaLqK2LR",
|
||||
"name": "添加RGBWW Strip light3(彩色灯带3) 验证 "
|
||||
},
|
||||
{
|
||||
"number": 301586,
|
||||
"uuid": "DXWPJi2t",
|
||||
"name": "添加RGBWW Floor Lamp (落地灯)验证"
|
||||
},
|
||||
{
|
||||
"number": 302288,
|
||||
"uuid": "ABWcTyTS",
|
||||
"name": "添加Relay Switch验证"
|
||||
},
|
||||
{
|
||||
"number": 302289,
|
||||
"uuid": "SwhcmiAT",
|
||||
"name": "添加Garage Door Opener验证"
|
||||
},
|
||||
{
|
||||
"number": 304130,
|
||||
"uuid": "5EZBMbyC",
|
||||
"name": "添加Lock Ultra验证"
|
||||
},
|
||||
{
|
||||
"number": 304131,
|
||||
"uuid": "8FytfzPz",
|
||||
"name": "添加Keypad Vision验证"
|
||||
},
|
||||
{
|
||||
"number": 304132,
|
||||
"uuid": "CquFqWd6",
|
||||
"name": "添加Lock Lite验证"
|
||||
},
|
||||
{
|
||||
"number": 304133,
|
||||
"uuid": "UtDXaxef",
|
||||
"name": "添加Lock Pro WiFi验证"
|
||||
},
|
||||
{
|
||||
"number": 310703,
|
||||
"uuid": "GRuid7nr",
|
||||
"name": "添加Climate Panel验证"
|
||||
},
|
||||
{
|
||||
"number": 310704,
|
||||
"uuid": "4e4Anr8B",
|
||||
"name": "添加暖气阀Smart Radiator Thermostat验证"
|
||||
},
|
||||
{
|
||||
"number": 311018,
|
||||
"uuid": "Evr4zoiU",
|
||||
"name": "添加RGBICWW Strip Light(炫彩灯带) 验证"
|
||||
},
|
||||
{
|
||||
"number": 311019,
|
||||
"uuid": "2mnWm4kU",
|
||||
"name": "添加RGBICWW Floor Lamp(炫彩落地灯)验证"
|
||||
},
|
||||
{
|
||||
"number": 311020,
|
||||
"uuid": "NrBUAnYg",
|
||||
"name": "添加RGBIC Neon Wire Rope Light(炫彩钢丝霓虹灯)验证"
|
||||
},
|
||||
{
|
||||
"number": 319303,
|
||||
"uuid": "3ibhGBWz",
|
||||
"name": "添加Hub Mini Matter S3验证"
|
||||
},
|
||||
{
|
||||
"number": 319304,
|
||||
"uuid": "Vu6x1gpU",
|
||||
"name": "添加Hub2 S3验证"
|
||||
},
|
||||
{
|
||||
"number": 319305,
|
||||
"uuid": "2qvq8JZa",
|
||||
"name": "添加Hub Mini S3验证"
|
||||
},
|
||||
{
|
||||
"number": 332917,
|
||||
"uuid": "8UFZSbeC",
|
||||
"name": "添加人体存在传感器"
|
||||
},
|
||||
{
|
||||
"number": 332918,
|
||||
"uuid": "Wmyuosou",
|
||||
"name": "添加eu plug"
|
||||
},
|
||||
{
|
||||
"number": 332919,
|
||||
"uuid": "8CXeNBcG",
|
||||
"name": "添加safety alarm"
|
||||
},
|
||||
{
|
||||
"number": 393234,
|
||||
"uuid": "9t9W6kLx",
|
||||
"name": "添加Standing Circulator fan验证"
|
||||
},
|
||||
{
|
||||
"number": 393235,
|
||||
"uuid": "87y8Y8Ni",
|
||||
"name": "添加RGBIC Neon Rope Light(炫彩霓虹灯)验证"
|
||||
},
|
||||
{
|
||||
"number": 393236,
|
||||
"uuid": "DAFQxUYA",
|
||||
"name": "添加Candle lamp(融蜡灯)验证(融蜡灯需要插110V电压)"
|
||||
}
|
||||
],
|
||||
"controlCases": {
|
||||
"15974": {
|
||||
"name": "WiFi控制设备",
|
||||
"uuid": "Vp7vuhbu",
|
||||
"proto": "wifi",
|
||||
"steps": [
|
||||
"Qt4hcgB4",
|
||||
"7cEkbh4Y",
|
||||
"GPT3wJd9",
|
||||
"63kDVNx1",
|
||||
"8yGTCPwY",
|
||||
"Ay79Pj23",
|
||||
"8tx3Rg76",
|
||||
"Kaj2UdKf",
|
||||
"QgX4dX7X",
|
||||
"3nXsFW9n",
|
||||
"N7fSHmH3",
|
||||
"JKSgHHbC",
|
||||
"4oeeJEV8",
|
||||
"16szaUdD",
|
||||
"Hj5Z9jSf",
|
||||
"SaKmFWLw",
|
||||
"LDqSDPPT",
|
||||
"HoDdVUm1",
|
||||
"WH7kyypF",
|
||||
"J8BQ1p9G",
|
||||
"KNk1HaB2",
|
||||
"67oRHoYJ",
|
||||
"YAeiQE6n",
|
||||
"TcnL3gFM",
|
||||
"WL8KSQLb",
|
||||
"6gv1WLKE",
|
||||
"B5N4u7Fj",
|
||||
"5yYJmwLL",
|
||||
"GYtvdMTW",
|
||||
"44FEY1z2",
|
||||
"6RC22BGo",
|
||||
"Virku9zd",
|
||||
"BhNuXgg7",
|
||||
"9bcyjgJD",
|
||||
"KNyzKhn6",
|
||||
"W7nK4VEB",
|
||||
"GWAbic5S",
|
||||
"ScBpcSfJ",
|
||||
"AWVhA6za",
|
||||
"MmodMTvh",
|
||||
"VbqWg59G",
|
||||
"N5i3oQP2",
|
||||
"LAtdvNBV",
|
||||
"N1kzcb7S",
|
||||
"2hm6sXrk",
|
||||
"XvFF8AK7",
|
||||
"Y9gosqbQ",
|
||||
"Vro1QBrg",
|
||||
"c34Tk92u",
|
||||
"2nRaC87F",
|
||||
"MAwYyCaC",
|
||||
"ML8CSkWX",
|
||||
"5sJcsTgi",
|
||||
"U5z9wocr",
|
||||
"BiEXxZDr",
|
||||
"RXm3iNLR"
|
||||
]
|
||||
},
|
||||
"15975": {
|
||||
"name": "蓝牙控制设备",
|
||||
"uuid": "Lqpkx6mp",
|
||||
"proto": "ble",
|
||||
"steps": [
|
||||
"6e4uZSVe",
|
||||
"5YEfmpJz",
|
||||
"774V8bND",
|
||||
"NvpUv9Tt",
|
||||
"3Jm5FAdW",
|
||||
"QfYLdj8s",
|
||||
"GkkrZpaC",
|
||||
"TNhSAuUC",
|
||||
"4poyiVwD",
|
||||
"2528azD6",
|
||||
"V15M64cP",
|
||||
"2jSFfY4k",
|
||||
"UZCtZRPy",
|
||||
"9mheLMDJ",
|
||||
"GSZv8BYm",
|
||||
"HrigrD2X",
|
||||
"A8C4BsCm",
|
||||
"3frMRDZj",
|
||||
"Qbm9VB72",
|
||||
"QwnZJARA",
|
||||
"Ncg7BDeR",
|
||||
"k6cqKqtZ",
|
||||
"QqsnPMc3",
|
||||
"CiFPEbNS",
|
||||
"LnPNEJmS",
|
||||
"3PmZMk1s",
|
||||
"JBszgxRS",
|
||||
"SmzuHtWi",
|
||||
"7qxA1ajE",
|
||||
"R5PDp8BR",
|
||||
"3wM6fW8W",
|
||||
"JggzNXDo",
|
||||
"L967RGHa",
|
||||
"8Wzaj5pB",
|
||||
"3bYsmiZG",
|
||||
"NDqRQxVx",
|
||||
"DxKG9Acs",
|
||||
"3ob75cjf",
|
||||
"JVnRz7xY",
|
||||
"MKgLq1oy",
|
||||
"A1TLxsFw",
|
||||
"PG62s725",
|
||||
"AafJ5tj5",
|
||||
"E5YRm4Pg",
|
||||
"QAHNrXrm",
|
||||
"AfxmoAEq",
|
||||
"E2QCS8xy",
|
||||
"TSVKWcJ3",
|
||||
"XWmNA9UL"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +133,7 @@ describe('AIHub Connect - 添加AI Hub设备', () => {
|
|||
const alreadyExists = await isDeviceOnHomepage(driver, AIHUB_NAME);
|
||||
if (alreadyExists) {
|
||||
console.log(`${AIHUB_NAME}已在首页,跳过重新添加`);
|
||||
reporter.record(`添加${AIHUB_NAME}`, 'PASS', Date.now() - start, `${AIHUB_NAME}已存在, 无需重新添加`);
|
||||
reporter.record(`添加${AIHUB_NAME}`, 'SKIP', Date.now() - start, `${AIHUB_NAME}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ describe('Air Condition Connect - 添加红外空调设备', () => {
|
|||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('[P0][ONES:196802] 添加红外空调设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record('[P0][ONES:196802] 添加红外空调设备', 'SKIP', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -259,11 +259,11 @@ describe('【通用自动化】- 创建/删除流程', () => {
|
|||
await logPageElements(pageSrc);
|
||||
steps.push('Add后页面元素已打印');
|
||||
|
||||
reporter.record('点击Add发现创建页面', 'PASS', Date.now() - start, steps.join(' → '));
|
||||
reporter.record('[P0][ONES:15976] 场景操作-点击Add发现创建页面', 'PASS', Date.now() - start, steps.join(' → '));
|
||||
} catch (e: any) {
|
||||
const src = await driver.getSource();
|
||||
await logPageElements(src);
|
||||
reporter.record('点击Add发现创建页面', 'FAIL', Date.now() - start, e.message);
|
||||
reporter.record('[P0][ONES:15976] 场景操作-点击Add发现创建页面', 'FAIL', Date.now() - start, e.message);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
|
||||
import { TestReporter } from '../../utils/test-reporter';
|
||||
import { getTicketRnPlugins, resolveTicketId } from '../../utils/common';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
/**
|
||||
* 必测项「获取RN插件」—— 读取 Zendesk 工单里 app 自动上报的 RN 插件包版本(Bundle_Version)。
|
||||
*
|
||||
* 纯 API 用例,不依赖设备 driver。工单号是**变量**,不写死:
|
||||
* ① 入参/env ZENDESK_TICKET_ID ② 上游用例(feedback 提交)setSharedTicketId() 写的共享文件。
|
||||
* 无工单号或未配 Zendesk 连接时 → SKIP(便于无人值守时不阻塞)。
|
||||
*
|
||||
* ONES 锚点:插件热更 48734(库 EPfZfC9Y / 模块 其他功能 Eeme1qVk)。可用 env ONES_RN_PLUGIN 覆盖。
|
||||
*/
|
||||
const ONES = `[P0][ONES:${process.env.ONES_RN_PLUGIN || '48734'}]`;
|
||||
|
||||
describe('【平台】插件热更 - 获取RN插件版本(工单 Bundle_Version)', () => {
|
||||
let reporter: TestReporter;
|
||||
|
||||
beforeAll(() => {
|
||||
reporter = new TestReporter('RN_Plugin', 'API');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
reporter.generate();
|
||||
});
|
||||
|
||||
it(`${ONES} 获取RN插件版本`, async () => {
|
||||
const start = Date.now();
|
||||
const name = `${ONES} 获取RN插件版本`;
|
||||
|
||||
// 前置:工单号 + Zendesk 连接;缺任一则 SKIP(无人值守安全)
|
||||
let ticketId: string;
|
||||
try {
|
||||
ticketId = resolveTicketId(process.env.ZENDESK_TICKET_ID);
|
||||
} catch (e: any) {
|
||||
console.log('跳过:', e.message);
|
||||
reporter.record(name, 'SKIP', Date.now() - start, e.message);
|
||||
return;
|
||||
}
|
||||
if (!process.env.ZENDESK_DOMAIN || !process.env.ZENDESK_TOKEN) {
|
||||
const msg = '跳过:未配置 ZENDESK_DOMAIN/ZENDESK_TOKEN';
|
||||
console.log(msg);
|
||||
reporter.record(name, 'SKIP', Date.now() - start, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const info = await getTicketRnPlugins(ticketId);
|
||||
|
||||
// 断言:工单评论里取到了 Bundle_Version,且解析出至少一个 RN 插件模块
|
||||
expect(info.bundleRaw).not.toBeNull();
|
||||
const modules = Object.keys(info.bundle);
|
||||
expect(modules.length).toBeGreaterThan(0);
|
||||
|
||||
const detail = [
|
||||
`ticket=${info.ticketId}`,
|
||||
`App=${info.appVersion ?? '-'}`,
|
||||
`JS=${info.jsVersion ?? '-'}`,
|
||||
`RN插件(${modules.length}): ` + modules.map((m) => `${m}:${info.bundle[m]}`).join(' / '),
|
||||
].join(' | ');
|
||||
console.log(detail);
|
||||
|
||||
reporter.record(name, 'PASS', Date.now() - start, detail);
|
||||
} catch (e: any) {
|
||||
reporter.record(name, 'FAIL', Date.now() - start, e.message);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
|
@ -37,7 +37,7 @@ describe('Bot Connect - 添加Bot设备', () => {
|
|||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record(`[P0][ONES:15968] 添加${deviceName}`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`[P0][ONES:15968] 添加${deviceName}`, 'SKIP', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import * as path from 'path';
|
|||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('ceilingLight', 'CEILING_LIGHT_DEVICE');
|
||||
import { onesAdd } from "../../utils/common";
|
||||
const ADD_ANCHOR = onesAdd("ceiling_light", deviceName);
|
||||
|
||||
describe('CeilingLight Connect - 添加吸顶灯设备', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
@ -43,13 +45,13 @@ describe('CeilingLight Connect - 添加吸顶灯设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('[ONES:15950] 通过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('[P0][ONES:15950] 添加吸顶灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加吸顶灯设备`, 'SKIP', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -65,10 +67,10 @@ describe('CeilingLight Connect - 添加吸顶灯设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('[P0][ONES:15950] 添加吸顶灯设备', '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('[P0][ONES:15950] 添加吸顶灯设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${ADD_ANCHOR} 添加吸顶灯设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ 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]';
|
||||
import { onesCtrl } from "../../utils/common";
|
||||
const CTRL_CEIL = onesCtrl("ceiling_light", deviceName);
|
||||
|
||||
describe('CeilingLight Control - 吸顶灯功能页', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ describe('ColorBulb Connect - 添加彩灯设备', () => {
|
|||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('[P0][ONES:15960] 添加彩灯设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record('[P0][ONES:15960] 添加彩灯设备', 'SKIP', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,22 +4,32 @@ import { createDriver } from '../../drivers/factory';
|
|||
import { TestReporter } from '../../utils/test-reporter';
|
||||
import {
|
||||
sleep,
|
||||
addDeviceViaBLE,
|
||||
isDeviceOnHomepage,
|
||||
addDeviceWithSerialPairing,
|
||||
findDeviceNameOnHomepage,
|
||||
saveAddedDevice,
|
||||
onesAdd,
|
||||
curtain3PostConnect,
|
||||
curtainPostConnect,
|
||||
} from '../../utils/common';
|
||||
import { getDeviceName } from '../../config/device.config';
|
||||
import { getDeviceName, getModelName } from '../../config/device.config';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('curtain', 'CURTAIN_DEVICE');
|
||||
const model = getModelName('curtain', 'CURTAIN_DEVICE'); // 型号名(锚点用)
|
||||
// curtain 与 curtain3 校准流程不同:按型号区分 relay 设备 + 连接后流程
|
||||
const isCurtain3 = /curtain\s*3|curtain3/i.test(model);
|
||||
const RELAY_DEVICE = isCurtain3 ? 'curtain3' : 'curtain';
|
||||
const ADD_ANCHOR = onesAdd('curtain', model);
|
||||
// 首页实际设备名匹配(真机名如 "Curtain 89" / "Curtain3 2B" 带 2 位十六进制后缀);
|
||||
// 要求十六进制后缀,避免误匹配校准页提示文字(如 "...SwitchBot Curtain to the desired...")。可用 env 覆盖
|
||||
const CURTAIN_PATTERN = process.env.CURTAIN_PATTERN || 'Curtain\\w*\\s+[0-9A-Fa-f]{2}\\b';
|
||||
// 是否做行程校验(需设备装在真实导轨上);默认跳过(Install and calibrate later),CURTAIN_CALIBRATE=1 开启
|
||||
const CALIBRATE = process.env.CURTAIN_CALIBRATE === '1';
|
||||
|
||||
// 设备维度动态添加锚点:按当前 CURTAIN_DEVICE 解析(Curtain/Curtain3/BlindTilt 各自的 ONES 号)
|
||||
import { onesAdd } from '../../utils/common';
|
||||
const ADD_ANCHOR = onesAdd('curtain', deviceName);
|
||||
|
||||
describe('Curtain Connect - 通过BLE添加窗帘设备', () => {
|
||||
describe('Curtain Connect - 添加窗帘设备(串口配对 + 行程校验)', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
||||
|
|
@ -34,30 +44,31 @@ describe('Curtain Connect - 通过BLE添加窗帘设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it(`${ADD_ANCHOR} 通过BLE添加窗帘设备`, async () => {
|
||||
it(`${ADD_ANCHOR} 通过BLE添加窗帘设备`, { timeout: 360000 }, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
const existing = await findDeviceNameOnHomepage(driver, CURTAIN_PATTERN);
|
||||
if (existing) {
|
||||
console.log(`${existing}已在首页,跳过重新添加`);
|
||||
saveAddedDevice('curtain', existing);
|
||||
reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, 'SKIP', Date.now() - start, `${existing}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await addDeviceViaBLE(driver, {
|
||||
categoryName: 'Curtain',
|
||||
const result = await addDeviceWithSerialPairing(driver, {
|
||||
categoryName: 'Curtain', // curtain / curtain3 同一添加入口
|
||||
deviceKeyword: deviceName,
|
||||
scanTimeout: 30000,
|
||||
connectionKeywords: [
|
||||
'Initial Setup', 'Start Using', 'Done', 'added successfully',
|
||||
'Got it', 'cloud service', 'Pick a room', 'Calibrate',
|
||||
'Open & Close', 'Set Direction', 'Next',
|
||||
],
|
||||
relayDevice: RELAY_DEVICE, // 按开关键 2s(curtain ch39 / curtain3 ch37)
|
||||
registryCategory: 'curtain',
|
||||
namePattern: CURTAIN_PATTERN,
|
||||
// 连接后专属流程:选模式 + (curtain3 的行程校验 / curtain 稍后校准)
|
||||
postConnectSteps: (d) =>
|
||||
isCurtain3 ? curtain3PostConnect(d, { calibrate: CALIBRATE }) : curtainPostConnect(d, { calibrate: CALIBRATE }),
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, 'PASS', Date.now() - start, `${model}添加成功${CALIBRATE ? '(含行程校验)' : ''}, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record(`${ADD_ANCHOR} 添加窗帘设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import * as path from 'path';
|
|||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('fan', 'FAN_DEVICE');
|
||||
import { onesAdd } from "../../utils/common";
|
||||
const ADD_ANCHOR = onesAdd("fan", deviceName);
|
||||
|
||||
describe('Fan Connect - 添加风扇设备', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
@ -53,13 +55,13 @@ describe('Fan Connect - 添加风扇设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('[ONES:122775] 通过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('[P0][ONES:122775] 通过BLE添加风扇', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 通过BLE添加风扇`, 'SKIP', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -75,10 +77,10 @@ describe('Fan Connect - 添加风扇设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('[P0][ONES:122775] 通过BLE添加风扇', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record(`${ADD_ANCHOR} 通过BLE添加风扇`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('[P0][ONES:122775] 通过BLE添加风扇', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${ADD_ANCHOR} 通过BLE添加风扇`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ 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]';
|
||||
import { onesCtrl } from "../../utils/common";
|
||||
const CTRL_FAN = onesCtrl("fan", deviceName);
|
||||
|
||||
describe('Fan Control - 风扇控制页', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
|
|||
|
|
@ -3,21 +3,27 @@ import { DeviceDriver } from '../../drivers/types';
|
|||
import { createDriver } from '../../drivers/factory';
|
||||
import { TestReporter } from '../../utils/test-reporter';
|
||||
import {
|
||||
sleep,
|
||||
addDeviceViaBLE,
|
||||
isDeviceOnHomepage,
|
||||
addDeviceWithSerialPairing,
|
||||
findDeviceNameOnHomepage,
|
||||
saveAddedDevice,
|
||||
configureHubWifi,
|
||||
onesAdd,
|
||||
} from '../../utils/common';
|
||||
import { getDeviceName } from '../../config/device.config';
|
||||
import { getDeviceName, getModelName } from '../../config/device.config';
|
||||
import { getWifiCredentials } from '../../config/wifi.config';
|
||||
import * as dotenv from 'dotenv';
|
||||
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);
|
||||
const ADD_ANCHOR = onesAdd('hub', getModelName('hub', 'HUB_DEVICE'));
|
||||
// App 添加目录里的具体型号入口(Hub 是分类标题,下挂 Hub Mini / Hub 2 / …);可用 env 覆盖
|
||||
const HUB_CATEGORY = process.env.HUB_CATEGORY || 'Hub Mini';
|
||||
// 首页实际设备名匹配(真机名,如 "Hub Mini XX");可用 env 覆盖
|
||||
const HUB_PATTERN = process.env.HUB_PATTERN || 'Hub ?Mini\\s+\\w+';
|
||||
|
||||
describe('Hub Connect - 添加Hub设备', () => {
|
||||
describe('Hub Connect - 添加Hub Mini设备(WiFi配网)', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
||||
|
|
@ -32,32 +38,39 @@ describe('Hub Connect - 添加Hub设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it(`${ADD_ANCHOR} 通过BLE添加Hub设备`, async () => {
|
||||
it(`${ADD_ANCHOR} 通过BLE+WiFi添加Hub Mini`, { timeout: 240000 }, async () => {
|
||||
const start = Date.now();
|
||||
const wifi = getWifiCredentials();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Hub设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
const existing = await findDeviceNameOnHomepage(driver, HUB_PATTERN);
|
||||
if (existing) {
|
||||
console.log(`${existing}已在首页,跳过重新添加`);
|
||||
saveAddedDevice('hub', existing);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Hub Mini设备`, 'SKIP', Date.now() - start, `${existing}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await addDeviceViaBLE(driver, {
|
||||
categoryName: 'Hub',
|
||||
const result = await addDeviceWithSerialPairing(driver, {
|
||||
categoryName: HUB_CATEGORY,
|
||||
categoryScrollHint: 1, // Hub Mini 在添加目录第 1 屏可见
|
||||
deviceKeyword: deviceName,
|
||||
scanTimeout: 30000,
|
||||
relayDevice: 'hub', // 继电器 ch40 长按 4s(config/relay.config)
|
||||
registryCategory: 'hub',
|
||||
namePattern: HUB_PATTERN,
|
||||
// 连上设备后进入 Configure Wi-Fi 页 → 填 SSID/密码(woanmarket/woankeji,env 可覆盖)→ Next → 走完向导
|
||||
postConnectSteps: (d) => configureHubWifi(d, wifi),
|
||||
connectionKeywords: [
|
||||
'Initial Setup', 'Start Using', 'Done', 'added successfully',
|
||||
'Got it', 'cloud service', 'Pick a room', 'Display Type',
|
||||
'Configure Wi-Fi', 'Your device is connected', 'Connect Device',
|
||||
'Connecting', 'Initial Setup', 'Pick a room', 'Done', 'Next',
|
||||
],
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Hub设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Hub Mini设备`, 'PASS', Date.now() - start, `${deviceName}添加成功(WiFi:${wifi.ssid}), 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record(`${ADD_ANCHOR} 添加Hub设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Hub Mini设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ describe('Humidifier Connect - 添加加湿器设备', () => {
|
|||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加加湿器设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加加湿器设备`, 'SKIP', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import * as path from 'path';
|
|||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('keypad', 'KEYPAD_DEVICE');
|
||||
import { onesAdd } from "../../utils/common";
|
||||
const ADD_ANCHOR = onesAdd("keypad", deviceName);
|
||||
|
||||
describe('Keypad Connect - 添加Keypad设备', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
@ -53,13 +55,13 @@ describe('Keypad Connect - 添加Keypad设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('[ONES:15949] 通过BLE添加Keypad', async () => {
|
||||
it(`${ADD_ANCHOR} 通过BLE添加Keypad`, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('[P0][ONES:15949] 通过BLE添加Keypad', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 通过BLE添加Keypad`, 'SKIP', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -75,10 +77,10 @@ describe('Keypad Connect - 添加Keypad设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('[P0][ONES:15949] 通过BLE添加Keypad', 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
reporter.record(`${ADD_ANCHOR} 通过BLE添加Keypad`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record('[P0][ONES:15949] 通过BLE添加Keypad', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${ADD_ANCHOR} 通过BLE添加Keypad`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ describe('Lock Connect - 添加Lock设备', () => {
|
|||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Lock设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Lock设备`, 'SKIP', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,18 +4,22 @@ import { createDriver } from '../../drivers/factory';
|
|||
import { TestReporter } from '../../utils/test-reporter';
|
||||
import {
|
||||
sleep,
|
||||
addDeviceViaBLE,
|
||||
isDeviceOnHomepage,
|
||||
addDeviceWithSerialPairing,
|
||||
findDeviceNameOnHomepage,
|
||||
saveAddedDevice,
|
||||
} from '../../utils/common';
|
||||
import { getDeviceName } from '../../config/device.config';
|
||||
import { getDeviceName, getModelName } from '../../config/device.config';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('meter', 'METER_DEVICE');
|
||||
// 首页实际设备名的匹配模式(真机名如 "Meter 0B");要求十六进制后缀,且排除 "Outdoor Meter XX"(户外计前缀不同)。可用 env 覆盖
|
||||
const METER_PATTERN = process.env.METER_PATTERN || '(?<!Outdoor )Meter\\s+[0-9A-Fa-f]{2}';
|
||||
import { onesAdd } from "../../utils/common";
|
||||
const ADD_ANCHOR = onesAdd("meter", deviceName);
|
||||
// 锚点按型号名解析(不用注册表的实际名,否则锚点表查不到)
|
||||
const ADD_ANCHOR = onesAdd("meter", getModelName('meter', 'METER_DEVICE'));
|
||||
|
||||
describe('Meter Connect - 添加Meter设备', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
@ -32,20 +36,25 @@ describe('Meter Connect - 添加Meter设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it(`${ADD_ANCHOR} 通过BLE添加Meter设备`, async () => {
|
||||
it(`${ADD_ANCHOR} 通过BLE添加Meter设备`, { timeout: 150000 }, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Meter设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
// 已添加则按模式拿到首页实际名并回写;否则走串口配对添加
|
||||
const existing = await findDeviceNameOnHomepage(driver, METER_PATTERN);
|
||||
if (existing) {
|
||||
console.log(`${existing}已在首页,跳过重新添加`);
|
||||
saveAddedDevice('meter', existing); // 记录实际名作为功能页入口
|
||||
reporter.record(`${ADD_ANCHOR} 添加Meter设备`, 'SKIP', Date.now() - start, `${existing}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await addDeviceViaBLE(driver, {
|
||||
const result = await addDeviceWithSerialPairing(driver, {
|
||||
categoryName: 'Meter',
|
||||
categoryScrollHint: 4, // Meter 在添加目录约第 4 次下滑可见(提速,见 selectDeviceCategory)
|
||||
deviceKeyword: deviceName,
|
||||
scanTimeout: 30000,
|
||||
relayDevice: 'meter', // 继电器按 meter 主键(config/relay.config)
|
||||
registryCategory: 'meter', // 成功后回写注册表(进入功能页入口)
|
||||
namePattern: METER_PATTERN, // 首页实际名匹配(如 "Meter 0B")
|
||||
connectionKeywords: [
|
||||
'Initial Setup', 'Start Using', 'Done', 'added successfully',
|
||||
'Got it', 'cloud service', 'Pick a room', 'Display Type',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
|
||||
import { DeviceDriver } from '../../drivers/types';
|
||||
import { createDriver } from '../../drivers/factory';
|
||||
import { TestReporter } from '../../utils/test-reporter';
|
||||
import {
|
||||
sleep,
|
||||
addDeviceWithSerialPairing,
|
||||
findDeviceNameOnHomepage,
|
||||
saveAddedDevice,
|
||||
onesAdd,
|
||||
} from '../../utils/common';
|
||||
import { getModelName } from '../../config/device.config';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
// outdoor meter 是套件:选 "Outdoor Meter" → 过免责 → 选择页先点 "Add Meter" → 按 ch36 → 连接。
|
||||
// 首页实际名 "Outdoor Meter XX"(与 indoor "Meter XX" 前缀不同,要求十六进制后缀避免误匹配)。
|
||||
const OUTDOOR_PATTERN = process.env.OUTDOOR_METER_PATTERN || 'Outdoor Meter\\s+[0-9A-Fa-f]{2}';
|
||||
const ADD_ANCHOR = onesAdd('meter', getModelName('outdoor_meter')); // 暂无独立锚点映射,留接口
|
||||
|
||||
describe('Outdoor Meter Connect - 添加户外温湿度计(套件)', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
||||
beforeAll(async () => {
|
||||
driver = createDriver();
|
||||
await driver.createSession();
|
||||
reporter = new TestReporter('OutdoorMeter_Connect', driver.platform.toUpperCase());
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
reporter.generate();
|
||||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it(`${ADD_ANCHOR} 通过BLE添加Outdoor Meter设备`, { timeout: 150000 }, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const existing = await findDeviceNameOnHomepage(driver, OUTDOOR_PATTERN);
|
||||
if (existing) {
|
||||
console.log(`${existing}已在首页,跳过重新添加`);
|
||||
saveAddedDevice('outdoor_meter', existing);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Outdoor Meter设备`, 'SKIP', Date.now() - start, `${existing}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await addDeviceWithSerialPairing(driver, {
|
||||
categoryName: 'Outdoor Meter',
|
||||
deviceKeyword: 'Outdoor Meter',
|
||||
relayDevice: 'outdoor_meter', // 继电器按主键 2s(ch36)
|
||||
registryCategory: 'outdoor_meter',
|
||||
namePattern: OUTDOOR_PATTERN,
|
||||
// 套件:过免责后先点 "Add Meter" 进计量器子流程,再按继电器
|
||||
beforePairing: async (d) => {
|
||||
const el = await d.findElementRaw('-android uiautomator', 'new UiSelector().text("Add Meter")');
|
||||
if (el) { await d.tapElement(el); console.log('点 Add Meter 进计量器子流程'); }
|
||||
},
|
||||
connectionKeywords: [
|
||||
'Initial Setup', 'Start Using', 'Done', 'added successfully', 'Added successfully',
|
||||
'Got it', 'Pick a room', 'Select Room', 'Display Type',
|
||||
],
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Outdoor Meter设备`, 'PASS', Date.now() - start, `Outdoor Meter添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record(`${ADD_ANCHOR} 添加Outdoor Meter设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -46,7 +46,7 @@ describe('OSC Connect - 通过BLE添加OSC', () => {
|
|||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record('[P0][ONES:113942] 通过BLE添加OSC', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record('[P0][ONES:113942] 通过BLE添加OSC', 'SKIP', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ describe('Plug Connect - 添加Plug设备', () => {
|
|||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Plug设备`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Plug设备`, 'SKIP', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
|
||||
import { DeviceDriver } from '../../drivers/types';
|
||||
import { createDriver } from '../../drivers/factory';
|
||||
import { TestReporter } from '../../utils/test-reporter';
|
||||
import {
|
||||
sleep,
|
||||
addDeviceWithSerialPairing,
|
||||
findDeviceNameOnHomepage,
|
||||
saveAddedDevice,
|
||||
onesAdd,
|
||||
} from '../../utils/common';
|
||||
import { getDeviceName, getModelName } from '../../config/device.config';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('remote', 'REMOTE_DEVICE');
|
||||
// 锚点按型号名解析(不用注册表实际名)
|
||||
const ADD_ANCHOR = onesAdd('remote', getModelName('remote', 'REMOTE_DEVICE'));
|
||||
// App 内添加品类名(Android 调试时若不符,改这一处即可)
|
||||
const REMOTE_CATEGORY = process.env.REMOTE_CATEGORY || 'Remote';
|
||||
// 首页实际设备名匹配模式(真机名与配置默认名可能不同);可用 env 覆盖
|
||||
const REMOTE_PATTERN = process.env.REMOTE_PATTERN || 'Remote \\w+';
|
||||
|
||||
describe('Remote Connect - 添加Remote设备', () => {
|
||||
let driver: DeviceDriver;
|
||||
let reporter: TestReporter;
|
||||
|
||||
beforeAll(async () => {
|
||||
driver = createDriver();
|
||||
await driver.createSession();
|
||||
reporter = new TestReporter('Remote_Connect', driver.platform.toUpperCase());
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
reporter.generate();
|
||||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it(`${ADD_ANCHOR} 通过BLE添加Remote设备`, { timeout: 150000 }, async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const existing = await findDeviceNameOnHomepage(driver, REMOTE_PATTERN);
|
||||
if (existing) {
|
||||
console.log(`${existing}已在首页,跳过重新添加`);
|
||||
saveAddedDevice('remote', existing);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Remote设备`, 'SKIP', Date.now() - start, `${existing}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await addDeviceWithSerialPairing(driver, {
|
||||
categoryName: REMOTE_CATEGORY,
|
||||
categoryScrollHint: 8, // Remote 在添加目录约第 8 次下滑可见(提速)
|
||||
deviceKeyword: deviceName,
|
||||
relayDevice: 'remote', // 继电器同时按凸键+凹键(config/relay.config)
|
||||
registryCategory: 'remote',
|
||||
namePattern: REMOTE_PATTERN,
|
||||
repressOnRetry: true, // 凸+凹是切换式键:首按给足检测;若起始恰在"已配对"态,允许补按一次切回
|
||||
pairRetries: 2, // 最多 2 轮
|
||||
pairPollIters: 22, // 每轮检测窗口 ~33s:App 对 remote 进配对的 BLE 检测慢(实测可达16s+),窗口须大于它,否则超窗+补按反而失败
|
||||
connectionKeywords: [
|
||||
'Initial Setup', 'Start Using', 'Done', 'added successfully',
|
||||
'Got it', 'cloud service', 'Pick a room', 'Next',
|
||||
],
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record(`${ADD_ANCHOR} 添加Remote设备`, 'PASS', Date.now() - start, `${deviceName}添加成功, 耗时${elapsed}s`);
|
||||
} catch (e: any) {
|
||||
const ss = await driver.screenshot().catch(() => '');
|
||||
reporter.record(`${ADD_ANCHOR} 添加Remote设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -48,7 +48,7 @@ describe('Robot Connect - 添加扫地机', () => {
|
|||
const alreadyExists = await isDeviceOnHomepage(driver, deviceName);
|
||||
if (alreadyExists) {
|
||||
console.log(`${deviceName}已在首页,跳过重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加扫地机`, 'SKIP', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import * as path from 'path';
|
|||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const deviceName = getDeviceName('stripLight', 'STRIP_LIGHT_DEVICE');
|
||||
import { onesAdd } from "../../utils/common";
|
||||
const ADD_ANCHOR = onesAdd("strip_light", deviceName);
|
||||
|
||||
describe('StripLight Connect - 添加灯带设备', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
@ -43,13 +45,13 @@ describe('StripLight Connect - 添加灯带设备', () => {
|
|||
await driver.destroySession();
|
||||
});
|
||||
|
||||
it('[ONES:15959] 通过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('[P0][ONES:15959] 添加灯带设备', 'PASS', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
reporter.record(`${ADD_ANCHOR} 添加灯带设备`, 'SKIP', Date.now() - start, `${deviceName}已存在, 无需重新添加`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -65,10 +67,10 @@ describe('StripLight Connect - 添加灯带设备', () => {
|
|||
expect(result).toBe(true);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
reporter.record('[P0][ONES:15959] 添加灯带设备', '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('[P0][ONES:15959] 添加灯带设备', 'FAIL', Date.now() - start, e.message, ss);
|
||||
reporter.record(`${ADD_ANCHOR} 添加灯带设备`, 'FAIL', Date.now() - start, e.message, ss);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ 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]';
|
||||
import { onesCtrl } from "../../utils/common";
|
||||
const CTRL_STRIP = onesCtrl("strip_light", deviceName);
|
||||
|
||||
describe('StripLight Control - 灯带功能页', () => {
|
||||
let driver: DeviceDriver;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* Curtain / Curtain 3 连接后专属流程(选模式 + 行程校验)。
|
||||
* **curtain 与 curtain3 校准流程不同**,分开实现:
|
||||
* - curtain3:已实测全程跑通(Custom Calibration:朝向→已关闭→跳磁铁→Move left/right)。
|
||||
* - curtain(base):校准流程与 curtain3 不同,待该设备可连接后补全;当前先走"稍后校准"。
|
||||
*
|
||||
* 作为 addDeviceWithSerialPairing 的 postConnectSteps 传入。所有点击用文字/desc 定位,不写死坐标。
|
||||
*/
|
||||
import { DeviceDriver } from '../../drivers/types';
|
||||
import { sleep, waitForSource } from './element.helper';
|
||||
|
||||
export interface CurtainAddOptions {
|
||||
/** true=做行程校验(需设备装在真实导轨上、电机能跑);false=Install and calibrate later 跳过校验。 */
|
||||
calibrate?: boolean;
|
||||
/** Determine 位置每个方向的行进等待(ms),默认 15000。 */
|
||||
moveWaitMs?: number;
|
||||
}
|
||||
|
||||
async function tapText(driver: DeviceDriver, txt: string): Promise<boolean> {
|
||||
const el = driver.platform === 'android'
|
||||
? await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${txt}")`)
|
||||
: await driver.findElementRaw('name', txt);
|
||||
if (el) { await driver.tapElement(el); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
async function tapDesc(driver: DeviceDriver, desc: string): Promise<boolean> {
|
||||
const el = driver.platform === 'android'
|
||||
? await driver.findElementRaw('-android uiautomator', `new UiSelector().descriptionContains("${desc}")`)
|
||||
: await driver.findElementRaw('predicate string', `label CONTAINS "${desc}" OR name CONTAINS "${desc}"`);
|
||||
if (el) { await driver.tapElement(el); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 连接后第一步(curtain/curtain3 共用):Select Mode 选"单边开合" → Confirm。 */
|
||||
async function selectModeOpenOneSide(driver: DeviceDriver): Promise<void> {
|
||||
await waitForSource(driver, 'Select Mode', 60000).catch(() => {});
|
||||
if (!(await tapDesc(driver, 'Open from one side'))) await tapText(driver, 'Open from one side');
|
||||
await sleep(1000);
|
||||
await tapText(driver, 'Confirm');
|
||||
}
|
||||
|
||||
/** Curtain 3 连接后流程(含已验证的 Custom Calibration)。 */
|
||||
export async function curtain3PostConnect(driver: DeviceDriver, opts: CurtainAddOptions = {}): Promise<void> {
|
||||
const calibrate = opts.calibrate ?? false;
|
||||
const moveWait = opts.moveWaitMs ?? 15000;
|
||||
|
||||
await selectModeOpenOneSide(driver);
|
||||
|
||||
await waitForSource(driver, 'Install', 30000).catch(() => {});
|
||||
if (!calibrate) {
|
||||
await tapText(driver, 'Install and calibrate later');
|
||||
console.log('curtain3: 走 Install and calibrate later(跳过行程校验)');
|
||||
return;
|
||||
}
|
||||
await tapText(driver, 'Install');
|
||||
|
||||
// Curtain Track 选轨道 → 底部 Skip
|
||||
await waitForSource(driver, 'rail type', 15000).catch(() => {});
|
||||
await tapText(driver, 'Skip');
|
||||
|
||||
// Calibrate → Custom Calibration
|
||||
await waitForSource(driver, 'Custom Calibration', 15000).catch(() => {});
|
||||
await tapText(driver, 'Custom Calibration');
|
||||
|
||||
// ① Confirm Orientation:勾选"logo 朝内"单选行 → Next
|
||||
// 注意:用唯一子串 "logo is facing inwards"(单选行 "SwitchBot logo is facing inwards"),
|
||||
// 不能用 "facing inwards" —— 会误匹配描述段 "...logo on your device is facing inwards",导致单选没勾上、Next 灰着。
|
||||
await waitForSource(driver, 'Confirm Orientation', 15000).catch(() => {});
|
||||
if (!(await tapDesc(driver, 'logo is facing inwards'))) await tapDesc(driver, 'facing inwards');
|
||||
await sleep(800);
|
||||
await tapText(driver, 'Next');
|
||||
|
||||
// ② Adjust Position:勾选"已关闭窗帘"单选行 → Start Calibration(同样用唯一子串避免误匹配描述)
|
||||
await waitForSource(driver, 'fully closed', 15000).catch(() => {});
|
||||
if (!(await tapDesc(driver, 'fully closed my curtains'))) await tapDesc(driver, 'fully closed');
|
||||
await sleep(800);
|
||||
await tapText(driver, 'Start Calibration');
|
||||
|
||||
// ③ Magnet Detection:Skip magnet detection → 弹框 Skip
|
||||
await waitForSource(driver, 'Magnet Detection', 15000).catch(() => {});
|
||||
await tapText(driver, 'Skip magnet detection');
|
||||
await sleep(1500);
|
||||
await tapText(driver, 'Skip');
|
||||
|
||||
// ④ Determine Fully open:Move left 行进 → 点"暂停"停在位 → Auto-Calibrate Close。
|
||||
// Move left 点击后按钮会变成 "Pause"(窗帘行进中),必须先点 Pause 停下,否则位置没记录 → "Not calibrated"。
|
||||
await waitForSource(driver, 'Move left', 20000).catch(() => {});
|
||||
await tapText(driver, 'Move left');
|
||||
await sleep(moveWait); // 让窗帘行进到目标全开位
|
||||
// 行进中按钮变 Pause:点它停下(中英/可能为 "Pause"/"暂停")
|
||||
if (!(await tapText(driver, 'Pause'))) await tapText(driver, '暂停');
|
||||
await sleep(1500);
|
||||
await tapText(driver, 'Auto-Calibrate Close');
|
||||
await sleep(5000); // 等自动关闭校准结算(期间可能出现 "Something went wrong",仍可 Finish)
|
||||
|
||||
// ⑤ Finish:点一次可能不离开校验页(页面结算中),重试直到离开 Custom Calibration 页
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const src = await driver.getSource();
|
||||
if (!/Custom Calibration|Move left|Auto-Calibrate/i.test(src)) break; // 已离开校验页 = 完成
|
||||
await tapText(driver, 'Finish');
|
||||
await sleep(3000);
|
||||
}
|
||||
console.log('curtain3: Custom Calibration 完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* Base Curtain 连接后流程 —— 校准流程与 curtain3 **不同**:行程是 Move left → 等待 → Move right → 等待。
|
||||
* 该设备目前连接不上,校准流程待可连接后实测补全;当前默认走"稍后校准"。
|
||||
*/
|
||||
export async function curtainPostConnect(driver: DeviceDriver, opts: CurtainAddOptions = {}): Promise<void> {
|
||||
await selectModeOpenOneSide(driver);
|
||||
await waitForSource(driver, 'Install', 30000).catch(() => {});
|
||||
await tapText(driver, 'Install and calibrate later');
|
||||
if (opts.calibrate) {
|
||||
console.log('curtain(base): 行程校验流程(Move left→Move right)待该设备可连接后实测补全;当前已走"稍后校准"');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { DeviceDriver } from '../../drivers/types';
|
||||
import { sleep, waitForElement, waitForSource, scrollUntilFound } from './element.helper';
|
||||
import { ensureHomeTab, navigateToAddPage, navigateThroughWizard } from './navigation.helper';
|
||||
import { ensureHomeTab, navigateToAddPage, navigateThroughWizard, restartAndroidApp } from './navigation.helper';
|
||||
import { enterPairingMode } from './relay.helper';
|
||||
import { saveAddedDevice, clearAddedDevices } from './device-registry.helper';
|
||||
|
||||
export interface AddDeviceOptions {
|
||||
categoryName: string;
|
||||
|
|
@ -21,6 +23,12 @@ const DEFAULT_CONNECTION_KEYWORDS = [
|
|||
|
||||
const DEFAULT_WIZARD_BUTTONS = ['Use now', 'Start Using', 'Done', 'Got it', 'OK', 'Skip', 'Next'];
|
||||
|
||||
// "连接已真正开始"的标志(用于判断配对是否触发);**不含引导页常驻的 Next/Got it**,避免误判已连。
|
||||
const CONNECT_STARTED_KEYWORDS = [
|
||||
'Connecting', 'connected', 'Connect Device', 'Discover device', 'Initial Setup',
|
||||
'added successfully', 'Added successfully', 'Pick a room', 'Select Room', 'Select Mode',
|
||||
];
|
||||
|
||||
export async function isDeviceOnHomepage(driver: DeviceDriver, keyword: string): Promise<boolean> {
|
||||
await ensureHomeTab(driver);
|
||||
let source = await driver.getSource();
|
||||
|
|
@ -31,19 +39,59 @@ export async function isDeviceOnHomepage(driver: DeviceDriver, keyword: string):
|
|||
return source.includes(keyword);
|
||||
}
|
||||
|
||||
export async function selectDeviceCategory(driver: DeviceDriver, categoryName: string): Promise<boolean> {
|
||||
/**
|
||||
* 在首页按正则找设备卡,返回**实际显示名**(如 "Meter 0B"),找不到返回 null。
|
||||
* 用于"设备实际名与配置默认名不符"时拿到真名(写入注册表作功能页入口)。
|
||||
*/
|
||||
export async function findDeviceNameOnHomepage(driver: DeviceDriver, pattern: string): Promise<string | null> {
|
||||
// 先退出可能残留的添加向导/Attention 弹框(否则首页被遮挡)
|
||||
for (const t of ['Return Home', 'Quit', 'Exit', 'Leave']) {
|
||||
const el = driver.platform === 'android'
|
||||
? await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${t}")`)
|
||||
: await driver.findElementRaw('name', t);
|
||||
if (el) { await driver.tapElement(el); await sleep(2000); break; }
|
||||
}
|
||||
await driver.dismissPopupIfPresent();
|
||||
await ensureHomeTab(driver);
|
||||
const re = new RegExp(pattern);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const source = await driver.getSource();
|
||||
const m = source.match(re);
|
||||
if (m) return m[0];
|
||||
await driver.scrollDown(400);
|
||||
await sleep(500);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function selectDeviceCategory(driver: DeviceDriver, categoryName: string, scrollHint = 0): Promise<boolean> {
|
||||
if (driver.platform === 'android') {
|
||||
let el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${categoryName}")`);
|
||||
if (!el) {
|
||||
await driver.scrollDown(300);
|
||||
await sleep(800);
|
||||
el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${categoryName}")`);
|
||||
const find = () => driver.findElementRaw('-android uiautomator', `new UiSelector().text("${categoryName}")`);
|
||||
// 品类网格异步加载:短轮询等其渲染(idle=1s 下通常很快;Meter 等非热门品类首屏不可见,少等即可)
|
||||
let el = await find();
|
||||
for (let i = 0; i < 2 && !el; i++) { await sleep(400); el = await find(); }
|
||||
// 首屏没有 → 手动下滑查找(不用慢的 UiScrollable.scrollIntoView,它每微滚都重查整棵树 ~12s)。
|
||||
// 已知大概滑几次的品类(scrollHint):先快滑 hint-1 次不校验,再边滑边找——大幅省时。
|
||||
if (!el && scrollHint > 1) {
|
||||
for (let i = 0; i < scrollHint - 1; i++) await driver.scrollDown(900);
|
||||
el = await find();
|
||||
}
|
||||
let swipes = 0;
|
||||
for (let i = 0; i < 14 && !el; i++) {
|
||||
await driver.scrollDown(900);
|
||||
swipes++;
|
||||
el = await find(); // idle=1s 下 find 很快,无需额外 sleep
|
||||
}
|
||||
if (!el) { console.log(`FAIL: no ${categoryName} option`); return false; }
|
||||
if (scrollHint <= 1 && swipes) console.log(`[hint] ${categoryName} 实际滑动 ${(scrollHint || 0) + swipes} 次找到(可设为该品类 scrollHint 提速)`);
|
||||
await driver.tapElement(el);
|
||||
await sleep(3000);
|
||||
await sleep(1200);
|
||||
// 点击品类后通常进入 免责声明/引导/扫描 页:只要离开了品类网格即视为成功
|
||||
const src = await driver.getSource();
|
||||
return src.includes(categoryName) || src.includes('Select') || src.includes('Scanning') || src.includes('searching');
|
||||
const leftGrid = !src.includes('Add Device') || src.includes('Disclaimers') || src.includes('Next') ||
|
||||
src.includes('Scanning') || src.includes('searching') || src.includes(categoryName);
|
||||
if (!leftGrid) { console.log(`FAIL: cannot select category ${categoryName}`); return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
// iOS - scroll to reveal category, check visibility, then click
|
||||
|
|
@ -106,6 +154,7 @@ export async function waitForConnection(
|
|||
for (const kw of connectionKeywords) {
|
||||
if (source.includes(kw)) return true;
|
||||
}
|
||||
await driver.dismissPopupIfPresent(); // 连接/配网过程中途突然冒出的弹框,顺手关掉
|
||||
await sleep(2000);
|
||||
}
|
||||
return false;
|
||||
|
|
@ -190,6 +239,370 @@ export async function addDeviceViaBLE(driver: DeviceDriver, options: AddDeviceOp
|
|||
return false;
|
||||
}
|
||||
|
||||
export interface SerialPairingAddOptions {
|
||||
categoryName: string; // 添加目录里的品类/产品名(如 'Meter' / 'Remote')
|
||||
categoryScrollHint?: number; // 该品类在添加目录里大概滑动几次可见(提速:先快滑 hint-1 次再校验);日志会打印实测值
|
||||
deviceKeyword: string; // 首页校验关键字(如 'Meter 3M')
|
||||
relayDevice: string; // config/relay.config 的设备 key(如 'meter' / 'remote')
|
||||
registryCategory?: string; // 成功后把设备名写入注册表的品类 key(如 'meter');默认用 relayDevice
|
||||
namePattern?: string; // 首页实际设备名的正则(如 'Meter \\w+');匹配到则以实际名回写注册表
|
||||
beforePairing?: (driver: DeviceDriver) => Promise<void>; // 过免责声明后、按继电器配对前的额外步骤(如 outdoor meter 套件先点 "Add Meter")
|
||||
pairRetries?: number; // 配对未触发(按完没连上)时的最大尝试轮数,默认 3
|
||||
pairPollIters?: number; // 每轮检测轮询次数(窗口=次数×1.5s),默认 12(~18s);remote 检测慢需调大
|
||||
repressOnRetry?: boolean; // 重试时是否重按继电器;默认 true。切换式配对键(如 remote 凸+凹,按一次进/再按退)须设 false——只按一次,重试只重点 Next
|
||||
postConnectSteps?: (driver: DeviceDriver) => Promise<void>; // 连接后专属流程(如 curtain 选模式+行程校验);提供则替代默认 navigateThroughWizard
|
||||
holdMs?: number; // 覆盖按键时长
|
||||
disclaimerMaxNext?: number; // 到"引导页(按按键处)"前需点几次 Next(过免责声明),默认 1
|
||||
connectionKeywords?: string[];
|
||||
wizardButtons?: string[];
|
||||
}
|
||||
|
||||
/** 点"下一步"类按钮。Android 用 text("Next");iOS 配对页按钮叫 "Connect device"(非 Next),故按名依次尝试。返回是否点到。 */
|
||||
async function tapNext(driver: DeviceDriver): Promise<boolean> {
|
||||
if (driver.platform === 'android') {
|
||||
const el = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Next")');
|
||||
if (!el) return false;
|
||||
await driver.tapElement(el);
|
||||
return true;
|
||||
}
|
||||
// iOS:不同页的推进按钮名不同(免责声明="Next"、配对页="Connect device"、连接失败页="Retry"),依次尝试
|
||||
for (const name of ['Next', 'Connect device', 'Connect Device', 'Retry', 'Continue', 'Done']) {
|
||||
const el = await driver.findElementRaw('name', name).catch(() => null);
|
||||
if (el) { await driver.tapElement(el); return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从"输入设备名"页读取默认设备名(连接成功/失败后都会跳到该页)。
|
||||
* 取输入框(Android EditText / iOS TextField)的当前文本 = 设备默认名。找不到返回 null。
|
||||
*/
|
||||
export async function captureDeviceNameFromInput(driver: DeviceDriver): Promise<string | null> {
|
||||
const el = driver.platform === 'android'
|
||||
? await driver.findElementRaw('class name', 'android.widget.EditText')
|
||||
: (await driver.findElementRaw('class name', 'XCUIElementTypeTextField'))
|
||||
|| (await driver.findElementRaw('class name', 'XCUIElementTypeTextView'));
|
||||
if (!el) return null;
|
||||
let v: string | null = null;
|
||||
for (const attr of ['text', 'value']) {
|
||||
v = await driver.getElementAttribute(el, attr).catch(() => null);
|
||||
if (v && String(v).trim()) break;
|
||||
}
|
||||
const name = v ? String(v).trim() : '';
|
||||
// 过滤占位符/提示文案
|
||||
if (!name || /enter|name|请输入|名称/i.test(name)) return null;
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 串口配对添加流程(meter / remote 等用继电器按物理键进配对的设备):
|
||||
* 进添加页 → 选品类 → (有则过 Disclaimers)到引导页 → **继电器按键进配对** → 点 Next 连接 → 走完向导 → 首页校验。
|
||||
*
|
||||
* 通用兼容:没有 Disclaimers 的设备会直接到引导页(循环里检测不到 Disclaimer 即跳过)。
|
||||
* 成功判据 = **设备出现在首页**(不靠成功页关键字硬判,避免文案不符导致误判失败);
|
||||
* 成功后把设备名写入注册表(config/device.config 的 getDeviceName 会优先用它作功能页入口)。
|
||||
*/
|
||||
export async function addDeviceWithSerialPairing(
|
||||
driver: DeviceDriver,
|
||||
options: SerialPairingAddOptions
|
||||
): Promise<boolean> {
|
||||
const {
|
||||
categoryName,
|
||||
categoryScrollHint = 0,
|
||||
deviceKeyword,
|
||||
relayDevice,
|
||||
registryCategory = relayDevice,
|
||||
namePattern,
|
||||
beforePairing,
|
||||
pairRetries = 3,
|
||||
pairPollIters = 12,
|
||||
repressOnRetry = true,
|
||||
postConnectSteps,
|
||||
holdMs,
|
||||
disclaimerMaxNext = 1,
|
||||
connectionKeywords,
|
||||
wizardButtons = DEFAULT_WIZARD_BUTTONS,
|
||||
} = options;
|
||||
|
||||
const addDevicePage = await navigateToAddPage(driver, 'Device');
|
||||
if (!addDevicePage) { console.log('FAIL: cannot navigate to Add Device'); return false; }
|
||||
|
||||
const catSelected = await selectDeviceCategory(driver, categoryName, categoryScrollHint);
|
||||
if (!catSelected) { console.log(`FAIL: cannot select category ${categoryName}`); return false; }
|
||||
|
||||
// 过免责声明到引导页(无 Disclaimers 的设备自动跳过)
|
||||
for (let i = 0; i < disclaimerMaxNext; i++) {
|
||||
const src = await driver.getSource();
|
||||
if (!src.includes('Disclaimer')) break;
|
||||
if (!(await tapNext(driver))) break;
|
||||
await sleep(1500);
|
||||
}
|
||||
|
||||
// 配对前额外步骤(如 outdoor meter 套件:先点 "Add Meter" 进计量器子流程)
|
||||
if (beforePairing) { await beforePairing(driver); await sleep(2000); }
|
||||
|
||||
// 引导页:继电器按物理键进配对 → 点 Next 触发连接。
|
||||
// 注意:有些设备(如 remote 凸+凹)的配对键是"切换式"——按一次进配对、再按一次退出。
|
||||
// 这类设备传 repressOnRetry=false:只按一次,重试时仅重点 Next + 延长检测,绝不重按(重按会把它切出配对)。
|
||||
const startedRe = namePattern ? new RegExp(namePattern) : null;
|
||||
// 连接失败页标志(iOS "Failed to connect to your device" + Retry;curtain3 首连尤其易失败)
|
||||
const FAIL_MARKERS = ['Failed to connect', 'Failed to Connect', 'Connection failed', 'Try Again', '连接失败', '无法连接'];
|
||||
const pairingStarted = async (): Promise<boolean> => {
|
||||
const src = await driver.getSource();
|
||||
if (FAIL_MARKERS.some((k) => src.includes(k))) return false; // 失败页不算"已开始"(否则会跳过重试)
|
||||
if (CONNECT_STARTED_KEYWORDS.some((k) => src.includes(k))) return true; // 进入连接/设置页
|
||||
if (startedRe && startedRe.test(src)) return true; // 设备已被发现(如 "Remote 63")
|
||||
return false;
|
||||
};
|
||||
let started = false;
|
||||
let pressed = false;
|
||||
for (let attempt = 0; attempt < pairRetries && !started; attempt++) {
|
||||
// 第一次必按;之后是否重按取决于 repressOnRetry(切换式配对键不重按,只重点 Next)
|
||||
if (!pressed || repressOnRetry) {
|
||||
await enterPairingMode(relayDevice, holdMs ? { holdMs } : {}).catch((e) =>
|
||||
console.log(`relay 配对按键失败(检查 64 路开发板接线/端口):${e.message}`)
|
||||
);
|
||||
pressed = true;
|
||||
await sleep(800);
|
||||
}
|
||||
if (!(await tapNext(driver))) console.log('WARN: 引导页未找到 Next');
|
||||
// 检测轮询:出现连接关键字/设备名即视为进配对成功。窗口 = pairPollIters×1.5s。
|
||||
// remote 的 App BLE 检测延迟大且不稳(实测可达 16s+),需更长窗口(见 remote 用例 pairPollIters)。
|
||||
for (let t = 0; t < pairPollIters && !started; t++) {
|
||||
if (await pairingStarted()) { started = true; break; }
|
||||
await driver.dismissPopupIfPresent(); // 配对/连接中途突然冒出的弹框,顺手关掉
|
||||
await sleep(1500);
|
||||
}
|
||||
if (started) break;
|
||||
console.log(`配对未触发/未发现设备(第 ${attempt + 1}/${pairRetries} 次)${repressOnRetry ? ',清弹框后重按继电器重试...' : ',仅重点 Next(切换式配对键不重按)...'}`);
|
||||
await driver.dismissPopupIfPresent(); // 清可能的"连接失败"弹框,回到引导页再试
|
||||
}
|
||||
if (!started) console.log('WARN: 多次重试后仍未检测到连接开始,继续走后续(大概率失败)');
|
||||
|
||||
// 收尾等待(连上则多等让其完成);连接失败弹框主动关掉,避免残留挡住后续(尤其 curtain3)
|
||||
await waitForConnection(driver, connectionKeywords, started ? 9000 : 3000);
|
||||
await driver.dismissPopupIfPresent();
|
||||
|
||||
// 连接成功(或失败)后通常跳到"输入设备名"页:字段里的默认名即设备名 → 最可靠的取名处
|
||||
const capturedName = await captureDeviceNameFromInput(driver);
|
||||
if (capturedName) console.log(`从"输入设备名"页捕获默认名: "${capturedName}"`);
|
||||
|
||||
// 连接后:有专属流程(如 curtain 选模式 + 行程校验)则走它,否则走默认向导
|
||||
if (postConnectSteps) await postConnectSteps(driver);
|
||||
else await navigateThroughWizard(driver, wizardButtons);
|
||||
await driver.dismissPopupIfPresent();
|
||||
|
||||
// 成功判据:输入页捕获到名字 或 设备出现在首页 → 记录实际设备名作为功能页入口
|
||||
await ensureHomeTab(driver);
|
||||
await sleep(1200);
|
||||
const candidates = [capturedName, deviceKeyword].filter(Boolean) as string[];
|
||||
const re = namePattern ? new RegExp(namePattern) : null;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await driver.dismissPopupIfPresent(); // 添加完成后弹出的"更新/评分/新功能"等弹框,先关再校验
|
||||
const source = await driver.getSource();
|
||||
for (const c of candidates) {
|
||||
if (source.includes(c)) { saveAddedDevice(registryCategory, c); return true; }
|
||||
}
|
||||
if (re) {
|
||||
const m = source.match(re);
|
||||
if (m) { saveAddedDevice(registryCategory, m[0]); return true; } // 实际名(如 Meter 0B)
|
||||
}
|
||||
await driver.scrollDown(400);
|
||||
await sleep(600);
|
||||
}
|
||||
// 首页没匹配上,但已从输入页拿到名字 → 仍记录并视为成功(连接成功/失败都会到输入名页)
|
||||
if (capturedName) { saveAddedDevice(registryCategory, capturedName); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// 首页设备卡容器 id(网格左/右列)。各设备类型 name 控件 id 不同(nameText / nameTextMeter…),
|
||||
// 但卡片容器 leftCard/rightCard 一致 → 用它通用枚举/长按删除,与设备类型无关。
|
||||
const HOME_CARD_IDS = [
|
||||
'com.theswitchbot.switchbot:id/leftCard',
|
||||
'com.theswitchbot.switchbot:id/rightCard',
|
||||
];
|
||||
|
||||
/** 若处于多选模式(顶部 Finish),退出它,回到正常首页。 */
|
||||
async function exitSelectMode(driver: DeviceDriver): Promise<void> {
|
||||
const finish = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Finish")');
|
||||
if (finish) { await driver.tapElement(finish); await sleep(1500); }
|
||||
}
|
||||
|
||||
/** 找首页第一张设备卡容器(滚动查找),返回 {el, name};没有返回 null。 */
|
||||
async function firstDeviceCard(driver: DeviceDriver): Promise<{ el: string; name: string } | null> {
|
||||
// 轻量恢复到干净首页(退残留向导 + 关弹框 + 回首页),不做冷重启(冷启动会让首页卡片晚渲染→漏删)
|
||||
await exitSelectMode(driver);
|
||||
for (const t of ['Return Home', 'Quit', 'Exit', 'Leave']) {
|
||||
const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${t}")`);
|
||||
if (el) { await driver.tapElement(el); await sleep(1500); break; }
|
||||
}
|
||||
await driver.dismissPopupIfPresent();
|
||||
await ensureHomeTab(driver);
|
||||
await sleep(1500); // 等首页渲染
|
||||
// 充分轮询(~20s):设备卡渲染慢也能等到。优先卡片容器,兜底设备名控件(各型号 id 不同)
|
||||
for (let s = 0; s < 12; s++) {
|
||||
for (const id of HOME_CARD_IDS) {
|
||||
const els = await driver.findElementsRaw('id', id);
|
||||
if (els.length) {
|
||||
const nameEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().resourceIdMatches(".*:id/name.*")').catch(() => null);
|
||||
const name = (nameEl && (await driver.getElementAttribute(nameEl, 'text').catch(() => ''))) || '(device)';
|
||||
return { el: els[0], name };
|
||||
}
|
||||
}
|
||||
const nameEls = await driver.findElementsRaw('-android uiautomator', 'new UiSelector().resourceIdMatches(".*:id/name.*")').catch(() => []);
|
||||
if (nameEls && nameEls.length) {
|
||||
const name = (await driver.getElementAttribute(nameEls[0], 'text').catch(() => '')) || '(device)';
|
||||
return { el: nameEls[0], name };
|
||||
}
|
||||
await driver.scrollDown(500);
|
||||
await sleep(900);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 长按某设备卡 → 多选模式 → 底部 Delete → 确认(删除单个,与房间无关)。 */
|
||||
/**
|
||||
* 批量删除:长按首张卡 → 进多选模式 → 选中本屏其它卡(同房间一次性多选)→ 一次 Delete → 确认。
|
||||
* 跨房间不能一起删(点跨房间卡会被忽略),其余轮次再处理。返回本批删除的设备名。
|
||||
*/
|
||||
async function deleteVisibleBatch(driver: DeviceDriver): Promise<string[]> {
|
||||
const card = await firstDeviceCard(driver);
|
||||
if (!card) return [];
|
||||
const r0 = await driver.getElementRect(card.el);
|
||||
const cx0 = Math.round(r0.x + r0.width / 2), cy0 = Math.round(r0.y + r0.height / 2);
|
||||
|
||||
await driver.longPress(cx0, cy0, 1.0);
|
||||
await sleep(1000); // 等多选模式出现
|
||||
|
||||
// 是否仍处于"可删除"的有效多选(底部有 Delete)。跨房间多选会让 Delete 消失。
|
||||
const hasDelete = () => driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")');
|
||||
const exitSelect = async () => {
|
||||
const finish = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Finish")');
|
||||
if (finish) { await driver.tapElement(finish); await sleep(800); }
|
||||
};
|
||||
|
||||
// 长按未进入有效多选(无 Delete)→ 退出,交给外层下一轮/恢复
|
||||
if (!(await hasDelete())) { await exitSelect(); return []; }
|
||||
|
||||
// 逐张点选本屏其它卡:点完校验 Delete 是否还在。
|
||||
// 跨房间卡会让 Delete 消失 → 立即再点一次取消该卡(恢复有效选择),跳过它;
|
||||
// 这样一批只删"同房间"设备,其它房间留给 removeAllDevices 外层循环。
|
||||
const picked = [card.name];
|
||||
const others: { cx: number; cy: number }[] = [];
|
||||
for (const id of HOME_CARD_IDS) {
|
||||
for (const el of await driver.findElementsRaw('id', id)) {
|
||||
try {
|
||||
const r = await driver.getElementRect(el);
|
||||
const cx = Math.round(r.x + r.width / 2), cy = Math.round(r.y + r.height / 2);
|
||||
if (Math.abs(cx - cx0) < 12 && Math.abs(cy - cy0) < 12) continue; // 跳过长按那张
|
||||
others.push({ cx, cy });
|
||||
} catch { /* 卡片可能已变,忽略 */ }
|
||||
}
|
||||
}
|
||||
for (const { cx, cy } of others) {
|
||||
await driver.tap(cx, cy);
|
||||
await sleep(400);
|
||||
if (await hasDelete()) {
|
||||
picked.push('(device)'); // 同房间,保留选中
|
||||
} else {
|
||||
await driver.tap(cx, cy); // 跨房间→Delete消失,取消该卡恢复有效选择
|
||||
await sleep(400);
|
||||
}
|
||||
}
|
||||
|
||||
// 点底部 Delete
|
||||
const delBtn = await hasDelete();
|
||||
if (!delBtn) { await exitSelect(); return []; }
|
||||
await driver.tapElement(delBtn);
|
||||
await sleep(900);
|
||||
|
||||
// 确认弹框(快速):Delete / OK / 删除 / Confirm
|
||||
for (let retry = 0; retry < 3; retry++) {
|
||||
const src = await driver.getSource();
|
||||
if (/Cancel|取消|delete|删除/i.test(src)) {
|
||||
let confirm: string | null = null;
|
||||
for (const t of ['Delete', 'OK', '删除', 'Confirm']) {
|
||||
confirm = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${t}")`);
|
||||
if (confirm) break;
|
||||
}
|
||||
if (confirm) { await driver.tapElement(confirm); break; }
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
await sleep(1800); // 等删除生效 + 首页刷新
|
||||
return picked;
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页是否还有设备卡 —— 用于 reset 后校验"是否真的清空了"。
|
||||
* 只看卡片容器(leftCard/rightCard),与设备类型无关。Android 专用;iOS 待补返回 false。
|
||||
*/
|
||||
export async function homepageHasDeviceCards(driver: DeviceDriver): Promise<boolean> {
|
||||
if (driver.platform !== 'android') return false;
|
||||
await ensureHomeTab(driver);
|
||||
for (const id of HOME_CARD_IDS) {
|
||||
const els = await driver.findElementsRaw('id', id);
|
||||
if (els.length) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当前平台账号下的**所有**设备 —— 必测执行前置,确保 connect 用例真正走添加流程。
|
||||
* 通用按设备卡(nameText)枚举,逐个长按删除(跨房间不能一起删,逐个最稳),与设备名无关。
|
||||
* 删完清空设备名注册表。返回已删除的设备名列表。
|
||||
*/
|
||||
export async function removeAllDevices(
|
||||
driver: DeviceDriver,
|
||||
opts: { max?: number } = {}
|
||||
): Promise<string[]> {
|
||||
const removed: string[] = [];
|
||||
if (driver.platform === 'ios') {
|
||||
// iOS:逐个删(最稳)。按设备名找卡 → 长按进多选 → 底部 Delete → 弹框 "Confirm"。循环到无设备。
|
||||
const NAME_RE = /(Meter|Outdoor Meter|Remote|Curtain|Hub Mini|Hub 2|Hub|Bot|Plug|Lock|Keypad|Blind Tilt)\s+[0-9A-Za-z]{2}\b/;
|
||||
const maxIos = opts.max ?? 50;
|
||||
for (let round = 0; round < maxIos; round++) {
|
||||
const fin = await driver.findElementRaw('name', 'Finish').catch(() => null);
|
||||
if (fin) { await driver.tapElement(fin); await sleep(800); } // 退出残留多选
|
||||
const src = await driver.getSource();
|
||||
const m = src.match(NAME_RE);
|
||||
if (!m) break; // 首页已无设备
|
||||
const name = m[0];
|
||||
const card = await driver.findElementRaw('predicate string', `name == "${name}" OR label == "${name}"`).catch(() => null);
|
||||
if (!card) break;
|
||||
const r = await driver.getElementRect(card);
|
||||
await driver.longPress(Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2), 1.2);
|
||||
await sleep(1500);
|
||||
const del = await driver.findElementRaw('name', 'Delete').catch(() => null);
|
||||
if (!del) { const f2 = await driver.findElementRaw('name', 'Finish').catch(() => null); if (f2) await driver.tapElement(f2); break; }
|
||||
await driver.tapElement(del); await sleep(1200);
|
||||
const confirm = await driver.findElementRaw('name', 'Confirm').catch(() => null); // 确认框 "Delete the selected items?" → Confirm
|
||||
if (confirm) await driver.tapElement(confirm);
|
||||
await sleep(2500);
|
||||
removed.push(name);
|
||||
console.log(`已删 ${name}`);
|
||||
}
|
||||
clearAddedDevices();
|
||||
console.log(`removeAllDevices[ios] 完成,共删除 ${removed.length} 个: ${removed.join(', ') || '(无)'}`);
|
||||
return removed;
|
||||
}
|
||||
if (driver.platform !== 'android') {
|
||||
console.log('removeAllDevices: 该平台未实现');
|
||||
return removed;
|
||||
}
|
||||
const max = opts.max ?? 50;
|
||||
// 不在开头冷重启(会让首页卡片晚渲染→漏删);firstDeviceCard 内已做轻量恢复 + 充分轮询
|
||||
for (let round = 0; round < max; round++) {
|
||||
const batch = await deleteVisibleBatch(driver);
|
||||
if (!batch.length) break; // 首页已无任何设备卡
|
||||
removed.push(...batch);
|
||||
console.log(`本批删除 ${batch.length} 个: ${batch.join(', ')}`);
|
||||
}
|
||||
clearAddedDevices();
|
||||
console.log(`removeAllDevices 完成,共删除 ${removed.length} 个: ${removed.join(', ') || '(无)'}`);
|
||||
return removed;
|
||||
}
|
||||
|
||||
export async function removeDeviceFromAccount(driver: DeviceDriver, keyword: string): Promise<boolean> {
|
||||
await ensureHomeTab(driver);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* 已添加设备注册表 —— 添加成功后把"首页实际设备名"写入,供功能/设置用例作为进入设备页的入口。
|
||||
*
|
||||
* **按平台分文件**:test-plan/added-devices.<platform>.json(android / ios 各一份)。
|
||||
* - 同机同时跑 Android + iOS 时是两套真机、设备名不同 → 各读各的,互不干扰;
|
||||
* - 两个进程写不同文件,天然无写冲突。
|
||||
* 平台取 process.env.PLATFORM(与 config/app.config 一致,默认 ios)。
|
||||
*
|
||||
* 读取优先级见 config/device.config.ts 的 getDeviceName:env > 本注册表 > 配置默认值。
|
||||
* 设备名是变量、随真机实际显示而定,不写死。
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
function currentPlatform(): string {
|
||||
return (process.env.PLATFORM || 'ios').toLowerCase();
|
||||
}
|
||||
|
||||
function registryFile(platform = currentPlatform()): string {
|
||||
return path.resolve(__dirname, '../../test-plan', `added-devices.${platform}.json`);
|
||||
}
|
||||
|
||||
function readRegistry(platform = currentPlatform()): Record<string, string> {
|
||||
try {
|
||||
const f = registryFile(platform);
|
||||
if (fs.existsSync(f)) return JSON.parse(fs.readFileSync(f, 'utf-8'));
|
||||
} catch { /* 忽略损坏文件 */ }
|
||||
return {};
|
||||
}
|
||||
|
||||
/** 写入某品类添加后的实际设备名(进入功能页的入口),写到当前平台的注册表。 */
|
||||
export function saveAddedDevice(category: string, deviceName: string): void {
|
||||
const platform = currentPlatform();
|
||||
const f = registryFile(platform);
|
||||
const reg = readRegistry(platform);
|
||||
reg[category] = deviceName;
|
||||
fs.mkdirSync(path.dirname(f), { recursive: true });
|
||||
fs.writeFileSync(f, JSON.stringify(reg, null, 2), 'utf-8');
|
||||
console.log(`已记录设备入口[${platform}]: ${category} → "${deviceName}" (${f})`);
|
||||
}
|
||||
|
||||
/** 读当前平台某品类已添加的设备名;无则返回 undefined。 */
|
||||
export function getAddedDevice(category: string): string | undefined {
|
||||
return readRegistry()[category];
|
||||
}
|
||||
|
||||
/** 清空当前平台的注册表(必测重跑前清设备后调用,避免残留旧入口名)。 */
|
||||
export function clearAddedDevices(): void {
|
||||
const platform = currentPlatform();
|
||||
const f = registryFile(platform);
|
||||
if (fs.existsSync(f)) fs.writeFileSync(f, '{}\n', 'utf-8');
|
||||
console.log(`已清空设备注册表[${platform}] (${f})`);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { DeviceDriver } from '../../drivers/types';
|
||||
import { sleep, waitForSource } from './element.helper';
|
||||
import { extractTicketId, setSharedTicketId } from './zendesk.helper';
|
||||
|
||||
export async function navigateToFeedback(driver: DeviceDriver): Promise<boolean> {
|
||||
if (driver.platform === 'android') {
|
||||
|
|
@ -60,3 +61,25 @@ export async function submitFeedback(
|
|||
await sleep(2000);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* feedback 提交成功后,从结果页抓取工单号并持久化(供「获取RN插件 / 插件热更」用例消费)。
|
||||
* 返回工单号;未抓到返回 null。抓到即写共享文件 reports/.zendesk-ticket.json。
|
||||
* 工单号是用例间传值的变量,绝不写死——这里负责"产出",下游 resolveTicketId() 负责"取用"。
|
||||
*/
|
||||
export async function captureFeedbackTicketId(driver: DeviceDriver): Promise<string | null> {
|
||||
// 提交成功页可能需要一两秒渲染工单号
|
||||
let ticketId: string | null = null;
|
||||
for (let i = 0; i < 3 && !ticketId; i++) {
|
||||
const source = await driver.getSource();
|
||||
ticketId = extractTicketId(source);
|
||||
if (!ticketId) await sleep(1500);
|
||||
}
|
||||
if (ticketId) {
|
||||
setSharedTicketId(ticketId);
|
||||
console.log(`feedback 工单号已捕获并保存: ${ticketId}`);
|
||||
} else {
|
||||
console.log('feedback 提交页未解析到工单号(检查 extractTicketId 规则与实际 UI)');
|
||||
}
|
||||
return ticketId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* Hub Mini WiFi 配网 —— 连上设备后进入 "Configure Wi-Fi" 页:
|
||||
* - SSID 字段:默认预填手机当前 WiFi(如 "Deco"),需清空改填目标 SSID
|
||||
* - 密码字段:hint "Enter Wi-Fi password",填密码
|
||||
* - 点 Next 提交,等待联网完成
|
||||
* 仅支持 2.4GHz WiFi(页面有提示)。凭据来自 config/wifi.config(env 可覆盖,默认 woanmarket/woankeji)。
|
||||
*/
|
||||
import { DeviceDriver } from '../../drivers/types';
|
||||
import { sleep } from './element.helper';
|
||||
import { getWifiCredentials, WifiCredentials } from '../../config/wifi.config';
|
||||
import { navigateThroughWizard } from './navigation.helper';
|
||||
|
||||
const EDIT = 'new UiSelector().className("android.widget.EditText")';
|
||||
|
||||
/** 在 "Configure Wi-Fi" 页填 SSID + 密码并 Next;随后走完向导。creds 缺省取 config/wifi.config。 */
|
||||
export async function configureHubWifi(driver: DeviceDriver, creds?: WifiCredentials): Promise<void> {
|
||||
const { ssid, password } = creds || getWifiCredentials();
|
||||
|
||||
// 等 "Configure Wi-Fi" 页出现(文本判定,跨平台)。BLE 连接较慢(relay 后约 24s 连上、27s 出页),窗口要够长(~37s)。
|
||||
let onWifiPage = false;
|
||||
for (let i = 0; i < 25; i++) {
|
||||
const src = await driver.getSource();
|
||||
if (/Configure Wi-?Fi|Wi-?Fi password|配置 ?Wi-?Fi/i.test(src)) { onWifiPage = true; break; }
|
||||
await driver.dismissPopupIfPresent();
|
||||
await sleep(1500);
|
||||
}
|
||||
if (!onWifiPage) { console.log('WARN: 未到达 Configure Wi-Fi 页,跳过配网'); return; }
|
||||
|
||||
if (driver.platform === 'android') {
|
||||
// Android:SSID/密码都是 EditText,直接定位元素 clear + type
|
||||
const eds = await driver.findElementsRaw('-android uiautomator', EDIT);
|
||||
if (eds.length < 2) { console.log('WARN: 未找到 SSID/密码输入框,跳过配网'); return; }
|
||||
let ssidEl = eds[0], pwdEl = eds[1];
|
||||
try { const t0 = (await driver.getElementAttribute(eds[0], 'text')) || ''; if (/password|密码/i.test(t0)) { pwdEl = eds[0]; ssidEl = eds[1]; } } catch { /* 默认顺序 */ }
|
||||
await driver.clearText(ssidEl).catch(() => {});
|
||||
await driver.typeText(ssidEl, ssid); await sleep(500);
|
||||
await driver.typeText(pwdEl, password); await sleep(500);
|
||||
try { await driver.goBack(); } catch { /* 收键盘 */ }
|
||||
await sleep(500);
|
||||
} else {
|
||||
// iOS:SSID/密码框是自绘控件、无障碍不可见 → 按坐标点 + 模拟键盘键入(/wda/keys)。
|
||||
// 坐标按 iPhone 14 Pro(393×852pt):SSID≈(118,358)、密码≈(118,443);换机型需调。
|
||||
// 重要:SSID 预填手机当前 WiFi(如 Deco),聚焦**不会**自动全选,必须先清空——双击选中该词再键入替换;
|
||||
// 最后必须用键盘 "Return" 收起键盘,否则 Next 被键盘挡住点不到。
|
||||
const d = driver as any;
|
||||
await d.tap(118, 358); await sleep(800); // 聚焦 SSID
|
||||
await d.doubleTap(118, 358); await sleep(700); // 双击选中预填值(单词)
|
||||
await d.sendKeys(['\b']); await sleep(300); // 删除选中(退格)
|
||||
await d.sendKeys([ssid]); await sleep(800); // 键入目标 SSID
|
||||
await d.tap(118, 443); await sleep(800); // 聚焦密码(通常为空)
|
||||
await d.sendKeys([password]); await sleep(800);
|
||||
const ret = await driver.findElementRaw('name', 'Return').catch(() => null);
|
||||
if (ret) { await driver.tapElement(ret); await sleep(1000); } // 收起键盘
|
||||
}
|
||||
console.log(`Hub WiFi 配网:SSID="${ssid}" 密码已填,提交...`);
|
||||
|
||||
// 点 Next(iOS 名为 Next;Android text=Next)
|
||||
const next = driver.platform === 'android'
|
||||
? await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Next")')
|
||||
: await driver.findElementRaw('name', 'Next');
|
||||
if (next) await driver.tapElement(next);
|
||||
|
||||
// 提交后是多步配网过程:连接 WiFi → 验证成功 → 设备配网成功 → 跳添加成功页,过程较久。
|
||||
// 期间都是进度页,**不要乱点按钮**(误点会打断配网),只被动等"成功"强标志出现 + 关弹框。
|
||||
const SUCCESS = [
|
||||
'added successfully', 'Added successfully', 'Start Using', 'Use now', 'Pick a room',
|
||||
'Set up complete', 'Setup complete', 'configured successfully', 'connected to',
|
||||
'添加成功', '配网成功', '连接成功', '设置完成',
|
||||
];
|
||||
const FAIL = ['Failed', 'failed', 'incorrect', 'wrong', 'Unable', '失败', '密码错误', 'try again', 'Try Again'];
|
||||
let reached = false;
|
||||
for (let t = 0; t < 30; t++) { // 最多 ~90s 等配网过程
|
||||
const src = await driver.getSource();
|
||||
if (SUCCESS.some((k) => src.includes(k))) { console.log(`Hub 配网成功(到结果页, +${t * 3}s)`); reached = true; break; }
|
||||
if (FAIL.some((k) => src.includes(k))) {
|
||||
const vis = (src.match(/text="([^"]+)"/g) || []).map((x) => x.replace(/text="|"/g, '')).filter(Boolean).slice(0, 12).join(' | ');
|
||||
console.log(`WARN: Hub 配网疑似失败(+${t * 3}s)。页面:${vis}`);
|
||||
break;
|
||||
}
|
||||
if (t % 3 === 0) { // 每 ~9s 记一次当前阶段,便于排查"连接/验证/配网"卡在哪
|
||||
const vis = (src.match(/text="([^"]+)"/g) || []).map((x) => x.replace(/text="|"/g, '')).filter(Boolean).slice(0, 8).join(' | ');
|
||||
console.log(` 配网进行中 +${t * 3}s: ${vis}`);
|
||||
}
|
||||
await driver.dismissPopupIfPresent();
|
||||
await sleep(3000);
|
||||
}
|
||||
if (!reached) console.log('WARN: 未等到 Hub 配网成功页(可能 WiFi 不可达/非2.4G/密码错,或过程超 90s)');
|
||||
|
||||
// 到结果页后走完结尾向导(Start Using / Done / 房间等)
|
||||
await driver.dismissPopupIfPresent();
|
||||
await navigateThroughWizard(driver, ['Use now', 'Start Using', 'Done', 'Got it', 'OK', 'Skip', 'Next'], 8);
|
||||
}
|
||||
|
||||
|
|
@ -10,3 +10,10 @@ export * from './scene.helper';
|
|||
export * from './feedback.helper';
|
||||
export * from './network.helper';
|
||||
export * from './ones-anchor.helper';
|
||||
export * from './zendesk.helper';
|
||||
export * from './serial_controller';
|
||||
export * from './relay.helper';
|
||||
export * from './device-registry.helper';
|
||||
export * from './qr.helper';
|
||||
export * from './curtain.helper';
|
||||
export * from './hub.helper';
|
||||
|
|
|
|||
|
|
@ -1,13 +1,37 @@
|
|||
import { DeviceDriver } from '../../drivers/types';
|
||||
import { sleep, waitForElement, waitForSource } from './element.helper';
|
||||
import { execSync } from 'child_process';
|
||||
import { APP_CONFIG } from '../../config/app.config';
|
||||
|
||||
/**
|
||||
* 强制重启 Android App(force-stop + 重新拉起)—— 清掉卡住的弹框/崩溃页/残留向导,让首页"+"恢复可用。
|
||||
* 夜间 soak 实测:curtain3 连接失败弹框未处理会一直挡住"+",导致后续整轮导航失败。
|
||||
*/
|
||||
export async function restartAndroidApp(driver: DeviceDriver): Promise<void> {
|
||||
const pkg = APP_CONFIG.android.appPackage;
|
||||
try {
|
||||
execSync(`adb shell am force-stop ${pkg}`, { stdio: 'ignore' });
|
||||
await sleep(1500);
|
||||
await driver.activateApp(pkg).catch(() => {
|
||||
try { execSync(`adb shell monkey -p ${pkg} -c android.intent.category.LAUNCHER 1`, { stdio: 'ignore' }); } catch { /* ignore */ }
|
||||
});
|
||||
await sleep(6000);
|
||||
await driver.dismissPopupIfPresent();
|
||||
await ensureHomeTab(driver);
|
||||
await driver.dismissPopupIfPresent();
|
||||
} catch (e: any) {
|
||||
console.log('restartAndroidApp 异常:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function ensureHomeTab(driver: DeviceDriver): Promise<void> {
|
||||
await driver.goBackToHomepage();
|
||||
await sleep(1000);
|
||||
await sleep(600);
|
||||
if (driver.platform === 'android') {
|
||||
try {
|
||||
const homeTab = await driver.findElementRaw('accessibility id', 'Home');
|
||||
if (homeTab) { await driver.tapElement(homeTab); await sleep(1000); }
|
||||
if (homeTab) { await driver.tapElement(homeTab); await sleep(600); }
|
||||
} catch { /* element may be stale after navigation */ }
|
||||
} else {
|
||||
const homeTab = await driver.findElementRaw('predicate string', 'name BEGINSWITH "HomeScene"');
|
||||
|
|
@ -19,25 +43,43 @@ export async function navigateToAddPage(
|
|||
driver: DeviceDriver,
|
||||
type: 'Device' | 'Scene'
|
||||
): Promise<boolean> {
|
||||
await driver.dismissPopupIfPresent();
|
||||
await ensureHomeTab(driver);
|
||||
await driver.dismissPopupIfPresent();
|
||||
|
||||
if (driver.platform === 'android') {
|
||||
const addBtn = await driver.findElementRaw('id', 'com.theswitchbot.switchbot:id/addBto');
|
||||
if (!addBtn) { console.log('FAIL: addBto not found'); return false; }
|
||||
await driver.tapElement(addBtn);
|
||||
await sleep(1500);
|
||||
|
||||
const addBtoId = 'com.theswitchbot.switchbot:id/addBto';
|
||||
const menuText = type === 'Device' ? 'Add Device' : 'Add Scene';
|
||||
const menuEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${menuText}")`);
|
||||
if (!menuEl) { console.log(`FAIL: no ${menuText} in menu`); return false; }
|
||||
await driver.tapElement(menuEl);
|
||||
await sleep(3000);
|
||||
|
||||
const src = await driver.getSource();
|
||||
if (type === 'Device') return src.includes('Bot') || src.includes('Add Device');
|
||||
return src.includes('Create Scene');
|
||||
// 最多 3 次。快路:直接找"+";在则我们已在可加设备的首页,跳过 dismiss/ensureHomeTab 恢复(省 ~6s)。
|
||||
// 找不到"+"才恢复(首轮轻量:dismiss+ensureHomeTab;再不行才重启 App 清残留弹框)。
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
let addBtn = await driver.findElementRaw('id', addBtoId);
|
||||
if (!addBtn) {
|
||||
await driver.dismissPopupIfPresent();
|
||||
await ensureHomeTab(driver);
|
||||
await driver.dismissPopupIfPresent();
|
||||
if (attempt > 0) await restartAndroidApp(driver); // 轻量恢复无效 → 重启清残留弹框
|
||||
addBtn = await driver.findElementRaw('id', addBtoId);
|
||||
}
|
||||
if (addBtn) {
|
||||
await driver.tapElement(addBtn);
|
||||
// 轮询菜单出现(替代固定 sleep1000),出现即走
|
||||
let menuEl: string | null = null;
|
||||
for (let i = 0; i < 8 && !menuEl; i++) {
|
||||
menuEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${menuText}")`);
|
||||
if (!menuEl) await sleep(250);
|
||||
}
|
||||
if (menuEl) {
|
||||
await driver.tapElement(menuEl);
|
||||
// 轮询目标页加载(替代固定 sleep1500)
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const src = await driver.getSource();
|
||||
const ok = type === 'Device' ? (src.includes('Bot') || src.includes('Add Device')) : src.includes('Create Scene');
|
||||
if (ok) return true;
|
||||
await sleep(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`navigateToAddPage 第${attempt + 1}次未成(addBto/菜单缺失或残留弹框)...`);
|
||||
}
|
||||
console.log('FAIL: cannot navigate to Add Device(重试后仍失败)');
|
||||
return false;
|
||||
}
|
||||
|
||||
// iOS
|
||||
|
|
@ -102,31 +144,24 @@ export async function navigateToManageScenes(driver: DeviceDriver): Promise<bool
|
|||
export async function navigateThroughWizard(
|
||||
driver: DeviceDriver,
|
||||
buttonNames = ['Use now', 'Start Using', 'Done', 'Got it', 'OK', 'Skip', 'Next'],
|
||||
maxSteps = 10
|
||||
maxSteps = 6
|
||||
): Promise<boolean> {
|
||||
if (driver.platform === 'android') {
|
||||
for (let i = 0; i < maxSteps; i++) {
|
||||
await sleep(1000);
|
||||
const src = await driver.getSource();
|
||||
if (!src.includes('Add Bot') && !src.includes('Added successfully') && !src.includes('Install')) {
|
||||
if (await driver.isOnHomepage()) return true;
|
||||
}
|
||||
let tapped = false;
|
||||
for (const btn of buttonNames) {
|
||||
try {
|
||||
const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${btn}")`);
|
||||
if (el) {
|
||||
await driver.tapElement(el);
|
||||
await sleep(2000);
|
||||
tapped = true;
|
||||
break;
|
||||
}
|
||||
} catch { /* stale element, continue */ }
|
||||
}
|
||||
if (!tapped) {
|
||||
await driver.goBack();
|
||||
await sleep(1000);
|
||||
const src = await driver.getSource(); // 每轮只取一次整页,避免重复 getSource
|
||||
// 到首页:有底部导航/首页列表,且无添加向导标记
|
||||
const onWizard = /Add (Bot|Meter|Curtain|Remote|Outdoor|Lock|Plug|Hub|Keypad|Fan|Light|Sensor|Device)|Added successfully|Install|Initial Setup|Select Mode|Disclaimers|Calibrat/i.test(src);
|
||||
const onHome = src.includes('homeRecyclerView') || src.includes('addBto') ||
|
||||
src.includes('content-desc="Home"') || src.includes('Automations') || src.includes('Add a Device');
|
||||
if (!onWizard && onHome) return true;
|
||||
// 从已取的 src 判断当前页存在哪个向导按钮,只查这一个(省去逐个 7 次查询)
|
||||
const btn = buttonNames.find(b => src.includes(`text="${b}"`));
|
||||
if (btn) {
|
||||
const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${btn}")`);
|
||||
if (el) { await driver.tapElement(el); await sleep(1200); continue; }
|
||||
}
|
||||
// 没有可点按钮:可能已完成或加载中,短等再判
|
||||
await sleep(700);
|
||||
}
|
||||
return await driver.isOnHomepage();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,15 +23,26 @@ const ANCHORS: Record<string, Record<string, DeviceAnchor>> = {
|
|||
lock: {
|
||||
'Lock 6F': { add: 15970, ctrlBle: '5YEfmpJz', ctrlWifi: '7cEkbh4Y' },
|
||||
'LockPro 7G': { add: 66694, ctrlBle: 'LnPNEJmS', ctrlWifi: 'Virku9zd' },
|
||||
// 以下变体 UI 相似:仅添加(控制必测无独立 step;Lock Ultra 为全新 UI,归新写)
|
||||
'Lock Lite': { add: 304132 },
|
||||
'Lock & Kit': { add: 51440 },
|
||||
'Lock Pro & Kit': { add: 105436 },
|
||||
'Lock Pro WiFi': { add: 304133 },
|
||||
},
|
||||
plug: {
|
||||
// base Plug 仅 WiFi 必测控制(BLE 无)
|
||||
'Plug 4D': { add: 15966, ctrlWifi: 'Ay79Pj23' },
|
||||
'PlugMini 5E': { add: 15958, ctrlBle: '2jSFfY4k', ctrlWifi: '3nXsFW9n' },
|
||||
'eu Plug': { add: 332918, ctrlBle: 'TSVKWcJ3', ctrlWifi: 'U5z9wocr' },
|
||||
'Garage Door Opener': { add: 302289, ctrlBle: 'E5YRm4Pg', ctrlWifi: '2nRaC87F' },
|
||||
// Relay Switch 有 1pm/1/2pm开关/2pm覆盖 多个物理型号,共用添加用例 302288;此处取 1PM 为代表
|
||||
'Relay Switch 1PM': { add: 302288, ctrlBle: 'MKgLq1oy', ctrlWifi: 'XvFF8AK7' },
|
||||
},
|
||||
sensor: {
|
||||
'Contact Sensor 5O': { add: 15962, ctrlBle: 'A8C4BsCm', ctrlWifi: 'JKSgHHbC' },
|
||||
'Motion Sensor 6P': { add: 15963, ctrlBle: 'HrigrD2X', ctrlWifi: 'N7fSHmH3' },
|
||||
// IOSensor 控制必测是历史数据报警/校正(现有 sensor_control 未覆盖)→ 仅添加
|
||||
'IOSensor': { add: 35871 },
|
||||
},
|
||||
humidifier: {
|
||||
'Humidifier 1K': { add: 15964, ctrlBle: 'GkkrZpaC', ctrlWifi: '8tx3Rg76' },
|
||||
|
|
@ -43,18 +54,51 @@ const ANCHORS: Record<string, Record<string, DeviceAnchor>> = {
|
|||
'Hub2 8H': { add: 21787 },
|
||||
'HubMini 9I': { add: 15972 },
|
||||
'HubMiniMatter 0J': { add: 65336 },
|
||||
'Hub2 S3': { add: 319304 },
|
||||
'Hub Mini S3': { add: 319305 },
|
||||
'Hub Mini Matter S3': { add: 319303 },
|
||||
'Hub Plus': { add: 15971 },
|
||||
},
|
||||
meter: {
|
||||
// meter 控制必测(报警/校正)现有 control 未覆盖;仅锚添加。Outdoor Meter 暂无明确 ONES 对应,未映射
|
||||
'Meter 3M': { add: 15965 },
|
||||
// meter 控制必测(报警/校正)现有 control 未覆盖;仅锚添加。Climate Panel/Radiator 为全新 UI,归新写
|
||||
'Meter 3M': { add: 15965 },
|
||||
'Meter Plus': { add: 15956 },
|
||||
'Meter Pro': { add: 267367 },
|
||||
'Meter Pro CO2': { add: 267366 },
|
||||
},
|
||||
robot: {
|
||||
// robot 控制必测(清扫/暂停/回充)为 WiFi,现有无 _control;仅锚添加
|
||||
// robot 控制必测(清扫/暂停/回充)为 WiFi,现有无 _control;仅锚添加。K11+/S20 为全新 UI,归新写
|
||||
'Robot S1': { add: 15952 },
|
||||
'Robot S1P': { add: 15951 },
|
||||
'Robot K10+': { add: 40954 },
|
||||
'Robot K10+ Pro': { add: 122737 },
|
||||
'Robot K10+ Pro Combo': { add: 162261 },
|
||||
'Robot S10': { add: 78369 },
|
||||
},
|
||||
keypad: {
|
||||
// keypad 无控制必测;keypad touch UI 相似仅添加。Keypad Vision 为全新 UI,归新写
|
||||
'Keypad 2V': { add: 15949 },
|
||||
'Keypad Touch': { add: 15948 },
|
||||
},
|
||||
strip_light: {
|
||||
'StripLight 9S': { add: 15959, ctrlBle: '4poyiVwD', ctrlWifi: 'QgX4dX7X' },
|
||||
'RGBWW Strip Light3': { add: 301585, ctrlBle: '3ob75cjf', ctrlWifi: 'N1kzcb7S' },
|
||||
'RGBICWW Strip Light': { add: 311018, ctrlBle: 'QAHNrXrm', ctrlWifi: 'MAwYyCaC' },
|
||||
'RGBIC Neon Wire Rope': { add: 311020, ctrlBle: 'E2QCS8xy', ctrlWifi: '5sJcsTgi' },
|
||||
'RGBIC Neon Rope': { add: 393235, ctrlWifi: 'BiEXxZDr' },
|
||||
},
|
||||
ceiling_light: {
|
||||
'CeilingLight 7Q': { add: 15950, ctrlBle: '2528azD6', ctrlWifi: 'TcnL3gFM' }, // pro
|
||||
'CeilingLight lte': { add: 15950, ctrlBle: 'V15M64cP', ctrlWifi: 'WL8KSQLb' }, // lte 与 pro 共用添加用例
|
||||
'RGBWW Floor Lamp': { add: 301586, ctrlBle: 'JVnRz7xY', ctrlWifi: '2hm6sXrk' },
|
||||
'RGBICWW Floor Lamp': { add: 311019, ctrlBle: 'AfxmoAEq', ctrlWifi: 'ML8CSkWX' },
|
||||
'Candle Lamp': { add: 393236, ctrlWifi: 'RXm3iNLR' },
|
||||
},
|
||||
fan: {
|
||||
'Fan 0T': { add: 122775, ctrlBle: 'JBszgxRS', ctrlWifi: 'KNyzKhn6' }, // 无电池款
|
||||
'Battery Circulator Fan': { add: 48159, ctrlBle: 'CiFPEbNS', ctrlWifi: '6RC22BGo' },
|
||||
'Standing Circulator Fan': { add: 393234, ctrlBle: 'XWmNA9UL' },
|
||||
},
|
||||
};
|
||||
|
||||
const curProto = (): 'ble' | 'wifi' => (process.env.PROTO === 'wifi' ? 'wifi' : 'ble');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
/**
|
||||
* 二维码解码 helper —— 截屏 → 解码,用于摄像头 WiFi 配网 QR 的**凭据校验**。
|
||||
* (摄像头联网那步是镜头光学扫描,需物理治具;本 helper 负责解码+断言 QR 内容正确。)
|
||||
*
|
||||
* 依赖:pngjs(解 PNG)+ jsqr(解码)。截图是 PNG。
|
||||
*/
|
||||
import { PNG } from 'pngjs';
|
||||
import jsQR from 'jsqr';
|
||||
import { DeviceDriver } from '../../drivers/types';
|
||||
|
||||
/** 从 PNG buffer 解码二维码;失败返回 null。可选裁剪中心区域提高小码命中率。 */
|
||||
export function decodeQRFromPngBuffer(buf: Buffer, cropCenter = false): string | null {
|
||||
const png = PNG.sync.read(buf);
|
||||
let data = png.data, width = png.width, height = png.height;
|
||||
if (cropCenter) {
|
||||
// 取中心 60% 区域(二维码通常居中)重采样,提高识别率
|
||||
const cw = Math.floor(width * 0.6), ch = Math.floor(height * 0.6);
|
||||
const ox = Math.floor((width - cw) / 2), oy = Math.floor((height - ch) / 2);
|
||||
const cropped = Buffer.alloc(cw * ch * 4);
|
||||
for (let y = 0; y < ch; y++) {
|
||||
png.data.copy(cropped, y * cw * 4, ((oy + y) * width + ox) * 4, ((oy + y) * width + ox + cw) * 4);
|
||||
}
|
||||
data = cropped; width = cw; height = ch;
|
||||
}
|
||||
const code = jsQR(new Uint8ClampedArray(data), width, height);
|
||||
return code ? code.data : null;
|
||||
}
|
||||
|
||||
/** 从 base64 PNG 解码。先整图,失败再试中心裁剪。 */
|
||||
export function decodeQRFromBase64(b64: string): string | null {
|
||||
const buf = Buffer.from(b64, 'base64');
|
||||
return decodeQRFromPngBuffer(buf, false) || decodeQRFromPngBuffer(buf, true);
|
||||
}
|
||||
|
||||
/** 截当前屏并解码(Appium 截图)。 */
|
||||
export async function decodeQRFromScreen(driver: DeviceDriver): Promise<string | null> {
|
||||
const b64 = await driver.screenshot();
|
||||
return decodeQRFromBase64(b64);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 QR 内容里的 WiFi 字段。
|
||||
* SwitchBot 摄像头配网码实测为 JSON:{"s":"<ssid>","p":"<password>","t":"<token>","r":"<region>"}。
|
||||
* 也兼容标准 WiFi 码 `WIFI:S:<ssid>;T:WPA;P:<pass>;;`,及常见 key=value 启发式。
|
||||
*/
|
||||
export interface WifiQR {
|
||||
ssid?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
region?: string;
|
||||
raw: string;
|
||||
}
|
||||
|
||||
export function parseWifiQR(text: string): WifiQR {
|
||||
const out: WifiQR = { raw: text };
|
||||
// 1) SwitchBot JSON 码
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
if (j && typeof j === 'object') {
|
||||
out.ssid = j.s ?? j.ssid;
|
||||
out.password = j.p ?? j.password ?? j.pwd;
|
||||
out.token = j.t ?? j.token;
|
||||
out.region = j.r ?? j.region;
|
||||
if (out.ssid || out.password) return out;
|
||||
}
|
||||
} catch { /* 非 JSON,继续 */ }
|
||||
// 2) 标准 WiFi 码
|
||||
const std = text.match(/WIFI:(?:[^;]*;)*S:([^;]*);(?:[^;]*;)*P:([^;]*);/i);
|
||||
if (std) { out.ssid = std[1]; out.password = std[2]; return out; }
|
||||
// 3) 启发式
|
||||
const ssid = text.match(/(?:ssid|wifi_?name)["':=\s]+([^"',;&}\s]+)/i);
|
||||
const pwd = text.match(/(?:password|pass|pwd|psk)["':=\s]+([^"',;&}\s]+)/i);
|
||||
if (ssid) out.ssid = ssid[1];
|
||||
if (pwd) out.password = pwd[1];
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 校验 QR 内 WiFi 凭据是否与期望一致(用于摄像头配网断言)。 */
|
||||
export function verifyWifiQR(text: string, expect: { ssid?: string; password?: string }): { ok: boolean; parsed: WifiQR; reason?: string } {
|
||||
const parsed = parseWifiQR(text);
|
||||
if (expect.ssid && parsed.ssid !== expect.ssid) return { ok: false, parsed, reason: `SSID 不符: 期望 ${expect.ssid} 实际 ${parsed.ssid}` };
|
||||
if (expect.password && parsed.password !== expect.password) return { ok: false, parsed, reason: `密码不符: 期望 ${expect.password} 实际 ${parsed.password}` };
|
||||
return { ok: true, parsed };
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* 继电器高层动作 helper —— 用语义名驱动物理按键,所有参数可配/可传,不写死。
|
||||
*
|
||||
* 典型用法(添加流程前置):
|
||||
* await enterPairingMode('remote'); // 按 config 默认(凸+凹 3s)
|
||||
* await enterPairingMode('meter', { holdMs: 2500 }); // 临时覆盖时长
|
||||
* await pressButton('remote', 'convex', 800); // 单键点按
|
||||
*
|
||||
* 端口/波特率默认走 env(SERIAL_PORT/SERIAL_BAUD),也可经 opts 传入。
|
||||
* 默认每次自建并关闭控制器(一次性动作);要在一个会话里连续操作,传 opts.controller 复用。
|
||||
*/
|
||||
import { SerialController, SerialOptions } from './serial_controller';
|
||||
import { resolveRelayAction, resolveRelayButton } from '../../config/relay.config';
|
||||
|
||||
export interface RelayActionOptions extends SerialOptions {
|
||||
/** 覆盖配置里的按住时长(毫秒)。 */
|
||||
holdMs?: number;
|
||||
/** 复用已打开的控制器(不则自建并在结束时关闭)。 */
|
||||
controller?: SerialController;
|
||||
}
|
||||
|
||||
/** 用一个已开/自建的控制器执行 fn,自建的用完即关。 */
|
||||
async function withController<T>(
|
||||
opts: RelayActionOptions,
|
||||
fn: (c: SerialController) => Promise<T>
|
||||
): Promise<T> {
|
||||
if (opts.controller) return fn(opts.controller);
|
||||
const c = new SerialController(opts);
|
||||
await c.open();
|
||||
try {
|
||||
return await fn(c);
|
||||
} finally {
|
||||
await c.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发设备的命名动作(默认 'pairing' 进配对):按 config 解析通道列表+时长,
|
||||
* 多通道则同帧"同时按下",按住后整体释放。返回实际触发的通道与时长。
|
||||
*/
|
||||
export async function triggerRelayAction(
|
||||
device: string,
|
||||
action = 'pairing',
|
||||
opts: RelayActionOptions = {}
|
||||
): Promise<{ channels: number[]; holdMs: number; desc?: string }> {
|
||||
const resolved = resolveRelayAction(device, action);
|
||||
const holdMs = opts.holdMs ?? resolved.holdMs;
|
||||
await withController(opts, (c) => c.pulseChannels(resolved.channels, holdMs));
|
||||
console.log(`relay: ${device}.${action} → 通道[${resolved.channels.join(',')}] 按住 ${holdMs}ms${resolved.desc ? ' (' + resolved.desc + ')' : ''}`);
|
||||
return { ...resolved, holdMs };
|
||||
}
|
||||
|
||||
/** 让设备进入添加/配对模式(= triggerRelayAction(device,'pairing'))。 */
|
||||
export function enterPairingMode(device: string, opts: RelayActionOptions = {}) {
|
||||
return triggerRelayAction(device, 'pairing', opts);
|
||||
}
|
||||
|
||||
/** 点按设备某个命名按键 ms 毫秒(单键)。 */
|
||||
export async function pressButton(
|
||||
device: string,
|
||||
button: string,
|
||||
ms = 500,
|
||||
opts: RelayActionOptions = {}
|
||||
): Promise<number> {
|
||||
const ch = resolveRelayButton(device, button);
|
||||
await withController(opts, (c) => c.pulse(ch, ms));
|
||||
console.log(`relay: 点按 ${device}.${button} → 通道 ${ch} ${ms}ms`);
|
||||
return ch;
|
||||
}
|
||||
|
||||
/** 同时按住一组命名按键 ms 毫秒后释放(任意组合)。 */
|
||||
export async function pressButtonsTogether(
|
||||
device: string,
|
||||
buttons: string[],
|
||||
ms = 500,
|
||||
opts: RelayActionOptions = {}
|
||||
): Promise<number[]> {
|
||||
const channels = buttons.map((b) => resolveRelayButton(device, b));
|
||||
await withController(opts, (c) => c.pulseChannels(channels, ms));
|
||||
console.log(`relay: 同时按 ${device}.[${buttons.join('+')}] → 通道[${channels.join(',')}] ${ms}ms`);
|
||||
return channels;
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
/**
|
||||
* 64 路继电器板串口控制器 —— 昆明创泽 KMCZE-I64O64-V2.5(RS232/RS485,自定义 16 进制协议)。
|
||||
*
|
||||
* 用途:设备「添加」流程里,用继电器通断模拟按物理按键(配对/复位键)、给被测设备上下电。
|
||||
*
|
||||
* 串口参数(规格书):无校验 / 1 停止位 / 无流控;波特率 RS232 默认 115200、RS485 默认 9600。
|
||||
* 端口与波特率走 env,绝不写死:SERIAL_PORT / SERIAL_BAUD。
|
||||
*
|
||||
* 帧格式(16 进制):
|
||||
* 控制单路: 55 C8 <ch> <01|00> 55 (ch=1..64 的十六进制;01 通 / 00 断)
|
||||
* 控制全 64: 55 C8 41 <8 字节状态> 55
|
||||
* 读状态: 55 C7 01 00 55 → 1B DB <8字节输入><8字节输出> 0C
|
||||
* 查版本: 55 D3 D3 00 55 → AB 2D AA 20 ...(只读,安全)
|
||||
* 延时通断: 55 C9 <ch> <01断|02通> <2字节×0.1s> 55
|
||||
*/
|
||||
import { SerialPort } from 'serialport';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const DEFAULT_PORT = '/dev/cu.usbserial-A50285BI';
|
||||
// 按钮点击寿命统计:按「板子(串口序列号)→通道」累加,持久化。继电器寿命约 20 万次。
|
||||
const PRESS_COUNT_FILE = path.resolve(__dirname, '../../test-plan/relay-press-counts.json');
|
||||
const RELAY_LIFESPAN = 200000;
|
||||
|
||||
export interface SerialOptions {
|
||||
port?: string;
|
||||
baudRate?: number;
|
||||
/** 发送后等待应答的毫秒数(默认 300ms) */
|
||||
responseTimeout?: number;
|
||||
}
|
||||
|
||||
export class SerialController {
|
||||
private sp: SerialPort | null = null;
|
||||
private readonly path: string;
|
||||
private readonly baudRate: number;
|
||||
private readonly responseTimeout: number;
|
||||
|
||||
constructor(opts: SerialOptions = {}) {
|
||||
this.path = opts.port || process.env.SERIAL_PORT || DEFAULT_PORT;
|
||||
this.baudRate = opts.baudRate || parseInt(process.env.SERIAL_BAUD || '115200', 10);
|
||||
this.responseTimeout = opts.responseTimeout ?? 300;
|
||||
}
|
||||
|
||||
get portPath(): string {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
/** 板子标识:从串口路径取序列号(如 usbserial-A50285BI → A50285BI),区分多块板;env SERIAL_BOARD_ID 可覆盖。 */
|
||||
get boardId(): string {
|
||||
if (process.env.SERIAL_BOARD_ID) return process.env.SERIAL_BOARD_ID;
|
||||
const m = this.path.match(/usbserial[-_]?([A-Za-z0-9]+)/i);
|
||||
return m ? m[1] : this.path.replace(/^.*\//, '');
|
||||
}
|
||||
|
||||
/** 累加该板各通道的按钮点击次数(继电器寿命约 20 万次),持久化到 test-plan/relay-press-counts.json。 */
|
||||
private countPress(channels: number[]): void {
|
||||
try {
|
||||
let data: Record<string, Record<string, number>> = {};
|
||||
if (fs.existsSync(PRESS_COUNT_FILE)) data = JSON.parse(fs.readFileSync(PRESS_COUNT_FILE, 'utf-8'));
|
||||
const board = this.boardId;
|
||||
data[board] = data[board] || {};
|
||||
for (const ch of channels) data[board][ch] = (data[board][ch] || 0) + 1;
|
||||
fs.mkdirSync(path.dirname(PRESS_COUNT_FILE), { recursive: true });
|
||||
fs.writeFileSync(PRESS_COUNT_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
||||
} catch { /* 计数失败不影响主流程 */ }
|
||||
}
|
||||
|
||||
open(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sp = new SerialPort(
|
||||
{ path: this.path, baudRate: this.baudRate, parity: 'none', stopBits: 1, dataBits: 8 },
|
||||
(err) => (err ? reject(err) : resolve())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.sp?.isOpen) await new Promise<void>((r) => this.sp!.close(() => r()));
|
||||
this.sp = null;
|
||||
}
|
||||
|
||||
/** 发送一帧并收集 responseTimeout 窗口内的所有回包,返回拼接后的 Buffer。 */
|
||||
send(frame: Buffer | number[]): Promise<Buffer> {
|
||||
const buf = Buffer.isBuffer(frame) ? frame : Buffer.from(frame);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.sp?.isOpen) return reject(new Error('串口未打开,请先 open()'));
|
||||
const chunks: Buffer[] = [];
|
||||
const onData = (d: Buffer) => chunks.push(d);
|
||||
this.sp.on('data', onData);
|
||||
this.sp.write(buf, (err) => {
|
||||
if (err) {
|
||||
this.sp?.off('data', onData);
|
||||
return reject(err);
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.sp?.off('data', onData);
|
||||
resolve(Buffer.concat(chunks));
|
||||
}, this.responseTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
private assertChannel(ch: number): void {
|
||||
if (!Number.isInteger(ch) || ch < 1 || ch > 64) throw new Error(`通道号必须是 1..64,收到 ${ch}`);
|
||||
}
|
||||
|
||||
/** 第 ch 路接通(ch: 1..64) */
|
||||
relayOn(ch: number): Promise<Buffer> {
|
||||
this.assertChannel(ch);
|
||||
this.countPress([ch]);
|
||||
return this.send([0x55, 0xc8, ch, 0x01, 0x55]);
|
||||
}
|
||||
|
||||
/** 第 ch 路断开(ch: 1..64) */
|
||||
relayOff(ch: number): Promise<Buffer> {
|
||||
this.assertChannel(ch);
|
||||
return this.send([0x55, 0xc8, ch, 0x00, 0x55]);
|
||||
}
|
||||
|
||||
/** 脉冲:接通 ch 路 ms 毫秒后断开 —— 用于模拟"按一下物理按键"。 */
|
||||
async pulse(ch: number, ms = 500): Promise<void> {
|
||||
await this.relayOn(ch);
|
||||
await new Promise((r) => setTimeout(r, ms));
|
||||
await this.relayOff(ch);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把通道列表编码成 64 路状态掩码(8 字节)。
|
||||
* 规格:高位在左、低位在右;最左字节=ch57-64,最右字节=ch1-8。
|
||||
* 已实测:ch33→第4字节 0x01、ch34→0x02、ch35→0x04(ch34+35=0x06)。
|
||||
*/
|
||||
static buildMask(channels: number[]): Buffer {
|
||||
const mask = Buffer.alloc(8);
|
||||
for (const ch of channels) {
|
||||
if (!Number.isInteger(ch) || ch < 1 || ch > 64) throw new Error(`通道号必须是 1..64,收到 ${ch}`);
|
||||
const group = Math.floor((ch - 1) / 8); // 0=ch1-8 ... 7=ch57-64
|
||||
const byteIdx = 7 - group; // 高位在左
|
||||
const bit = (ch - 1) % 8;
|
||||
mask[byteIdx] |= 1 << bit;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一帧设置全部 64 路状态:列表内通道接通、其余全部断开(用 55 C8 41 命令)。
|
||||
* 列表多于一个通道时即"同时按下"(同一帧闭合,无先后)。传 [] 即全断。
|
||||
*/
|
||||
setChannels(channels: number[]): Promise<Buffer> {
|
||||
if (channels.length) this.countPress(channels); // 仅"通"计入点击;allOff([]) 不计
|
||||
const mask = SerialController.buildMask(channels);
|
||||
return this.send([0x55, 0xc8, 0x41, ...mask, 0x55]);
|
||||
}
|
||||
|
||||
/** 全部继电器断开。 */
|
||||
allOff(): Promise<Buffer> {
|
||||
return this.setChannels([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多键脉冲:同时接通 channels 列表 ms 毫秒后全部断开 —— 模拟"同时按住多个物理键"。
|
||||
* (如 remote 进配对:同时按凸键+凹键。)
|
||||
*/
|
||||
async pulseChannels(channels: number[], ms = 500): Promise<void> {
|
||||
await this.setChannels(channels);
|
||||
await new Promise((r) => setTimeout(r, ms));
|
||||
await this.allOff();
|
||||
}
|
||||
|
||||
/** 读 64 路输入(采集)+ 64 路继电器输出状态。返回原始回包(1B DB ... 0C)。 */
|
||||
readStatus(): Promise<Buffer> {
|
||||
return this.send([0x55, 0xc7, 0x01, 0x00, 0x55]);
|
||||
}
|
||||
|
||||
/** 查询控制器版本(只读,不影响任何继电器)。返回应答里第 5..18 字节是版本字符串。 */
|
||||
async queryVersion(): Promise<{ raw: string; version: string }> {
|
||||
const resp = await this.send([0x55, 0xd3, 0xd3, 0x00, 0x55]);
|
||||
const raw = resp.toString('hex').toUpperCase();
|
||||
// 应答:AB 2D AA <版本ASCII...> BA;版本从第 5 字节(index 4)起
|
||||
const ascii = resp.length > 5 ? resp.subarray(4, resp.length - 1).toString('ascii').replace(/[^\x20-\x7E]/g, '').trim() : '';
|
||||
return { raw, version: ascii };
|
||||
}
|
||||
}
|
||||
|
||||
/** 便捷探测:打开 → 查版本 → 关闭。用于确认板子在线与波特率。 */
|
||||
export async function probeBoard(opts: SerialOptions = {}): Promise<{ port: string; baudRate: number; alive: boolean; version: string; raw: string }> {
|
||||
const c = new SerialController(opts);
|
||||
await c.open();
|
||||
try {
|
||||
const { version, raw } = await c.queryVersion();
|
||||
return { port: c.portPath, baudRate: opts.baudRate || parseInt(process.env.SERIAL_BAUD || '115200', 10), alive: raw.length > 0, version, raw };
|
||||
} finally {
|
||||
await c.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* Zendesk 工单 helper —— 服务于必测项「获取RN插件」用例。
|
||||
*
|
||||
* 数据来源:App 提交 feedback 后,Zendesk 工单的**评论**里会自动带一段环境信息
|
||||
* (App Version / JS_Version / Bundle_Version)。Bundle_Version 即各 RN 插件包版本。
|
||||
* 注意:这些信息不在工单的结构化字段(custom_fields)里,只在评论正文。
|
||||
*
|
||||
* 工单号是**变量**,不写死:
|
||||
* 入参 ticketId > env ZENDESK_TICKET_ID > 上游用例写的共享文件 reports/.zendesk-ticket.json
|
||||
* 典型链路:`15945 feedback提交` 提交后拿到工单号 → setSharedTicketId() →
|
||||
* 「获取RN插件」用例 resolveTicketId() 取到 → getTicketRnPlugins() 读版本。
|
||||
*
|
||||
* 鉴权:Zendesk API Basic 认证 `<email>/token:<api_token>`,从 .env 读
|
||||
* ZENDESK_DOMAIN / ZENDESK_EMAIL / ZENDESK_TOKEN。
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const SHARED_TICKET_FILE = path.resolve(__dirname, '../../reports/.zendesk-ticket.json');
|
||||
|
||||
export interface RnPluginInfo {
|
||||
ticketId: string;
|
||||
appVersion: string | null; // 如 "9.26(23)"
|
||||
jsVersion: string | null; // 如 "1.0.0.2"
|
||||
bundleRaw: string | null; // Bundle_Version 原始串
|
||||
bundle: Record<string, string>; // 解析后的 模块 -> 版本,如 { Home: "default 1.0.49.0", Fan2: "1.0.44.0" }
|
||||
}
|
||||
|
||||
function env(key: string): string {
|
||||
const v = process.env[key];
|
||||
if (!v) throw new Error(`缺少环境变量 ${key}(请在 .env 配置 Zendesk 连接)`);
|
||||
return v;
|
||||
}
|
||||
|
||||
/** 上游用例(如 feedback 提交)拿到工单号后写入,供「获取RN插件」用例消费。 */
|
||||
export function setSharedTicketId(ticketId: string | number): void {
|
||||
fs.mkdirSync(path.dirname(SHARED_TICKET_FILE), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
SHARED_TICKET_FILE,
|
||||
JSON.stringify({ ticketId: String(ticketId), ts: new Date().toISOString() }, null, 2),
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 feedback 提交成功页的 UI 源码里抽取工单号(纯函数,便于单测)。
|
||||
* 优先匹配带标签的格式(工单号/Ticket/Feedback/Request + 数字),兜底取页面里的长数字串。
|
||||
*/
|
||||
export function extractTicketId(source: string): string | null {
|
||||
const labeled = [
|
||||
/工\s*单\s*号\s*[::#]?\s*(\d{4,})/i,
|
||||
/(?:ticket|feedback|request|case)\s*(?:id|no|number|#)?\s*[::#]?\s*(\d{4,})/i,
|
||||
/#\s*(\d{6,})/,
|
||||
];
|
||||
for (const re of labeled) {
|
||||
const m = source.match(re);
|
||||
if (m) return m[1];
|
||||
}
|
||||
// 兜底:页面里第一个 6~10 位独立数字(多数工单号长度)
|
||||
const fallback = source.match(/(?<!\d)(\d{6,10})(?!\d)/);
|
||||
return fallback ? fallback[1] : null;
|
||||
}
|
||||
|
||||
/** 解析工单号:入参 > env ZENDESK_TICKET_ID > 共享文件。都没有则抛错。 */
|
||||
export function resolveTicketId(explicit?: string | number): string {
|
||||
if (explicit != null && String(explicit).trim()) return String(explicit).trim();
|
||||
if (process.env.ZENDESK_TICKET_ID) return process.env.ZENDESK_TICKET_ID.trim();
|
||||
if (fs.existsSync(SHARED_TICKET_FILE)) {
|
||||
const j = JSON.parse(fs.readFileSync(SHARED_TICKET_FILE, 'utf-8'));
|
||||
if (j?.ticketId) return String(j.ticketId);
|
||||
}
|
||||
throw new Error('未提供工单号:请传入 ticketId、设置 ZENDESK_TICKET_ID,或由上游用例 setSharedTicketId() 写入');
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 Bundle_Version 原始串拆成 模块 -> 版本。
|
||||
* 原始形如 "Home:default 1.0.49.0Scene:default 1.0.46.0Fan2:1.0.44.0..."
|
||||
* (各模块首尾相连,模块名是大写开头的驼峰 token,值可能带 "default " 前缀)。
|
||||
*/
|
||||
export function parseBundleVersion(raw: string): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
// 模块名:大写字母开头的驼峰/带数字 token,后跟冒号;值取到下一个这样的 token 之前。
|
||||
const re = /([A-Z][A-Za-z0-9]*)\s*:\s*(.*?)(?=[A-Z][A-Za-z0-9]*\s*:|$)/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(raw)) !== null) {
|
||||
const name = m[1].trim();
|
||||
const ver = m[2].trim();
|
||||
if (name) out[name] = ver;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 从一段评论正文里抽取 App/JS/Bundle 版本(取第一处命中)。 */
|
||||
export function parseDeviceInfo(text: string): Omit<RnPluginInfo, 'ticketId'> {
|
||||
const grab = (re: RegExp): string | null => {
|
||||
const m = text.match(re);
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
const appVersion = grab(/App\s*Version\s*[::]\s*([^\n\r]+)/i);
|
||||
const jsVersion = grab(/JS[_\s]*Version\s*[::]\s*([^\n\r]+)/i);
|
||||
const bundleRaw = grab(/Bundle[_\s]*Version\s*[::]\s*([^\n\r]+)/i);
|
||||
return {
|
||||
appVersion,
|
||||
jsVersion,
|
||||
bundleRaw,
|
||||
bundle: bundleRaw ? parseBundleVersion(bundleRaw) : {},
|
||||
};
|
||||
}
|
||||
|
||||
/** 拉取工单全部评论(按时间倒序返回正文数组,最新在前)。 */
|
||||
export async function fetchTicketComments(ticketId: string): Promise<string[]> {
|
||||
const domain = env('ZENDESK_DOMAIN').replace(/\/+$/, '');
|
||||
const auth = Buffer.from(`${env('ZENDESK_EMAIL')}/token:${env('ZENDESK_TOKEN')}`).toString('base64');
|
||||
const url = `${domain}/api/v2/tickets/${ticketId}/comments.json?sort_order=desc`;
|
||||
const resp = await fetch(url, { headers: { Authorization: `Basic ${auth}` } });
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Zendesk 取评论失败 ticket=${ticketId} HTTP ${resp.status} ${await resp.text()}`);
|
||||
}
|
||||
const data: any = await resp.json();
|
||||
return (data.comments || []).map((c: any) => String(c.body || c.html_body || ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* 「获取RN插件」用例主入口:给定工单号(可省略,走 resolveTicketId),
|
||||
* 返回该工单 app 自动上报的 App/JS/Bundle 版本(扫所有评论,取第一条含 Bundle_Version 的)。
|
||||
*/
|
||||
export async function getTicketRnPlugins(ticketId?: string | number): Promise<RnPluginInfo> {
|
||||
const id = resolveTicketId(ticketId);
|
||||
const comments = await fetchTicketComments(id);
|
||||
for (const body of comments) {
|
||||
if (/Bundle[_\s]*Version/i.test(body)) {
|
||||
return { ticketId: id, ...parseDeviceInfo(body) };
|
||||
}
|
||||
}
|
||||
return { ticketId: id, appVersion: null, jsVersion: null, bundleRaw: null, bundle: {} };
|
||||
}
|
||||
|
|
@ -181,6 +181,35 @@ export class TestReporter {
|
|||
const products = Object.keys(productMap);
|
||||
const moduleCount = Object.values(productMap).reduce((s, m) => s + Object.keys(m).length, 0);
|
||||
|
||||
// ---- 结论 / 存在的问题 / 解决方案 ----
|
||||
const esc = (s: string) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const failedResults = results.filter(r => r.status === 'FAIL');
|
||||
const failSuites = Array.from(new Set(failedResults.map(r => r.suite)));
|
||||
const SOL_KB: { re: RegExp; sol: string }[] = [
|
||||
{ re: /cannot navigate|addBto|添加页/i, sol: '导航到添加页失败(首页"+"被残留弹框/卡死页挡住):已加 navigateToAddPage 找不到"+"时重启 App 兜底 + dismissPopupIfPresent 识别连接失败弹框。' },
|
||||
{ re: /connection timeout|连接超时|未连上|配对未触发/i, sol: '连接/配对未触发:检查蓝牙与设备在线;已加"配对未发现设备就重按继电器重试(pairRetries)"。' },
|
||||
{ re: /Something went wrong|行程|calibrat|校验/i, sol: '行程校验异常:需设备装在真实导轨上;Auto-Calibrate Close 后重试 Finish 已加。' },
|
||||
{ re: /select category|no .* option|品类/i, sol: '品类选择失败:已用 UiScrollable.scrollIntoView 直达深层品类 + 重启兜底。' },
|
||||
{ re: /expected false to be true|not found|未在首页|未找到/i, sol: '设备未成功添加/首页未识别:核对配对触发(pairRetries)与设备名匹配(namePattern,需含十六进制后缀)。' },
|
||||
];
|
||||
const conclusionLines: string[] = [];
|
||||
if (effective === 0) conclusionLines.push('本次无有效用例(全部跳过)。');
|
||||
else if (failed === 0) conclusionLines.push(`全部 <b>${effective}</b> 条有效用例通过,通过率 <b style="color:#97cc64">${passRate}%</b>,功能稳定。`);
|
||||
else conclusionLines.push(`通过率 <b>${passRate}%</b>(${passed}/${effective}),失败 <b style="color:#fd5a3e">${failed}</b> 条,涉及:${failSuites.map(esc).join('、')}。`);
|
||||
if (skipped > 0) conclusionLines.push(`另有 ${skipped} 条跳过(未计入通过率)。`);
|
||||
|
||||
const problemItems = failedResults.map(r =>
|
||||
`<li><span class="badge failed">FAIL</span> <b>${esc(r.suite)}</b> / ${esc(r.name)}<div class="kv-detail">${esc((r.detail || '无详情').slice(0, 400))}</div>${r.screenshot ? `<div class="case-screenshot"><img src="data:image/png;base64,${r.screenshot}"/></div>` : ''}</li>`
|
||||
).join('\n');
|
||||
const solSet = new Set<string>();
|
||||
for (const r of failedResults) {
|
||||
const hit = SOL_KB.find(x => x.re.test(`${r.name} ${r.detail || ''}`));
|
||||
solSet.add(hit ? hit.sol : '查看该用例的失败详情/截图定位原因。');
|
||||
}
|
||||
const conclusionHtml = `<ul class="kv-list">${conclusionLines.map(c => `<li>${c}</li>`).join('')}</ul>`;
|
||||
const problemsHtml = failedResults.length ? `<ul class="kv-list">${problemItems}</ul>` : `<p class="kv-ok">无失败用例 ✓</p>`;
|
||||
const solutionsHtml = solSet.size ? `<ul class="kv-list">${Array.from(solSet).map(s => `<li>${esc(s)}</li>`).join('')}</ul>` : `<p class="kv-ok">无需处理,继续监控。</p>`;
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
|
|
@ -269,6 +298,12 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
|
|||
.case-screenshot img{max-width:300px;border-radius:6px;border:1px solid #3d4550}
|
||||
|
||||
.footer{text-align:center;padding:24px;color:#555;font-size:11px}
|
||||
.kv-box{background:#2d333b;border-radius:10px;padding:14px 18px;margin-bottom:18px;border:1px solid #373e47}
|
||||
.kv-list{list-style:none;padding:0}
|
||||
.kv-list li{padding:8px 0;border-bottom:1px solid #353c45;font-size:13px;line-height:1.6}
|
||||
.kv-list li:last-child{border-bottom:none}
|
||||
.kv-detail{color:#9aa;font-size:12px;margin-top:4px;white-space:pre-wrap;background:#1e2228;padding:6px 10px;border-radius:5px}
|
||||
.kv-ok{color:#97cc64;font-size:13px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -300,13 +335,20 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
|
|||
<div class="section-hd">Products (${products.length}) · ${moduleCount} Modules · ${total} Cases</div>
|
||||
${productSections}
|
||||
|
||||
<div class="section-hd">结论 (Conclusion)</div>
|
||||
<div class="kv-box">${conclusionHtml}</div>
|
||||
<div class="section-hd">存在的问题 (Problems)</div>
|
||||
<div class="kv-box">${problemsHtml}</div>
|
||||
<div class="section-hd">解决方案 (Solutions)</div>
|
||||
<div class="kv-box">${solutionsHtml}</div>
|
||||
|
||||
<div class="footer">Generated by AI_UIAutomation · ${products.length} products · ${moduleCount} modules · ${total} cases</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
fs.writeFileSync(reportFile, html);
|
||||
fs.unlinkSync(SHARED_RESULTS_FILE);
|
||||
if (!process.env.SOAK_MODE) fs.unlinkSync(SHARED_RESULTS_FILE); // soak 模式保留,累积全程结果
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log(' 全量测试报告');
|
||||
|
|
|
|||
|
|
@ -288,6 +288,11 @@ export class WDAHelper {
|
|||
await this.request('POST', `/session/${this.sessionId}/element/${elementId}/clear`);
|
||||
}
|
||||
|
||||
/** 向当前聚焦的输入框发送键盘按键。自绘/RN 输入框对 element/value 无效,需模拟真实键入。 */
|
||||
async sendKeys(keys: string[]): Promise<void> {
|
||||
await this.request('POST', `/session/${this.sessionId}/wda/keys`, { value: keys });
|
||||
}
|
||||
|
||||
async findBotCard(): Promise<string | null> {
|
||||
return this.findDeviceCard('Bot');
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue