# -*- coding: utf-8 -*- """probe: 字典项「设为分组默认项」链路(弹窗勾选 / 新建反查联动 / 同组原默认自动取消 / 编辑回显 / 列表独立列)""" 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_d_{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_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 page_items(gid): r = api_post(f"{API}/api/dict/item/page", {"current": 1, "size": 500, "groupId": gid}) return (r.get("data") or {}).get("content", []) 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 "" 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) # 建组 page.click("button:has-text('+ 新建分组')") page.wait_for_selector("#fgName") page.fill("#fgName", P_NAME); page.fill("#fgCode", P_CODE) page.click(".dfoot button.ok") page.wait_for_selector(".toast-item:has-text('分组保存成功')", timeout=15000) page.wait_for_timeout(400) 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) def create_item(name, value, set_default): page.click("button:has-text('+ 新建字典项')") page.wait_for_selector("#fiName") page.select_option("#fiGroup", p_id) page.wait_for_timeout(300) if set_default: page.check("#fiDefault") page.fill("#fiName", name); page.fill("#fiValue", value) page.evaluate("document.getElementById('toast').innerHTML = ''") page.click(".dfoot button.ok") page.wait_for_selector(".toast-item:has-text('字典项保存成功')", timeout=15000) page.wait_for_timeout(700) # 1. 新建 A1 勾选默认(新建态:saveOrUpdate 返回 Void → groupId+键值反查联动 set-default) create_item("e2d-默认A1", "av1", True) rows = page_items(p_id) a1 = next((x for x in rows if x.get("value") == "av1"), {}) ok_all &= step("新建勾默认 → 反查联动 set-default", a1.get("isDefault") is True, f"A1.isDefault={a1.get('isDefault')}") # 2. 新建 A2 勾默认 → 同组原默认自动取消 create_item("e2d-默认A2", "av2", True) rows = page_items(p_id) a1 = next((x for x in rows if x.get("value") == "av1"), {}) a2 = next((x for x in rows if x.get("value") == "av2"), {}) ok_all &= step("同组新默认生效且原默认自动取消", a2.get("isDefault") is True and a1.get("isDefault") is False, f"A1={a1.get('isDefault')} A2={a2.get('isDefault')}") # 3. 编辑当前默认项 A2:checkbox 回显 checked+disabled a2_id = str(a2.get("id")) page.click(f"#iBody button[onclick*=\"dlgItem('{a2_id}')\"]") page.wait_for_selector("#fiDefault") page.wait_for_timeout(400) ck = page.eval_on_selector("#fiDefault", "e => ({c: e.checked, d: e.disabled})") ok_all &= step("编辑默认项勾选框回显 checked+disabled", ck["c"] is True and ck["d"] is True, str(ck)) page.click(".dfoot button:has-text('取消')") page.wait_for_timeout(300) # 4. 编辑非默认项 A1:可勾 → 勾上保存 → 默认切换 a1_id = str(a1.get("id")) page.click(f"#iBody button[onclick*=\"dlgItem('{a1_id}')\"]") page.wait_for_selector("#fiDefault") page.wait_for_timeout(400) ck = page.eval_on_selector("#fiDefault", "e => ({c: e.checked, d: e.disabled})") ok_all &= step("编辑非默认项勾选框可选", ck["c"] is False and ck["d"] is False, str(ck)) page.check("#fiDefault") page.evaluate("document.getElementById('toast').innerHTML = ''") page.click(".dfoot button.ok") page.wait_for_selector(".toast-item:has-text('字典项保存成功')", timeout=15000) page.wait_for_timeout(700) rows = page_items(p_id) a1 = next((x for x in rows if x.get("value") == "av1"), {}) a2 = next((x for x in rows if x.get("value") == "av2"), {}) ok_all &= step("编辑态勾默认保存 → 默认切换(A1是/A2否)", a1.get("isDefault") is True and a2.get("isDefault") is False, f"A1={a1.get('isDefault')} A2={a2.get('isDefault')}") # 5. 列表独立「默认」列渲染 page.wait_for_selector(f"#iBody tr:has-text('e2d-默认A1') .badge.b5:has-text('默认')", timeout=10000) a2_no = page.locator("#iBody tr:has-text('e2d-默认A2')").locator("td span.mark").filter(has_text="否").count() ok_all &= step("列表独立「默认」列(badge/否)", a2_no >= 1, f"A2行否标记={a2_no}") # 清理 for iid in (a1_id, a2_id): try: api_post(f"{API}/api/dict/item/delete?id={iid}", {}) except Exception: api_post(f"{API}/api/dict/item/force-delete?id={iid}", {}) try: api_post(f"{API}/api/dict/group/delete?id={p_id}", {}) except Exception as ex: print("组删除失败:", ex) page.screenshot(path=str(ROOT / "shots" / f"probe-default-{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)