169 lines
8.1 KiB
Python
169 lines
8.1 KiB
Python
"""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()
|