You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

153 lines
7.2 KiB

# -*- coding: utf-8 -*-
"""probe: demo 二级字典项录入链路(父项下拉填充 / 分组联动刷新 / 保存 / 两级树 / 编辑回显)"""
import json, re, time, pathlib, sys, io, urllib.request, urllib.parse
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
from playwright.sync_api import sync_playwright
ROOT = pathlib.Path(__file__).resolve().parent
DEMO = ROOT / "demo" / "index.html"
API = "http://localhost:8080"
TS = time.strftime("%H%M%S")
P_CODE, P_NAME = f"e2d_probe_p_{TS}", f"e2d-探针父组-{TS}"
Q_CODE, Q_NAME = f"e2d_probe_q_{TS}", f"e2d-探针空组-{TS}"
def step(n, ok, note=""):
print(("PASS " if ok else "FAIL ") + n + (" | " + note if note else ""))
return bool(ok)
tok = json.loads(urllib.request.urlopen(
f"{API}/api/auth/debug/token?userId=739564171091247104", timeout=10).read().decode())["data"]
def api_get(url):
req = urllib.request.Request(url)
req.add_header("Authorization", "Bearer " + tok)
with urllib.request.urlopen(req, timeout=20) as r:
return json.loads(r.read().decode())
def api_post(url, data=None):
body = urllib.parse.urlencode(data or {}).encode()
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
req.add_header("Authorization", "Bearer " + tok)
with urllib.request.urlopen(req, timeout=20) as r:
return json.loads(r.read().decode())
def opt_val(page, sel, pat):
pairs = page.eval_on_selector_all(sel + " option", "os => os.map(o => [o.value, o.textContent])")
for v, t in pairs:
if re.search(pat, t): return v
return ""
def del_item(iid):
try: api_post(f"{API}/api/dict/item/delete?id={iid}", {})
except Exception: api_post(f"{API}/api/dict/item/force-delete?id={iid}", {})
console_errors, page_errors = [], []
ok_all = True
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.set_default_timeout(20000)
page.on("console", lambda m: console_errors.append(m.text) if m.type == "error" else None)
page.on("pageerror", lambda e: page_errors.append(str(e)))
page.on("dialog", lambda d: d.accept())
page.goto(DEMO.as_uri() + "?api=" + API)
page.wait_for_load_state("networkidle")
page.click("button:has-text('uid 登录')")
page.wait_for_selector(".toast-item:has-text('登录成功')", timeout=15000)
# 建父组 P + 空组 Q
for code, name in ((P_CODE, P_NAME), (Q_CODE, Q_NAME)):
page.click("button:has-text('+ 新建分组')")
page.wait_for_selector("#fgName")
page.fill("#fgName", name); page.fill("#fgCode", code)
page.click(".dfoot button.ok")
page.wait_for_selector(".toast-item:has-text('分组保存成功')", timeout=15000)
page.wait_for_timeout(400)
# 字典项 tab,切到父组 P
page.click("nav.tabs button[data-tab='item']")
page.wait_for_selector("#iGroup option", state="attached", timeout=15000)
p_id = opt_val(page, "#iGroup", re.escape(P_NAME))
page.select_option("#iGroup", p_id)
page.wait_for_timeout(400)
# 弹窗1:组内尚无一级项 → 父项下拉只有「(无,作为一级项)」
page.click("button:has-text('+ 新建字典项')")
page.wait_for_selector("#fiName")
page.select_option("#fiGroup", p_id)
page.wait_for_timeout(300)
n0 = page.eval_on_selector_all("#fiParent option", "os => os.length")
ok_all &= step("空组新建弹窗父项下拉仅「无」", n0 == 1, f"options={n0}")
# 建一级项 P1(不选父项)
page.fill("#fiName", "e2d-父级P1"); page.fill("#fiValue", "pv1"); page.fill("#fiCode", "p1")
page.click(".dfoot button.ok")
page.wait_for_selector(".toast-item:has-text('字典项保存成功')", timeout=15000)
page.wait_for_timeout(600)
# 弹窗2:父项下拉已填充同组一级项 P1 → 选中 → 建二级项 C1
page.click("button:has-text('+ 新建字典项')")
page.wait_for_selector("#fiName")
page.select_option("#fiGroup", p_id)
page.wait_for_timeout(300)
par_val = opt_val(page, "#fiParent", "e2d-父级P1")
ok_all &= step("新建弹窗父项下拉填充同组一级项", bool(par_val), f"parent option id={par_val}")
page.select_option("#fiParent", par_val)
page.fill("#fiName", "e2d-子级C1"); page.fill("#fiValue", "cv1")
page.click(".dfoot button.ok")
page.wait_for_selector(".toast-item:has-text('字典项保存成功')", timeout=15000)
ok_all &= step("二级项保存(parentId=同组一级项)", True, f"parent_id={par_val}")
page.wait_for_timeout(600)
# 两级树端点断言:顶层 P1 节点 children 含 C1
resp = api_get(f"{API}/api/dict/item/tree?groupCode={P_CODE}")
nodes = resp.get("data") or []
p1node = next((n for n in nodes if n.get("code") == "p1"), None)
kids = (p1node or {}).get("children") or []
hit = any(c.get("name") == "e2d-子级C1" and str(c.get("parentId")) == str(par_val) for c in kids)
ok_all &= step("两级树端点 children 含二级项", hit,
f"顶层{len(nodes)}个 / P1子项{len(kids)}")
# 联动:弹窗内切换分组 → 父项下拉刷新(空组 Q 仅剩「无」,切回 P 恢复 P1)
page.click("button:has-text('+ 新建字典项')")
page.wait_for_selector("#fiName")
page.select_option("#fiGroup", p_id)
page.wait_for_timeout(200)
q_id = opt_val(page, "#fiGroup", re.escape(Q_NAME))
page.select_option("#fiGroup", q_id)
page.wait_for_timeout(400)
nq = page.eval_on_selector_all("#fiParent option", "os => os.length")
ok_all &= step("切换到空组后父项下拉联动刷新", nq == 1, f"Q组options={nq}")
page.select_option("#fiGroup", p_id)
page.wait_for_timeout(400)
back = opt_val(page, "#fiParent", "e2d-父级P1")
ok_all &= step("切回原组父项下拉恢复一级项", bool(back), "")
page.click(".dfoot button:has-text('取消')")
page.wait_for_timeout(300)
# 编辑二级项 C1:父项回显选中 P1
c_row = page.locator("#iBody tr:has-text('e2d-子级C1')")
c_id = re.search(r"dlgItem\('(\d+)'",
c_row.locator("td.ops button:has-text('编辑')").get_attribute("onclick")).group(1)
page.click(f"#iBody button[onclick*=\"dlgItem('{c_id}')\"]")
page.wait_for_selector("#fiName")
page.wait_for_selector(f"#fiParent option[value='{par_val}']", state="attached", timeout=10000)
sel_val = page.eval_on_selector("#fiParent", "e => e.value")
ok_all &= step("编辑二级项父项回显选中", sel_val == par_val, f"selected={sel_val}")
page.click(".dfoot button:has-text('取消')")
page.wait_for_timeout(300)
# 清理:删 C1 → P1 → 组 P/Q
del_item(c_id)
p1_id = par_val
del_item(p1_id)
for gid in (p_id, q_id):
try: api_post(f"{API}/api/dict/group/delete?id={gid}", {})
except Exception as ex: print("组删除失败:", gid, ex)
page.screenshot(path=str(ROOT / "shots" / f"probe-parent-{TS}.png"))
browser.close()
ok_all &= (len(console_errors) == 0 and len(page_errors) == 0)
step("console/page error 零", len(console_errors) == 0 and len(page_errors) == 0,
f"console={console_errors[:2]} page={page_errors[:2]}")
print("ALL_PASS =", ok_all)