174 lines
6.5 KiB
Python
174 lines
6.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""E2E tests for SPA shell navigation/runtime contracts."""
|
|
|
|
import pytest
|
|
import re
|
|
from playwright.sync_api import Page, expect
|
|
|
|
|
|
def _sidebar_links(page: Page):
|
|
"""Support both legacy and current shell nav selectors."""
|
|
return page.locator("a.drawer-link[href], a.sidebar-item[data-route]")
|
|
|
|
|
|
def _fetch_json_status(page: Page, url: str):
|
|
"""Run fetch in browser context and return status/payload metadata."""
|
|
return page.evaluate(
|
|
"""
|
|
async (targetUrl) => {
|
|
const response = await fetch(targetUrl, { cache: 'no-store' });
|
|
let payload = null;
|
|
try {
|
|
payload = await response.json();
|
|
} catch (_) {
|
|
payload = null;
|
|
}
|
|
return { ok: response.ok, status: response.status, payload };
|
|
}
|
|
""",
|
|
url,
|
|
)
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestPortalPage:
|
|
"""E2E tests for portal shell routing and drawer navigation."""
|
|
|
|
def test_portal_loads_successfully(self, page: Page, app_server: str):
|
|
page.goto(app_server)
|
|
expect(page.locator("h1")).to_contain_text("MES 報表入口")
|
|
|
|
def test_portal_has_sidebar_routes(self, page: Page, app_server: str):
|
|
page.goto(app_server)
|
|
|
|
expect(_sidebar_links(page).first).to_be_visible()
|
|
expect(page.locator(".drawer-link:has-text('WIP 即時概況')")).to_be_visible()
|
|
expect(page.locator(".drawer-link:has-text('設備即時概況')")).to_be_visible()
|
|
expect(page.locator(".drawer-link:has-text('設備歷史績效')")).to_be_visible()
|
|
expect(page.locator(".drawer-link:has-text('設備維修查詢')")).to_be_visible()
|
|
|
|
def test_portal_sidebar_navigation_uses_direct_routes(self, page: Page, app_server: str):
|
|
page.goto(app_server)
|
|
|
|
first_route = _sidebar_links(page).first
|
|
expect(first_route).to_be_visible()
|
|
target_href = first_route.get_attribute("href")
|
|
assert target_href, "sidebar route href missing"
|
|
|
|
first_route.click()
|
|
expect(page).to_have_url(re.compile(f".*{re.escape(target_href)}$"))
|
|
assert page.locator("iframe").count() == 0, "Shell content must not use iframe"
|
|
|
|
def test_portal_health_popup_clickable(self, page: Page, app_server: str):
|
|
page.goto(app_server)
|
|
|
|
trigger = page.locator(".health-trigger")
|
|
expect(trigger).to_be_visible()
|
|
expect(page.locator("#shellHealthPopup")).to_have_count(0)
|
|
|
|
trigger.click()
|
|
expect(page.locator("#shellHealthPopup")).to_be_visible()
|
|
|
|
page.keyboard.press("Escape")
|
|
expect(page.locator("#shellHealthPopup")).to_have_count(0)
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestFrontendApiRuntime:
|
|
"""E2E tests for runtime API availability in browser context."""
|
|
|
|
def test_wip_overview_can_call_summary_api_via_fetch(self, page: Page, app_server: str):
|
|
page.goto(f"{app_server}/wip-overview")
|
|
result = _fetch_json_status(page, "/api/wip/overview/summary")
|
|
assert result["ok"] is True
|
|
assert result["status"] == 200
|
|
assert isinstance(result.get("payload"), dict)
|
|
|
|
def test_wip_detail_can_call_workcenter_api_via_fetch(self, page: Page, app_server: str):
|
|
page.goto(f"{app_server}/wip-detail")
|
|
result = _fetch_json_status(page, "/api/wip/meta/workcenters")
|
|
assert result["ok"] is True
|
|
assert result["status"] == 200
|
|
assert isinstance(result.get("payload"), dict)
|
|
|
|
def test_global_mesapi_bridge_is_optional(self, page: Page, app_server: str):
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
runtime = page.evaluate(
|
|
"""
|
|
() => ({
|
|
hasFetch: typeof window.fetch === 'function',
|
|
hasMesApi: typeof window.MesApi !== 'undefined',
|
|
hasMesApiGet: Boolean(window.MesApi && typeof window.MesApi.get === 'function'),
|
|
})
|
|
"""
|
|
)
|
|
assert runtime["hasFetch"] is True
|
|
if runtime["hasMesApi"]:
|
|
assert runtime["hasMesApiGet"] is True
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestRoutePagesSmoke:
|
|
"""Basic smoke checks for key route pages."""
|
|
|
|
def test_wip_overview_loads(self, page: Page, app_server: str):
|
|
response = page.goto(f"{app_server}/wip-overview")
|
|
assert response is not None and response.ok
|
|
expect(page.locator("body")).to_be_visible()
|
|
|
|
def test_wip_detail_loads(self, page: Page, app_server: str):
|
|
response = page.goto(f"{app_server}/wip-detail")
|
|
assert response is not None and response.ok
|
|
expect(page.locator("body")).to_be_visible()
|
|
|
|
def test_resource_page_loads(self, page: Page, app_server: str):
|
|
response = page.goto(f"{app_server}/resource")
|
|
assert response is not None and response.ok
|
|
expect(page.locator("body")).to_be_visible()
|
|
|
|
def test_tables_page_loads(self, page: Page, app_server: str):
|
|
response = page.goto(f"{app_server}/tables")
|
|
assert response is not None
|
|
assert response.status in {200, 403}
|
|
expect(page.locator("body")).to_be_visible()
|
|
if response.status == 200:
|
|
header = page.locator("h1")
|
|
expect(header).to_be_visible()
|
|
text = header.inner_text()
|
|
assert "MES 數據表查詢工具" in text or "頁面開發中" in text
|
|
|
|
def test_excel_query_page_loads(self, page: Page, app_server: str):
|
|
response = page.goto(f"{app_server}/excel-query")
|
|
assert response is not None
|
|
assert response.status in {200, 403}
|
|
expect(page.locator("body")).to_be_visible()
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestConsoleAndErrorSignals:
|
|
"""Console/pageerror checks for SPA runtime stability."""
|
|
|
|
def test_wip_overview_has_no_uncaught_page_errors(self, page: Page, app_server: str):
|
|
errors = []
|
|
page.on("pageerror", lambda error: errors.append(str(error)))
|
|
page.goto(f"{app_server}/wip-overview")
|
|
page.wait_for_timeout(2000)
|
|
assert errors == [], f"Unexpected page errors: {errors[:3]}"
|
|
|
|
def test_wip_overview_triggers_expected_api_requests(self, page: Page, app_server: str):
|
|
observed = set()
|
|
|
|
def on_response(resp):
|
|
if "/api/wip/overview/summary" in resp.url:
|
|
observed.add("summary")
|
|
if "/api/wip/overview/matrix" in resp.url:
|
|
observed.add("matrix")
|
|
|
|
page.on("response", on_response)
|
|
page.goto(f"{app_server}/wip-overview", wait_until="domcontentloaded")
|
|
|
|
page.wait_for_timeout(5000)
|
|
assert "summary" in observed
|
|
assert "matrix" in observed
|