更新压缩机制,提高前缀缓存命中率,优化UI渲染机制,提高帧数
No files matched your search
@@ -0,0 +1,31 @@
|
||||
"""按 steps/*.json 的 actions 列表驱动 web_page 工具(真实浏览器/CDP 9222)。
|
||||
|
||||
用法: python drive.py steps/xxx.json [--save-png 前缀]
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "Agent" / "src"))
|
||||
|
||||
from agent.tools.web_page import WebPage as WebPageTool
|
||||
|
||||
|
||||
def main() -> None:
|
||||
path = Path(sys.argv[1] if len(sys.argv) > 1 else "steps/tmp.json")
|
||||
actions = json.loads(path.read_text(encoding="utf-8"))
|
||||
tool = WebPageTool()
|
||||
msg, artifact = tool._run(actions)
|
||||
print(msg)
|
||||
png = artifact.get("image_bytes")
|
||||
if png:
|
||||
out = Path(__file__).parent / "output"
|
||||
out.mkdir(exist_ok=True)
|
||||
name = path.stem + f"_{int(time.time())}.png"
|
||||
(out / name).write_bytes(png)
|
||||
print(f"[screenshot saved] {out / name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,250 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WebPage 测试页</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 20px; }
|
||||
.ant-form-item { display: flex; align-items: center; margin: 8px 0; }
|
||||
.ant-form-item label { width: 90px; }
|
||||
.ant-calendar-picker { position: relative; display: inline-block; }
|
||||
.ant-calendar-picker-container {
|
||||
position: absolute; top: 40px; left: 0; z-index: 1000;
|
||||
background: #fff; border: 1px solid #ddd; box-shadow: 0 2px 8px rgba(0,0,0,.15);
|
||||
padding: 8px; display: none; width: 260px;
|
||||
}
|
||||
.ant-calendar-picker-container.open { display: block; }
|
||||
.ant-calendar-ym-select a { margin: 0 6px; cursor: pointer; }
|
||||
.ant-calendar-date { display: inline-block; width: 26px; height: 24px; line-height: 24px;
|
||||
text-align: center; cursor: pointer; }
|
||||
.ant-calendar-date:hover { background: #e6f7ff; }
|
||||
.ant-calendar-year-panel-cell, .ant-calendar-month-panel-cell {
|
||||
display: inline-block; width: 60px; height: 26px; line-height: 26px; text-align: center;
|
||||
cursor: pointer; }
|
||||
.ant-calendar-year-panel-cell a, .ant-calendar-month-panel-cell a { color: #333; text-decoration: none; }
|
||||
.ant-calendar-year-panel-prev-decade-btn, .ant-calendar-year-panel-next-decade-btn,
|
||||
.ant-calendar-prev-month-btn, .ant-calendar-next-month-btn { cursor: pointer; margin: 0 4px; }
|
||||
.ant-select-dropdown { display: none; border: 1px solid #ccc; background: #fff; max-height: 200px; overflow: auto; }
|
||||
.ant-select-dropdown.open { display: block; }
|
||||
.ant-select-dropdown-menu-item { padding: 4px 12px; cursor: pointer; }
|
||||
.ant-select-dropdown-menu-item:hover { background: #e6f7ff; }
|
||||
.dialog { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 500; }
|
||||
.dialog.open { display: flex; align-items: center; justify-content: center; }
|
||||
.dialog-box { background: #fff; padding: 20px; width: 380px; border-radius: 6px; }
|
||||
.popup { display: none; position: fixed; background: #fff; border: 1px solid #999;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,.2); padding: 12px; z-index: 600; }
|
||||
.popup.open { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebPage 测试页</h1>
|
||||
|
||||
<!-- 登录区 -->
|
||||
<div class="ant-form-item"><label>账户名</label><input placeholder="账户名"></div>
|
||||
<div class="ant-form-item"><label>密码</label><input type="password" placeholder="密码"></div>
|
||||
<button id="btn-login">登录</button>
|
||||
<span id="login-result"></span>
|
||||
|
||||
<!-- 打开弹窗按钮 -->
|
||||
<button id="btn-dialog">打开弹窗</button>
|
||||
|
||||
<!-- 弹窗:表单 + 下拉 + 纯文本标题 -->
|
||||
<div class="dialog" id="my-dialog" role="dialog" aria-modal="true">
|
||||
<div class="dialog-box">
|
||||
<button class="close" onclick="closeDialog()">Close</button>
|
||||
<div class="ant-form-item"><label>姓名</label><input id="dlg-name" placeholder="姓名"></div>
|
||||
<div class="ant-form-item">
|
||||
<label>产品</label>
|
||||
<div class="ant-select-selection" onclick="toggleSelect(this)">请选择产品</div>
|
||||
<div class="ant-select-dropdown">
|
||||
<div class="result-title" onclick="">检索结果</div>
|
||||
<ul style="list-style:none;margin:0;padding:0">
|
||||
<li class="ant-select-dropdown-menu-item">意外险</li>
|
||||
<li class="ant-select-dropdown-menu-item">健康险</li>
|
||||
<li class="ant-select-dropdown-menu-item">060003旅游意外伤害保险</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button id="dlg-ok" onclick="confirmDialog()">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日期控件(antd 1.x 结构模拟:点击输入框弹出面板) -->
|
||||
<div class="ant-form-item">
|
||||
<label>证件有效起期</label>
|
||||
<span class="ant-calendar-picker">
|
||||
<input class="ant-calendar-picker-input" placeholder="证件有效起期" readonly onclick="openCalendar(this)">
|
||||
<div class="ant-calendar-picker-container">
|
||||
<div class="ant-calendar-header">
|
||||
<a class="ant-calendar-prev-month-btn" title="上个月" onclick="navMonth(-1)">‹</a>
|
||||
<span class="ant-calendar-ym-select">
|
||||
<a class="ant-calendar-month-select" title="选择月份" onclick="showMonthPanel()">8月</a>
|
||||
<a class="ant-calendar-year-select" title="选择年份" onclick="showYearPanel()">2026年</a>
|
||||
</span>
|
||||
<a class="ant-calendar-next-month-btn" title="下个月" onclick="navMonth(1)">›</a>
|
||||
</div>
|
||||
<div class="ant-calendar-date-panel" id="date-panel"></div>
|
||||
<div class="ant-calendar-year-panel" id="year-panel" style="display:none"></div>
|
||||
<div class="ant-calendar-month-panel" id="month-panel" style="display:none"></div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- shadow DOM 组件 -->
|
||||
<x-widget></x-widget>
|
||||
|
||||
<!-- 机构树(angular-tree-component / ant-tree 结构模拟:switcher + content-wrapper + title) -->
|
||||
<div class="ant-form-item"><label>机构</label>
|
||||
<div id="org-tree" class="angular-tree-component">
|
||||
<div class="tree-children">
|
||||
<div class="tree-node">
|
||||
<span class="ant-tree-switcher ant-tree-switcher-open" onclick="toggleOrg(this)">▾</span>
|
||||
<span class="ant-tree-node-content-wrapper">
|
||||
<span class="ant-tree-title">北京分公司</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tree-node" style="padding-left: 20px">
|
||||
<span class="ant-tree-switcher ant-tree-switcher-noop">·</span>
|
||||
<span class="ant-tree-node-content-wrapper">
|
||||
<span class="ant-tree-title">北京分公司车险管理部</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tree-node">
|
||||
<span class="ant-tree-switcher ant-tree-switcher-close" onclick="toggleOrg(this)">▸</span>
|
||||
<span class="ant-tree-node-content-wrapper">
|
||||
<span class="ant-tree-title">上海分公司</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span id="org-selected"></span>
|
||||
|
||||
<script>
|
||||
// 树节点点击选中(title 可点)
|
||||
document.querySelectorAll('#org-tree .ant-tree-title').forEach(function (t) {
|
||||
t.addEventListener('click', function () {
|
||||
document.getElementById('org-selected').textContent = '已选: ' + t.textContent.trim();
|
||||
});
|
||||
});
|
||||
// 树 switcher 点击仅展开/折叠
|
||||
window.toggleOrg = function (sw) {
|
||||
var node = sw.closest('.tree-node');
|
||||
var kids = node.querySelector('.tree-children');
|
||||
var folded = sw.classList.toggle('ant-tree-switcher-close');
|
||||
if (kids) kids.style.display = folded ? 'none' : 'block';
|
||||
};
|
||||
</script>
|
||||
<script>
|
||||
// ---------- 弹窗 ----------
|
||||
window.openDialog = function () { document.getElementById('my-dialog').classList.add('open'); };
|
||||
window.closeDialog = function () { document.getElementById('my-dialog').classList.remove('open'); };
|
||||
window.confirmDialog = function () {
|
||||
var v = document.getElementById('dlg-name').value;
|
||||
document.getElementById('login-result').textContent = '确认:' + v;
|
||||
closeDialog();
|
||||
};
|
||||
document.getElementById('btn-dialog').addEventListener('click', openDialog);
|
||||
document.getElementById('btn-login').addEventListener('click', function () {
|
||||
document.getElementById('login-result').textContent = '登录成功';
|
||||
});
|
||||
|
||||
// ---------- 模拟 antd select ----------
|
||||
window.toggleSelect = function (sel) {
|
||||
var dd = sel.parentElement.querySelector('.ant-select-dropdown');
|
||||
dd.classList.toggle('open');
|
||||
};
|
||||
document.querySelectorAll('.ant-select-dropdown-menu-item').forEach(function (li) {
|
||||
li.addEventListener('click', function () {
|
||||
var sel = li.closest('.ant-form-item').querySelector('.ant-select-selection');
|
||||
sel.textContent = li.textContent;
|
||||
li.closest('.ant-select-dropdown').classList.remove('open');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- 模拟 antd 日历 ----------
|
||||
var curYear = 2026, curMonth = 8, selYear = 2026, selMonth = 8, selDay = 20;
|
||||
window.renderDate = function () {
|
||||
var p = document.getElementById('date-panel');
|
||||
var first = new Date(curYear, curMonth - 1, 1).getDay();
|
||||
var days = new Date(curYear, curMonth, 0).getDate();
|
||||
var html = '';
|
||||
for (var i = 0; i < first; i++) html += '<span class="ant-calendar-date" style="visibility:hidden">-</span>';
|
||||
for (var d = 1; d <= days; d++) {
|
||||
html += '<span class="ant-calendar-date" onclick="pickDate(' + d + ')">' + d + '</span>';
|
||||
}
|
||||
p.innerHTML = html;
|
||||
document.querySelector('.ant-calendar-month-select').textContent = curMonth + '月';
|
||||
document.querySelector('.ant-calendar-year-select').textContent = curYear + '年';
|
||||
};
|
||||
window.openCalendar = function (inp) {
|
||||
var c = inp.parentElement.querySelector('.ant-calendar-picker-container');
|
||||
var open = c.classList.toggle('open');
|
||||
if (open) { renderDate(); showDatePanel(); }
|
||||
};
|
||||
window.showDatePanel = function () {
|
||||
document.getElementById('date-panel').style.display = 'block';
|
||||
document.getElementById('year-panel').style.display = 'none';
|
||||
document.getElementById('month-panel').style.display = 'none';
|
||||
};
|
||||
window.showYearPanel = function () {
|
||||
var p = document.getElementById('year-panel');
|
||||
var start = Math.floor(curYear / 10) * 10;
|
||||
var html = '<a class="ant-calendar-year-panel-prev-decade-btn" onclick="navDecade(-1)">«</a> ';
|
||||
for (var y = start; y < start + 10; y++) {
|
||||
html += '<span class="ant-calendar-year-panel-cell"><a onclick="pickYear(' + y + ')">' + y + '</a></span>';
|
||||
}
|
||||
html += ' <a class="ant-calendar-year-panel-next-decade-btn" onclick="navDecade(1)">»</a>';
|
||||
p.innerHTML = html;
|
||||
document.getElementById('date-panel').style.display = 'none';
|
||||
document.getElementById('year-panel').style.display = 'block';
|
||||
document.getElementById('month-panel').style.display = 'none';
|
||||
};
|
||||
window.navDecade = function (d) {
|
||||
var start = Math.floor(curYear / 10) * 10 + d * 10;
|
||||
curYear = start;
|
||||
showYearPanel();
|
||||
};
|
||||
window.showMonthPanel = function () {
|
||||
var p = document.getElementById('month-panel');
|
||||
var html = '';
|
||||
for (var m = 1; m <= 12; m++) {
|
||||
html += '<span class="ant-calendar-month-panel-cell"><a onclick="pickMonth(' + m + ')">' + m + '月</a></span>';
|
||||
}
|
||||
p.innerHTML = html;
|
||||
document.getElementById('date-panel').style.display = 'none';
|
||||
document.getElementById('year-panel').style.display = 'none';
|
||||
document.getElementById('month-panel').style.display = 'block';
|
||||
};
|
||||
window.pickYear = function (y) {
|
||||
curYear = y; showDatePanel(); renderDate();
|
||||
};
|
||||
window.pickMonth = function (m) {
|
||||
curMonth = m; showDatePanel(); renderDate();
|
||||
};
|
||||
window.navMonth = function (d) {
|
||||
curMonth += d;
|
||||
if (curMonth > 12) { curMonth = 1; curYear++; }
|
||||
if (curMonth < 1) { curMonth = 12; curYear--; }
|
||||
renderDate();
|
||||
};
|
||||
window.pickDate = function (d) {
|
||||
selYear = curYear; selMonth = curMonth; selDay = d;
|
||||
var pad = function (n) { return n < 10 ? '0' + n : '' + n; };
|
||||
var inp = document.querySelector('.ant-calendar-picker-input');
|
||||
inp.value = selYear + '-' + pad(selMonth) + '-' + pad(selDay);
|
||||
document.querySelector('.ant-calendar-picker-container').classList.remove('open');
|
||||
};
|
||||
renderDate();
|
||||
|
||||
// ---------- shadow DOM ----------
|
||||
class XWidget extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const root = this.attachShadow({ mode: 'open' });
|
||||
root.innerHTML = '<button id="x-btn">影子按钮</button><input id="x-input" placeholder="影子输入框">';
|
||||
}
|
||||
}
|
||||
customElements.define('x-widget', XWidget);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 325 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 325 KiB |
|
After Width: | Height: | Size: 325 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 188 KiB |
@@ -0,0 +1,134 @@
|
||||
"""web_page 工具冒烟测试(fixture 本地页面,覆盖本次改动核心能力)。
|
||||
|
||||
运行: python Agent/tests/web_page/smoke_test.py
|
||||
依赖: 无浏览器时自动分离启动(browser_control 逻辑)
|
||||
"""
|
||||
import http.server
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE.parents[2] / "Agent" / "src"))
|
||||
|
||||
from agent.tools.web_page import WebPage as WebPageTool # noqa: E402
|
||||
|
||||
PORT = 8766
|
||||
|
||||
PASS = 0
|
||||
FAIL = 0
|
||||
|
||||
|
||||
def check(name: str, cond: bool, detail: str = "") -> None:
|
||||
global PASS, FAIL
|
||||
if cond:
|
||||
PASS += 1
|
||||
print(f"[PASS] {name}")
|
||||
else:
|
||||
FAIL += 1
|
||||
print(f"[FAIL] {name} {detail}")
|
||||
|
||||
|
||||
class NoCacheHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def end_headers(self):
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
super().end_headers()
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
|
||||
def serve() -> None:
|
||||
handler = lambda *a, **kw: NoCacheHandler(*a, directory=str(HERE / "fixture"), **kw)
|
||||
http.server.ThreadingHTTPServer(("127.0.0.1", PORT), handler).serve_forever()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
threading.Thread(target=serve, daemon=True).start()
|
||||
|
||||
tool = WebPageTool()
|
||||
|
||||
def run(actions: list[dict]) -> str:
|
||||
return tool._run(actions)[0]
|
||||
|
||||
# 1. 打开 fixture 页面
|
||||
msg = run([{"action": "goto", "url": f"http://127.0.0.1:{PORT}/index.html"}])
|
||||
check("goto fixture", "已打开" in msg, msg)
|
||||
|
||||
# 2. 登录区元素
|
||||
msg = run([{"action": "snapshot"}])
|
||||
check("快照含账户名", '"账户名"' in msg, msg[:200])
|
||||
check("快照含密码", '"密码"' in msg)
|
||||
check("快照含登录按钮", 'button "登录"' in msg)
|
||||
|
||||
# 3. 打开弹窗 → 层级断言
|
||||
msg = run([
|
||||
{"action": "click", "selector": "text=\"打开弹窗\""},
|
||||
{"action": "wait", "wait_seconds": 0.3},
|
||||
{"action": "snapshot"},
|
||||
])
|
||||
check("弹窗标注 [弹窗]", "[弹窗]" in msg, msg[:300])
|
||||
check("弹窗含 Close", 'button "Close"' in msg)
|
||||
check("弹窗含确认按钮", 'button "确认"' in msg)
|
||||
|
||||
# 4. 展开下拉 → 选项进快照 + 纯文本标题标注
|
||||
msg = run([
|
||||
{"action": "click", "selector": "text=\"请选择产品\"", "note": "展开下拉"},
|
||||
{"action": "wait", "wait_seconds": 0.3},
|
||||
{"action": "snapshot"},
|
||||
])
|
||||
check("纯文本容器标 [文本]", "[文本]" in msg, msg[:300])
|
||||
check("下拉选项进快照", "意外险" in msg and "060003旅游意外伤害保险" in msg, msg[:400])
|
||||
|
||||
# 5. text= 精确匹配点击下拉选项
|
||||
msg = run([{"action": "click", "selector": "text=\"意外险\"", "note": "选项"}])
|
||||
check("text= 点选项成功", "已点击" in msg, msg)
|
||||
msg = run([{"action": "eval_js", "js": "document.querySelector('.ant-select-selection').textContent"}])
|
||||
check("下拉选中意外险", "意外险" in msg, msg)
|
||||
|
||||
# 6. date 动作:2005-07-09(年份面板需翻两个十年;先关闭弹窗避免遮挡)
|
||||
msg = run([
|
||||
{"action": "click", "selector": "text=\"Close\"", "note": "关弹窗"},
|
||||
{"action": "wait", "wait_seconds": 0.3},
|
||||
{"action": "date", "selector": "input.ant-calendar-picker-input", "value": "2005-07-09"},
|
||||
])
|
||||
check("date 动作成功", "已选择日期: 2005-07-09" in msg, msg)
|
||||
msg = run([{"action": "eval_js", "js": "document.querySelector('.ant-calendar-picker-input').value"}])
|
||||
check("date 写入输入框", "2005-07-09" in msg, msg)
|
||||
|
||||
# 7. shadow DOM 穿透
|
||||
msg = run([{"action": "snapshot", "max_elements": 100}])
|
||||
check("shadow DOM 按钮可见", "影子按钮" in msg, msg[:300])
|
||||
|
||||
# 8. text= 精确匹配失败 → 自动子串匹配报告真实候选
|
||||
# (打开弹窗使下拉选项可见:text=旅游 精确匹配不到,子串候选含 060003旅游意外伤害保险)
|
||||
msg = run([
|
||||
{"action": "click", "selector": "text=\"打开弹窗\""},
|
||||
{"action": "wait", "wait_seconds": 0.3},
|
||||
{"action": "click", "selector": "text=旅游"},
|
||||
])
|
||||
check("text= 失败给出候选报告", "候选" in msg and "060003旅游" in msg, msg[:300])
|
||||
|
||||
# 9. 树节点:switcher 标 [展开],title 正常显示且可点击选中
|
||||
msg = run([{"action": "snapshot", "max_elements": 200}])
|
||||
check("树 switcher 标 [展开]", "[展开]" in msg, msg[:400])
|
||||
check("树节点标题可见", "北京分公司车险管理部" in msg, msg[:400])
|
||||
msg = run([
|
||||
{"action": "click", "selector": "text=\"Close\"", "note": "关弹窗"},
|
||||
{"action": "wait", "wait_seconds": 0.3},
|
||||
{"action": "click", "selector": "text=\"北京分公司车险管理部\"", "note": "树节点标题"},
|
||||
])
|
||||
check("树节点标题可点击", "已点击" in msg, msg)
|
||||
msg = run([{"action": "eval_js", "js": "document.getElementById('org-selected').textContent"}])
|
||||
check("树节点点击选中生效", "已选: 北京分公司车险管理部" in msg, msg)
|
||||
|
||||
print()
|
||||
if FAIL:
|
||||
print(f"✗ {FAIL} 项失败 / {PASS} 项通过")
|
||||
sys.exit(1)
|
||||
print(f"全部通过 ✓({PASS} 项断言)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{"action": "goto", "url": "http://117.78.36.208:9099/"},
|
||||
{"action": "wait", "wait_seconds": 3},
|
||||
{"action": "snapshot"},
|
||||
{"action": "fill", "ref": 1, "text": "10010381"},
|
||||
{"action": "fill", "ref": 2, "text": "uat"},
|
||||
{"action": "click", "ref": 5},
|
||||
{"action": "wait", "wait_seconds": 3},
|
||||
{"action": "snapshot"},
|
||||
{"action": "click", "ref": 5},
|
||||
{"action": "wait", "wait_seconds": 4},
|
||||
{"action": "snapshot"}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{"action": "goto", "url": "http://127.0.0.1:8766/index.html"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "selector": "text=产品"}
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 50}
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 300}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{"action": "click", "ref": 37, "note": "业务来源(已是直销业务,重选验证)"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "selector": "text=\"直销业务\""}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 300},
|
||||
{"action": "click", "ref": 37, "note": "业务来源"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "selector": "text=\"直销业务\"", "note": "业务来源选项"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "ref": 39, "note": "渠道中级分类"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "selector": "text=\"传统直销业务\"", "note": "中级分类选项"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "ref": 41, "note": "渠道子类"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "selector": "text=\"员工直销\"", "note": "渠道子类选项"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "eval_js", "js": "(() => { const nz = Array.from(document.querySelectorAll('nz-select')).map(e => e.textContent.trim().slice(0, 15)); return JSON.stringify(nz.filter(t => ['直销业务', '传统直销业务', '员工直销'].includes(t))); })()"}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 300},
|
||||
{"action": "date", "ref": 83, "value": "2005-07-09", "note": "证件有效起期"},
|
||||
{"action": "date", "ref": 84, "value": "2006-07-09", "note": "证件有效止期"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "eval_js", "js": "(() => { const a = document.querySelector('input[placeholder=\"证件有效起期\"]')?.value; const b = document.querySelector('input[placeholder=\"证件有效止期\"]')?.value; return '起期=' + a + ' 止期=' + b; })()"}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{"action": "snapshot"},
|
||||
{"action": "click", "ref": 2, "note": "申请单录入"},
|
||||
{"action": "wait", "wait_seconds": 4},
|
||||
{"action": "eval_js", "js": "(() => { const m = document.querySelector('.ant-modal-wrap'); return m ? 'MODAL OPEN' : 'NO MODAL'; })()"},
|
||||
{"action": "snapshot", "max_elements": 40}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"action": "snapshot"},
|
||||
{"action": "click", "ref": 2, "note": "申请单录入"},
|
||||
{"action": "wait", "wait_seconds": 4},
|
||||
{"action": "snapshot", "max_elements": 40}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
[{"action": "eval_js", "js": "(() => { const b = document.getElementById('btn-dialog'); const r = document.getElementById('login-result'); return JSON.stringify({btnExists: !!b, resultText: r ? r.textContent : 'no-result', dialogOpen: document.getElementById('my-dialog') ? document.getElementById('my-dialog').className : 'no-dialog'}); })()"}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"action": "goto", "url": "http://127.0.0.1:8766/index.html"}, {"action": "wait", "wait_seconds": 1}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"action": "snapshot", "max_elements": 300}, {"action": "eval_js", "js": "(() => { const u = document.querySelector('#Applicant\\.CWorkDpt'); return '初始 disabled=' + (u ? u.getAttribute('ng-reflect-nz-disabled') : 'NF'); })()"}, {"action": "click", "ref": 78, "note": "投保人性质"}, {"action": "wait", "wait_seconds": 1}, {"action": "click", "selector": "text=\"个人\"", "note": "投保人性质选项"}, {"action": "wait", "wait_seconds": 1}, {"action": "eval_js", "js": "(() => { const u = document.querySelector('#Applicant\\.CWorkDpt'); return '选个人后 disabled=' + (u ? u.getAttribute('ng-reflect-nz-disabled') : 'NF'); })()"}]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"action": "snapshot"},
|
||||
{"action": "click", "ref": 1, "note": "系统菜单"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "snapshot", "max_elements": 40}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{"action": "snapshot"},
|
||||
{"action": "click", "ref": 1, "note": "系统菜单"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "eval_js", "js": "(() => { const m = document.querySelector('.ant-modal-wrap'); return m ? 'MODAL OPEN' : 'NO MODAL'; })()"},
|
||||
{"action": "snapshot", "max_elements": 40}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 40},
|
||||
{"action": "click", "ref": 4, "note": "承保机构放大镜"},
|
||||
{"action": "wait", "wait_seconds": 3},
|
||||
{"action": "snapshot", "max_elements": 60}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 120},
|
||||
{"action": "click", "selector": "text=\"北京分公司车险管理部\"", "note": "机构节点"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "snapshot", "max_elements": 30}
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
[
|
||||
{"action": "eval_js", "js": "(() => { const els = Array.from(document.querySelectorAll('div,span')).filter(e => (e.textContent||'').trim() === '检索结果' && e.children.length <= 3); return JSON.stringify(els.map(e => { const a = {}; for (const x of e.attributes) a[x.name] = x.value.slice(0, 40); return {tag: e.tagName, cls: e.className, attrs: a, kids: e.children.length}; })); })()"}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
[{"action": "snapshot", "max_elements": 300}, {"action": "click", "ref": 118, "note": "单位性质"}, {"action": "wait", "wait_seconds": 2}, {"action": "eval_js", "js": "(() => { const lis = Array.from(document.querySelectorAll('.ant-select-dropdown-menu-item')).filter(e => e.offsetParent); return JSON.stringify(lis.slice(0, 8).map(e => e.textContent.trim().replace(/\\s+/g, ' ').slice(0, 40))); })()"}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"action": "eval_js", "js": "(() => { const out = []; Array.from(document.querySelectorAll('nz-select')).forEach(e => { const t = (e.textContent||'').trim().replace(/\\s+/g, ' ').slice(0, 25); if (t) out.push(t); }); return JSON.stringify(out); })()"}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"action": "eval_js", "js": "(() => { const all = Array.from(document.querySelectorAll('nz-select')); const u = all.find(e => (e.getAttribute('id')||'').includes('WorkDpt') || (e.getAttribute('ng-reflect-name')||'').includes('WorkDpt')); if (!u) return 'NOT FOUND, ids=' + JSON.stringify(all.slice(0, 10).map(e => e.id)); return JSON.stringify({id: u.id, ngName: u.getAttribute('ng-reflect-name'), disabled: u.getAttribute('ng-reflect-nz-disabled'), clsDisabled: (u.className||'').includes('ant-select-disabled'), text: (u.textContent||'').trim().slice(0, 20)}); })()"}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"action": "eval_js", "js": "(() => {\n // 通讯地址-市下拉展开后看选项 li 文本\n const lis = Array.from(document.querySelectorAll('.ant-select-dropdown-menu-item')).map(e => JSON.stringify(e.textContent.trim().slice(0, 20)));\n return '当前可见 li: ' + lis.join(', ');\n})()"}]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 60},
|
||||
{"action": "fill", "ref": 2, "text": "车险管理部"},
|
||||
{"action": "press", "key": "Enter"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "snapshot", "max_elements": 60}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
[{"action": "eval_js", "js": "(() => { const nz = Array.from(document.querySelectorAll('nz-select')).map(e => (e.textContent||'').trim().replace(/\\s+/g, ' ').slice(0, 20)); const keys = ['业务来源', '渠道中级分类', '渠道子类', '机构部门', '产品']; return JSON.stringify({all: nz}); })()"}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"action": "eval_js", "js": "(() => {\n const out = {legends: [], expands: [], unitDisabled: null, addrVisible: null};\n // 1) 所有 fieldset legend 标题 + 展开链接状态\n out.legends = Array.from(document.querySelectorAll('fieldset > legend')).map(l => {\n const fs = l.closest('fieldset');\n const a = l.querySelector('a');\n return {\n title: (l.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 30),\n link: a ? a.textContent.trim() : null,\n bodyHidden: fs ? getComputedStyle(fs.querySelector('div, table, .ant-table, form') || fs).display : '?'\n };\n });\n // 2) 通讯地址省下拉可见性\n const addr = Array.from(document.querySelectorAll('nz-select')).find(e => (e.textContent||'').includes('省'));\n out.addrVisible = addr ? (addr.offsetParent !== null ? 'VISIBLE' : 'HIDDEN') : 'NOT FOUND';\n // 3) 单位性质禁用\n const unit = document.querySelector('#Applicant\\.CWorkDpt, [id=\"Applicant.CWorkDpt\"]');\n out.unitDisabled = unit ? (unit.getAttribute('ng-reflect-nz-disabled') || unit.classList.contains('ant-select-disabled') || '?') : 'NOT FOUND';\n // 4) 8 个展开链接的父级标题\n out.expands = Array.from(document.querySelectorAll('a.legrnd-ta')).map(a => {\n const leg = a.closest('legend');\n return (leg ? leg.textContent.trim().replace(/\\s+/g, ' ').slice(0, 30) : 'no-legend');\n });\n return JSON.stringify(out);\n})()"}]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{"action": "snapshot"},
|
||||
{"action": "click", "ref": 2, "note": "申请单录入"},
|
||||
{"action": "wait", "wait_seconds": 4},
|
||||
{"action": "snapshot", "max_elements": 40},
|
||||
{"action": "click", "ref": 4, "note": "承保机构放大镜"},
|
||||
{"action": "wait", "wait_seconds": 3},
|
||||
{"action": "snapshot", "max_elements": 120}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{"action": "goto", "url": "http://117.78.36.208:9099/"},
|
||||
{"action": "wait", "wait_seconds": 3},
|
||||
{"action": "snapshot"},
|
||||
{"action": "click", "ref": 2, "note": "申请单录入"},
|
||||
{"action": "wait", "wait_seconds": 4},
|
||||
{"action": "snapshot", "max_elements": 40},
|
||||
{"action": "click", "ref": 4, "note": "承保机构放大镜"},
|
||||
{"action": "wait", "wait_seconds": 3},
|
||||
{"action": "snapshot", "max_elements": 120}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 300},
|
||||
{"action": "click", "ref": 54, "note": "单位性质"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "click", "selector": "text=机关团体"}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 300},
|
||||
{"action": "eval_js", "js": "(() => { const nz = Array.from(document.querySelectorAll('nz-select')); const u = nz.find(e => (e.textContent||'').includes('单位性质') || (e.getAttribute('ng-reflect-name')||'').includes('Unit')); return '单位性质nz-select: ' + (u ? u.getAttribute('ng-reflect-name') : '未按名找到'); })()"}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 300},
|
||||
{"action": "click", "ref": 118, "note": "单位性质"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "click", "selector": "text=机关团体"}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 300},
|
||||
{"action": "click", "ref": 118, "note": "单位性质"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "click", "selector": "text=\"机关团体-社会团体\"", "note": "单位性质选项"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "eval_js", "js": "(() => { const nz = Array.from(document.querySelectorAll('nz-select')).filter(e => (e.textContent||'').includes('机关团体-社会团体')); return '选中: ' + nz.length + ' 个'; })()"}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 300},
|
||||
{"action": "click", "ref": 118, "note": "单位性质"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "click", "selector": "text=\"机关团体-社会团体\"", "note": "单位性质选项"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "eval_js", "js": "(() => { const nz = Array.from(document.querySelectorAll('nz-select')).filter(e => (e.textContent||'').includes('机关团体')); return '含机关团体 select 数: ' + nz.length; })()"}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 300},
|
||||
{"action": "click", "selector": "nz-select:has([ng-reflect-name]) >> text=\"机关团体\"", "note": "定位单位性质"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "eval_js", "js": "(() => { const lis = Array.from(document.querySelectorAll('.ant-select-dropdown-menu-item')).filter(e => e.offsetParent).map(e => e.textContent.trim().slice(0, 25)); return JSON.stringify(lis.slice(0, 8)); })()"}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 300},
|
||||
{"action": "click", "ref": 118, "note": "单位性质"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "click", "selector": "text=\"机关团体-社会团体\"", "note": "单位性质选项"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "eval_js", "js": "(() => { const nz = Array.from(document.querySelectorAll('nz-select')).filter(e => (e.textContent||'').includes('机关团体-社会团体')); return '选中数: ' + nz.length; })()"}
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 60},
|
||||
{"action": "fill", "ref": 2, "text": ""},
|
||||
{"action": "press", "key": "Enter"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "click", "selector": "text=\"北京分公司车险管理部\"", "note": "机构树节点"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "snapshot", "max_elements": 40},
|
||||
{"action": "click", "ref": 5, "note": "产品大类"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "type", "text": "意外险"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "selector": "text=\"意外险\"", "note": "产品大类选项"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "ref": 7, "note": "产品"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "type", "text": "060003"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "selector": "text=\"060003旅游意外伤害保险\"", "note": "产品选项"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "ref": 11, "note": "联共保业务"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "selector": "text=\"司内联保_主联\"", "note": "联共保选项"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "ref": 15, "note": "确认"},
|
||||
{"action": "wait", "wait_seconds": 6},
|
||||
{"action": "snapshot", "max_elements": 20}
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
[
|
||||
{"action": "snapshot", "max_elements": 40},
|
||||
{"action": "click", "ref": 4, "note": "承保机构放大镜"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "fill", "selector": "input[placeholder*=\"机构代码\"]", "text": ""},
|
||||
{"action": "press", "key": "Enter"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "click", "selector": "text=\"北京分公司车险管理部\"", "note": "机构树节点"},
|
||||
{"action": "wait", "wait_seconds": 2},
|
||||
{"action": "snapshot", "max_elements": 40},
|
||||
{"action": "click", "ref": 5, "note": "产品大类"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "type", "text": "意外险"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "selector": "text=\"意外险\"", "note": "产品大类选项"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "ref": 7, "note": "产品"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "type", "text": "060003"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "selector": "text=\"060003旅游意外伤害保险\"", "note": "产品选项"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "ref": 11, "note": "联共保业务"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "selector": "text=\"司内联保_主联\"", "note": "联共保选项"},
|
||||
{"action": "wait", "wait_seconds": 1},
|
||||
{"action": "click", "ref": 15, "note": "确认"},
|
||||
{"action": "wait", "wait_seconds": 6},
|
||||
{"action": "snapshot", "max_elements": 20}
|
||||
]
|
||||