提交 35a58b2e authored 作者: 陈泽健's avatar 陈泽健

feat(ui-automation): 视讯设备升级为 navigate 直达用例

sut-explore 全量盘点首页 24 入口 + 抽屉 38 入口,唯一可落地缺口=视讯设备
(旧 drawer+click 用例 headless 下历史执行全部中断/失败)。已升级为
meetingV3 TerminalMaintenance navigate 直达 3 用例(页面访问/表格/树形
筛选),DOM 特征:列头带腾讯后缀、树选中节点 .is-current、当前无数据
行=0 用 element_count_min expected=0 兼容。探测脚本 7 个沉淀。

「会议运维 6 + 待办巡检 2 + 视讯设备 3」11 用例回归 100% 通过,已同步
三台服务器(5.44/5.202→410、5.60→412,无重启)。
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 6a507a1f
......@@ -3,11 +3,42 @@
> **生成时间**: 2026-09-01
> **当前分支**: `platform-auto-test`
> **最近提交**: `720bf764` feat(ui-automation): sut-explore 补齐会议运维+待办巡检缺口用例 8/8 通过
> **状态**: 🟢 **会话48:sut-explore 缺口补齐(会议运维 6 Tab + 待办巡检 2 用例)——待办巡检 monitor 直达 URL 内层路由竞态定位并改用首页「巡检报表」入口点击绕过,最终 8/8 用例 100% 通过;已提交推送,8 条缺口用例已同步三台(5.44/5.202→407、5.60→409,无重启)**(同日另一会话 48 完成定时任务看门狗修复,见下双章节)
> **状态**: 🟢 **会话49:/sut-explore 全量盘点(首页 .block 24 入口 + 左侧抽屉 38 入口)——唯一可落地缺口=视讯设备(drawer+click 弱用例历史全中断),已升级为 navigate 直达 3 用例;「缺口+视讯设备」11 用例回归 100% 通过;已同步三台(5.44/5.202→410、5.60→412,无重启)**(历史会话见下双章节)
---
## 📊 当前状态(会话 48,2026-09-01 · sut-explore 缺口补齐)
## 📊 当前状态(会话 49,2026-09-01 · sut-explore 全量盘点 + 视讯设备直达升级)
**/sut-explore 全量盘点**:首页 .block 24 个功能入口 + 左侧抽屉 38 个入口逐一对比用例库。
**结论**:全部入口名称覆盖,无"从零缺口";仅 **视讯设备** 存在可执行性缺口——旧 3 条 drawer+click 用例(首页→.home_nav_left→.el-drawer→点击)headless 下历史执行**全部中断/失败**
### ✅ 视讯设备(终端维护)navigate 直达升级
- **真实直达 URL**`https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FTerminalMaintenance`(meetingV3 微应用,2s 表格即出现)
- **DOM 特征**:列头=终端名称腾讯/终端型号/通话状态/所属MCU/终端号码/终端IP/终端SN(断言 `:text("终端名称")` 前缀匹配);区域树点击后节点带 `.el-tree-node.is-current`**当前终端无数据(行=0)**`element_count_min` 用 expected=0 兼容空态
- **3 条用例**:页面访问验证 / 表格数据验证 / 树形筛选验证(`generate_terminal_maintenance_cases.py` 幂等,先删同名再插)
### ✅ 验证结果
| 执行 | 范围 | 结果 |
|------|------|------|
| R1 单跑视讯设备 3 用例 | 3 | 3/3 通过(16s/16s/21s) |
| 11 用例全量复验 | 会议运维 6 + 待办巡检 2 + 视讯设备 3 | **11/11 100% 通过** |
### 📋 盘点其余结论(不再重复探测)
- **会议概览/会议室概览**:首页入口点击后弹「当前功能正在开发,敬请期待」toast,无真实页面 → 不生成用例
- **抽屉 38 入口**:名称全覆盖(每入口 3+ 用例),其中 12 个入口仍是 drawer+click 弱模式(运维区域/告警列表/会议巡检/运维巡检/转录列表/壁纸推送/脚本命令/软件卸载/设备网络/会议审批/播放器发布/视讯设备旧版),远期可逐一探测直达 URL 升级
**改动清单**
| 文件 | 说明 |
|------|------|
| `backend/data/test_platform.db` | 视讯设备 3 条用例(替换旧 drawer 版) |
| `backend/scripts/generate_terminal_maintenance_cases.py` | 视讯设备用例生成(幂等) |
| `backend/scripts/probe_home_gap_entries*.py` / `probe_drawer_menu.py` / `probe_terminal_*.py` / `probe_terminal_tree_state.py` | 探测脚本沉淀(7 个) |
| `memory/sut-explore-terminal-device.md` | 固化直达 URL + DOM 特征 |
| `sync_cases_to_servers.py` 执行 | 三台同步:5.44/5.202→410、5.60→412(各 INSERTED 3,无重启) |
---
## 📊 历史状态(会话 48,2026-09-01 · sut-explore 缺口补齐)
**sut-explore 缺口补齐:会议运维 6 Tab + 待办巡检 2 用例,最终 8/8 100% 通过**。应 `/sut-explore` 全量盘点确认 7 个缺口功能(待办巡检 + 会议运维 6 Tab),生成 8 个可执行 UI 用例写入 SQLite(`generate_gap_cases.py`,幂等)。会议运维 6 用例 navigate 直达一次全绿;**待办巡检 2 用例经历 monitor 直达 URL 内层路由竞态(首轮通过 / 复验 2 次全失败)攻坚后,改用首页「巡检报表」入口点击稳定通过**
......@@ -63,4 +94,4 @@
---
*本文档由 Claude Code 于 2026-09-01 更新(会话 48 两档并存:① 定时任务看门狗失败执行无报告输出修复——已完成 A/B1/B2/B3 + 容器日志轮转 50m×5,全部提交并部署 5.44/5.202/5.60 三台,364/364 测试全绿,6/6 代码标记验证通过,LogConfig 生效;遗留:可选执行级端到端抽查。② sut-explore 缺口补齐——会议运维 6 Tab + 待办巡检 2 用例;待办巡检 monitor 直达 URL 内层路由竞态定位→改走首页「巡检报表」入口点击;最终 8/8 用例 100% 通过;`generate_gap_cases.py` 幂等可重复执行;已提交推送 `720bf764`,8 条缺口用例已同步 5.44/5.202/5.60 三台(无重启,定时任务不受影响))。*
*本文档由 Claude Code 于 2026-09-01 更新(会话 49:/sut-explore 全量盘点首页 .block 24 入口 + 抽屉 38 入口;唯一可落地缺口=视讯设备 drawr+click 弱用例历史全中断,升级为 meetingV3 TerminalMaintenance navigate 直达 3 用例;「会议运维 6 + 待办巡检 2 + 视讯设备 3」11 用例回归 100% 通过;已同步三台 5.44/5.202→410、5.60→412(无重启);探测脚本 7 个沉淀 + memory 固化。历史:会话 48 ①定时任务看门狗失败执行无报告输出修复已完成 A/B1/B2/B3 + 日志轮转 50m×5,部署三台 364/364 测试全绿;会话 48 ②sut-explore 缺口补齐会议运维 6 Tab + 待办巡检 2 用例 8/8 通过已推送 `720bf764`)。*
# -*- coding: utf-8 -*-
"""
模块名称: generate_terminal_maintenance_cases.py
模块描述: 生成「视讯设备(终端维护)」navigate 直达用例
替代现有 drawer+click 弱用例(首页→.home_nav_left→抽屉→点击,
headless 下不稳且历史执行全部中断/失败)。
视讯设备 = meetingV3 微应用 TerminalMaintenance 页面,可 navigate 直达
(探测确认 2s 表格即出现),树形区域筛选后选中节点带 .is-current。
注意:当前终端无数据(表格行=0),断言用 min=0 兼容空态。
作者: czj
创建日期: 2026-09-01
最后修改日期: 2026-09-01
用法:
cd backend
python scripts/generate_terminal_maintenance_cases.py
"""
import json
import os
import sqlite3
import sys
import uuid
from datetime import datetime
sys.stdout.reconfigure(encoding='utf-8')
DB_PATH = os.path.join(os.path.dirname(__file__), '..', 'data', 'test_platform.db')
# 视讯设备现有用例归属 会议运维 模块
MODULE_MEETING_OP = 'module_c9496f382be1430c958936d5cf473f62'
# 视讯设备(终端维护) navigate 直达 URL —— 从抽屉点击 maintain_list.video_dev 捕获
TERMINAL_URL = "https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FTerminalMaintenance"
now = datetime.now().isoformat(' ', 'seconds')
DEFAULT_CONFIG = {
"timeout": 120000,
"screenshot": True,
"auto_login": True,
"headless": True,
"step_retry_count": 2,
}
def gid(prefix='case'):
return f'{prefix}_{uuid.uuid4().hex}'
def build_case(module_id, name, steps, desc, priority='P1', tags=None):
return {
'id': gid(),
'module_id': module_id,
'name': name,
'description': desc,
'status': 'active',
'priority': priority,
'tags': json.dumps(tags or [], ensure_ascii=False),
'steps': json.dumps(steps, ensure_ascii=False),
'config': json.dumps(DEFAULT_CONFIG, ensure_ascii=False),
'depends_on': '[]',
'parameters': '[]',
'condition': '{}',
'order': 1,
'version': 1,
'created_at': now,
'updated_at': now,
'case_type': 'ui',
}
def nav(order, name, url):
return {'order': order, 'name': name, 'action': 'navigate', 'params': {'url': url}}
def wait(order, name, selector, timeout=15000):
return {'order': order, 'name': name, 'action': 'wait', 'params': {'selector': selector, 'timeout': timeout}}
def assert_exists(order, name, selector):
return {'order': order, 'name': name, 'action': 'assert', 'params': {'type': 'element_exists', 'selector': selector}}
def assert_min(order, name, selector, expected='0'):
return {'order': order, 'name': name, 'action': 'assert',
'params': {'type': 'element_count_min', 'selector': selector, 'expected': expected}}
def click_sel(order, name, selectors, force=True, timeout=8000):
return {'order': order, 'name': name, 'action': 'click',
'params': {'selectors': selectors, 'force': force, 'timeout': timeout}}
# ================= 视讯设备(navigate 直达 + 树形筛选) =================
# DOM 特征(探测 probe_terminal_maintenance*.py):
# - 表格列头: 终端名称腾讯 | 终端型号 | 通话状态 | 所属MCU | 终端号码 | 终端IP | 终端SN
# (列头带'腾讯'字样,断言用 :text('终端名称') 前缀包含匹配)
# - 树节点: 公司总部/研发区域/外部区域/111/222/办公区域/展厅区域
# - 点击区域后选中节点 .el-tree-node.is-current
# - 当前无终端数据(行=0),element_count_min expected=0 兼容空态
base = [
nav(1, '导航到视讯设备页面', TERMINAL_URL),
wait(2, '等待终端列表表格渲染', '.el-table', 25000),
]
# 1. 页面访问验证
steps = base + [
assert_exists(3, '验证终端维护表格存在', '.el-table'),
assert_exists(4, '验证区域树形结构存在', '.el-tree'),
assert_exists(5, '验证终端名称列存在', '.el-table :text("终端名称")'),
]
cases = [build_case(MODULE_MEETING_OP, '视讯设备-页面访问验证', steps,
'navigate 直达终端维护页,验证表格/区域树/列头渲染', 'P0',
['视讯设备', '页面访问', '终端'])]
# 2. 表格数据验证:关键列头
steps = base + [
assert_exists(3, '验证终端名称列', '.el-table :text("终端名称")'),
assert_exists(4, '验证终端型号列', '.el-table :text("终端型号")'),
assert_exists(5, '验证通话状态列', '.el-table :text("通话状态")'),
assert_exists(6, '验证终端号码列', '.el-table :text("终端号码")'),
assert_min(7, '验证表格行数不小于0', '.el-table__row', '0'),
]
cases.append(build_case(MODULE_MEETING_OP, '视讯设备-表格数据验证', steps,
'验证终端维护表格关键列(终端名称/型号/通话状态/号码)渲染', 'P1',
['视讯设备', '表格', '终端']))
# 3. 树形筛选验证:点击 公司总部 → 选中节点高亮
steps = base + [
assert_exists(3, '验证区域树存在', '.el-tree'),
click_sel(4, '点击公司总部区域', [".el-tree-node__content:has-text('公司总部')",
"text='公司总部'"]),
wait(5, '等待筛选结果刷新', 'body', 5000),
assert_exists(6, '验证选中节点高亮', '.el-tree-node.is-current'),
assert_exists(7, '验证表格仍存在', '.el-table'),
]
cases.append(build_case(MODULE_MEETING_OP, '视讯设备-树形筛选验证', steps,
'点击公司总部区域节点,验证树选中高亮且表格正常', 'P1',
['视讯设备', '树形筛选', '终端']))
# ================= 写入数据库(幂等:先删同名) =================
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
names = [c['name'] for c in cases]
placeholders = ','.join('?' for _ in names)
cur.execute(f"DELETE FROM test_cases WHERE name IN ({placeholders}) AND case_type='ui'", names)
print(f'已清理同名旧用例 {cur.rowcount} 条')
for i, c in enumerate(cases, start=1):
c['order'] = i
cur.execute('''INSERT INTO test_cases
(id, module_id, name, description, status, priority, tags, steps, config, depends_on, parameters, condition, "order", version, created_at, updated_at, case_type)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
(c['id'], c['module_id'], c['name'], c['description'], c['status'], c['priority'],
c['tags'], c['steps'], c['config'], c['depends_on'], c['parameters'], c['condition'],
c['order'], c['version'], c['created_at'], c['updated_at'], c['case_type']))
print(f' OK: {c["name"]} ({c["id"]})')
conn.commit()
conn.close()
print(f'\n✅ 共生成 {len(cases)} 个视讯设备用例')
# -*- coding: utf-8 -*-
"""
模块名称: 探测首页左侧抽屉完整菜单
模块描述: 登录后展开左侧抽屉(.home_nav_left),dump 全部入口
(data-id + 名称),识别未被用例覆盖的功能入口。
作者: czj
创建日期: 2026-09-01
最后修改日期: 2026-09-01
"""
import json
import sys
from playwright.sync_api import sync_playwright
SUT = "https://192.168.5.44"
USERNAME = "admin@xty"
PASSWORD = "Ubains@13579"
CAPTCHA = "csba"
def login(page):
page.goto(SUT, wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(4000)
page.fill("input[placeholder='手机号/用户名/邮箱']", USERNAME)
page.fill("input[placeholder='密码']", PASSWORD)
page.fill("input[placeholder='图形验证']", CAPTCHA)
page.wait_for_timeout(500)
for b in page.query_selector_all("button"):
if (b.inner_text() or "").strip() == "登录":
b.click()
break
for _ in range(6):
page.wait_for_timeout(2000)
page.evaluate(
"""() => {
const btns = Array.from(document.querySelectorAll('button'))
.filter(b => (b.innerText||'').trim() === '确定');
if (btns.length) btns[btns.length-1].click();
}"""
)
page.wait_for_timeout(1500)
if "/login" not in page.url:
break
page.wait_for_timeout(4000)
def main():
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
ctx = browser.new_context(ignore_https_errors=True, viewport={"width": 1920, "height": 1080})
page = ctx.new_page()
page.set_default_timeout(15000)
login(page)
# 展开抽屉
page.evaluate("() => { const el = document.querySelector('.home_nav_left'); if (el) el.click(); }")
page.wait_for_timeout(4000)
# 抽屉内全部 item_content_item
items = page.evaluate(
"""() => {
const out = [];
const seen = new Set();
for (const el of document.querySelectorAll('.item_content_item, [data-id], .drawer_item, [class*=item]')) {
const did = el.getAttribute('data-id') || '';
const name = (el.innerText||'').trim().split('\\n')[0].slice(0,40);
if (name && !seen.has(name)) { seen.add(name); out.push({name, did}); }
}
return out;
}"""
)
print("=== 抽屉入口 ===")
for it in items:
print(json.dumps(it, ensure_ascii=False))
# 分组标题(父级)
groups = page.evaluate(
"""() => {
const out = [];
for (const el of document.querySelectorAll('.el-collapse-item__header, .group_title, [class*=group], [class*=title]')) {
const t = (el.innerText||'').trim().slice(0,30);
if (t && t.length > 1 && t.length < 20) out.push(t);
}
return [...new Set(out)].slice(0, 40);
}"""
)
print("\n=== 抽屉分组标题 ===")
for g in groups:
print(" -", g)
page.screenshot(path="tmp/drawer_menu.png", full_page=False)
print("\n截图: tmp/drawer_menu.png")
browser.close()
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
模块名称: 探测首页缺口入口(会议概览/会议室概览/视频设备)
模块描述: 稳定登录 192.168.5.44,抓取首页 .block 卡片 id,
点击缺口入口后读取 frame URL + micro-app 属性,
获取真实直达 URL(双重 hash)与 DOM 特征,供生成用例。
作者: czj
创建日期: 2026-09-01
最后修改日期: 2026-09-01
"""
import json
import sys
from playwright.sync_api import sync_playwright
SUT = "https://192.168.5.44"
USERNAME = "admin@xty"
PASSWORD = "Ubains@13579"
CAPTCHA = "csba"
def login(page):
page.goto(SUT, wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(4000)
page.fill("input[placeholder='手机号/用户名/邮箱']", USERNAME)
page.fill("input[placeholder='密码']", PASSWORD)
page.fill("input[placeholder='图形验证']", CAPTCHA)
page.wait_for_timeout(500)
for b in page.query_selector_all("button"):
if (b.inner_text() or "").strip() == "登录":
b.click()
break
for _ in range(6):
page.wait_for_timeout(2000)
page.evaluate(
"""() => {
const btns = Array.from(document.querySelectorAll('button'))
.filter(b => (b.innerText||'').trim() === '确定');
if (btns.length) btns[btns.length-1].click();
}"""
)
page.wait_for_timeout(1500)
if "/login" not in page.url:
break
page.wait_for_timeout(4000)
def dump_state(page, tag):
print(f"\n===== {tag} =====")
print("主 frame URL:", page.url[:220])
for f in page.frames:
try:
if f.url and f.url != "about:blank":
print(" frame:", f.url[:200])
except Exception:
pass
ma = page.evaluate(
"""() => {
const m = document.querySelector('micro-app');
if (!m) return {found: false};
const attrs = {};
for (const a of m.attributes) attrs[a.name] = a.value;
return {found: true, attrs};
}"""
)
print("micro-app:", json.dumps(ma, ensure_ascii=False)[:400])
# body 关键文本(前 600 字)
body = page.evaluate("() => document.body ? document.body.innerText.slice(0, 600) : ''")
print("body 文本:", body.replace("\n", " | ")[:600])
def click_block(page, text):
"""在首页 .block 卡片中按文本点击,返回是否成功"""
return page.evaluate(
"""(text) => {
const blocks = Array.from(document.querySelectorAll('.block'));
const b = blocks.find(x => (x.innerText||'').trim() === text);
if (b) { b.click(); return b.outerHTML.slice(0, 300); }
return null;
}""", text
)
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
ctx = browser.new_context(ignore_https_errors=True, viewport={"width": 1920, "height": 1080})
page = ctx.new_page()
page.set_default_timeout(15000)
login(page)
# ===== 0. 首页 .block 卡片(含 id) =====
cards = page.evaluate(
"""() => Array.from(document.querySelectorAll('.block')).map(b => ({
id: b.id || '',
text: (b.innerText||'').trim().slice(0,40),
}))"""
)
print("=== 首页 .block 卡片(带 id)===")
for c in cards:
print(json.dumps(c, ensure_ascii=False))
# ===== 1. 会议概览 =====
# 回到首页(可能在详情页)
page.goto(SUT + "/", wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(5000)
r = click_block(page, "会议概览")
print("\n[click 会议概览] 返回:", json.dumps(r, ensure_ascii=False) if r else "None(未找到)")
page.wait_for_timeout(9000)
dump_state(page, "会议概览")
# ===== 2. 会议室概览 =====
page.goto(SUT + "/", wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(5000)
r = click_block(page, "会议室概览")
print("\n[click 会议室概览] 返回:", json.dumps(r, ensure_ascii=False) if r else "None(未找到)")
page.wait_for_timeout(9000)
dump_state(page, "会议室概览")
# ===== 3. 视频设备(抽屉入口 maintain_list.video_dev)=====
page.goto(SUT + "/", wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(5000)
page.evaluate("() => { const el = document.querySelector('.home_nav_left'); if (el) el.click(); }")
page.wait_for_timeout(3500)
did = "maintain_list.video_dev"
r = page.evaluate(
"""(did) => {
const el = document.querySelector(`[data-id="${did}"]`);
if (el) { el.click(); return el.outerHTML.slice(0, 300); }
return null;
}""", did
)
print("\n[click 视频设备] 返回:", json.dumps(r, ensure_ascii=False) if r else "None(未找到)")
page.wait_for_timeout(9000)
dump_state(page, "视频设备")
page.screenshot(path="tmp/home_gap_entries.png", full_page=False)
print("\n截图: tmp/home_gap_entries.png")
browser.close()
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
模块名称: 探测会议概览/会议室概览真实落地页 (v2)
模块描述: 捕获 popup 新标签页 + 长轮询 URL 变化,找到会议概览/会议室概览
点击后的真实微应用 URL(双重 hash)与 DOM 特征。
作者: czj
创建日期: 2026-09-01
最后修改日期: 2026-09-01
"""
import json
import sys
from playwright.sync_api import sync_playwright
SUT = "https://192.168.5.44"
USERNAME = "admin@xty"
PASSWORD = "Ubains@13579"
CAPTCHA = "csba"
def login(page):
page.goto(SUT, wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(4000)
page.fill("input[placeholder='手机号/用户名/邮箱']", USERNAME)
page.fill("input[placeholder='密码']", PASSWORD)
page.fill("input[placeholder='图形验证']", CAPTCHA)
page.wait_for_timeout(500)
for b in page.query_selector_all("button"):
if (b.inner_text() or "").strip() == "登录":
b.click()
break
for _ in range(6):
page.wait_for_timeout(2000)
page.evaluate(
"""() => {
const btns = Array.from(document.querySelectorAll('button'))
.filter(b => (b.innerText||'').trim() === '确定');
if (btns.length) btns[btns.length-1].click();
}"""
)
page.wait_for_timeout(1500)
if "/login" not in page.url:
break
page.wait_for_timeout(4000)
def dump_state(page, tag):
print(f"\n===== {tag} =====")
print("主 frame URL:", page.url[:240])
for f in page.frames:
try:
if f.url and f.url != "about:blank":
print(" frame:", f.url[:200])
except Exception:
pass
ma = page.evaluate(
"""() => {
const m = document.querySelector('micro-app');
if (!m) return {found: false};
const attrs = {};
for (const a of m.attributes) attrs[a.name] = a.value;
return {found: true, attrs};
}"""
)
print("micro-app:", json.dumps(ma, ensure_ascii=False)[:300])
body = page.evaluate("() => document.body ? document.body.innerText.slice(0, 800) : ''")
print("body 文本:", body.replace("\n", " | ")[:800])
def enter_and_track(browser, page, text, wait_sec=25):
"""点击首页指定 .block 入口并跟踪主frame/popup 变化"""
popups = []
page.on("popup", lambda p: popups.append(p))
clicked = page.evaluate(
"""(text) => {
const blocks = Array.from(document.querySelectorAll('.block'));
const b = blocks.find(x => (x.innerText||'').trim() === text);
if (b) { b.click(); return true; }
return false;
}""", text
)
print(f"\n[点击 {text}] clicked={clicked}")
# 轮询 25s:主 frame URL 是否离开 Home / popup 是否出现
for i in range(wait_sec):
page.wait_for_timeout(1000)
if popups:
print(f" [POPUP] 第{i+1}s 出现 popup:")
for p in popups:
try:
print(" popup url:", p.url[:240])
except Exception:
pass
dump_state(p, f"popup-{text}")
return popups[0]
u = page.url
if "/Home" not in u or "/login" in u:
print(f" [URL变化] 第{i+1}s: {u[:240]}")
dump_state(page, text)
return page
print(f" [超时] {wait_sec}s 内主 frame 未跳转,popup={len(popups)}")
dump_state(page, text)
return page
def main():
browser = p = None
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
ctx = browser.new_context(ignore_https_errors=True, viewport={"width": 1920, "height": 1080})
page = ctx.new_page()
page.set_default_timeout(15000)
login(page)
# 会议概览
page.goto(SUT + "/", wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(6000)
enter_and_track(browser, page, "会议概览", wait_sec=30)
page.screenshot(path="tmp/meeting_summary.png", full_page=False)
# 会议室概览
page.goto(SUT + "/", wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(6000)
enter_and_track(browser, page, "会议室概览", wait_sec=30)
page.screenshot(path="tmp/room_summary.png", full_page=False)
browser.close()
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
模块名称: 探测视讯设备(终端维护) navigate 直达稳定性
模块描述: navigate 直达 meetingV3 TerminalMaintenance 微应用 URL,
检查表格/树形筛选/DOM 特征,验证是否可用作用例首步。
作者: czj
创建日期: 2026-09-01
最后修改日期: 2026-09-01
"""
import json
import sys
from playwright.sync_api import sync_playwright
SUT = "https://192.168.5.44"
USERNAME = "admin@xty"
PASSWORD = "Ubains@13579"
CAPTCHA = "csba"
# 视讯设备(终端维护) 直达 URL —— 从抽屉点击 maintain_list.video_dev 捕获
TERMINAL_URL = "https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FTerminalMaintenance"
def login(page):
page.goto(SUT, wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(4000)
page.fill("input[placeholder='手机号/用户名/邮箱']", USERNAME)
page.fill("input[placeholder='密码']", PASSWORD)
page.fill("input[placeholder='图形验证']", CAPTCHA)
page.wait_for_timeout(500)
for b in page.query_selector_all("button"):
if (b.inner_text() or "").strip() == "登录":
b.click()
break
for _ in range(6):
page.wait_for_timeout(2000)
page.evaluate(
"""() => {
const btns = Array.from(document.querySelectorAll('button'))
.filter(b => (b.innerText||'').trim() === '确定');
if (btns.length) btns[btns.length-1].click();
}"""
)
page.wait_for_timeout(1500)
if "/login" not in page.url:
break
page.wait_for_timeout(4000)
def main():
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
ctx = browser.new_context(ignore_https_errors=True, viewport={"width": 1920, "height": 1080})
page = ctx.new_page()
page.set_default_timeout(15000)
login(page)
print("登录后 URL:", page.url)
# ===== navigate 直达 终端维护 =====
print("\n=== navigate 直达 TerminalMaintenance ===")
page.goto(TERMINAL_URL, wait_until="domcontentloaded", timeout=45000)
for i in range(20):
page.wait_for_timeout(1000)
if page.query_selector(".el-table"):
print(f" [OK] 第{i+1}s 表格出现")
break
print("URL:", page.url[:200])
# micro-app 状态
ma = page.evaluate(
"""() => {
const m = document.querySelector('micro-app');
if (!m) return {found: false};
const attrs = {};
for (const a of m.attributes) attrs[a.name] = a.value;
return {found: true, attrs};
}"""
)
print("micro-app:", json.dumps(ma, ensure_ascii=False)[:250])
# 表格列头
headers = page.evaluate(
"""() => Array.from(document.querySelectorAll('.el-table__header th'))
.map(t => (t.innerText||'').trim()).filter(Boolean)"""
)
print("表格列头:", headers)
# 行数
rows = page.evaluate("() => document.querySelectorAll('.el-table__row').length")
print("数据行数:", rows)
# 左侧树形结构
tree = page.evaluate(
"""() => Array.from(document.querySelectorAll('.el-tree-node__label, .el-tree-node__content'))
.map(e => (e.innerText||'').trim()).filter(Boolean).slice(0, 30)"""
)
print("树节点:", tree)
# 输入框/按钮
inputs = page.evaluate(
"""() => Array.from(document.querySelectorAll('input')).slice(0,15).map(i => ({
ph: i.placeholder||'', ro: i.readOnly, cls: (i.className||'').slice(0,40)
}))"""
)
print("inputs:", json.dumps(inputs, ensure_ascii=False))
btns = page.evaluate(
"""() => Array.from(document.querySelectorAll('button')).slice(0,20).map(b => ({
t: (b.innerText||'').trim().slice(0,15), cls: (b.className||'').slice(0,40)
})).filter(b => b.t)"""
)
print("buttons:", json.dumps(btns, ensure_ascii=False))
# ===== 树形筛选点击验证 =====
print("\n=== 点击树节点(华为终端) 筛选 ===")
clicked = page.evaluate(
"""() => {
const nodes = Array.from(document.querySelectorAll('.el-tree-node__content'));
for (const n of nodes) {
if ((n.innerText||'').includes('华为终端')) { n.click(); return true; }
}
return false;
}"""
)
print("点击树节点:", clicked)
page.wait_for_timeout(3000)
rows2 = page.evaluate("() => document.querySelectorAll('.el-table__row').length")
print("筛选后行数:", rows2)
first_row = page.evaluate(
"""() => {
const r = document.querySelector('.el-table__row');
return r ? (r.innerText||'').trim().slice(0,200) : '';
}"""
)
print("首行数据:", first_row)
page.screenshot(path="tmp/terminal_maintenance.png", full_page=False)
print("\n截图: tmp/terminal_maintenance.png")
browser.close()
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
模块名称: 探测视讯设备(终端维护) 树筛选与稳定断言 (v2)
模块描述: navigate 直达后点击区域树节点,观察表格行数变化,
dump 按钮/搜索框/表头,确定稳定断言选择器。
作者: czj
创建日期: 2026-09-01
最后修改日期: 2026-09-01
"""
import json
import sys
from playwright.sync_api import sync_playwright
SUT = "https://192.168.5.44"
USERNAME = "admin@xty"
PASSWORD = "Ubains@13579"
CAPTCHA = "csba"
TERMINAL_URL = "https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FTerminalMaintenance"
def login(page):
page.goto(SUT, wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(4000)
page.fill("input[placeholder='手机号/用户名/邮箱']", USERNAME)
page.fill("input[placeholder='密码']", PASSWORD)
page.fill("input[placeholder='图形验证']", CAPTCHA)
page.wait_for_timeout(500)
for b in page.query_selector_all("button"):
if (b.inner_text() or "").strip() == "登录":
b.click()
break
for _ in range(6):
page.wait_for_timeout(2000)
page.evaluate(
"""() => {
const btns = Array.from(document.querySelectorAll('button'))
.filter(b => (b.innerText||'').trim() === '确定');
if (btns.length) btns[btns.length-1].click();
}"""
)
page.wait_for_timeout(1500)
if "/login" not in page.url:
break
page.wait_for_timeout(4000)
def dump_buttons(page):
btns = page.evaluate(
"""() => Array.from(document.querySelectorAll('button')).map(b => ({
t: (b.innerText||'').trim().slice(0,15),
cls: (b.className||'').toString().slice(0,60),
icon: (b.querySelector('i') ? b.querySelector('i').className : ''),
}))"""
)
print("buttons:", json.dumps(btns, ensure_ascii=False))
def main():
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
ctx = browser.new_context(ignore_https_errors=True, viewport={"width": 1920, "height": 1080})
page = ctx.new_page()
page.set_default_timeout(15000)
login(page)
page.goto(TERMINAL_URL, wait_until="domcontentloaded", timeout=45000)
for i in range(15):
page.wait_for_timeout(1000)
if page.query_selector(".el-table"):
break
print("URL:", page.url[:180])
# 全部按钮
dump_buttons(page)
# 搜索框 placeholder
inputs = page.evaluate(
"""() => Array.from(document.querySelectorAll('input')).map(i => ({
ph: i.placeholder||'', ro: i.readOnly, cls: (i.className||'').slice(0,40)
}))"""
)
print("inputs:", json.dumps(inputs, ensure_ascii=False))
# 完整树结构(含层级)
tree = page.evaluate(
"""() => {
const out = [];
for (const n of document.querySelectorAll('.el-tree-node')) {
const lbl = n.querySelector('.el-tree-node__label');
const expanded = n.className.includes('is-expanded');
out.push({text: lbl ? lbl.innerText.trim() : '', expanded, cls: n.className.slice(0,60)});
}
return out;
}"""
)
print("树节点(含状态):", json.dumps(tree, ensure_ascii=False))
# 点击 公司总部 树节点
print("\n=== 点击 公司总部 树节点 ===")
clicked = page.evaluate(
"""() => {
const contents = Array.from(document.querySelectorAll('.el-tree-node__content'));
for (const c of contents) {
if ((c.innerText||'').includes('公司总部')) { c.click(); return true; }
}
return false;
}"""
)
print("点击:", clicked)
page.wait_for_timeout(4000)
rows = page.evaluate("() => document.querySelectorAll('.el-table__row').length")
print("点击后表格行数:", rows)
if rows:
first = page.evaluate(
"""() => {
const r = document.querySelector('.el-table__row');
return (r.innerText||'').trim().replace(/\\n/g,' | ').slice(0,250);
}"""
)
print("首行:", first)
# 搜索框填入终端名
print("\n=== 搜索框过滤 ===")
sr = page.evaluate(
"""() => {
const inp = document.querySelector('input:not([readonly])');
if (!inp) return 'no input';
inp.focus();
inp.value = '华为';
inp.dispatchEvent(new Event('input', {bubbles: true}));
return 'filled';
}"""
)
print("搜索:", sr)
page.wait_for_timeout(2500)
# 点查询按钮(图标按钮)
q = page.evaluate(
"""() => {
const btns = Array.from(document.querySelectorAll('button'));
for (const b of btns) {
if (b.className.includes('primary') || (b.querySelector('i') && (b.querySelector('i').className.includes('search') || b.querySelector('i').className.includes('el-icon-search')))) {
b.click(); return b.className.slice(0,60);
}
}
return 'no query btn';
}"""
)
print("点查询:", q)
page.wait_for_timeout(3000)
rows2 = page.evaluate("() => document.querySelectorAll('.el-table__row').length")
print("查询后行数:", rows2)
page.screenshot(path="tmp/terminal_maintenance_v2.png", full_page=False)
print("截图: tmp/terminal_maintenance_v2.png")
browser.close()
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
模块名称: 视讯设备树形筛选稳定性探测 (v3)
模块描述: navigate 直达 TerminalMaintenance,耐心等待树渲染(最长25s),
点击「公司总部」区域节点,观察表格行数变化,验证树筛选可用。
作者: czj
创建日期: 2026-09-01
最后修改日期: 2026-09-01
"""
import json
import sys
from playwright.sync_api import sync_playwright
SUT = "https://192.168.5.44"
USERNAME = "admin@xty"
PASSWORD = "Ubains@13579"
CAPTCHA = "csba"
TERMINAL_URL = "https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FTerminalMaintenance"
def login(page):
page.goto(SUT, wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(4000)
page.fill("input[placeholder='手机号/用户名/邮箱']", USERNAME)
page.fill("input[placeholder='密码']", PASSWORD)
page.fill("input[placeholder='图形验证']", CAPTCHA)
page.wait_for_timeout(500)
for b in page.query_selector_all("button"):
if (b.inner_text() or "").strip() == "登录":
b.click()
break
for _ in range(6):
page.wait_for_timeout(2000)
page.evaluate(
"""() => {
const btns = Array.from(document.querySelectorAll('button'))
.filter(b => (b.innerText||'').trim() === '确定');
if (btns.length) btns[btns.length-1].click();
}"""
)
page.wait_for_timeout(1500)
if "/login" not in page.url:
break
page.wait_for_timeout(4000)
def main():
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
ctx = browser.new_context(ignore_https_errors=True, viewport={"width": 1920, "height": 1080})
page = ctx.new_page()
page.set_default_timeout(15000)
login(page)
page.goto(TERMINAL_URL, wait_until="domcontentloaded", timeout=45000)
# 耐心等待树节点出现(最长 25s)
tree_found_at = None
for i in range(25):
page.wait_for_timeout(1000)
n = page.evaluate(
"() => document.querySelectorAll('.el-tree-node__content').length"
)
if n >= 3:
tree_found_at = i + 1
break
print(f"树节点出现时间: {tree_found_at}s" if tree_found_at else "树未出现(25s)")
# dump 树节点
tree = page.evaluate(
"""() => Array.from(document.querySelectorAll('.el-tree-node__content'))
.map(e => (e.innerText||'').trim()).filter(Boolean)"""
)
print("树节点:", tree)
# 表格初始行数
rows0 = page.evaluate("() => document.querySelectorAll('.el-table__row').length")
print("初始行数:", rows0)
# 点击「公司总部」
clicked = page.evaluate(
"""() => {
const contents = Array.from(document.querySelectorAll('.el-tree-node__content'));
for (const c of contents) {
if ((c.innerText||'').trim().includes('公司总部')) { c.click(); return true; }
}
return false;
}"""
)
print("点击公司总部:", clicked)
page.wait_for_timeout(4000)
rows1 = page.evaluate("() => document.querySelectorAll('.el-table__row').length")
print("点击后行数:", rows1)
# 点击「研发区域」
clicked2 = page.evaluate(
"""() => {
const contents = Array.from(document.querySelectorAll('.el-tree-node__content'));
for (const c of contents) {
if ((c.innerText||'').trim().includes('研发区域')) { c.click(); return true; }
}
return false;
}"""
)
print("点击研发区域:", clicked2)
page.wait_for_timeout(4000)
rows2 = page.evaluate("() => document.querySelectorAll('.el-table__row').length")
print("点击后行数:", rows2)
# 表格列头(去重)
headers = page.evaluate(
"""() => Array.from(document.querySelectorAll('.el-table__header th'))
.map(t => (t.innerText||'').trim()).filter(Boolean)"""
)
print("列头:", headers)
page.screenshot(path="tmp/terminal_tree_v3.png", full_page=False)
print("截图: tmp/terminal_tree_v3.png")
browser.close()
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
模块名称: 探测树节点点击后 is-current 状态
模块描述: navigate 直达 TerminalMaintenance,点「公司总部」,验证
节点是否带 .is-current / .is-selected 高亮(用作断言)。
作者: czj
创建日期: 2026-09-01
"""
import sys
from playwright.sync_api import sync_playwright
SUT = "https://192.168.5.44"
USERNAME = "admin@xty"
PASSWORD = "Ubains@13579"
CAPTCHA = "csba"
TERMINAL_URL = "https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FTerminalMaintenance"
def login(page):
page.goto(SUT, wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(4000)
page.fill("input[placeholder='手机号/用户名/邮箱']", USERNAME)
page.fill("input[placeholder='密码']", PASSWORD)
page.fill("input[placeholder='图形验证']", CAPTCHA)
page.wait_for_timeout(500)
for b in page.query_selector_all("button"):
if (b.inner_text() or "").strip() == "登录":
b.click()
break
for _ in range(6):
page.wait_for_timeout(2000)
page.evaluate(
"""() => {
const btns = Array.from(document.querySelectorAll('button'))
.filter(b => (b.innerText||'').trim() === '确定');
if (btns.length) btns[btns.length-1].click();
}"""
)
page.wait_for_timeout(1500)
if "/login" not in page.url:
break
page.wait_for_timeout(4000)
def main():
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
ctx = browser.new_context(ignore_https_errors=True, viewport={"width": 1920, "height": 1080})
page = ctx.new_page()
page.set_default_timeout(15000)
login(page)
page.goto(TERMINAL_URL, wait_until="domcontentloaded", timeout=45000)
for i in range(15):
page.wait_for_timeout(1000)
if page.evaluate("() => document.querySelectorAll('.el-tree-node__content').length") >= 3:
break
# 点击 公司总部
page.evaluate(
"""() => {
const contents = Array.from(document.querySelectorAll('.el-tree-node__content'));
for (const c of contents) {
if ((c.innerText||'').trim().includes('公司总部')) { c.click(); return; }
}
}"""
)
page.wait_for_timeout(2500)
# dump 各节点 class
cls = page.evaluate(
"""() => Array.from(document.querySelectorAll('.el-tree-node')).slice(0,10).map(n => ({
text: (n.querySelector('.el-tree-node__label')||{}).innerText || '',
cls: n.className.toString(),
}))"""
)
for c in cls:
print(c["text"], "->", c["cls"][:120])
browser.close()
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论