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.
207 lines
11 KiB
207 lines
11 KiB
|
19 hours ago
|
# -*- coding: utf-8 -*-
|
||
|
|
"""票 10 demo 契约同步 · 浏览器级主流程点通(Playwright, chromium headless)
|
||
|
|
被测: .scratch/customer-e2e/demo/index.html?api=http://localhost:8080
|
||
|
|
流程: 登录→列表→汇总卡→新建→编辑(CAS)→详情页签→成员加/移除→导入预检→交割预览
|
||
|
|
附带断言: 网络层零 PUT/DELETE、零路径变量形态 /api/customer/<digits>/、console/page error 零
|
||
|
|
"""
|
||
|
|
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(r"e:\code\crm-backend-matt\.scratch\customer-e2e")
|
||
|
|
SHOTS = ROOT / "shots10"; SHOTS.mkdir(exist_ok=True)
|
||
|
|
DEMO = ROOT / "demo" / "index.html"
|
||
|
|
XLSX = ROOT / "_import-heavy.xlsx"
|
||
|
|
ADMIN = "739564171091247104"
|
||
|
|
BUDDY = "744842318024015872"
|
||
|
|
FULL_ID = "750844477165273088" # e2c-全字段-科技(有联系人)
|
||
|
|
MEMBER_ID = "750844483406397440" # e2c-成员-样例
|
||
|
|
TS = time.strftime("%H%M%S")
|
||
|
|
NEW_NAME = f"e2c-demo-浏览器-{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 ""))
|
||
|
|
|
||
|
|
console_errors, page_errors, api_urls = [], [], []
|
||
|
|
|
||
|
|
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)))
|
||
|
|
def on_resp(r):
|
||
|
|
u = r.url
|
||
|
|
if "/api/customer" in u or "/api/rule" in u:
|
||
|
|
api_urls.append(f"{r.request.method} {u}")
|
||
|
|
page.on("response", on_resp)
|
||
|
|
|
||
|
|
# 0. 打开 demo(?api= 注入本地后端)
|
||
|
|
page.goto(DEMO.as_uri() + "?api=http://localhost:8080")
|
||
|
|
page.wait_for_load_state("networkidle")
|
||
|
|
page.screenshot(path=str(SHOTS / "00-open.png"))
|
||
|
|
|
||
|
|
# 1. 登录(uid 选择器默认 admin)
|
||
|
|
page.click("button:has-text('uid 登录')")
|
||
|
|
page.wait_for_selector(".toast-item:has-text('登录成功')")
|
||
|
|
step("登录 debug/token + /auth/me", True)
|
||
|
|
|
||
|
|
# 2. 列表(新契约 workspace 表单字段)
|
||
|
|
page.wait_for_selector("#listBody tr td:not(.empty)", timeout=15000)
|
||
|
|
rows = page.locator("#listBody tr").count()
|
||
|
|
info = page.text_content("#pageInfo").strip()
|
||
|
|
step("列表 workspace/page", rows > 0 and "第" in info, f"rows={rows}, {info}")
|
||
|
|
|
||
|
|
# 3. 汇总卡
|
||
|
|
page.wait_for_selector("#summaryBox .card", timeout=15000)
|
||
|
|
cards = page.locator("#summaryBox .card").count()
|
||
|
|
step("汇总卡 workspace/board/summary", cards > 0, f"cards={cards}")
|
||
|
|
|
||
|
|
# 4. 新建(/create)
|
||
|
|
page.click("button:has-text('+ 新建客户')")
|
||
|
|
page.wait_for_selector("#cName")
|
||
|
|
page.fill("#cName", NEW_NAME)
|
||
|
|
page.fill("#cType", "customer_type_01")
|
||
|
|
page.fill("#cProv", "440000"); page.fill("#cCity", "440100"); page.fill("#cDist", "440103")
|
||
|
|
page.fill("#cInd", "gov")
|
||
|
|
page.fill("#cOwner", ADMIN)
|
||
|
|
page.click(".dfoot button.ok")
|
||
|
|
confirm_needed = False
|
||
|
|
try:
|
||
|
|
page.wait_for_selector(".toast-item:has-text('创建成功')", timeout=6000)
|
||
|
|
except Exception:
|
||
|
|
confirm_needed = True # L2 相似命中(同前缀历史数据)→ demo 三层查重确认流
|
||
|
|
page.wait_for_selector("#cOut .simbox", timeout=8000)
|
||
|
|
page.click("#cOut button:has-text('确认创建')")
|
||
|
|
page.wait_for_selector(".toast-item:has-text('创建成功')", timeout=15000)
|
||
|
|
toast = page.text_content(".toast-item:has-text('创建成功')").strip()
|
||
|
|
m = re.search(r"创建成功 id=(\d+)", toast)
|
||
|
|
new_id = m.group(1) if m else ""
|
||
|
|
page.wait_for_selector("dialog#dlg:not([open])", state="attached")
|
||
|
|
step("新建客户 create", bool(new_id), f"id={new_id}, confirmSimilar={'是' if confirm_needed else '否'}")
|
||
|
|
page.screenshot(path=str(SHOTS / "01-created.png"))
|
||
|
|
|
||
|
|
# 5. 编辑(GET detail?id= → POST edit?id= 带 version/isBizNegotiated/isChild)
|
||
|
|
page.click(f"#listBody td.row:has-text('{NEW_NAME}')") # 从列表行打开详情
|
||
|
|
page.wait_for_selector("#headBox b", timeout=15000) # loadHead 完成
|
||
|
|
page.click("#headBox button:has-text('编辑')")
|
||
|
|
page.wait_for_selector("#eName")
|
||
|
|
page.click(".dfoot button.ok")
|
||
|
|
page.wait_for_selector(".toast-item:has-text('保存成功')", timeout=15000)
|
||
|
|
step("编辑客户 detail?id= + edit?id=(CAS)", True, f"customer={NEW_NAME}")
|
||
|
|
page.screenshot(path=str(SHOTS / "02-edited.png"))
|
||
|
|
|
||
|
|
# 6. 详情页签(seed 全字段客户:contacts/follow/opps/members/oplog 全点一遍)
|
||
|
|
page.reload(); page.wait_for_load_state("networkidle") # loadHead 会重渲染 headBox,reload 恢复 didInput
|
||
|
|
page.click("nav.tabs button[data-tab='detail']") # reload 后默认列表 tab,先切详情让 didInput 可见
|
||
|
|
page.fill("#didInput", FULL_ID)
|
||
|
|
page.click("button:has-text('打开')")
|
||
|
|
page.wait_for_selector("#headBox b", timeout=15000)
|
||
|
|
sub_ok, sub_note = True, []
|
||
|
|
for s in ["contacts", "follow", "opps", "members", "oplog"]:
|
||
|
|
page.click(f"#detailPanel .subtabs button[data-s='{s}']")
|
||
|
|
page.wait_for_timeout(700)
|
||
|
|
if page.locator("#subBox .errbox").count():
|
||
|
|
sub_ok = False
|
||
|
|
sub_note.append(page.text_content("#subBox .errbox").strip()[:80])
|
||
|
|
step("详情页签五页签无错误", sub_ok, "; ".join(sub_note))
|
||
|
|
page.click("#detailPanel .subtabs button[data-s='contacts']")
|
||
|
|
page.wait_for_selector("#subBox table", timeout=15000)
|
||
|
|
contact_rows = page.locator("#subBox table tbody tr").count()
|
||
|
|
step("联系人列表 contacts?customerId=", contact_rows > 0, f"rows={contact_rows}")
|
||
|
|
page.screenshot(path=str(SHOTS / "03-detail-tabs.png"))
|
||
|
|
|
||
|
|
# 7. 成员加/移除(member/add?id= 数组表单 → member/remove?id=&memberUserId=)
|
||
|
|
page.reload(); page.wait_for_load_state("networkidle")
|
||
|
|
page.click("nav.tabs button[data-tab='detail']")
|
||
|
|
page.fill("#didInput", MEMBER_ID)
|
||
|
|
page.click("button:has-text('打开')")
|
||
|
|
page.wait_for_selector("#headBox b", timeout=15000)
|
||
|
|
page.click("#detailPanel .subtabs button[data-s='members']")
|
||
|
|
page.wait_for_timeout(700)
|
||
|
|
# 防御:上一轮残留的协同人先移除(member/remove 软删后可重复加入;重复添加会被两阶段校验整批拒绝)
|
||
|
|
if page.locator("#subBox tr:has-text('曾偲青') button:has-text('移除')").count():
|
||
|
|
page.click("#subBox tr:has-text('曾偲青') button:has-text('移除')")
|
||
|
|
page.wait_for_selector(".toast-item:has-text('已移除')", timeout=10000)
|
||
|
|
page.wait_for_timeout(700)
|
||
|
|
before = page.locator("#subBox table tbody tr").count()
|
||
|
|
page.click("#subBox button:has-text('+ 添加协同人')")
|
||
|
|
page.wait_for_selector("#mIds")
|
||
|
|
page.fill("#mIds", BUDDY)
|
||
|
|
page.click(".dfoot button.ok")
|
||
|
|
page.wait_for_selector(".toast-item:has-text('成员已添加')", timeout=15000)
|
||
|
|
page.wait_for_selector(f"#subBox:has-text('曾偲青')", timeout=15000)
|
||
|
|
page.click("#subBox tr:has-text('曾偲青') button:has-text('移除')")
|
||
|
|
page.wait_for_selector(".toast-item:has-text('已移除')", timeout=15000)
|
||
|
|
page.wait_for_timeout(700)
|
||
|
|
after = page.locator("#subBox table tbody tr").count()
|
||
|
|
step("成员加/移除 member/add+remove", True, f"rows {before}->{page.locator('#subBox table tbody tr').count()} (加后={before+1})")
|
||
|
|
page.screenshot(path=str(SHOTS / "04-members.png"))
|
||
|
|
|
||
|
|
# 8. 导入(page 列表 + upload 预检,不 confirm)
|
||
|
|
page.click("nav.tabs button[data-tab='import']")
|
||
|
|
page.wait_for_timeout(800)
|
||
|
|
page.set_input_files("#impFile", str(XLSX))
|
||
|
|
page.click("button:has-text('上传预检')")
|
||
|
|
page.wait_for_selector(".toast-item:has-text('预检完成')", timeout=30000)
|
||
|
|
toast = page.text_content(".toast-item:has-text('预检完成')").strip()
|
||
|
|
step("导入 upload 预检 + import/page", True, toast[:110])
|
||
|
|
page.screenshot(path=str(SHOTS / "05-import.png"))
|
||
|
|
|
||
|
|
# 9. 交割(page 列表 + preview)
|
||
|
|
page.click("nav.tabs button[data-tab='transfer']")
|
||
|
|
page.wait_for_timeout(900)
|
||
|
|
tr_rows = page.locator("#trBody tr").count()
|
||
|
|
has_err = page.locator("#trBody .errbox").count()
|
||
|
|
page.click("button:has-text('预览名下待交接客户')")
|
||
|
|
page.wait_for_selector("dialog#dlg[open]", timeout=15000)
|
||
|
|
page.wait_for_timeout(600)
|
||
|
|
prev_fail = page.locator(".toast-item:has-text('预览失败')").count()
|
||
|
|
prev_rows = page.locator("dialog#dlg table tbody tr").count()
|
||
|
|
page.keyboard.press("Escape")
|
||
|
|
step("交割 transfer/page + preview", tr_rows >= 0 and has_err == 0 and prev_fail == 0,
|
||
|
|
f"交接单行={tr_rows}, 预览行={prev_rows}, 预览失败toast={prev_fail}")
|
||
|
|
page.screenshot(path=str(SHOTS / "06-transfer.png"))
|
||
|
|
|
||
|
|
# 10. 网络层断言:新契约形态(零 PUT/DELETE、零 /api/customer/<digits>/ 路径变量)
|
||
|
|
old_style = [u for u in api_urls if re.search(r"/api/customer/\d+/", u)]
|
||
|
|
bad_method = [u for u in api_urls if u.startswith(("PUT ", "DELETE "))]
|
||
|
|
step("网络层零旧契约形态", not old_style and not bad_method,
|
||
|
|
f"old_style={old_style[:3]} bad_method={bad_method[:3]} (共{len(api_urls)}个请求)")
|
||
|
|
browser.close()
|
||
|
|
|
||
|
|
# ---- 正门清理:归档本次 + 上次孤儿(e2c-demo-浏览器* 前缀,admin 名下) ----
|
||
|
|
import urllib.request, urllib.parse
|
||
|
|
def post(url, data, token=None):
|
||
|
|
req = urllib.request.Request(url, data=urllib.parse.urlencode(data).encode(), 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=15) as r:
|
||
|
|
return json.loads(r.read().decode())
|
||
|
|
tok = json.loads(urllib.request.urlopen(f"http://localhost:8080/api/auth/debug/token?userId={ADMIN}", timeout=10).read().decode())["data"]
|
||
|
|
found = post("http://localhost:8080/api/customer/workspace/page",
|
||
|
|
{"workspace": "mine", "keyword": "e2c-demo-浏览器", "archiveStatus": 1, "current": 1, "size": 50}, tok)
|
||
|
|
ids = [str(r["id"]) for r in (found.get("data") or {}).get("content", [])]
|
||
|
|
archived = []
|
||
|
|
for cid in ids:
|
||
|
|
try:
|
||
|
|
post("http://localhost:8080/api/customer/archive", {"id": cid}, tok)
|
||
|
|
archived.append(cid)
|
||
|
|
except Exception as ex:
|
||
|
|
print("archive fail", cid, ex)
|
||
|
|
print("cleanup archived:", archived)
|
||
|
|
|
||
|
|
# 汇总
|
||
|
|
summary = {"ts": TS, "new_customer": NEW_NAME, "new_id": new_id if 'new_id' in dir() else "",
|
||
|
|
"console_errors": console_errors, "page_errors": page_errors,
|
||
|
|
"results": results,
|
||
|
|
"all_pass": all(r["ok"] for r in results) and not console_errors and not page_errors}
|
||
|
|
(ROOT / "demo-smoke10-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"])
|