"""Read-only Playwright verification for dense outbound and relocation screens.""" import json import os from pathlib import Path from playwright.sync_api import TimeoutError as PlaywrightTimeoutError from playwright.sync_api import sync_playwright BASE_URL = os.getenv("WMS_TEST_BASE_URL", "http://127.0.0.1") USERNAME = os.environ["WMS_TEST_USERNAME"] PASSWORD = os.environ["WMS_TEST_PASSWORD"] OUTPUT_DIR = Path(os.getenv( "WMS_TEST_SCREENSHOT_DIR", "docs/testing/screenshots/2026-07-16-dense-outbound", )).resolve() def wait_for_app(page): try: page.wait_for_load_state("networkidle", timeout=15000) except PlaywrightTimeoutError: page.wait_for_load_state("domcontentloaded") page.wait_for_timeout(800) def assert_text(page, *labels): body = page.locator("body").inner_text() for label in labels: if label not in body: raise AssertionError(f"Missing text {label!r} at {page.url}") def page_overflow(page): return page.evaluate("""() => ({ viewport: window.innerWidth, documentWidth: document.documentElement.scrollWidth, bodyWidth: document.body.scrollWidth })""") def wait_for_preview_dialog(page): dialog = page.locator(".el-dialog:visible") dialog.get_by_text("指定通道整托出库", exact=True).wait_for(timeout=10000) dialog.locator(".el-loading-mask").wait_for(state="hidden", timeout=10000) dialog.locator("tbody .el-table__row").first.wait_for(timeout=10000) def main(): OUTPUT_DIR.mkdir(parents=True, exist_ok=True) results = {"pages": {}, "consoleErrors": [], "requestFailures": []} with sync_playwright() as playwright: browser = playwright.chromium.launch(channel="chrome", headless=True) context = browser.new_context(viewport={"width": 1440, "height": 1000}) page = context.new_page() page.on("console", lambda msg: results["consoleErrors"].append(f"{page.url} :: {msg.text}") if msg.type == "error" else None) page.on("requestfailed", lambda request: results["requestFailures"].append( f"{request.method} {request.url}: {request.failure}")) page.goto(BASE_URL + "/login", wait_until="domcontentloaded") wait_for_app(page) page.get_by_placeholder("账号").fill(USERNAME) page.get_by_placeholder("密码").fill(PASSWORD) page.get_by_role("button", name="登 录").click() page.wait_for_url(lambda url: "/login" not in url, timeout=15000) wait_for_app(page) pages = [ ("outbound-job", "/out/outJob", ("WCS状态", "自动重试", "最后下发错误", "手动下发 WCS")), ("dense-lane-outbound", "/out/denseLaneOutbound", ("通道组编码", "满托盘数", "可出库托盘", "当前最外侧托盘", "整托出库")), ("kpi-plan", "/kpi/planKpi", ("货位平面图", "指定移库")), ] for name, route, labels in pages: page.goto(BASE_URL + route, wait_until="domcontentloaded") wait_for_app(page) assert_text(page, *labels) screenshot = OUTPUT_DIR / f"{name}-desktop.png" page.screenshot(path=str(screenshot), full_page=True) results["pages"][name] = { "url": page.url, "desktopScreenshot": str(screenshot), "desktopOverflow": page_overflow(page), } if name == "dense-lane-outbound": lane_buttons = page.get_by_role("button", name="整托出库") enabled_button = next((lane_buttons.nth(index) for index in range(lane_buttons.count()) if lane_buttons.nth(index).is_enabled()), None) if enabled_button is not None: enabled_button.click() wait_for_preview_dialog(page) assert_text(page, "整托数量", "出库口", "本次准确出库托盘") dialog_shot = OUTPUT_DIR / "dense-lane-outbound-dialog-real.png" page.screenshot(path=str(dialog_shot), full_page=True) results["pages"][name]["realDialogScreenshot"] = str(dialog_shot) page.get_by_role("button", name="取消").last.click() else: results["pages"][name]["dialog"] = "skipped-no-available-lane" synthetic_lane = { "groupCoding": "VERIFY-LANE-01", "cargoArea": "G", "thePlatoon": 7, "theFloor": 1, "directionType": "1", "fullTrayCount": 3, "availableTrayCount": 2, "totalQuantity": 24, "outermostTrayCode": "A-VERIFY-001", "outermostCargoSpaceId": "VERIFY-CS-003", } synthetic_detail = { "projectNo": "VERIFY-PROJECT", "productId": "VERIFY-MATERIAL", "productName": "验证物料", "lotNo": "VERIFY-LOT", "stockQuantity": 12, } synthetic_tray = { "cargoSpaceId": "VERIFY-CS-003", "cargoSpaceName": "验证端头货位", "trayCode": "A-VERIFY-001", "theColumn": 3, "stockQuantity": 12, "stockDetails": [synthetic_detail], } page.route("**/dev-api/out/denseLaneOutbound/list*", lambda route: route.fulfill( status=200, content_type="application/json", body=json.dumps({"code": 200, "data": [synthetic_lane]}, ensure_ascii=False))) page.route("**/dev-api/out/denseLaneOutbound/preview*", lambda route: route.fulfill( status=200, content_type="application/json", body=json.dumps({"code": 200, "data": {"trays": [synthetic_tray], "totalQuantity": 12, "previewToken": "verify-token"}}, ensure_ascii=False))) page.route("**/dev-api/bas/stationpoint/list*", lambda route: route.fulfill( status=200, content_type="application/json", body=json.dumps({"code": 200, "rows": [{"pointId": "VERIFY-OUT-1", "pointName": "验证出库口", "pointType": "2", "pointStatus": "0", "status": "0", "trayType": "A"}], "total": 1}, ensure_ascii=False))) page.set_viewport_size({"width": 1440, "height": 1000}) page.goto(BASE_URL + "/out/denseLaneOutbound", wait_until="domcontentloaded") wait_for_app(page) page.get_by_role("button", name="整托出库").click() wait_for_preview_dialog(page) page.get_by_placeholder("请选择与托盘类型匹配的出库口").click() page.get_by_text("验证出库口(VERIFY-OUT-1,出库,A类托盘)", exact=True).wait_for(timeout=5000) assert_text(page, "VERIFY-LANE-01", "A-VERIFY-001", "VERIFY-CS-003", "验证出库口", "本次准确出库托盘") dialog_shot = OUTPUT_DIR / "dense-lane-outbound-dialog.png" page.screenshot(path=str(dialog_shot), full_page=True) results["pages"]["dense-lane-outbound"]["mockDialogScreenshot"] = str(dialog_shot) results["pages"]["dense-lane-outbound"]["mockDialog"] = "verified-with-read-only-mocks" page.set_viewport_size({"width": 390, "height": 844}) page.goto(BASE_URL + "/out/denseLaneOutbound", wait_until="domcontentloaded") wait_for_app(page) assert_text(page, "指定通道", "通道组编码") narrow_shot = OUTPUT_DIR / "dense-lane-outbound-narrow.png" page.screenshot(path=str(narrow_shot), full_page=True) results["pages"]["dense-lane-outbound"]["narrowScreenshot"] = str(narrow_shot) results["pages"]["dense-lane-outbound"]["narrowOverflow"] = page_overflow(page) context.close() browser.close() ignored_console_fragments = ("ResizeObserver loop",) results["consoleErrors"] = [message for message in results["consoleErrors"] if not any(fragment in message for fragment in ignored_console_fragments)] if results["consoleErrors"] or results["requestFailures"]: raise AssertionError(json.dumps(results, ensure_ascii=False, indent=2)) print(json.dumps(results, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()