Files
dealerhub/backend/scripts/verify_salesman_h5.py
T

213 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""D2 业务员移动开单 H5 全流程验证(Chrome 移动模拟)。
跑法: python scripts/verify_salesman_h5.py
前置: 后端 dev 在 127.0.0.1:18099, vite(verify配置) 在 127.0.0.1:15173
流程: 登录(alice/default) → 开单tab(客户/仓库/商品/单位/取价/数量/抹零/保存过账)
→ 欠款tab(未结单+余额) → 收款tab(登记收款单) → 断言后端落库
"""
import asyncio
import json
import sys
import urllib.request
BASE = "http://127.0.0.1:18099"
PAGE = "http://127.0.0.1:15173/static/#/salesman"
def api(method, path, token=None, tenant="default", body=None):
req = urllib.request.Request(
BASE + path,
data=json.dumps(body).encode() if body is not None else None,
method=method,
headers={"Content-Type": "application/json"},
)
if token:
req.add_header("Authorization", f"Bearer {token}")
req.add_header("X-Tenant-Id", tenant)
try:
with urllib.request.urlopen(req, timeout=20) as r:
return r.status, json.loads(r.read() or b"{}")
except urllib.error.HTTPError as e:
try:
return e.code, json.loads(e.read() or b"{}")
except Exception:
return e.code, {}
async def main():
from playwright.async_api import async_playwright
errors = []
# --- 后端预检: 登录拿 token ---
import urllib.error # noqa
s, login = api("POST", "/api/v1/auth/token/",
body={"username": "alice", "password": "alice12345"})
assert s == 200, f"login failed: {s} {login}"
token = login["access"]
s, custs = api("GET", "/api/v1/partner/customers/?page_size=200",
token=token)
assert s == 200 and custs["count"] >= 1, f"customers: {s}"
s, whs = api("GET", "/api/v1/inventory/warehouses/?page_size=200",
token=token)
assert s == 200 and whs["count"] >= 1, f"warehouses: {s}"
s, prods = api("GET", "/api/v1/catalog/products/?page_size=200",
token=token)
assert s == 200 and prods["count"] >= 1, f"products: {s}"
customer = custs["results"][0]
warehouse = whs["results"][0]
product = prods["results"][0]
print(f"[pre] customer={customer['code']} warehouse={warehouse['code']} "
f"product={product['code']} token=ok")
async with async_playwright() as pw:
browser = await pw.chromium.launch()
# Chrome 移动模拟: Pixel 5 视口 + 触摸 + 移动 UA
ctx = await browser.new_context(
viewport={"width": 393, "height": 851},
has_touch=True,
is_mobile=True,
user_agent=("Mozilla/5.0 (Linux; Android 13; Pixel 5) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0 Mobile Safari/537.36"),
)
page = await ctx.new_page()
page.on("console", lambda m: errors.append(m.text)
if m.type == "error" else None)
page.on("pageerror", lambda e: errors.append(str(e)))
await page.goto(PAGE, wait_until="networkidle")
# --- 登录 ---(密码框无 placeholder,用 input 序号定位)
inputs = page.locator(".sm-login input")
await inputs.nth(0).fill("alice")
await inputs.nth(1).fill("alice12345")
await page.get_by_role("button", name="登 录").click()
await page.wait_for_timeout(3000)
body = await page.content()
assert "外勤开单" in body or "开单" in body, "login did not land"
print("[1] login ok (mobile viewport 393x851)")
# --- 开单: 选客户/仓库 ---
# 移动视口下 el-option 常判定不可见,用 JS 直点可见 option
async def pick(select_idx, opt_idx=0):
sels = page.locator(".el-select")
await sels.nth(select_idx).click()
await page.wait_for_timeout(900)
txt = await page.evaluate("""(idx) => {
const opts = document.querySelectorAll('li[role=option]');
const vis = [...opts].filter(o => o.offsetParent !== null);
if (!vis.length || idx >= vis.length) return 'NONE total='+opts.length;
vis[idx].click();
return 'OK:' + vis[idx].textContent.trim().slice(0, 30);
}""", opt_idx)
await page.wait_for_timeout(1200)
return txt
print("[2] customer:", await pick(0, 0))
print("[2] warehouse:", await pick(1, 0))
# 搜商品并加一行: 取第一行的数量框填 2, 点"加"
await page.wait_for_timeout(2000)
add_btns = page.get_by_role("button", name="加", exact=True)
n_add = await add_btns.count()
assert n_add >= 1, "no product rows rendered"
qty_boxes = page.locator(".sm-item .el-input-number input")
await qty_boxes.first.fill("2")
await add_btns.first.click()
await page.wait_for_timeout(800)
order_title = await page.locator(".sm-order-title").count()
assert order_title == 1, "order section not shown after add"
print("[3] add-to-order ok (qty=2)")
# 抹零选抹角 + 保存并过账
await page.locator(".sm-totals .el-select").click()
await page.wait_for_timeout(700)
print("[4] round_to:",
await page.evaluate("""() => {
const opts = document.querySelectorAll('li[role=option]');
const vis = [...opts].filter(o => o.offsetParent !== null);
if (vis.length < 3) return 'NONE total='+opts.length;
vis[2].click();
return 'OK:' + vis[2].textContent.trim().slice(0, 20);
}"""))
await page.wait_for_timeout(500)
await page.get_by_role("button", name="保存并过账").click()
await page.wait_for_timeout(4000)
print("[4] submit+confirm clicked")
# --- 欠款 tab ---(tab 项是 div#tab-xxx,直接点)
await page.locator("#tab-debt").click()
await page.wait_for_timeout(2500)
debt_body = await page.locator("#pane-debt").text_content()
print(f"[5] debt tab rendered: {(debt_body or '').strip()[:100]}")
assert "未结" in (debt_body or "") or "暂无欠款" in (debt_body or ""), \
f"debt pane unexpected: {(debt_body or '')[:100]}"
# --- 收款 tab: 填单登记 ---
await page.locator("#tab-receipt").click()
await page.wait_for_timeout(1000)
# 收款面板内第一个 select = 客户
cust_sel = page.locator("#pane-receipt .el-select").first
await cust_sel.scroll_into_view_if_needed()
await cust_sel.click()
await page.wait_for_timeout(900)
print("[6] receipt customer:",
await page.evaluate("""() => {
const opts = document.querySelectorAll('li[role=option]');
const vis = [...opts].filter(o => o.offsetParent !== null);
if (!vis.length) return 'NONE';
vis[0].click();
return 'OK:' + vis[0].textContent.trim().slice(0, 30);
}"""))
await page.wait_for_timeout(500)
# 关掉可能残留的下拉浮层后再填单号(Esc 关闭 popper)
await page.keyboard.press("Escape")
await page.wait_for_timeout(400)
bill_no = "SMH50002"
await page.locator("#pane-receipt input[placeholder]").nth(0).scroll_into_view_if_needed()
await page.locator("#pane-receipt input[placeholder]").nth(0).fill(bill_no)
amt_input = page.locator("#pane-receipt .el-input-number input")
await amt_input.fill("50")
await page.get_by_role("button", name="登记收款").click()
await page.wait_for_timeout(3000)
print("[6] receipt submit clicked")
errs = [e for e in errors if "favicon" not in e.lower()]
print(f"[7] console errors: {len(errs)}")
for e in errs[:10]:
print(" ERR:", e[:200])
await browser.close()
# --- 后端断言落库 ---
s, bills = api("GET", "/api/v1/sales/bills/?page_size=5", token=token)
assert s == 200, f"bills: {s}"
latest = bills["results"][0] if bills["results"] else None
print(f"[8] latest bill: {latest['bill_no'] if latest else None} "
f"state={latest['state'] if latest else None} "
f"total={latest['total_amount'] if latest else None}")
assert latest and latest["state"] == "confirmed", \
f"expected confirmed bill, got {latest}"
s, found = api("GET", "/api/v1/finance/receipts/?page_size=50&search=SMH50002",
token=token)
assert s == 200, f"receipts: {s}"
hit = [r for r in found["results"] if r["bill_no"] == "SMH50002"]
assert hit, f"receipt SMH50002 not found: {[r['bill_no'] for r in found['results'][:5]]}"
print(f"[9] receipt SMH50002 ok amount={hit[0]['amount']}")
s, debts = api("GET", "/api/v1/finance/receivables/?page_size=50",
token=token)
assert s == 200
open_debts = [d for d in debts["results"]
if d["status"] in ("open", "partial")]
assert open_debts, "expected open receivables"
print(f"[10] open receivables: {len(open_debts)}, "
f"first balance={open_debts[0]['balance']}")
real_errors = [e for e in errors if "favicon" not in e.lower()]
assert not real_errors, f"console errors: {real_errors[:5]}"
print("ALL SALESMAN-H5 CHECKS PASSED")
if __name__ == "__main__":
sys.exit(asyncio.run(main()) or 0)