176 lines
7.1 KiB
Python
176 lines
7.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""E2E UI/UX resilience tests for query-tool interactions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from urllib.parse import quote
|
|
|
|
import pytest
|
|
import requests
|
|
from playwright.sync_api import Page, expect
|
|
|
|
|
|
QUERY_TOOL_BASE = "/portal-shell/query-tool"
|
|
|
|
|
|
def _intercept_navigation_as_admin(page: Page):
|
|
"""Force admin navigation payload and ensure query-tool route is visible."""
|
|
|
|
def handle_route(route):
|
|
response = route.fetch()
|
|
body = response.json()
|
|
|
|
body["is_admin"] = True
|
|
drawers = body.get("drawers", [])
|
|
|
|
query_tool_entry = {
|
|
"name": "批次追蹤工具",
|
|
"order": 4,
|
|
"route": "/query-tool",
|
|
"status": "dev",
|
|
}
|
|
|
|
has_query_tool = any(
|
|
page_item.get("route") == "/query-tool"
|
|
for drawer in drawers
|
|
for page_item in drawer.get("pages", [])
|
|
)
|
|
|
|
if not has_query_tool:
|
|
target_drawer = next((drawer for drawer in drawers if not drawer.get("admin_only")), None)
|
|
if target_drawer is None:
|
|
drawers.append(
|
|
{
|
|
"id": "e2e-test-drawer",
|
|
"name": "E2E Test",
|
|
"order": 999,
|
|
"admin_only": False,
|
|
"pages": [query_tool_entry],
|
|
}
|
|
)
|
|
else:
|
|
target_drawer.setdefault("pages", []).append(query_tool_entry)
|
|
|
|
body["drawers"] = drawers
|
|
route.fulfill(
|
|
status=response.status,
|
|
headers={**response.headers, "content-type": "application/json"},
|
|
body=json.dumps(body),
|
|
)
|
|
|
|
page.route("**/api/portal/navigation", handle_route)
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestQueryToolUiUxE2E:
|
|
"""User-centric UI/UX flows on query-tool page."""
|
|
|
|
def test_lot_multi_query_counter_and_url_round_trip(self, page: Page, app_server: str):
|
|
"""Multi-query input should sync counter + URL and survive reload."""
|
|
_intercept_navigation_as_admin(page)
|
|
page.goto(f"{app_server}{QUERY_TOOL_BASE}?tab=lot", wait_until="domcontentloaded", timeout=60000)
|
|
page.wait_for_timeout(1500)
|
|
|
|
visible_textarea = page.locator("textarea.query-tool-textarea:visible").first
|
|
visible_textarea.fill("GA26010001\nGA26010002, GA26010003")
|
|
expect(page.locator(".query-tool-input-counter:visible").first).to_contain_text("已輸入 3")
|
|
|
|
visible_select = page.locator("select.query-tool-select:visible").first
|
|
visible_select.select_option("work_order")
|
|
|
|
with page.expect_response(lambda resp: "/api/query-tool/resolve" in resp.url and resp.status < 500, timeout=90000):
|
|
page.locator("button:has-text('解析'):visible").first.click()
|
|
|
|
page.wait_for_timeout(1000)
|
|
assert "tab=lot" in page.url
|
|
assert "lot_values=" in page.url or "values=" in page.url
|
|
|
|
page.reload(wait_until="domcontentloaded")
|
|
page.wait_for_timeout(1000)
|
|
|
|
restored_text = page.locator("textarea.query-tool-textarea:visible").first.input_value()
|
|
restored_values = [v.strip() for v in re.split(r"[\n,]", restored_text) if v.strip()]
|
|
assert len(restored_values) >= 3
|
|
|
|
def test_equipment_tab_cross_navigation_preserves_filters(self, page: Page, app_server: str):
|
|
"""Equipment filter/date state should persist across tab switching."""
|
|
equipment_resp = requests.get(f"{app_server}/api/query-tool/equipment-list", timeout=30)
|
|
if equipment_resp.status_code != 200:
|
|
pytest.skip("equipment-list API is unavailable")
|
|
equipment_items = equipment_resp.json().get("data") or []
|
|
if not equipment_items:
|
|
pytest.skip("No equipment item available for E2E test")
|
|
|
|
equipment_id = str(equipment_items[0].get("RESOURCEID") or "")
|
|
if not equipment_id:
|
|
pytest.skip("Unable to determine equipment id")
|
|
|
|
_intercept_navigation_as_admin(page)
|
|
start_date = "2026-01-01"
|
|
end_date = "2026-01-31"
|
|
page.goto(
|
|
f"{app_server}{QUERY_TOOL_BASE}"
|
|
f"?tab=equipment&equipment_sub_tab=timeline"
|
|
f"&equipment_ids={quote(equipment_id)}"
|
|
f"&start_date={start_date}&end_date={end_date}",
|
|
wait_until="domcontentloaded",
|
|
timeout=60000,
|
|
)
|
|
|
|
page.wait_for_timeout(1500)
|
|
date_inputs = page.locator("input[type='date']")
|
|
expect(date_inputs.first).to_have_value(start_date)
|
|
expect(date_inputs.nth(1)).to_have_value(end_date)
|
|
|
|
js_errors = []
|
|
page.on("pageerror", lambda error: js_errors.append(str(error)))
|
|
|
|
with page.expect_response(lambda resp: "/api/query-tool/equipment-period" in resp.url and resp.status < 500, timeout=120000):
|
|
page.locator("button:has-text('查詢'):visible").first.click()
|
|
|
|
page.wait_for_timeout(1500)
|
|
|
|
page.locator("button", has_text="批次追蹤(正向)").click()
|
|
page.wait_for_timeout(400)
|
|
page.locator("button", has_text="設備生產批次追蹤").click()
|
|
page.wait_for_timeout(600)
|
|
|
|
expect(date_inputs.first).to_have_value(start_date)
|
|
expect(date_inputs.nth(1)).to_have_value(end_date)
|
|
assert len(js_errors) == 0, f"JS errors found while switching tabs: {js_errors[:3]}"
|
|
|
|
def test_rapid_resolve_and_tab_switching_no_ui_crash(self, page: Page, app_server: str):
|
|
"""Rapid resolve + tab switching should keep page responsive without crashes."""
|
|
_intercept_navigation_as_admin(page)
|
|
page.goto(f"{app_server}{QUERY_TOOL_BASE}?tab=lot", wait_until="domcontentloaded", timeout=60000)
|
|
page.wait_for_timeout(1200)
|
|
|
|
js_errors = []
|
|
page.on("pageerror", lambda error: js_errors.append(str(error)))
|
|
|
|
# Seed lot tab query input.
|
|
page.locator("select.query-tool-select:visible").first.select_option("work_order")
|
|
page.locator("textarea.query-tool-textarea:visible").first.fill("GA26010001")
|
|
|
|
for idx in range(4):
|
|
with page.expect_response(lambda resp: "/api/query-tool/resolve" in resp.url and resp.status < 500, timeout=90000):
|
|
page.locator("button:has-text('解析'):visible").first.click()
|
|
page.wait_for_timeout(350)
|
|
|
|
page.locator("button", has_text="流水批反查(反向)").click()
|
|
page.wait_for_timeout(300)
|
|
page.locator("select.query-tool-select:visible").first.select_option("serial_number")
|
|
page.locator("textarea.query-tool-textarea:visible").first.fill(f"GMSN-STRESS-{idx:03d}")
|
|
|
|
with page.expect_response(lambda resp: "/api/query-tool/resolve" in resp.url and resp.status < 500, timeout=90000):
|
|
page.locator("button:has-text('解析'):visible").first.click()
|
|
page.wait_for_timeout(300)
|
|
|
|
page.locator("button", has_text="批次追蹤(正向)").click()
|
|
page.wait_for_timeout(300)
|
|
|
|
expect(page.locator("body")).to_be_visible()
|
|
assert len(js_errors) == 0, f"Detected JS crash signals: {js_errors[:3]}"
|