test: verify dense outbound dispatch workflows
This commit is contained in:
168
scripts/verify_dense_outbound_ui.py
Normal file
168
scripts/verify_dense_outbound_ui.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user