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.
435 lines
24 KiB
435 lines
24 KiB
# -*- coding: utf-8 -*-
|
|
"""dict-alignment · demo 浏览器级 E2E v3(Playwright, chromium headless)
|
|
被测: .scratch/dict-alignment/demo/index.html?api=http://localhost:8080
|
|
覆盖: 原型 v3(20260909 票 11 拍板)——
|
|
① 键值字段删除:新增/编辑弹窗无键值输入、无默认勾选、复合弹窗子项行含编码列;
|
|
saveOrUpdate 载荷零 value / 零 isDefault(网络层断言);名称组内唯一 63007
|
|
② 默认值入分组列表:分组行「设置默认值」只列最低级项(两级组仅二级项),
|
|
换设唯一(同事务取消原默认),分组列表「默认值」列回显「父名 / 子名」
|
|
③ 最低级默认 63013 禁停两路(/status + saveOrUpdate)
|
|
沿承: v2 列表一级化+children 展开 / 换组放开+被引用拦截 63009 / 票07 ref_count 联测 / 票04 两级树
|
|
附带断言: 网络层零 PUT/DELETE、零 /api/dict/<digits>/ 路径变量、console/page error 零
|
|
清理: 演示分组/字典项 API 正门删除,演示客户归档
|
|
"""
|
|
import json, re, time, pathlib, sys, io
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parent
|
|
SHOTS = ROOT / "shots"; SHOTS.mkdir(exist_ok=True)
|
|
DEMO = ROOT / "demo" / "index.html"
|
|
API = "http://localhost:8080"
|
|
ADMIN = "739564171091247104"
|
|
TS = time.strftime("%H%M%S")
|
|
GA_CODE = f"e2d_group_a_{TS}"
|
|
GA_NAME = f"e2d-演示分组A-{TS}"
|
|
GB_CODE = f"e2d_group_b_{TS}"
|
|
GB_NAME = f"e2d-演示分组B-{TS}"
|
|
|
|
results = []
|
|
def step(name, ok, note=""):
|
|
results.append({"step": name, "ok": bool(ok), "note": note})
|
|
print(("PASS " if ok else "FAIL ") + name + (" | " + note if note else ""))
|
|
|
|
import urllib.request, urllib.parse
|
|
def api_post(url, data=None, token=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")
|
|
if token: req.add_header("Authorization", "Bearer " + token)
|
|
with urllib.request.urlopen(req, timeout=20) as r:
|
|
return json.loads(r.read().decode())
|
|
|
|
def _del_item(it, tok):
|
|
"""删字典项:被引用(残留归档客户)时普通删除 63009 → force-delete 兜底"""
|
|
try: api_post(f"{API}/api/dict/item/delete", {"id": str(it["id"])}, tok); return True
|
|
except Exception:
|
|
try: api_post(f"{API}/api/dict/item/force-delete?id=" + str(it["id"]), None, tok); return True
|
|
except Exception: return False
|
|
|
|
def pre_clean():
|
|
"""中断轮次残留清理(幂等重跑保障):上轮死在中途时演示分组/项/客户会污染本轮
|
|
(实例:残留分组B 里有同名项 → 本轮换组撞 63007 → 保存失败无 toast)"""
|
|
try: tok = json.loads(urllib.request.urlopen(f"{API}/api/auth/debug/token?userId={ADMIN}", timeout=10).read().decode())["data"]
|
|
except Exception as ex: print("pre_clean token fail:", ex); return
|
|
removed = {"items": 0, "groups": 0, "customers": 0}
|
|
for cur in (1, 2, 3, 4, 5):
|
|
pg = api_post(f"{API}/api/dict/item/page", {"current": cur, "size": 100}, tok)
|
|
for it in (pg.get("data") or {}).get("content", []):
|
|
if str(it.get("name", "")).startswith("e2d-") or str(it.get("code", "")).startswith("e2d_"):
|
|
if _del_item(it, tok): removed["items"] += 1
|
|
if cur >= int((pg.get("data") or {}).get("pages", 1) or 1): break
|
|
for kw in ("e2d_group_", "e2d_probe_"):
|
|
gp = api_post(f"{API}/api/dict/group/page", {"current": 1, "size": 50, "keyword": kw}, tok)
|
|
for g in (gp.get("data") or {}).get("content", []):
|
|
if not str(g.get("code", "")).startswith(("e2d_group_", "e2d_probe_")): continue
|
|
ip = api_post(f"{API}/api/dict/item/page", {"current": 1, "size": 200, "groupId": str(g["id"])}, tok)
|
|
for it in (ip.get("data") or {}).get("content", []): _del_item(it, tok)
|
|
try: api_post(f"{API}/api/dict/group/delete", {"id": str(g["id"])}, tok); removed["groups"] += 1
|
|
except Exception: pass
|
|
for ws in ("mine", "pool"):
|
|
found = api_post(f"{API}/api/customer/workspace/page", {"workspace": ws, "keyword": "e2d-引用联测", "current": 1, "size": 50}, tok)
|
|
for c in (found.get("data") or {}).get("content", []):
|
|
cid = str(c["id"])
|
|
try:
|
|
try: api_post(f"{API}/api/customer/claim?id=" + cid, {}, tok)
|
|
except Exception: pass
|
|
api_post(f"{API}/api/customer/archive", {"id": cid}, tok); removed["customers"] += 1
|
|
except Exception: pass
|
|
print("pre_clean:", removed)
|
|
|
|
pre_clean()
|
|
|
|
console_errors, page_errors, api_urls, save_bodies = [], [], [], []
|
|
|
|
def clear_toasts(page):
|
|
page.evaluate("document.getElementById('toast').innerHTML = ''")
|
|
|
|
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 ensure_expanded(page):
|
|
"""行内展开受异步渲染竞态影响可能被收起:子项行交互前确保展开态"""
|
|
if page.locator("#iBody tr.lv2row:has-text('e2d-子项1')").count() == 0:
|
|
page.click("#iBody .expander")
|
|
page.wait_for_selector("#iBody tr.lv2row:has-text('e2d-子项1')", timeout=10000)
|
|
|
|
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()) # demo 的 confirm(删除/换组)一律接受
|
|
def on_resp(r):
|
|
u = r.url
|
|
if "/api/dict" in u or "/api/customer" in u or "/api/auth" in u:
|
|
api_urls.append(f"{r.request.method} {u}")
|
|
if "/api/dict/item/saveOrUpdate" in u:
|
|
save_bodies.append(r.request.post_data or "")
|
|
page.on("response", on_resp)
|
|
|
|
# 0. 打开 demo
|
|
page.goto(DEMO.as_uri() + "?api=" + API)
|
|
page.wait_for_load_state("networkidle")
|
|
page.screenshot(path=str(SHOTS / "00-open.png"))
|
|
|
|
# 1. 登录
|
|
page.click("button:has-text('uid 登录')")
|
|
page.wait_for_selector(".toast-item:has-text('登录成功')", timeout=15000)
|
|
step("登录 debug/token + /auth/me", True)
|
|
|
|
# 2. 分组列表加载(含 v3 默认值列)
|
|
page.wait_for_selector("#gBody tr td:not(.empty)", timeout=15000)
|
|
g_rows = page.locator("#gBody tr").count()
|
|
has_def_col = page.locator("#tab-group th:has-text('默认值')").count() == 1
|
|
step("分组分页 group/page(含 v3 默认值列)", g_rows > 0 and has_def_col, f"rows={g_rows} 默认值列={has_def_col}")
|
|
|
|
# 3. 新建分组A(编码必填·全局唯一)
|
|
page.click("button:has-text('+ 新建分组')")
|
|
page.wait_for_selector("#fgName")
|
|
page.fill("#fgName", GA_NAME)
|
|
page.fill("#fgCode", GA_CODE)
|
|
page.fill("#fgDesc", "dict-alignment e2e 演示分组A")
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector(".toast-item:has-text('分组保存成功')", timeout=15000)
|
|
page.fill("#gKeyword", GA_CODE)
|
|
page.click("#tab-group .viewbar button:has-text('查询')")
|
|
page.wait_for_selector(f"#gBody tr:has-text('{GA_NAME}')", timeout=15000)
|
|
ga_row = page.locator(f"#gBody tr:has-text('{GA_NAME}')")
|
|
step("新建分组A saveOrUpdate", ga_row.count() == 1, f"{GA_CODE}")
|
|
page.screenshot(path=str(SHOTS / "01-group-created.png"))
|
|
ga_id = re.search(r"dlgGroup\('(\d+)'",
|
|
ga_row.locator("td.ops button:has-text('编辑')").get_attribute("onclick")).group(1)
|
|
|
|
# 4. 新建分组B(换组目标)
|
|
page.fill("#gKeyword", "")
|
|
page.click("button:has-text('+ 新建分组')")
|
|
page.wait_for_selector("#fgName")
|
|
page.fill("#fgName", GB_NAME)
|
|
page.fill("#fgCode", GB_CODE)
|
|
clear_toasts(page)
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector(".toast-item:has-text('分组保存成功')", timeout=15000)
|
|
step("新建分组B(换组目标)", True, GB_CODE)
|
|
|
|
# 5. 字典项:弹窗 v3 结构断言(无键值输入 / 无默认勾选 / 子项行含编码列)+ 名称空白 63001
|
|
page.click("nav.tabs button[data-tab='item']")
|
|
page.wait_for_selector(f"#iGroup option[value='{ga_id}']", state="attached", timeout=15000)
|
|
page.select_option("#iGroup", ga_id)
|
|
page.wait_for_timeout(400)
|
|
page.click("button:has-text('+ 新建字典项')")
|
|
page.wait_for_selector("#fiName")
|
|
no_value_input = page.locator("#fiValue").count() == 0
|
|
no_default_chk = page.locator("#fiDefault").count() == 0
|
|
kid_code_col = page.locator("#dlg th:has-text('子项编码')").count() == 1
|
|
page.select_option("#fiGroup", ga_id) # 显式钉分组A,防下拉默认值漂移
|
|
page.click(".dfoot button.ok") # 名称留空 → 63001
|
|
page.wait_for_selector("#fiOut .errbox", timeout=15000)
|
|
err1 = page.text_content("#fiOut .errbox")
|
|
ok1 = "63001" in err1 and "名称" in err1
|
|
step("弹窗 v3 结构(无键值/无默认勾选/子项编码列)", no_value_input and no_default_chk and kid_code_col,
|
|
f"键值输入={no_value_input} 默认勾选={no_default_chk} 子项编码列={kid_code_col}")
|
|
step("名称必填拦截 63001(键值已删,名称承接)", ok1, err1.strip()[:60])
|
|
page.screenshot(path=str(SHOTS / "02-dialog-v3-structure.png"))
|
|
|
|
# 6. 填名称保存 → 编码自动生成 dict_ 前缀(键值概念已不存在)
|
|
page.fill("#fiName", "e2d-项1")
|
|
clear_toasts(page)
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector(".toast-item:has-text('字典项保存成功')", timeout=15000)
|
|
page.wait_for_timeout(600)
|
|
row1 = page.locator(f"#iBody tr:has-text('e2d-项1')")
|
|
code1 = row1.locator("td").nth(2).text_content().strip()
|
|
auto_code = code1.startswith("dict_")
|
|
step("新建字典项(无键值)编码自动生成 dict_", row1.count() == 1 and auto_code, f"code={code1}")
|
|
|
|
# 7. 新建项2(自定义编码)
|
|
page.click("button:has-text('+ 新建字典项')")
|
|
page.wait_for_selector("#fiName")
|
|
page.select_option("#fiGroup", ga_id) # 显式钉分组A
|
|
page.fill("#fiName", "e2d-项2")
|
|
page.fill("#fiCode", f"e2d_item2_{TS}")
|
|
clear_toasts(page)
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector(".toast-item:has-text('字典项保存成功')", timeout=15000)
|
|
page.wait_for_timeout(600)
|
|
step("新建项2(自定义编码)", page.locator(f"#iBody tr:has-text('e2d-项2')").count() == 1)
|
|
|
|
# 8. 名称组内唯一(v3 Q1):再建同名 e2d-项1 → 63007
|
|
page.click("button:has-text('+ 新建字典项')")
|
|
page.wait_for_selector("#fiName")
|
|
page.select_option("#fiGroup", ga_id)
|
|
page.fill("#fiName", "e2d-项1")
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector("#fiOut .errbox", timeout=15000)
|
|
err_dup = page.text_content("#fiOut .errbox")
|
|
step("名称组内唯一拦截 63007(v3 Q1)", "63007" in err_dup and "名称" in err_dup, err_dup.strip()[:70])
|
|
page.click(".dfoot button:has-text('取消')")
|
|
|
|
# 9. 项1 复合编辑加子项(子项编码留空自动生成)→ GA 变两级
|
|
page.click(f"#iBody tr:has-text('e2d-项1') td.ops button:has-text('编辑')")
|
|
page.wait_for_selector("#kidsBody")
|
|
page.click("button:has-text('+ 添加二级字典项')")
|
|
page.fill("#kidsBody tr input", "e2d-子项1")
|
|
clear_toasts(page)
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector(".toast-item:has-text('字典项保存成功')", timeout=15000)
|
|
page.wait_for_timeout(600)
|
|
page.click("#iBody .expander") # 展开项1
|
|
page.wait_for_selector("#iBody tr.lv2row:has-text('e2d-子项1')", timeout=10000)
|
|
kid_row = page.locator("#iBody tr.lv2row:has-text('e2d-子项1')")
|
|
kid_code = kid_row.locator("td").nth(2).text_content().strip()
|
|
step("复合编辑加子项(编码留空自动生成)", kid_code.startswith("dict_"), f"子项code={kid_code}")
|
|
page.screenshot(path=str(SHOTS / "03-composite-edit-v3.png"))
|
|
|
|
# 10. 分组列表「设置默认值」:两级组只列二级项 → 选子项1 → 默认值列回显「父名 / 子名」
|
|
page.click("nav.tabs button[data-tab='group']")
|
|
page.fill("#gKeyword", GA_CODE)
|
|
page.click("#tab-group .viewbar button:has-text('查询')")
|
|
page.wait_for_selector(f"#gBody tr:has-text('{GA_NAME}')", timeout=15000)
|
|
page.click(f"#gBody tr:has-text('{GA_NAME}') td.ops button:has-text('设置默认值')")
|
|
page.wait_for_selector("input[name=sdpick]", timeout=10000)
|
|
dlg_title = page.text_content("#dlgTitle")
|
|
two_level_title = "只能选二级项" in dlg_title
|
|
radios = page.eval_on_selector_all("input[name=sdpick]", "os => os.map(o => o.value)")
|
|
item1_id = page.evaluate("""() => {
|
|
const l = [...document.querySelectorAll('#dlg label')].find(x => x.textContent.includes('e2d-子项1'));
|
|
return l ? l.querySelector('input').value : '';
|
|
}""")
|
|
page.check(f"input[name=sdpick][value='{item1_id}']")
|
|
clear_toasts(page)
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector(".toast-item:has-text('已设为分组默认值')", timeout=15000)
|
|
page.wait_for_timeout(600)
|
|
def_cell = page.locator(f"#gBody tr:has-text('{GA_NAME}') td .badge.b5").text_content().strip()
|
|
step("分组设默认(两级组仅二级项可选)", two_level_title and len(radios) == 1,
|
|
f"标题含两级提示={two_level_title} radio数={len(radios)}")
|
|
step("分组列表默认值列回显「父名 / 子名」", def_cell == "e2d-项1 / e2d-子项1", f"cell={def_cell}")
|
|
page.screenshot(path=str(SHOTS / "04-group-default-v3.png"))
|
|
|
|
# 11. 分组默认项禁停(默认=子项1):/status 路 63013
|
|
page.click("nav.tabs button[data-tab='item']")
|
|
page.wait_for_timeout(400)
|
|
page.select_option("#iGroup", opt_val(page, "#iGroup", re.escape(GA_NAME)))
|
|
page.wait_for_timeout(400)
|
|
page.click("#iBody .expander")
|
|
page.wait_for_selector("#iBody tr.lv2row:has-text('e2d-子项1')", timeout=10000)
|
|
ensure_expanded(page)
|
|
clear_toasts(page)
|
|
page.click("#iBody tr.lv2row:has-text('e2d-子项1') td.ops button:has-text('停用')")
|
|
page.wait_for_selector(".toast-item:has-text('63013')", timeout=15000)
|
|
t1 = page.text_content(".toast-item:has-text('63013')")
|
|
step("分组默认项禁停 /status 路 63013(拍板2①)", True, t1.strip()[:70])
|
|
|
|
# 12. 编辑弹窗改停用(saveOrUpdate 路)→ 63013
|
|
ensure_expanded(page)
|
|
page.click("#iBody tr.lv2row:has-text('e2d-子项1') td.ops button:has-text('编辑')")
|
|
page.wait_for_selector("#fiStatus")
|
|
page.select_option("#fiStatus", "0")
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector("#fiOut .errbox", timeout=15000)
|
|
err2 = page.text_content("#fiOut .errbox")
|
|
step("分组默认项禁停 saveOrUpdate 路 63013(拍板2②)", "63013" in err2 and "默认项" in err2, err2.strip()[:70])
|
|
page.screenshot(path=str(SHOTS / "05-default-disable-blocked.png"))
|
|
page.click(".dfoot button:has-text('取消')")
|
|
|
|
# 13. 换组放开(拍板6):项2 → 分组B(未被引用)
|
|
page.click(f"#iBody tr:has-text('e2d-项2') td.ops button:has-text('编辑')")
|
|
page.wait_for_selector("#fiGroup")
|
|
page.select_option("#fiGroup", opt_val(page, "#fiGroup", re.escape(GB_NAME)))
|
|
clear_toasts(page)
|
|
page.click(".dfoot button.ok") # confirm 换组 → dialog handler 接受
|
|
page.wait_for_selector(".toast-item:has-text('字典项保存成功')", timeout=15000)
|
|
page.wait_for_timeout(800)
|
|
page.select_option("#iGroup", opt_val(page, "#iGroup", re.escape(GB_NAME)))
|
|
page.wait_for_selector("#iBody tr:has-text('e2d-项2')", timeout=10000)
|
|
moved = page.locator("#iBody tr:has-text('e2d-项2')").count()
|
|
step("未被引用换组放行 + 计数行随迁(拍板6①)", moved == 1, f"项2已出现在分组B列表 rows={moved}")
|
|
page.screenshot(path=str(SHOTS / "06-move-group-ok.png"))
|
|
|
|
# 14. 两级树(票04 新端点,industry 分组有二级)
|
|
page.click("nav.tabs button[data-tab='tree']")
|
|
page.wait_for_selector("#tGroup option", state="attached")
|
|
ind_label = page.locator("#tGroup option", has_text="行业").first.get_attribute("value")
|
|
if not ind_label:
|
|
for i in range(page.locator("#tGroup option").count()):
|
|
o = page.locator("#tGroup option").nth(i)
|
|
if "industry" in (o.get_attribute("value") or "") or "行业" in o.text_content():
|
|
ind_label = o.get_attribute("value"); break
|
|
page.select_option("#tGroup", ind_label)
|
|
page.wait_for_selector("#treeBox .lv1", timeout=15000)
|
|
page.wait_for_timeout(800)
|
|
lv1 = page.locator("#treeBox .lv1").count()
|
|
lv2 = page.locator("#treeBox .lv2").count()
|
|
step("两级树 GET /api/dict/item/tree(票04增项)", lv1 > 0, f"一级={lv1}, 二级={lv2}")
|
|
page.screenshot(path=str(SHOTS / "07-tree.png"))
|
|
|
|
# 15. 引用计数联测:步骤0 准备演示项(customer_type 组,v3 载荷无 value)
|
|
page.click("nav.tabs button[data-tab='ref']")
|
|
page.evaluate("document.getElementById('refOut').innerHTML = ''")
|
|
page.click("button:has-text('准备演示字典项')")
|
|
page.wait_for_selector("#refOut .okbox:has-text('就绪')", timeout=20000)
|
|
step("联测步骤0 演示项幂等创建(v3 载荷)", True, page.text_content("#refOut .okbox").strip()[:80])
|
|
|
|
# 16. 步骤1 新建客户引用 → referenced=true(票07 入账)
|
|
page.evaluate("document.getElementById('refOut').innerHTML = ''")
|
|
page.click("button:has-text('新建客户引用此字典')")
|
|
page.wait_for_selector("#refOut .okbox, #refOut .errbox", timeout=30000)
|
|
out1 = page.text_content("#refOut")
|
|
ok_ref1 = "referenced=true" in out1 and "(接线生效)" in out1
|
|
step("联测步骤1 新建客户 ref_count 入账(票07)", ok_ref1, out1.strip()[:90])
|
|
page.screenshot(path=str(SHOTS / "08-ref-created.png"))
|
|
|
|
# 17. 步骤2 换引用 → a=false / b=true(票07 随迁)
|
|
page.evaluate("document.getElementById('refOut').innerHTML = ''")
|
|
page.click("button:has-text('编辑客户换引用到 e2d_type_b')")
|
|
page.wait_for_selector("#refOut .okbox, #refOut .errbox", timeout=30000)
|
|
out2 = page.text_content("#refOut")
|
|
ok_ref2 = "e2d_type_a.referenced=false" in out2 and "e2d_type_b.referenced=true" in out2
|
|
step("联测步骤2 换引用随迁(票07 applyFieldChange)", ok_ref2, out2.strip()[:110])
|
|
page.screenshot(path=str(SHOTS / "09-ref-swapped.png"))
|
|
|
|
# 18. 步骤3 删除被引用项 → 63009 阻断(票04/07 引用阻断生效)
|
|
page.evaluate("document.getElementById('refOut').innerHTML = ''")
|
|
page.click("button:has-text('尝试删除当前被引用项')")
|
|
page.wait_for_selector("#refOut .okbox, #refOut .errbox", timeout=20000)
|
|
out3 = page.text_content("#refOut")
|
|
ok_ref3 = "63009" in out3 and "阻断生效" in out3
|
|
step("联测步骤3 删除被引用项 63009 阻断", ok_ref3, out3.strip()[:90])
|
|
|
|
# 19. 名称被引用仍可改(键值已删,改名即业务回显语义):e2d_type_b 已被引用,改名放行
|
|
page.click("nav.tabs button[data-tab='item']")
|
|
page.wait_for_timeout(800)
|
|
page.select_option("#iGroup", "") # 清掉步骤13残留的分组B过滤
|
|
page.wait_for_timeout(400)
|
|
page.fill("#iKeyword", "e2d_type_b")
|
|
page.click("#tab-item .viewbar button:has-text('查询')")
|
|
row_b = page.locator("#iBody tr:has-text('e2d_type_b')")
|
|
for _ in range(3): # 长链路后偶发渲染竞态:查询重试
|
|
try:
|
|
row_b.wait_for(state="visible", timeout=8000); break
|
|
except Exception:
|
|
page.click("#tab-item .viewbar button:has-text('查询')")
|
|
page.click("#iBody tr:has-text('e2d_type_b') td.ops button:has-text('编辑')")
|
|
page.wait_for_selector("#fiName")
|
|
page.fill("#fiName", "e2d-演示类型B-已改名")
|
|
clear_toasts(page)
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector(".toast-item:has-text('字典项保存成功')", timeout=15000)
|
|
step("被引用项改名仍放行(v3:名称承接回显)", True, "e2d_type_b name→e2d-演示类型B-已改名")
|
|
page.screenshot(path=str(SHOTS / "10-name-mutable.png"))
|
|
|
|
# 20. 被引用项换组拦截 63009(拍板6②)——e2d_type_b 被引用,换组应拦
|
|
page.click("#iBody tr:has-text('e2d_type_b') td.ops button:has-text('编辑')")
|
|
page.wait_for_selector("#fiGroup")
|
|
page.select_option("#fiGroup", opt_val(page, "#fiGroup", re.escape(GA_NAME)))
|
|
page.click(".dfoot button.ok") # confirm 接受
|
|
page.wait_for_selector("#fiOut .errbox", timeout=15000)
|
|
err4 = page.text_content("#fiOut .errbox")
|
|
ok4 = "63009" in err4 and ("已被引用" in err4 or "不可修改分组" in err4)
|
|
step("被引用换组拦截 63009(拍板6②)", ok4, err4.strip()[:70])
|
|
page.screenshot(path=str(SHOTS / "11-move-blocked.png"))
|
|
page.click(".dfoot button:has-text('取消')")
|
|
|
|
# 21. 网络层断言:零 PUT/DELETE、零路径变量、saveOrUpdate 载荷零 value/isDefault
|
|
bad_method = [u for u in api_urls if u.startswith(("PUT ", "DELETE "))]
|
|
old_style = [u for u in api_urls if re.search(r"/api/dict/\d+/", u)]
|
|
bad_payload = [b for b in save_bodies if "value=" in b or "isDefault" in b]
|
|
step("网络层零旧契约形态 + saveOrUpdate 载荷零 value/isDefault",
|
|
not bad_method and not old_style and not bad_payload and len(save_bodies) >= 3,
|
|
f"bad_method={bad_method[:2]} old_style={old_style[:2]} bad_payload={bad_payload[:1]} save提交={len(save_bodies)}次")
|
|
browser.close()
|
|
|
|
# ---- API 清理:演示分组/项正门删除,演示客户归档 ----
|
|
def post(url, data=None, token=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")
|
|
if token: req.add_header("Authorization", "Bearer " + token)
|
|
with urllib.request.urlopen(req, timeout=20) as r:
|
|
return json.loads(r.read().decode())
|
|
|
|
cleanup = {"groups": [], "items": [], "customers": []}
|
|
try:
|
|
tok = json.loads(urllib.request.urlopen(f"{API}/api/auth/debug/token?userId={ADMIN}", timeout=10).read().decode())["data"]
|
|
for ws in ("mine", "pool"):
|
|
found = post(f"{API}/api/customer/workspace/page",
|
|
{"workspace": ws, "keyword": "e2d-引用联测", "current": 1, "size": 50}, tok)
|
|
for c in (found.get("data") or {}).get("content", []):
|
|
cid = str(c["id"])
|
|
if cid in cleanup["customers"]: continue
|
|
try:
|
|
try: post(f"{API}/api/customer/claim?id=" + cid, {}, tok)
|
|
except Exception: pass
|
|
post(f"{API}/api/customer/archive", {"id": cid}, tok)
|
|
cleanup["customers"].append(cid)
|
|
except Exception as ex: print("archive fail", cid, ex)
|
|
for gcode in (GB_CODE, GA_CODE):
|
|
gp = post(f"{API}/api/dict/group/page", {"current": 1, "size": 20, "keyword": gcode}, tok)
|
|
g = next((x for x in (gp.get("data") or {}).get("content", []) if x.get("code") == gcode), None)
|
|
if not g: continue
|
|
ip = post(f"{API}/api/dict/item/page", {"current": 1, "size": 100, "groupId": str(g["id"])}, tok)
|
|
for it in (ip.get("data") or {}).get("content", []):
|
|
try: post(f"{API}/api/dict/item/delete", {"id": str(it["id"])}, tok); cleanup["items"].append(it.get("code"))
|
|
except Exception as ex: print("item delete fail", it.get("code"), ex)
|
|
try: post(f"{API}/api/dict/group/delete", {"id": str(g["id"])}, tok); cleanup["groups"].append(gcode)
|
|
except Exception as ex: print("group delete fail", gcode, ex)
|
|
except Exception as ex:
|
|
print("cleanup error:", ex)
|
|
print("cleanup:", json.dumps(cleanup, ensure_ascii=False))
|
|
|
|
# ---- 汇总 ----
|
|
summary = {"ts": TS, "group_a": GA_CODE, "group_b": GB_CODE,
|
|
"console_errors": console_errors, "page_errors": page_errors,
|
|
"cleanup": cleanup, "results": results,
|
|
"all_pass": all(r["ok"] for r in results) and not console_errors and not page_errors}
|
|
(ROOT / "demo-smoke-result.json").write_text(json.dumps(summary, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
print("\n== SUMMARY ==")
|
|
for r in results:
|
|
print(("PASS " if r["ok"] else "FAIL ") + r["step"] + (" | " + r["note"] if r["note"] else ""))
|
|
print("console_errors:", len(console_errors), "page_errors:", len(page_errors))
|
|
print("ALL_PASS:", summary["all_pass"])
|
|
|