#!/usr/bin/env python3 """HEL-177: 转账往来页 360/820/1440 冒烟截图(mock 接口,不依赖登录库)。""" from __future__ import annotations import json import threading from functools import partial from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from playwright.sync_api import sync_playwright ROOT = Path(__file__).resolve().parents[1] WEB = ROOT / "web" OUT = ROOT / "hel177-shots" OUT.mkdir(parents=True, exist_ok=True) VIEWPORTS = [(1440, 900, "1440"), (820, 900, "820"), (360, 800, "360")] SUMMARY = { "status": "ok", "own_company": {"id": 3, "name": "河南金牛煤业有限公司"}, "window": { "start": "2026-01-01", "end": "2026-08-20", "has_opening": False, "opening": None, "ending": None, }, "confirmed": { "outflow_total": "123860000.00", "outflow_count": 48, "inflow_total": "184205000.00", "inflow_count": 77, "net_change": "-60345000.00", "net_direction": "payable", }, "pending": {"count": 3, "amount_total": "3200000.00"}, "counterparties": [ { "company_id": 5, "company_name": "河南金牛置业有限公司", "confirmed_outflow": "52000000.00", "confirmed_inflow": "86000000.00", "net": "-34000000.00", "pending_count": 2, "last_effective_at": "2026-08-18", }, { "company_id": 6, "company_name": "河南金牛贸易有限公司", "confirmed_outflow": "10000000.00", "confirmed_inflow": "12605000.00", "net": "-2605000.00", "pending_count": 1, "last_effective_at": "2026-08-10", }, { "company_id": 7, "company_name": "河南金牛农业科技发展有限公司", "confirmed_outflow": "61860000.00", "confirmed_inflow": "85600000.00", "net": "-23740000.00", "pending_count": 0, "last_effective_at": "2026-07-30", }, ], } # 修正 mock:按 HEL-169 净变动=转出-转入,正数应收 SUMMARY["confirmed"] = { "outflow_total": "184205000.00", "outflow_count": 80, "inflow_total": "123860000.00", "inflow_count": 48, "net_change": "60345000.00", "net_direction": "receivable", } SUMMARY["counterparties"] = [ { "company_id": 5, "company_name": "河南金牛置业有限公司", "confirmed_outflow": "86000000.00", "confirmed_inflow": "52000000.00", "net": "34000000.00", "pending_count": 2, "last_effective_at": "2026-08-18", }, { "company_id": 6, "company_name": "河南金牛贸易有限公司", "confirmed_outflow": "12605000.00", "confirmed_inflow": "10000000.00", "net": "2605000.00", "pending_count": 1, "last_effective_at": "2026-08-10", }, { "company_id": 7, "company_name": "河南金牛农业科技发展有限公司", "confirmed_outflow": "85600000.00", "confirmed_inflow": "61860000.00", "net": "23740000.00", "pending_count": 0, "last_effective_at": "2026-07-30", }, ] EVENTS = { "status": "ok", "window": {"start": "2026-01-01", "end": "2026-08-20"}, "events": [ { "event_id": 101, "direction": "in", "state": "confirmed", "pairing": "paired", "locked": False, "amount": "12000000.00", "currency": "CNY", "effective_at": "2026-08-18", "summary": "周转资金调拨", "status": "matched", }, { "event_id": 102, "direction": "out", "state": "pending", "pairing": "unilateral", "locked": False, "amount": "1500000.00", "currency": "CNY", "effective_at": "2026-08-12", "summary": "工程款结算(待确认)", "status": "unresolved", }, ], "next_cursor": None, "has_more": False, } DETAIL = { "status": "ok", "event": { "event_id": 101, "classification": "intercompany", "pairing": "paired", "status": "matched", "amount": "12000000.00", "currency": "CNY", "effective_at": "2026-08-18", "mode": "auto", "reason": "双边匹配", "counterparty": { "company_id": 5, "company_name": "河南金牛置业有限公司", "account_number_masked": "****8821", }, "observations": [ { "source_row_id": 1, "role": "payee", "source_row": 148, "sheet_name": "流水", "transaction_at": "2026-08-18 10:22:00", "income": "12000000.00", "expense": None, "import_batch_id": 19, "original_filename": "工行3305_202608.xls", "own_account_masked": "****3305", "counterparty_account_masked": "****8821", "counterparty_name": "河南金牛置业有限公司", "summary": "周转资金调拨", "reference": "JS20260818-00317", } ], }, } def main() -> None: handler = partial(SimpleHTTPRequestHandler, directory=str(WEB)) httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler) port = httpd.server_address[1] threading.Thread(target=httpd.serve_forever, daemon=True).start() base = f"http://127.0.0.1:{port}" mock_js = f""" window.__HEL177_MOCK__ = true; const SUMMARY = {json.dumps(SUMMARY, ensure_ascii=False)}; const EVENTS = {json.dumps(EVENTS, ensure_ascii=False)}; const DETAIL = {json.dumps(DETAIL, ensure_ascii=False)}; const _fetch = window.fetch.bind(window); window.fetch = async (input, init) => {{ const url = String(input); if (url.includes('/api/me') || url.includes('/api/session')) {{ return new Response(JSON.stringify({{role:'company', company_id:3, username:'牛女士', company_name:'河南金牛煤业有限公司'}}), {{status:200, headers:{{'Content-Type':'application/json'}}}}); }} if (url.includes('/api/company/workspace')) {{ return new Response(JSON.stringify({{status:'ok', pending_unilateral:0, pending_total:0, unilateral_events:[]}}), {{status:200, headers:{{'Content-Type':'application/json'}}}}); }} if (url.includes('/api/company/intercompany/summary')) {{ return new Response(JSON.stringify(SUMMARY), {{status:200, headers:{{'Content-Type':'application/json'}}}}); }} if (url.includes('/api/company/intercompany/events')) {{ return new Response(JSON.stringify(EVENTS), {{status:200, headers:{{'Content-Type':'application/json'}}}}); }} if (url.includes('/api/company/transfer-events/')) {{ return new Response(JSON.stringify(DETAIL), {{status:200, headers:{{'Content-Type':'application/json'}}}}); }} if (url.includes('/api/company/')) {{ return new Response(JSON.stringify({{status:'ok'}}), {{status:200, headers:{{'Content-Type':'application/json'}}}}); }} return _fetch(input, init); }}; """ console_errors: list[str] = [] with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.on("pageerror", lambda exc: console_errors.append(f"pageerror:{exc}")) page.on("console", lambda msg: console_errors.append(f"console:{msg.type}:{msg.text}") if msg.type == "error" else None) for width, height, label in VIEWPORTS: page.set_viewport_size({"width": width, "height": height}) page.add_init_script(mock_js) # bypass auth guard page.add_init_script( """ const orig = window.fetch; // initAuthGuard 读 /api/me;已在 mock 中处理 """ ) page.goto(f"{base}/company.html", wait_until="domcontentloaded") page.evaluate( """() => { // 若鉴权把页面踢走,强制停留 if (!document.body || document.body.dataset.portal !== 'company') return; }""" ) # 等 summary 渲染 page.wait_for_timeout(600) # 切到转账往来 page.evaluate("() => { if (typeof showView === 'function') showView('transfers'); }") page.wait_for_selector("#transfersData:not([hidden])", timeout=5000) page.wait_for_timeout(300) overflow = page.evaluate( "() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1" ) assert not overflow, f"{label} overview overflow" # 文案:期间净变动,无期末余额误用 net_title = page.inner_text("#tfStatNetTitle") assert "期间净变动" in net_title, net_title page.screenshot(path=str(OUT / f"overview-{label}.png"), full_page=True) # 下钻明细 page.click("#transfersCpBody tr.clickable") page.wait_for_selector("#transfersDetailLayer:not([hidden])", timeout=5000) page.wait_for_timeout(300) overflow2 = page.evaluate( "() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1" ) assert not overflow2, f"{label} detail overflow" page.screenshot(path=str(OUT / f"detail-{label}.png"), full_page=True) # 打开抽屉 page.click("[data-transfer-evidence]") page.wait_for_selector("#transferEvidenceDrawer.is-open", timeout=5000) page.wait_for_timeout(200) page.screenshot(path=str(OUT / f"drawer-{label}.png"), full_page=True) page.click("[data-close-transfer-evidence]") browser.close() hard = [e for e in console_errors if "Failed to load resource" not in e and "favicon" not in e] print("shots:", list(OUT.glob("*.png"))) print("console_errors:", hard) if hard: raise SystemExit(1) print("OK") if __name__ == "__main__": main()