feat: relax query limits for query-tool and mid-section-defect
This commit is contained in:
175
tests/e2e/test_query_tool_ui_ux_e2e.py
Normal file
175
tests/e2e/test_query_tool_ui_ux_e2e.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# -*- 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]}"
|
||||
Reference in New Issue
Block a user