D2业务员H5+移动模拟全绿 / P2-2部署清单+ALLOWED_HOSTS通配符门 / P3-1跳过
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user