feat: relax query limits for query-tool and mid-section-defect

This commit is contained in:
egg
2026-03-02 08:20:55 +08:00
parent 5d58ac551d
commit cdf6f67c54
15 changed files with 1177 additions and 103 deletions

View 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]}"