提交 720bf764 authored 作者: 陈泽健's avatar 陈泽健

feat(ui-automation): sut-explore 补齐会议运维+待办巡检缺口用例 8/8 通过

- 盘点确认 7 个缺口功能,生成 8 个可执行 UI 用例写入 SQLite(generate_gap_cases.py 幂等)
- 会议运维 6 用例 navigate 直达 + Tab 切换断言一次全绿
- 待办巡检 2 用例:monitor 直达 URL 内层路由竞态定位(整页加载后固定落设备列表页、
  外层 monitor= hash/navPayload 均无法切 Checklist、_ensure_micro_app_loaded 误判就绪)
  → 改走首页「巡检报表」入口点击 [id="data_list.inspect_report"] 绕过,100% 稳定
- 探测脚本沉淀:probe_gap_modules(_v2~v4)/probe_gap_meeting_tabs/probe_monitor_checklist_rerun
  /probe_sut_home_entries/probe_overview_modules/probe_funcenter
- HANDOFF_UI自动化.md 记录会话48 全流程
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 97e802a6
......@@ -2,12 +2,55 @@
> **生成时间**: 2026-09-01
> **当前分支**: `platform-auto-test`
> **最近提交**: `f9b37c6b` fix(deploy): deploy_fix_560 补充 performance_ai_service/resource_monitor 传递依赖上传
> **状态**: 🟢 **会话48:定时任务看门狗失败执行无报告输出修复(已全部完成并部署 5.44/5.202/5.60,日志轮转已生效,验证通过;仅剩可选执行级端到端抽查)**
> **最近提交**: `ec6b4382` feat(ui-automation): sut-explore 补齐会议运维+待办巡检缺口用例 8/8 通过
> **状态**: 🟢 **会话48:sut-explore 缺口补齐(会议运维 6 Tab + 待办巡检 2 用例)——待办巡检 monitor 直达 URL 内层路由竞态定位并改用首页「巡检报表」入口点击绕过,最终 8/8 用例 100% 通过**(同日另一会话 48 完成定时任务看门狗修复,见下双章节)
---
## 📊 当前状态(会话 48,2026-09-01)
## 📊 当前状态(会话 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 次全失败)攻坚后,改用首页「巡检报表」入口点击稳定通过**
### ✅ 会议运维 6 用例(navigate 直达,一次通过)
- 直达 URL:`https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FMeetingmaintenance`(登录态跨 hash 保留,可 navigate 直达)
- 断言模式:`body:has-text("工单列表")` + `.el-table` + `.el-table__row`(≥1)+ `.el-tabs__item.is-active:has-text("{Tab}")`
- 覆盖:页面访问验证(默认终端列表 Tab)+ 终端列表/终端链路图/当前告警/历史告警/工单列表 5 个 Tab 切换验证
- 探测沉淀:`probe_gap_meeting_tabs.py`(Tab 选择器)、`probe_gap_modules_v4.py`(微应用 frame/micro-app 属性)
### ✅ 待办巡检 2 用例(monitor 路由竞态攻坚,核心)
**失败现象**:navigate 直达 `MONITOR_URL``/backend/monitor?monitor=...Checklist`)+ 执行器二段式 warm-up,**间歇性**落入设备列表页而非巡检列表(首轮 exec_0f4627... 表格用例通过、复验 exec_926f... 0/2 全失败)。
**根因(探测证据链)**
| 项 | 结论 |
|----|------|
| monitor 整页加载后内层路由 | **默认落设备列表(Monitoringindex)页**(ths=设备名称/告警/在线/开关/运行),即使 URL hash 含 `...adminsystem/monitor/Checklist` |
| 绕不过的竞态 | navPayload.path 各候选(`/adminsystem/monitor/Checklist``/monitor/Checklist``list=Checklist`、空 path)+ 真实首页 navPayload(operationStatistics)+ JS 重设外层 hash 共 6+ 变体**全部落设备列表页**——平台壳派发内层 hash 的时序竞态,偶发可达 |
| 执行器就绪判定缺陷 | `_ensure_micro_app_loaded` marker("Checklist") 从外层 URL 解析恒存在 + 设备列表 body 非空 → **设备列表页被误判为"就绪"**,后续断言在错误页面失败 |
| 附带断言缺陷 | 「一键巡检」是 `<span>`(父级 `.company-edmit-right`),非 `<button>`;原 `button:has-text("一键巡检")` 即使到对页面也找不到 |
**稳定解法**:不 navigate 直达,改为 **navigate 首页 → 真实点击首页「巡检报表」入口 `[id="data_list.inspect_report"]`**(DIV.block,父级 `.first_box_flex`)→ 平台壳自身完成 monitor 初始化 + 内层路由派发 → **100% 落巡检列表页**。独立验证 5 次 + 执行器内全绿。
- 落地后特征:URL `/backend/monitor?monitor=...Checklist`;表格列头=巡检名称/区域名称/检测结果/启用/执行方式/定时方式/开始日期/结束日期/执行时间/更新时间/操作;5 行真实数据(急急急/测试 hhh);工具栏=一键巡检/新增/编辑/删除
### ✅ 最终复验(`exec_03e8940a29a34ae6b1633a24e747e521`)
**8/8 用例 100% 通过,0 失败**:会议运维 6 用例 + 待办巡检-页面访问验证 + 待办巡检-表格数据验证。
**改动清单**
| 文件 | 说明 |
|------|------|
| `backend/data/test_platform.db` | 8 条缺口用例(会议运维 6 已通过;待办巡检 2 条步骤重写为首页入口点击路径) |
| `backend/scripts/generate_gap_cases.py` | 待办巡检生成逻辑同步更新(含完整踩坑注释,幂等可重复执行) |
| `backend/scripts/probe_monitor_checklist_rerun.py` / `probe_real_entry_click.py` / `probe_gap_meeting_tabs.py` / `probe_gap_modules_v4.py` | 探测/复现脚本沉淀 |
| 记忆 `monitor-checklist-direct-url-race.md` | 固化"monitor 直达 URL 竞态 → 首页巡检报表入口点击"解法 |
---
## 📊 当前状态(会话 48,2026-09-01 · 定时任务看门狗修复)
**定时任务看门狗失败执行无报告输出(P0)**:5.44「每日定时自动化测试」连续 4 次被看门狗判失败、**报告目录无一文件**(最近一次 `exec_24b3a329` 09-01 01:25~06:09 约 4.7h 后看门狗标记 failed)。根因 = `scheduler_service.run_task_once()` 报告生成条件 `task.auto_report and run_ok`,看门狗置 failed 后 `run_ok=False` → 报告被跳过;而被判死时 213 passed / 95 failed 用例结果已实时写库,`ReportService.generate_html()` 只读 case_results 渲染,failed 执行完全能出报告。
......@@ -565,6 +608,7 @@ button:has-text('确定')
| 会话 | 日期 | 主题 | 结果 |
|------|------|------|------|
| 48 | 09-01 | **sut-explore 缺口补齐(会议运维 6 Tab + 待办巡检 2 用例)**:盘点确认 7 缺口;8 用例生成入库;会议运维 navigate 直达一次全绿;待办巡检 monitor 直达 URL 内层路由竞态(首轮通过/复验 2 次失败)→ 定位为设备列表页误判 → 改走首页「巡检报表」入口点击 `[id="data_list.inspect_report"]` 绕过 → 最终 **8/8 100% 通过** | ✅ 8/8 全绿 |
| 48 | 09-01 | **定时任务看门狗失败执行无报告输出(P0)**:根因=报告生成条件 `run_ok`,看门狗置 failed 后跳过报告;A 修复新增 `_has_any_case_results`,UI/Security 两条路径 `run_ok or has_results` 均生成报告;B1 start_time 回推 + B2 双层硬超时兜底 + B3 崩溃恢复;已部署 5.44/5.202/5.60 三台(提交 `80bbb6ac`/`f9b37c6b`,364 测试全绿,6/6 标记验证通过),容器日志轮转 50m×5 已生效;产出问题处理 + 执行计划文档 | ✅ 已部署三台 |
| 47 | 08-31 | **性能测试目标机监控增强与修复**:Batch A 5 Java 关键字 + 多 PID 聚合 + MySQL 趋势;Batch B JSTAT 瞬时 CPU 采样(ps 平均失真修复);15 项单测全绿;部署 5.60 | ✅ 已部署 + 已推送 origin |
| 46 | 08-31 | **执行耗时优化 P0/P1/P2**`step_retry_count` 默认 2→1;has-text 25s→10s / 表格行 150s→30s / checkbox 40s→20s / iframe 5s→3s;新增 `_selector_exists_fast` 快速失败探测;9 项单测 + 全量 352 通过;`deploy_timeout_opt.py` 部署三台;5.44 同一 10 用例定时任务 24.8min→10.2min(-59%),通过率 30% 无回归 | ✅ 三台部署 + 生产实测达标 |
......@@ -690,7 +734,7 @@ cd frontend && npm run build
- [x] ~~**定时任务僵尸执行 + 重叠守卫**~~(会话 42 已修复并部署三台:`recover_interrupted_executions()` 启动恢复 + DB 级重叠兜底守卫 + lifespan 调用;清理 5.44/5.202/5.60 的 18/14/68 条 running 僵尸,对应 2606/1814/401 用例结果标 failed;324 测试全绿)
- [x] ~~**薄弱模块 R7 修复部署三台服务器(5.44/5.202/5.60)**~~(会话 43 完成:`playwright_executor.py` commit `a63d090c` 按安全流程部署三台,容器内 SHA 一致、健康检查通过;会话 40 用例数据 + 本次代码修复完整生效,远端薄弱模块 NO-BODY 解除。`deploy_executor_r7.py` 新增 `--artifact` 参数支持指定部署制品)
- [x] ~~**P1/P2 执行器代码部署三台**~~(会话 43 已完成:`deploy_p1p2.py` 部署 `playwright_executor.py` P1b/P2 增强 + `smart_locate_service.py` + `smart_locate.py` + 前端 dist 三台,6/6 验证通过;已提交 `3a29883b`
- [ ] **monitor 内层路由被忽略**(微应用固定渲染设备列表,忽略 URL 内层 Maintenancelist/Filemanage 路由;相关用例已按实际渲染断言,如需真正内页需进一步探测侧边栏交互方案
- [ ] **monitor 内层路由被忽略**(微应用整页加载后固定渲染设备列表,忽略 URL 内层 Maintenancelist/Filemanage 路由;相关用例已按实际渲染断言。**例外:待办巡检 Checklist 已绕过**——经首页「巡检报表」入口点击 `[id="data_list.inspect_report"]` 可 100% 达巡检列表页,见会话 48
- [ ] 完善二级菜单映射表(`menu_mapping.py`
- [ ] 智能定位 API 与用例执行路径统一(当前两者逻辑分离)
- [ ] Claude 调用性能优化(缓存结果、减少超时时间)
......@@ -705,4 +749,4 @@ cd frontend && npm run build
---
*本文档由 Claude Code 于 2026-09-01 更新(会话 48:定时任务看门狗失败执行无报告输出修复——**已完成**:A `_has_any_case_results` + UI/Security 两条路径 `run_ok or has_results` + B1 start_time 回推 + B2 双层硬超时兜底 + B3 Chromium 崩溃恢复 + 容器日志轮转 50m×5,全部提交(`80bbb6ac`/`f9b37c6b`)并部署 5.44/5.202/5.60 三台,364/364 测试全绿,6/6 代码标记验证通过,LogConfig 生效;遗留:可选执行级端到端抽查)。*
\ No newline at end of file
*本文档由 Claude Code 于 2026-09-01 更新(会话 48 两档并存:① 定时任务看门狗失败执行无报告输出修复——已完成:A `_has_any_case_results` + UI/Security 两条路径 `run_ok or has_results` + B1 start_time 回推 + B2 双层硬超时兜底 + B3 Chromium 崩溃恢复 + 容器日志轮转 50m×5,全部提交(`80bbb6ac`/`f9b37c6b`)并部署 5.44/5.202/5.60 三台,364/364 测试全绿,6/6 代码标记验证通过,LogConfig 生效;遗留:可选执行级端到端抽查。② sut-explore 缺口补齐——会议运维 6 Tab + 待办巡检 2 用例;待办巡检 monitor 直达 URL 内层路由竞态定位(设备列表页误判)→ 改走首页「巡检报表」入口点击 `[id="data_list.inspect_report"]` 绕过;最终 8/8 用例 100% 通过;`generate_gap_cases.py` 幂等可重复执行)。*
# -*- coding: utf-8 -*-
"""
模块名称: generate_gap_cases.py
模块描述: 生成「会议运维」缺口 Tab 与「待办巡检(监测列表/巡检列表)」用例
会议运维 URL 可 navigate 直达(微前端 meetingV3,登录后登录态跨 hash 保留);
待办巡检经首页「巡检报表」入口点击进入(monitor 微应用直达 URL 有
内层路由竞态,见下 monkey-patch 注释,故走真实入口点击最稳定)。
作者: czj
创建日期: 2026-09-01
最后修改日期: 2026-09-01
用法:
cd backend
python scripts/generate_gap_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 = 'module_c9496f382be1430c958936d5cf473f62' # 会议运维
MODULE_MONITOR = 'module_432a65347d624b638f9623fe303af957' # 复用 运维管理 归属?待办巡检归监测/运维 -> 用运维管理模块
MODULE_YWGL = 'module_ddec220aa05f4a4b9b3728a648dfa62a' # 运维管理(巡检报表所在)
MEETING_URL = "https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FMeetingmaintenance"
MONITOR_URL = "https://192.168.5.44/#/backend/monitor?monitor=%2Fmonitor%2F%23%2Fadminsystem%2Fmonitor%2FChecklist"
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_count(order, name, selector, expected='1'):
return {'order': order, 'name': name, 'action': 'assert', 'params': {'type': 'element_count_min', 'selector': selector, 'expected': expected}}
def click_sel(order, name, selectors, timeout=8000):
return {'order': order, 'name': name, 'action': 'click', 'params': {'selectors': selectors, 'timeout': timeout}}
# ================= 会议运维(navigate 直达 + Tab 切换) =================
# Tab 选择器:el-tabs 渲染为 .el-tabs__item,active 带 .is-active
TAB_SEL = {
'终端列表': [".el-tabs__item:has-text('终端列表')", "text='终端列表'"],
'终端链路图': [".el-tabs__item:has-text('终端链路图')", "text='终端链路图'"],
'当前告警': [".el-tabs__item:has-text('当前告警')", "text='当前告警'"],
'历史告警': [".el-tabs__item:has-text('历史告警')", "text='历史告警'"],
'工单列表': [".el-tabs__item:has-text('工单列表')", "text='工单列表'"],
'我的工单': [".el-tabs__item:has-text('我的工单')", "text='我的工单'"],
}
meeting_base = [
nav(1, '导航到会议运维页面', MEETING_URL),
wait(2, '等待页面加载', 'body', 20000),
]
cases = []
# 1. 会议运维-页面访问验证(默认终端列表 Tab)
steps = meeting_base + [
assert_exists(3, '验证Tab栏存在', 'body:has-text("工单列表")'),
assert_exists(4, '验证表格存在', '.el-table'),
assert_count(5, '验证表格有数据行', '.el-table__row', '1'),
assert_exists(6, '验证名称列存在', '.el-table :text("名称")'),
]
cases.append(build_case(MODULE_MEETING, '会议运维-页面访问验证', steps,
'验证会议运维主页面加载,终端列表默认Tab且表格正常', 'P0',
['会议运维', '页面访问', 'Tab']))
# 2-6. 各 Tab 切换验证
for tab in ['终端列表', '终端链路图', '当前告警', '历史告警', '工单列表']:
steps = meeting_base + [
click_sel(3, f'点击{tab}Tab', TAB_SEL[tab]),
wait(4, '等待页面刷新', 'body', 5000),
assert_exists(5, f'验证{tab}Tab高亮', f'.el-tabs__item.is-active:has-text("{tab}")'),
assert_exists(6, '验证表格仍存在', '.el-table'),
]
cases.append(build_case(MODULE_MEETING, f'会议运维-{tab}Tab验证', steps,
f'验证会议运维{tab}Tab可切换且表格正常渲染', 'P1',
['会议运维', 'Tab', tab]))
# ================= 待办巡检(首页「巡检报表」入口点击,非 navigate 直达) =================
# ⚠️ 关键踩坑(probe_monitor_checklist_rerun.py / probe_real_entry_click.py 反复验证):
# /backend/monitor?monitor=...Checklist 直达 URL 存在竞态——warm-up 二段式整页 goto
# 后 monitor 微应用内部路由默认落在【设备列表】(Monitoringindex) 页而非巡检列表,
# 外层 monitor= hash 参数与 navPayload 均无法可靠驱动内层路由切到 Checklist
# (间歇性:首轮执行曾成功,复验 2 次全失败)。
# 稳定解法:navigate 首页 → 真实点击首页「巡检报表」入口
# (DIV.block id="data_list.inspect_report",父级 .first_box_flex)——平台壳自身
# 完成 monitor 初始化 + 内层路由派发,100% 稳定落在 Checklist(巡检列表)页。
# DOM 结构:点击后 url 变为 /backend/monitor?monitor=...Checklist;
# 表格列头=巡检名称/区域名称/检测结果;工具栏按钮=一键巡检/新增/编辑/删除。
HOME_ENTRY = '[id="data_list.inspect_report"]'
def home_click_base():
return [
nav(1, '导航到平台首页', 'https://192.168.5.44/'),
wait(2, '等待首页加载', 'body', 20000),
click_sel(3, '点击巡检报表入口', [HOME_ENTRY]),
wait(4, '等待巡检列表表格渲染', '.el-table', 30000),
]
monitor_steps = home_click_base() + [
assert_exists(5, '验证表格存在', '.el-table'),
assert_exists(6, '验证工具栏一键巡检', '.company-edmit-right :text("一键巡检")'),
assert_count(7, '验证表格有数据行', '.el-table__row', '1'),
]
cases.append(build_case(MODULE_YWGL, '待办巡检-页面访问验证', monitor_steps,
'通过首页巡检报表入口进入巡检列表,验证表格/一键巡检按钮/数据行渲染', 'P0',
['待办巡检', '页面访问', '巡检']))
# 表格数据验证:列头巡检名称/区域名称/检测结果
monitor_tbl = home_click_base() + [
assert_exists(5, '验证巡检名称列', '.el-table :text("巡检名称")'),
assert_exists(6, '验证区域名称列', '.el-table :text("区域名称")'),
assert_exists(7, '验证检测结果列', '.el-table :text("检测结果")'),
]
cases.append(build_case(MODULE_YWGL, '待办巡检-表格数据验证', monitor_tbl,
'验证巡检列表表格关键列(巡检名称/区域名称/检测结果)渲染', '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 -*-
"""
模块名称: 探测功能中心全部功能条目
模块描述: 登录 SUT 后打开右侧"功能中心"抽屉,抓取全部功能条目
(data-id / 名称 / 分类),与用例库比对找未覆盖项。
作者: czj
创建日期: 2026-08-31
最后修改日期: 2026-08-31
"""
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.set_default_timeout(15000)
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(2000)
if "/login" not in page.url:
break
page.wait_for_timeout(3000)
print("登录后 URL:", page.url)
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()
login(page)
# 打开功能中心抽屉(首页右上或侧边)
print("=== 尝试打开功能中心 ===")
try:
openers = page.evaluate(
"""() => {
const cands = [];
// 含"功能"文本的可点击元素
for (const el of document.querySelectorAll('div,span,button,i,li,a')) {
const t = (el.innerText||'').trim();
if (t === '功能中心' || t === '全部功能' || t === '功能') {
cands.push({tag: el.tagName, cls: el.className, text: t});
}
}
return cands;
}"""
)
print("候选入口:", json.dumps(openers, ensure_ascii=False))
except Exception as e:
print("open probe err:", str(e)[:100])
# 从左下角或 tab 找.尝试点击"功能中心"
clicked = page.evaluate(
"""() => {
const el = Array.from(document.querySelectorAll('div,span,button,li'))
.find(x => (x.innerText||'').trim() === '功能中心' && x.children.length === 0);
if (el) { el.click(); return true; }
// 兜底:找右上角图标
const ic = document.querySelector('[class*="menu"],[class*="more"],[class*="grid"]');
return false;
}"""
)
print("功能中心点击:", clicked)
page.wait_for_timeout(5000)
# 抓抽屉内全部条目
draw = page.evaluate(
"""() => {
const body = document.querySelector('micro-app-body') || document.body;
const items = [];
for (const el of body.querySelectorAll('[data-id]')) {
const t = (el.innerText||'').trim();
if (t) items.push({id: el.getAttribute('data-id'), text: t.slice(0,40), tag: el.tagName, cls: (el.className||'').toString().slice(0,40)});
}
// 也抓 tabs / 分类标题
const headers = [];
for (const el of body.querySelectorAll('.el-drawer__header,.el-tabs__item,.el-collapse-item__header,[class*=title]')) {
const t = (el.innerText||'').trim();
if (t && t.length < 30) headers.push({text: t.slice(0,30), cls: (el.className||'').toString().slice(0,40)});
}
return {items: items.slice(0,120), headers: headers.slice(0,40), bodyText: (body.innerText||'').slice(0,4000)};
}"""
)
print("=== 抽屉内容 ===")
print(json.dumps(draw, ensure_ascii=False, indent=2))
page.screenshot(path="tmp/sut_funcenter.png", full_page=True)
print("截图: tmp/sut_funcenter.png")
browser.close()
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
模块名称: 会议运维 Tab 结构探测
模块描述: 直达 Meetingmaintenance URL 后,探测 Tab 列表与各 Tab 切换后的
页面 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"
URLS = {
"会议运维": "https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FMeetingmaintenance",
"待办巡检": "https://192.168.5.44/#/backend/monitor?monitor=%2Fmonitor%2F%23%2Fadminsystem%2Fmonitor%2FChecklist",
}
def login(page):
page.set_default_timeout(15000)
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(3000)
def body_snapshot(page, label):
snap = page.evaluate(
"""() => {
const app = document.querySelector('micro-app-body') || document.body;
return {
tabs: Array.from(app.querySelectorAll('.el-tabs__item, [role=tab], .el-radio-button, .el-radio-button__inner'))
.map(e => (e.innerText||'').trim()).filter(t => t && t.length < 20).slice(0, 20),
activeTabs: Array.from(app.querySelectorAll('.el-tabs__item.is-active, .el-radio-button.is-active'))
.map(e => (e.innerText||'').trim()).filter(t => t && t.length < 20),
ths: Array.from(app.querySelectorAll('th, .el-table__header th')).map(e => (e.innerText||'').trim()).filter(t => t && t.length < 25).slice(0, 20),
btns: Array.from(app.querySelectorAll('button')).map(b => (b.innerText||'').trim()).filter(t => t && t.length < 15).slice(0, 25),
tables: app.querySelectorAll('.el-table').length,
rows: app.querySelectorAll('.el-table__body tr').length,
};
}"""
)
print(f"\n===== {label} =====")
print("tabs:", json.dumps(snap["tabs"], ensure_ascii=False))
print("activeTabs:", json.dumps(snap["activeTabs"], ensure_ascii=False))
print("ths:", json.dumps(snap["ths"], ensure_ascii=False))
print("btns:", json.dumps(snap["btns"], ensure_ascii=False))
print(f"tables={snap['tables']} rows={snap['rows']}")
return snap
def click_tab(page, text):
# 兼容 el-tabs / 自定义 tab / radio-button
done = page.evaluate(
"""(text) => {
const app = document.querySelector('micro-app-body') || document.body;
const cands = Array.from(app.querySelectorAll('.el-tabs__item, [role=tab], .el-radio-button, .el-radio-button__inner, .item_content, [class*=tab]'));
for (const el of cands) {
if ((el.innerText||'').trim() === text) { el.click(); return true; }
}
return false;
}""", text
)
page.wait_for_timeout(5000)
return done
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()
login(page)
# ---- 会议运维 ----
page.goto(URLS["会议运维"], wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(12000)
body_snapshot(page, "会议运维-初始(终端列表?)")
for t in ["终端链路图", "当前告警", "历史告警", "工单列表"]:
ok = click_tab(page, t)
print(f"[点击Tab {t}] 命中={ok}")
body_snapshot(page, f"会议运维-Tab:{t}")
# ---- 待办巡检 ----
page.goto(URLS["待办巡检"], wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(12000)
body_snapshot(page, "待办巡检-初始")
browser.close()
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
# -*- coding: utf-8 -*-
"""
模块名称: 探测 7 个缺口功能模块(待办巡检/会议运维/视频设备/文件下发/消息下发/壁纸下发/门牌发布)
模块描述: 登录 SUT 后打开功能中心抽屉,点击目标功能条目,
抓取真实直达 URL 与 DOM 结构。
作者: czj
创建日期: 2026-08-31
最后修改日期: 2026-08-31
"""
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"
TARGETS = ["待办巡检", "会议运维", "视频设备", "文件下发", "消息下发", "壁纸下发", "门牌发布"]
def login(page):
page.set_default_timeout(15000)
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(2000)
if "/login" not in page.url:
break
page.wait_for_timeout(3000)
print("登录后 URL:", page.url)
def open_drawer(page):
# 功能中心抽屉开关
try:
page.click(".home_nav_left", timeout=8000)
page.wait_for_timeout(3000)
return True
except Exception as e:
print(" [ERR] 打开抽屉失败:", str(e)[:80])
return False
def find_and_click(page, text):
"""在抽屉内找包含目标文本的条目并点击,返回点击成功与否"""
# 先列出抽屉里所有含 text 的元素
found = page.evaluate(
"""(text) => {
const drawer = document.querySelector('.el-drawer');
if (!drawer) return {drawer: false, items: []};
const items = [];
for (const el of drawer.querySelectorAll('[data-id],[data-key],.block,li,[class*=item]')) {
const t = (el.innerText||'').trim();
if (t && t.includes(text)) {
items.push({tag: el.tagName, id: el.getAttribute('data-id'), key: el.getAttribute('data-key'), text: t.slice(0,40), cls: (el.className||'').toString().slice(0,40)});
}
}
return {drawer: true, items};
}""",
text,
)
if not found.get("drawer"):
print(f" [ERR] 抽屉未打开")
return False
print(f" 抽屉内匹配 '{text}': {len(found['items'])} 个")
for it in found["items"][:5]:
print(" ", json.dumps(it, ensure_ascii=False))
if not found["items"]:
return False
# 点击第一个匹配
clicked = page.evaluate(
"""(text) => {
const drawer = document.querySelector('.el-drawer');
const el = Array.from(drawer.querySelectorAll('[data-id],[data-key],.block,li,[class*=item]'))
.find(x => (x.innerText||'').trim().includes(text));
if (el) { el.click(); return true; }
return false;
}""",
text,
)
return clicked
def probe_one(page, name):
print(f"\n{'='*20} 探测: {name} {'='*20}")
if not open_drawer(page):
return None
if not find_and_click(page, name):
print(" [WARN] 抽屉内未找到条目,尝试重新打开/搜索")
page.wait_for_timeout(2000)
return None
page.wait_for_timeout(7000)
url = page.url
print(" 点击后 URL:", url)
# 关闭抽屉避免遮挡
try:
page.keyboard.press("Escape")
page.wait_for_timeout(1000)
except Exception:
pass
# DOM 探测
dom = page.evaluate(
"""() => {
const app = document.querySelector('micro-app-body') || document.body;
const inputs = Array.from(app.querySelectorAll('input')).slice(0,25).map(i => ({
ph: i.placeholder, val: i.value, ro: i.readOnly, cls: (i.className||'').slice(0,40)
}));
const buttons = Array.from(app.querySelectorAll('button')).slice(0,30).map(b => ({
t: (b.innerText||'').trim().slice(0,20), cls: (b.className||'').slice(0,40)
}));
const tables = app.querySelectorAll('.el-table').length;
const selects = app.querySelectorAll('.el-select').length;
const text = (app.innerText||'').slice(0,1200);
return {inputs, buttons, tables, selects, text};
}"""
)
print(" DOM:", json.dumps(dom, ensure_ascii=False, indent=2)[:1500])
return {"name": name, "url": url, "dom": dom}
def main():
results = {}
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()
login(page)
for name in TARGETS:
# 回到首页
page.goto(SUT + "/#/platform?platform=%2Fplatform%2F%23%2FHome", wait_until="domcontentloaded")
page.wait_for_timeout(3000)
r = probe_one(page, name)
if r:
results[name] = r
page.wait_for_timeout(2000)
browser.close()
print("\n\n=== 探测结果汇总 ===")
print(json.dumps(results, ensure_ascii=False, indent=2))
with open("tmp/gap_modules_probe.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print("已保存 tmp/gap_modules_probe.json")
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
模块名称: 用 data-id 探测缺口功能(v2)
模块描述: 打开功能中心抽屉,用 elements_mapping 的 data-id 直接点击目标条目,
检测 micro-app 内部页面变化(外层 hash 不变时读 micro-app 状态),
抓取真实页面标题/表格/URL 特征。
作者: czj
创建日期: 2026-08-31
最后修改日期: 2026-08-31
"""
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"
# 7 个缺口功能 -> data-id(来自 elements_mapping)
TARGETS = {
"待办巡检": "maintain_list.inspect_list",
"会议运维": "maintain_list.meeting",
"视频设备": "maintain_list.video_dev",
"文件下发": "central_list.fileList",
"消息下发": "central_list.infoList",
"壁纸下发": "central_list.wallPaper",
"门牌发布": "publication_list.doorScreen",
}
def login(page):
page.set_default_timeout(15000)
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(2000)
if "/login" not in page.url:
break
page.wait_for_timeout(3000)
print("登录后 URL:", page.url)
def open_drawer(page):
try:
page.evaluate("() => { const el = document.querySelector('.home_nav_left'); if (el) el.click(); }")
page.wait_for_timeout(3500)
has_drawer = page.evaluate("() => !!document.querySelector('.el-drawer')")
print(" 抽屉打开:", has_drawer)
return has_drawer
except Exception as e:
print(" [ERR] open drawer:", str(e)[:80])
return False
def click_by_dataid(page, did):
clicked = page.evaluate(
"""(did) => {
const el = document.querySelector(`[data-id="${did}"]`);
if (el) { el.click(); return true; }
// 兜底:可能在 micro-app 内
const app = document.querySelector('micro-app-body');
if (app) {
const el2 = app.querySelector(`[data-id="${did}"]`);
if (el2) { el2.click(); return true; }
}
return false;
}""",
did,
)
return clicked
def microapp_info(page):
return page.evaluate(
"""() => {
const app = document.querySelector('micro-app-body');
if (!app) return {found: false};
const bodyText = (app.innerText || '').slice(0, 1500);
// micro-app 内部 hash / url
let innerUrl = '';
try {
const m = document.querySelector('micro-app');
innerUrl = m ? (m.getAttribute('url') || '') : '';
} catch (e) {}
return {
found: true,
innerUrl,
bodyText,
inputs: Array.from(app.querySelectorAll('input')).slice(0,15).map(i => ({ph: i.placeholder, ro: i.readOnly})),
buttons: Array.from(app.querySelectorAll('button')).slice(0,20).map(b => (b.innerText||'').trim().slice(0,15)),
tables: app.querySelectorAll('.el-table').length,
hasHome: /常用功能/.test((app.innerText||'')),
};
}"""
)
def probe_one(page, name, did):
print(f"\n{'='*20} 探测: {name} [{did}] {'='*20}")
# 回首页
page.goto(SUT + "/#/platform?platform=%2Fplatform%2F%23%2FHome", wait_until="domcontentloaded")
page.wait_for_timeout(3500)
if not open_drawer(page):
return None
before = microapp_info(page)
print(" 点击前 micro-app hasHome:", before.get("hasHome"), "| innerUrl:", before.get("innerUrl","")[:80])
if not click_by_dataid(page, did):
print(" [WARN] data-id 未命中")
return None
page.wait_for_timeout(8000)
after = microapp_info(page)
print(" 点击后 micro-app hasHome:", after.get("hasHome"), "| innerUrl:", after.get("innerUrl","")[:80])
print(" 点击后 bodyText:", after.get("bodyText","").replace("\n"," | ")[:400])
print(" inputs:", json.dumps(after.get("inputs"), ensure_ascii=False))
print(" buttons:", json.dumps(after.get("buttons"), ensure_ascii=False))
print(" tables:", after.get("tables"))
return {"name": name, "data_id": did, "before": before, "after": after}
def main():
results = {}
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()
login(page)
for name, did in TARGETS.items():
r = probe_one(page, name, did)
if r:
results[name] = r
browser.close()
print("\n\n=== 汇总 ===")
for name, r in results.items():
a = r["after"]
changed = (not a.get("hasHome")) and (a.get("bodyText") or "").strip()
print(f"{name}: 页面已切换={changed} | 表格数={a.get('tables')} | 输入框={len(a.get('inputs',[]))} | 文本预览: {a.get('bodyText','')[:120]!r}")
with open("tmp/gap_modules_probe_v2.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print("已保存 tmp/gap_modules_probe_v2.json")
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
模块名称: 缺口功能二次精确探测(独立会话逐个验证页面归属)
模块描述: 每个缺口功能用独立 context 登录 -> 打开抽屉 -> 点击 data-id
-> 抓取页面标题路径/表格特征,确认页面归属与内部路由参数。
作者: czj
创建日期: 2026-08-31
最后修改日期: 2026-08-31
"""
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"
TARGETS = {
"待办巡检": "maintain_list.inspect_list",
"会议运维": "maintain_list.meeting",
"视频设备": "maintain_list.video_dev",
"文件下发": "central_list.fileList",
"消息下发": "central_list.infoList",
"壁纸下发": "central_list.wallPaper",
"门牌发布": "publication_list.doorScreen",
}
def capture(page):
return page.evaluate(
"""() => {
const app = document.querySelector('micro-app-body');
if (!app) return {found: false};
// 抓标题路径(如 应用中心 / 文件推送)
let crumb = [];
for (const el of app.querySelectorAll('[class*=crumb] *, [class*=breadcrumb] *, .title, .el-page-header__content, h1,h2,h3')) {
const t = (el.innerText||'').trim();
if (t && t.length < 30 && !crumb.includes(t)) crumb.push(t);
}
let innerUrl = '';
try { innerUrl = (document.querySelector('micro-app')||{}).getAttribute('url') || ''; } catch(e) {}
return {
found: true,
crumbs: crumb.slice(0,8),
bodyText: (app.innerText||'').slice(0,600),
tables: app.querySelectorAll('.el-table').length,
hasHome: /常用功能/.test((app.innerText||'')),
innerUrl,
};
}"""
)
def run_one(p, name, did):
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)
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(3000)
# 打开抽屉
page.evaluate("() => { const el = document.querySelector('.home_nav_left'); if (el) el.click(); }")
page.wait_for_timeout(3500)
has = page.evaluate("() => !!document.querySelector('.el-drawer')")
# 确认目标条目存在
exists = page.evaluate(
"""(did) => !!document.querySelector(`.el-drawer [data-id="${did}"]`)""", did
)
print(f"[{name}] 抽屉={has} data-id命中={exists}")
# 点击
page.evaluate(
"""(did) => { const el = document.querySelector(`[data-id="${did}"]`); if (el) el.click(); }""", did
)
page.wait_for_timeout(10000)
info = capture(page)
# 读内部路由:hash / 微应用 window.location
inner_route = page.evaluate(
"""() => {
try {
const m = document.querySelector('micro-app');
const win = m && m.contentWindow;
if (win) return win.location.hash + ' | ' + win.location.pathname;
} catch(e) {}
return '';
}"""
)
print(f"[{name}] 内部路由: {inner_route}")
print(f"[{name}] crumbs: {json.dumps(info.get('crumbs'), ensure_ascii=False)}")
print(f"[{name}] hasHome: {info.get('hasHome')} | tables: {info.get('tables')}")
body0 = (info.get('bodyText') or '').replace("\n", " | ")
print(f"[{name}] 文本: {body0[:260]}")
print()
browser.close()
return {"name": name, "data_id": did, "inner_route": inner_route, "info": info}
def main():
with sync_playwright() as p:
for name, did in TARGETS.items():
run_one(p, name, did)
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
模块名称: 读取微应用内部真实路由 (v4)
模块描述: 点击缺口功能后,读取所有 frame URL + micro-app 元素属性,
尝试拿到内层真实 hash(决定用例能否 navigate 直达)。
作者: czj
创建日期: 2026-08-31
最后修改日期: 2026-08-31
"""
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"
TARGETS = {
"待办巡检": "maintain_list.inspect_list",
"会议运维": "maintain_list.meeting",
"视频设备": "maintain_list.video_dev",
}
def run_one(p, name, did):
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)
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(3000)
# 点击抽屉
page.evaluate("() => { const el = document.querySelector('.home_nav_left'); if (el) el.click(); }")
page.wait_for_timeout(3500)
page.evaluate(
"""(did) => { const el = document.querySelector(`[data-id="${did}"]`); if (el) el.click(); }""", did
)
page.wait_for_timeout(9000)
print(f"\n===== {name} [{did}] =====")
print("主 frame URL:", page.url)
# 所有 frame URL
for f in page.frames:
try:
print(" frame:", f.url[:160])
except Exception:
pass
# micro-app 元素属性 + shadow DOM
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;
let shadowUrl = '';
try {
const sr = m.shadowRoot;
if (sr) {
for (const el of sr.querySelectorAll('a,iframe,script')) {
if (el.getAttribute('src') || el.getAttribute('href')) {
shadowUrl += (el.getAttribute('src')||el.getAttribute('href')) + ' ; ';
}
}
}
} catch(e) {}
return {found: true, attrs, shadowUrl: shadowUrl.slice(0,300)};
}"""
)
print("micro-app:", json.dumps(ma, ensure_ascii=False, indent=1))
browser.close()
def main():
with sync_playwright() as p:
for name, did in TARGETS.items():
run_one(p, name, did)
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
模块名称: 待办巡检 Checklist 复验探测
模块描述: 复现执行器失败场景:登录 -> 二段式 warm-up -> 整页导航到
MONITOR_URL -> 探测最终 URL / 是否登录页 / .el-table 是否渲染 / body 文本。
判断 monitor 用例失败根因(登录重定向 or 页面未渲染 or 停在白屏)。
作者: 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"
MONITOR_URL = "https://192.168.5.44/#/backend/monitor?monitor=%2Fmonitor%2F%23%2Fadminsystem%2Fmonitor%2FChecklist"
def login(page):
page.set_default_timeout(15000)
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(3000)
def snapshot(page, label):
snap = page.evaluate(
"""() => {
const app = document.querySelector('micro-app-body') || document.body;
const loc = window.location.href;
return {
url: loc,
isLogin: /login/i.test(loc),
tables: app.querySelectorAll('.el-table').length,
rows: app.querySelectorAll('.el-table__body tr').length,
ths: Array.from(app.querySelectorAll('.el-table__header th')).map(e => (e.innerText||'').trim()).slice(0, 12),
btns: Array.from(app.querySelectorAll('button')).map(b => (b.innerText||'').trim()).filter(t => t && t.length < 12).slice(0, 20),
microApp: (document.querySelector('micro-app')||{}).getAttribute ? document.querySelector('micro-app').getAttribute('name') : '',
bodyText: (app.innerText||'').slice(0, 400).replace(/\\s+/g, ' '),
ls: {
projectCode: localStorage.getItem('projectCode'),
navPayload: localStorage.getItem('navPayload'),
pageOpen: localStorage.getItem('pageOpen'),
monitorLoginType: localStorage.getItem('monitorLoginType'),
},
};
}"""
)
print(f"\n===== {label} =====")
print("url:", snap["url"])
print("isLogin:", snap["isLogin"], "| microApp:", snap["microApp"])
print("tables:", snap["tables"], "| rows:", snap["rows"])
print("ths:", json.dumps(snap["ths"], ensure_ascii=False))
print("btns:", json.dumps(snap["btns"], ensure_ascii=False))
print("localStorage:", json.dumps(snap["ls"], ensure_ascii=False))
print("bodyText:", snap["bodyText"][:350])
return snap
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()
login(page)
snapshot(page, "登录后首页")
# 复现执行器 _do_navigate 路径:二段式 warm-up
page.evaluate(
"""() => {
localStorage.setItem('navPayload', JSON.stringify({
list: 'doorScreenPublish',
path: '/Backend/MediaPublish/MaterialManage'
}));
localStorage.setItem('pageOpen', 'true');
localStorage.setItem('monitorLoginType', '2');
localStorage.setItem('address', 'https://192.168.5.44:8000');
localStorage.setItem('selectGroup', '0');
}"""
)
page.goto(MONITOR_URL, wait_until="domcontentloaded", timeout=20000)
page.wait_for_timeout(6000)
snapshot(page, "阶段1 预热后(原始URL)")
page.evaluate(
"""() => {
localStorage.setItem('navPayload', JSON.stringify({
list: 'monitoring',
path: '/monitoring/Monitoringindex'
}));
}"""
)
page.goto(MONITOR_URL, wait_until="domcontentloaded", timeout=20000)
page.wait_for_timeout(12000)
snapshot(page, "阶段2 monitor 状态后")
browser.close()
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
# -*- coding: utf-8 -*-
"""
模块名称: 探测会议概览/会议室概览模块
模块描述: 登录 SUT 后点击"会议概览"和"会议室概览"首页入口,
抓取真实直达 URL 与 DOM 结构。
作者: czj
创建日期: 2026-08-31
最后修改日期: 2026-08-31
"""
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.set_default_timeout(15000)
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
# 协议弹窗 JS 点击确定
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(2000)
if "/login" not in page.url:
break
page.wait_for_timeout(3000)
print("登录后 URL:", page.url)
def probe_entry(page, name):
print(f"\n{'='*20} 探测: {name} {'='*20}")
# 找 .block 中文本为 name 的卡片,抓其结构
cards = page.evaluate(
"""(name) => Array.from(document.querySelectorAll('.block')).map(b => ({
text: (b.innerText||'').trim(),
html: b.outerHTML.slice(0, 500),
})).filter(c => c.text === name)""",
name,
)
print("匹配卡片:", json.dumps(cards, ensure_ascii=False, indent=2) if cards else "无")
# JS 点击该卡片
page.evaluate(
"""(name) => {
const b = Array.from(document.querySelectorAll('.block'))
.find(x => (x.innerText||'').trim() === name);
if (b) b.click();
}""",
name,
)
page.wait_for_timeout(6000)
url = page.url
print("点击后 URL:", url)
# 页面标题
title = page.evaluate("() => document.title")
print("页面标题:", title)
# micro-app 主内容 DOM
dom = page.evaluate(
"""() => {
const app = document.querySelector('micro-app-body') || document.body;
const inputs = Array.from(app.querySelectorAll('input')).slice(0, 20).map(i => ({
ph: i.placeholder, val: i.value, ro: i.readOnly, cls: i.className.slice(0,40)
}));
const buttons = Array.from(app.querySelectorAll('button')).slice(0, 30).map(b => ({
t: (b.innerText||'').trim().slice(0,20),
cls: b.className.slice(0,40),
}));
const tables = app.querySelectorAll('.el-table').length;
const cards = Array.from(app.querySelectorAll('[class*="card"],[class*="panel"],[class*="item"]')).slice(0,30).map(e => (e.className||'').toString().slice(0,50));
const h = app.querySelectorAll('h1,h2,h3,h4,h5,.title,.el-card__header').length;
const text = (app.innerText||'').slice(0,1500);
return {inputs, buttons, tables, cards, h, text};
}"""
)
print("DOM 探测:", json.dumps(dom, ensure_ascii=False, indent=2))
return url
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()
login(page)
# 先探测 会议概览
url1 = probe_entry(page, "会议概览")
# 回首页再探测 会议室概览
page.goto(SUT + "/#/platform?platform=%2Fplatform%2F%23%2FHome", wait_until="domcontentloaded")
page.wait_for_timeout(4000)
url2 = probe_entry(page, "会议室概览")
print("\n=== 直达 URL 汇总 ===")
print("会议概览:", url1)
print("会议室概览:", url2)
browser.close()
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
模块名称: 探测 SUT 首页功能入口(稳定版)
模块描述: 稳定登录 192.168.5.44(逐个等待字段 + 逐字段 fill),
抓取首页功能入口与直达 URL。
作者: czj
创建日期: 2026-08-31
最后修改日期: 2026-08-31
"""
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 safe_fill(page, selector, value, timeout=10000):
try:
el = page.wait_for_selector(selector, timeout=timeout)
if el is None:
print(f" [WARN] selector not found: {selector}")
return False
page.fill(selector, value, timeout=timeout)
print(f" [OK] filled {selector} = {value}")
return True
except Exception as e:
print(f" [ERR] fill {selector}: {str(e)[:120]}")
return False
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)
page.goto(SUT, wait_until="domcontentloaded", timeout=45000)
page.wait_for_timeout(4000)
print("登录页 URL:", page.url)
# 密码登录 tab(若默认不是,点一下)
try:
tab = page.query_selector("div:has-text('密码登录')")
if tab:
tab.click()
page.wait_for_timeout(800)
except Exception:
pass
# 逐个填字段(等可见再 fill)
safe_fill(page, "input[placeholder='手机号/用户名/邮箱']", USERNAME)
safe_fill(page, "input[placeholder='密码']", PASSWORD)
safe_fill(page, "input[placeholder='图形验证']", CAPTCHA)
page.wait_for_timeout(500)
# 登录按钮
try:
btns = page.query_selector_all("button")
for b in btns:
t = (b.inner_text() or "").strip()
if t == "登录":
b.click()
print("[OK] clicked 登录")
break
except Exception as e:
print("[ERR] login btn:", str(e)[:120])
page.wait_for_timeout(4000)
# 协议弹窗:JS 强制点击"确定"(绕过 Playwright actionability 检测)
for attempt in range(6):
page.wait_for_timeout(2000)
clicked = page.evaluate(
"""() => {
const btns = Array.from(document.querySelectorAll('button'))
.filter(b => (b.innerText||'').trim() === '确定');
if (btns.length) { btns[btns.length-1].click(); return btns.length; }
return 0;
}"""
)
print(f" [PROBE] 确定按钮 JS 点击: {clicked}")
page.wait_for_timeout(2500)
if "/login" not in page.url:
break
page.wait_for_timeout(3000)
page.wait_for_timeout(5000)
print("当前 URL:", page.url)
body_text = page.evaluate("() => document.body ? document.body.innerText.slice(0, 3000) : ''")
print("=== body 前3000字 ===")
print(body_text)
# 功能入口卡片
print("=== .block 卡片 ===")
cards = page.evaluate(
"""() => Array.from(document.querySelectorAll('.block')).map(b => ({
text: (b.innerText||'').trim().slice(0,80),
cls: (b.className||'').toString().slice(0,60),
}))"""
)
for c in cards[:80]:
print(json.dumps(c, ensure_ascii=False))
# 所有链接
print("=== a 链接 ===")
links = page.evaluate(
"""() => Array.from(document.querySelectorAll('a')).map(a => ({
text: (a.innerText||'').trim().slice(0,50),
href: a.getAttribute('href'),
})).filter(x => x.text)"""
)
seen = set()
for l in links:
if l["text"] not in seen:
seen.add(l["text"])
print(json.dumps(l, ensure_ascii=False))
page.screenshot(path="tmp/sut_home2.png", full_page=True)
print("截图: tmp/sut_home2.png")
browser.close()
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论