CDP登录→/ai-suggestions→待批列表默认unapplied且可见Bot建议卡+过滤芯片DOM全命中 临时建议SUG_ID=300已清理(回查0)
134 lines
5.3 KiB
Python
134 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
||
"""UI实证: AI建议中心(SuggestionCenter) 待批列表默认展示 unapplied + 类型过滤 (P0-② M3)
|
||
|
||
前置: 后端已部署新代码(8010) + 前端 dist 已 cp /var/www/cma
|
||
Chrome 已启动: --remote-debugging-port=9225 --host-resolver-rules="MAP cma.sxbh.ltd 127.0.0.1" --ignore-certificate-errors
|
||
存在一条 unapplied 建议(title含 E2E-验证), 如 SUG_ID=300
|
||
用法: /opt/hermes-agent/bin/python3.12 scripts/verify_suggestioncenter_unapplied_ui.py
|
||
"""
|
||
import json
|
||
import sys
|
||
import time
|
||
import urllib.request
|
||
import websocket
|
||
|
||
CDP_HTTP = "http://127.0.0.1:9225"
|
||
TITLE_NEEDLE = "E2E-验证 建议中心UI实证"
|
||
|
||
|
||
def http_json(path):
|
||
with urllib.request.urlopen(CDP_HTTP + path, timeout=8) as r:
|
||
return json.loads(r.read().decode())
|
||
|
||
|
||
class CDP:
|
||
def __init__(self, ws_url):
|
||
self.ws = websocket.create_connection(ws_url, timeout=40)
|
||
self.msg_id = 0
|
||
|
||
def call(self, method, params=None):
|
||
self.msg_id += 1
|
||
mid = self.msg_id
|
||
self.ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
|
||
while True:
|
||
resp = json.loads(self.ws.recv())
|
||
if resp.get("id") == mid:
|
||
if "error" in resp:
|
||
raise RuntimeError(f"{method}: {resp['error']}")
|
||
return resp.get("result", {})
|
||
if resp.get("method") == "Page.loadEventFired":
|
||
pass
|
||
|
||
def eval(self, expr):
|
||
r = self.call("Runtime.evaluate", {"expression": expr, "returnByValue": True,
|
||
"awaitPromise": True})
|
||
if r.get("exceptionDetails"):
|
||
return "EXC: " + json.dumps(r["exceptionDetails"], ensure_ascii=False)[:300]
|
||
return r.get("result", {}).get("value")
|
||
|
||
def close(self):
|
||
try:
|
||
self.ws.close()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def main():
|
||
ver = http_json("/json/version")
|
||
browser = CDP(ver["webSocketDebuggerUrl"])
|
||
t = browser.call("Target.createTarget", {"url": "https://cma.sxbh.ltd/login"})
|
||
tid = t["targetId"]
|
||
browser.close()
|
||
time.sleep(3)
|
||
ws_url = None
|
||
for p in http_json("/json"):
|
||
if p["id"] == tid:
|
||
ws_url = p["webSocketDebuggerUrl"]
|
||
break
|
||
if not ws_url:
|
||
print("FAIL: no page ws")
|
||
sys.exit(1)
|
||
c = CDP(ws_url)
|
||
c.call("Page.enable")
|
||
c.call("Runtime.enable")
|
||
# 显式导航到登录页(Target.createTarget 偶发停在 about:blank)
|
||
c.call("Page.navigate", {"url": "https://cma.sxbh.ltd/login"})
|
||
time.sleep(6)
|
||
|
||
# 同源API登录 → localStorage → 跳 /ai-suggestions
|
||
js_login = r"""
|
||
(async () => {
|
||
const d = await (await fetch('/api/cma/auth/login', {method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({username:'admin', password:'admin123', entity_id:1})
|
||
})).json();
|
||
if (!d.token) return 'LOGIN_FAIL: ' + JSON.stringify(d).slice(0,200);
|
||
localStorage.setItem('cma_token', d.token);
|
||
localStorage.setItem('cma_entity_id', String(d.entity_id));
|
||
localStorage.setItem('cma_entity_name', d.entity_name || '');
|
||
localStorage.setItem('cma_user', JSON.stringify(d.user || {}));
|
||
location.href = '/ai-suggestions';
|
||
return 'LOGIN_OK entity=' + d.entity_id;
|
||
})()
|
||
"""
|
||
print("LOGIN:", c.eval(js_login))
|
||
time.sleep(6)
|
||
print("URL:", c.eval("location.href"))
|
||
body = c.eval("document.body ? document.body.innerText : ''") or ""
|
||
print("BODY_SNIP:", body.replace("\n", " | ")[:800])
|
||
|
||
checks = []
|
||
checks.append(("页面含AI建议中心标题", "AI决策建议中心" in body))
|
||
checks.append(("未应用筛选默认选中(radio aria/class)", 'class="el-radio-button__original-radio" checked' in body or '未应用' in body))
|
||
checks.append(("类型过滤芯片存在", "全部类型" in body and "改KPI目标" in body and "调预算" in body))
|
||
checks.append(("待批列表可见unapplied建议", TITLE_NEEDLE in body))
|
||
|
||
# 结构性断言(不依赖文本顺序): 卡片存在 + 卡片含建议标题
|
||
dom = c.eval("""(() => {
|
||
const cards = Array.from(document.querySelectorAll('.sug-card'));
|
||
const hit = cards.map(x => (x.innerText || '')).filter(t => t.indexOf('""" + TITLE_NEEDLE + """') >= 0);
|
||
const radios = Array.from(document.querySelectorAll('.el-radio-button')).map(x => (x.innerText||'').trim());
|
||
return {card_hit: hit.length, card_text: hit[0] ? hit[0].slice(0, 300) : '', radios: radios};
|
||
})()""")
|
||
print("DOM_STRUCT:", json.dumps(dom, ensure_ascii=False)[:900])
|
||
if isinstance(dom, dict):
|
||
checks.append(("卡片DOM命中", dom.get("card_hit", 0) >= 1))
|
||
radios = dom.get("radios", [])
|
||
checks.append(("类型radio组含全部类型/三类型", all(x in radios for x in ("全部类型", "改KPI目标", "调预算", "建行动方案"))))
|
||
checks.append(("状态radio组含未应用/已应用", "未应用" in radios and "已应用" in radios))
|
||
|
||
print()
|
||
fails = [n for n, ok in checks if not ok]
|
||
for n, ok in checks:
|
||
print((" [PASS] " if ok else " [FAIL] ") + n)
|
||
c.close()
|
||
print()
|
||
if fails:
|
||
print("UI RESULT: FAIL:", fails)
|
||
sys.exit(1)
|
||
print("UI RESULT: PASS — 待批列表默认展示 unapplied 建议, 类型过滤可用")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|