"""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()