feat: finalize portal no-iframe migration baseline and archive change

This commit is contained in:
egg
2026-02-11 13:25:03 +08:00
parent cd54d7cdcb
commit ccab10bee8
117 changed files with 6673 additions and 1098 deletions

View File

@@ -23,26 +23,25 @@ class TestPortalPage:
# Wait for page to load
expect(page.locator('h1')).to_contain_text('MES 報表入口')
def test_portal_has_all_tabs(self, page: Page, app_server: str):
"""Portal should have all navigation tabs."""
def test_portal_has_all_sidebar_routes(self, page: Page, app_server: str):
"""Portal should expose route-based sidebar entries."""
page.goto(app_server)
# Check released tabs exist
expect(page.locator('.tab:has-text("WIP 即時概況")')).to_be_visible()
expect(page.locator('.tab:has-text("設備即時概況")')).to_be_visible()
expect(page.locator('.tab:has-text("設備歷史績效")')).to_be_visible()
expect(page.locator('.tab:has-text("設備維修查詢")')).to_be_visible()
expect(page.locator('.tab:has-text("批次追蹤工具")')).to_be_visible()
def test_portal_tab_switching(self, page: Page, app_server: str):
"""Portal tabs should switch iframe content."""
expect(page.locator('.sidebar-item:has-text("WIP 即時概況")')).to_be_visible()
expect(page.locator('.sidebar-item:has-text("設備即時概況")')).to_be_visible()
expect(page.locator('.sidebar-item:has-text("設備歷史績效")')).to_be_visible()
expect(page.locator('.sidebar-item:has-text("設備維修查詢")')).to_be_visible()
def test_portal_sidebar_navigation_uses_direct_routes(self, page: Page, app_server: str):
"""Sidebar click should navigate to direct route without iframe switching."""
page.goto(app_server)
# Click on a different tab
page.locator('.tab:has-text("設備即時概況")').click()
# Verify the tab is active
expect(page.locator('.tab:has-text("設備即時概況")')).to_have_class(re.compile(r'active'))
first_route = page.locator('.sidebar-item[data-route]').first
expect(first_route).to_be_visible()
target_href = first_route.get_attribute('href')
assert target_href and target_href.startswith('/'), "sidebar route href missing"
first_route.click()
expect(page).to_have_url(re.compile(f".*{re.escape(target_href)}$"))
def test_portal_health_popup_clickable(self, page: Page, app_server: str):
"""Health status pill should toggle popup visibility on click."""

View File

@@ -12,7 +12,6 @@ Run with: pytest tests/stress/test_frontend_stress.py -v -s
import pytest
import time
import re
import requests
from urllib.parse import quote
from playwright.sync_api import Page, expect
@@ -258,51 +257,55 @@ class TestMesApiStress:
assert total_resolved >= 5, f"Too many unresolved requests"
@pytest.mark.stress
@pytest.mark.stress
class TestPageNavigationStress:
"""Stress tests for rapid page navigation."""
"""Stress tests for rapid route navigation."""
def test_rapid_tab_switching(self, page: Page, app_server: str):
"""Test rapid tab switching in portal."""
def test_rapid_route_switching(self, page: Page, app_server: str):
"""Rapid direct-route switching should remain responsive."""
page.goto(app_server, wait_until='domcontentloaded', timeout=30000)
sidebar_items = page.locator('.sidebar-item[data-target]')
sidebar_items = page.locator('.sidebar-item[data-route]')
expect(sidebar_items.first).to_be_visible()
item_count = sidebar_items.count()
assert item_count >= 1, "No portal sidebar pages available for navigation stress test"
assert item_count >= 1, "No portal sidebar routes available for stress test"
route_hrefs = []
checked = min(item_count, 5)
for idx in range(checked):
href = sidebar_items.nth(idx).get_attribute('href')
if href and href.startswith('/'):
route_hrefs.append(href)
assert route_hrefs, "Unable to resolve route hrefs from sidebar"
js_errors = []
page.on("pageerror", lambda error: js_errors.append(str(error)))
start_time = time.time()
# Rapidly switch pages 20 times
for i in range(20):
item = sidebar_items.nth(i % item_count)
item.click()
page.wait_for_timeout(50)
page.goto(f"{app_server}{route_hrefs[i % len(route_hrefs)]}", wait_until='domcontentloaded', timeout=60000)
expect(page.locator('body')).to_be_visible()
page.wait_for_timeout(80)
switch_time = time.time() - start_time
print(f"\n 20 sidebar switches in {switch_time:.3f}s")
print(f"\n 20 route switches in {switch_time:.3f}s")
assert len(js_errors) == 0, f"JS errors detected during route switching: {js_errors[:3]}"
# Page should still be responsive
expect(page.locator('h1')).to_contain_text('MES 報表入口')
print(" Portal remained stable")
def test_portal_iframe_stress(self, page: Page, app_server: str):
"""Test portal remains responsive with iframe loading."""
def test_portal_navigation_contract_without_iframe(self, page: Page, app_server: str):
"""Portal sidebar should expose route metadata and no iframe DOM."""
page.goto(app_server, wait_until='domcontentloaded', timeout=30000)
sidebar_items = page.locator('.sidebar-item[data-target]')
sidebar_items = page.locator('.sidebar-item[data-route]')
expect(sidebar_items.first).to_be_visible()
item_count = sidebar_items.count()
assert item_count >= 1, "No portal sidebar pages available for iframe stress test"
assert sidebar_items.count() >= 1, "No route sidebar items found"
checked = min(item_count, 4)
for idx in range(checked):
item = sidebar_items.nth(idx)
item.click()
page.wait_for_timeout(200)
iframe_count = page.locator('iframe').count()
assert iframe_count == 0, "Portal should not render iframe after migration"
# Verify clicked item is active
expect(item).to_have_class(re.compile(r'active'))
for idx in range(min(sidebar_items.count(), 3)):
href = sidebar_items.nth(idx).get_attribute('href')
assert href and href.startswith('/'), f"Invalid sidebar href: {href}"
print(f"\n All {checked} sidebar pages clickable and responsive")
print("\n Portal route sidebar contract verified without iframe")
@pytest.mark.stress

View File

@@ -49,6 +49,7 @@ class AppFactoryTests(unittest.TestCase):
rules = {rule.rule for rule in app.url_map.iter_rules()}
expected = {
"/",
"/portal-shell",
"/tables",
"/resource",
"/wip-overview",
@@ -69,6 +70,7 @@ class AppFactoryTests(unittest.TestCase):
"/api/wip/meta/packages",
"/api/resource/status/summary",
"/api/dashboard/kpi",
"/api/portal/navigation",
"/api/excel-query/upload",
"/api/query-tool/resolve",
"/api/tmtt-defect/analysis",
@@ -76,6 +78,54 @@ class AppFactoryTests(unittest.TestCase):
missing = expected - rules
self.assertFalse(missing, f"Missing routes: {sorted(missing)}")
def test_portal_spa_flag_default_enabled(self):
old = os.environ.pop("PORTAL_SPA_ENABLED", None)
try:
app = create_app("testing")
self.assertTrue(app.config.get("PORTAL_SPA_ENABLED"))
client = app.test_client()
response = client.get("/", follow_redirects=False)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers.get("Location"), "/portal-shell")
finally:
if old is not None:
os.environ["PORTAL_SPA_ENABLED"] = old
def test_portal_spa_flag_disabled_via_env(self):
old = os.environ.get("PORTAL_SPA_ENABLED")
os.environ["PORTAL_SPA_ENABLED"] = "false"
try:
app = create_app("testing")
self.assertFalse(app.config.get("PORTAL_SPA_ENABLED"))
client = app.test_client()
response = client.get("/")
html = response.data.decode("utf-8")
self.assertIn('data-portal-spa-enabled="false"', html)
finally:
if old is None:
os.environ.pop("PORTAL_SPA_ENABLED", None)
else:
os.environ["PORTAL_SPA_ENABLED"] = old
def test_portal_spa_flag_enabled_via_env(self):
old = os.environ.get("PORTAL_SPA_ENABLED")
os.environ["PORTAL_SPA_ENABLED"] = "true"
try:
app = create_app("testing")
self.assertTrue(app.config.get("PORTAL_SPA_ENABLED"))
client = app.test_client()
response = client.get("/", follow_redirects=False)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers.get("Location"), "/portal-shell")
finally:
if old is None:
os.environ.pop("PORTAL_SPA_ENABLED", None)
else:
os.environ["PORTAL_SPA_ENABLED"] = old
if __name__ == "__main__":
unittest.main()

130
tests/test_cutover_gates.py Normal file
View File

@@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
"""Cutover gate enforcement tests for portal no-iframe migration."""
from __future__ import annotations
import json
from pathlib import Path
from mes_dashboard.app import create_app
ROOT = Path(__file__).resolve().parents[1]
BASELINE_VISIBILITY_FILE = ROOT / "docs" / "migration" / "portal-no-iframe" / "baseline_drawer_visibility.json"
BASELINE_API_FILE = ROOT / "docs" / "migration" / "portal-no-iframe" / "baseline_api_payload_contracts.json"
ROLLBACK_RUNBOOK = ROOT / "docs" / "migration" / "portal-no-iframe" / "rollback_rehearsal_runbook.md"
ROLLBACK_STRATEGY = ROOT / "docs" / "migration" / "portal-no-iframe" / "rollback_strategy_shell_and_wrappers.md"
LEGACY_REWRITE_SMOKE_CHECKLIST = ROOT / "docs" / "migration" / "portal-no-iframe" / "legacy_rewrite_smoke_checklists.md"
STRESS_SUITE = ROOT / "tests" / "stress" / "test_frontend_stress.py"
def _login_as_admin(client) -> None:
with client.session_transaction() as sess:
sess["admin"] = {"displayName": "Admin", "employeeNo": "A001"}
def _route_set(drawers: list[dict]) -> set[str]:
return {
str(page.get("route"))
for drawer in drawers
for page in drawer.get("pages", [])
if page.get("route")
}
def test_g1_route_availability_gate_p0_routes_are_2xx_or_3xx():
app = create_app("testing")
app.config["TESTING"] = True
client = app.test_client()
p0_routes = [
"/",
"/portal-shell",
"/api/portal/navigation",
"/wip-overview",
"/resource",
"/qc-gate",
]
statuses = [client.get(route).status_code for route in p0_routes]
assert all(200 <= status < 400 for status in statuses), statuses
def test_g2_drawer_parity_gate_matches_baseline_for_admin_and_non_admin():
baseline = json.loads(BASELINE_VISIBILITY_FILE.read_text(encoding="utf-8"))
app = create_app("testing")
app.config["TESTING"] = True
non_admin_client = app.test_client()
non_admin_payload = json.loads(non_admin_client.get("/api/portal/navigation").data.decode("utf-8"))
admin_client = app.test_client()
_login_as_admin(admin_client)
admin_payload = json.loads(admin_client.get("/api/portal/navigation").data.decode("utf-8"))
assert _route_set(non_admin_payload["drawers"]) == _route_set(baseline["non_admin"])
assert _route_set(admin_payload["drawers"]) == _route_set(baseline["admin"])
def test_g3_workflow_smoke_gate_critical_routes_reachable_for_admin():
app = create_app("testing")
app.config["TESTING"] = True
client = app.test_client()
_login_as_admin(client)
smoke_routes = [
"/",
"/wip-overview",
"/wip-detail?workcenter=TMTT&type=PJA3460&status=queue",
"/hold-detail?reason=YieldLimit",
"/hold-overview",
"/hold-history",
"/resource",
"/resource-history?start_date=2026-01-01&end_date=2026-01-31",
"/qc-gate",
"/job-query",
"/excel-query",
"/query-tool",
"/tmtt-defect",
]
statuses = [client.get(route).status_code for route in smoke_routes]
assert all(200 <= status < 400 for status in statuses), statuses
def test_g4_client_stability_gate_assertion_present_in_stress_suite():
content = STRESS_SUITE.read_text(encoding="utf-8")
assert 'page.on("pageerror"' in content
assert 'assert len(js_errors) == 0' in content
def test_g5_data_contract_gate_baseline_keys_are_defined_for_registered_apis():
baseline = json.loads(BASELINE_API_FILE.read_text(encoding="utf-8"))
app = create_app("testing")
app.config["TESTING"] = True
registered_routes = {rule.rule for rule in app.url_map.iter_rules()}
for api_route, contract in baseline.get("apis", {}).items():
assert api_route in registered_routes, f"Missing API route in app map: {api_route}"
required_keys = contract.get("required_keys", [])
assert required_keys, f"No required_keys defined for {api_route}"
assert all(isinstance(key, str) and key for key in required_keys)
def test_g7_rollback_readiness_gate_has_15_minute_slo_and_operator_steps():
rehearsal = ROLLBACK_RUNBOOK.read_text(encoding="utf-8")
strategy = ROLLBACK_STRATEGY.read_text(encoding="utf-8")
assert "15" in rehearsal
assert "PORTAL_SPA_ENABLED=false" in strategy
assert "/api/portal/navigation" in strategy
def test_legacy_rewrite_smoke_checklist_covers_all_wrapped_pages():
content = LEGACY_REWRITE_SMOKE_CHECKLIST.read_text(encoding="utf-8")
assert "tmtt-defect" in content
assert "job-query" in content
assert "excel-query" in content
assert "query-tool" in content
assert "SMOKE-01" in content

View File

@@ -114,3 +114,117 @@ def test_health_route_uses_internal_memoization(
assert response1.status_code == 200
assert response2.status_code == 200
assert mock_db.call_count == 1
@patch('mes_dashboard.routes.health_routes.get_portal_shell_asset_status')
def test_frontend_shell_health_endpoint_healthy(mock_status):
mock_status.return_value = {
"status": "healthy",
"route": "/portal-shell",
"checks": {
"portal_shell_html": {"exists": True},
"portal_shell_js": {"exists": True},
"portal_shell_css": {"exists": True},
"tailwind_css": {"exists": True},
"html_references": {
"portal_shell_js": True,
"portal_shell_css": True,
"tailwind_css": True,
},
},
"errors": [],
"warnings": [],
"http_code": 200,
}
response = _client().get('/health/frontend-shell')
assert response.status_code == 200
payload = response.get_json()
assert payload["status"] == "healthy"
assert payload["checks"]["portal_shell_css"]["exists"] is True
@patch('mes_dashboard.routes.health_routes.get_portal_shell_asset_status')
def test_frontend_shell_health_endpoint_unhealthy(mock_status):
mock_status.return_value = {
"status": "unhealthy",
"route": "/portal-shell",
"checks": {
"portal_shell_html": {"exists": False},
"portal_shell_js": {"exists": False},
"portal_shell_css": {"exists": False},
"tailwind_css": {"exists": False},
"html_references": {
"portal_shell_js": False,
"portal_shell_css": False,
"tailwind_css": False,
},
},
"errors": ["asset missing: static/dist/portal-shell.css"],
"warnings": [],
"http_code": 503,
}
response = _client().get('/health/frontend-shell')
assert response.status_code == 503
payload = response.get_json()
assert payload["status"] == "unhealthy"
assert any("portal-shell.css" in error for error in payload.get("errors", []))
def test_get_portal_shell_asset_status_reports_nested_html_as_healthy(tmp_path):
from mes_dashboard.routes.health_routes import get_portal_shell_asset_status
static_dir = tmp_path / "static"
dist_dir = static_dir / "dist"
nested_dir = dist_dir / "src" / "portal-shell"
nested_dir.mkdir(parents=True, exist_ok=True)
(nested_dir / "index.html").write_text(
"<html><head>"
"<link rel='stylesheet' href='/static/dist/tailwind.css'>"
"<link rel='stylesheet' href='/static/dist/portal-shell.css'>"
"<script type='module' src='/static/dist/portal-shell.js'></script>"
"</head><body><div id='app'></div></body></html>",
encoding="utf-8",
)
(dist_dir / "portal-shell.js").write_text("console.log('ok');", encoding="utf-8")
(dist_dir / "portal-shell.css").write_text(".shell{}", encoding="utf-8")
(dist_dir / "tailwind.css").write_text(".tw{}", encoding="utf-8")
app = create_app("testing")
app.config["TESTING"] = True
app.static_folder = str(static_dir)
with app.app_context():
result = get_portal_shell_asset_status()
assert result["status"] == "healthy"
assert result["checks"]["portal_shell_html"]["source"] == "nested"
assert result["checks"]["html_references"]["portal_shell_css"] is True
def test_get_portal_shell_asset_status_reports_missing_css_as_unhealthy(tmp_path):
from mes_dashboard.routes.health_routes import get_portal_shell_asset_status
static_dir = tmp_path / "static"
dist_dir = static_dir / "dist"
dist_dir.mkdir(parents=True, exist_ok=True)
(dist_dir / "portal-shell.html").write_text(
"<html><head>"
"<script type='module' src='/static/dist/portal-shell.js'></script>"
"</head><body><div id='app'></div></body></html>",
encoding="utf-8",
)
(dist_dir / "portal-shell.js").write_text("console.log('ok');", encoding="utf-8")
app = create_app("testing")
app.config["TESTING"] = True
app.static_folder = str(static_dir)
with app.app_context():
result = get_portal_shell_asset_status()
assert result["status"] == "unhealthy"
assert any("portal-shell.css" in error for error in result["errors"])

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
"""Tests for portal navigation migration contract helpers."""
from __future__ import annotations
import json
from pathlib import Path
from mes_dashboard.services.navigation_contract import (
compute_drawer_visibility,
validate_drawer_page_contract,
)
ROOT = Path(__file__).resolve().parent.parent
PAGE_STATUS_FILE = ROOT / "data" / "page_status.json"
BASELINE_VISIBILITY_FILE = ROOT / "docs" / "migration" / "portal-no-iframe" / "baseline_drawer_visibility.json"
def test_current_page_status_contract_has_no_validation_errors():
payload = json.loads(PAGE_STATUS_FILE.read_text(encoding="utf-8"))
errors = validate_drawer_page_contract(payload)
assert errors == []
def test_baseline_visibility_matches_computed_current_state():
payload = json.loads(PAGE_STATUS_FILE.read_text(encoding="utf-8"))
baseline = json.loads(BASELINE_VISIBILITY_FILE.read_text(encoding="utf-8"))
assert baseline["admin"] == compute_drawer_visibility(payload, is_admin=True)
assert baseline["non_admin"] == compute_drawer_visibility(payload, is_admin=False)

View File

@@ -170,16 +170,16 @@ class TestNavigationConfig:
reports = next(drawer for drawer in nav if drawer["id"] == "reports")
assert [page["route"] for page in reports["pages"]] == ["/wip-overview"]
assert reports["pages"][0]["frame_id"] == "wipOverviewFrame"
assert reports["pages"][0]["tool_src"] is None
assert "frame_id" not in reports["pages"][0]
assert "tool_src" not in reports["pages"][0]
queries = next(drawer for drawer in nav if drawer["id"] == "queries")
assert queries["pages"][0]["route"] == "/tables"
assert queries["pages"][-1]["route"] == "/dev-page"
dev_tools = next(drawer for drawer in nav if drawer["id"] == "dev-tools")
assert all(page["frame_id"] == "toolFrame" for page in dev_tools["pages"])
assert dev_tools["pages"][0]["tool_src"] == "/admin/pages"
assert all("frame_id" not in page for page in dev_tools["pages"])
assert all("tool_src" not in page for page in dev_tools["pages"])
class TestIsApiPublic:

View File

@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
"""Tests for portal shell routes and navigation API."""
from __future__ import annotations
import json
from mes_dashboard.app import create_app
def _login_as_admin(client) -> None:
with client.session_transaction() as sess:
sess["admin"] = {"displayName": "Admin", "employeeNo": "A001"}
def test_portal_shell_fallback_html_served_when_dist_missing(monkeypatch):
app = create_app("testing")
app.config["TESTING"] = True
# Force fallback path by simulating missing dist file.
monkeypatch.setattr("os.path.exists", lambda *_args, **_kwargs: False)
client = app.test_client()
response = client.get("/portal-shell")
assert response.status_code == 200
html = response.data.decode("utf-8")
assert "/static/dist/portal-shell.js" in html
assert "/static/dist/portal-shell.css" in html
assert "/static/dist/tailwind.css" in html
def test_portal_shell_uses_nested_dist_html_when_top_level_missing(monkeypatch):
app = create_app("testing")
app.config["TESTING"] = True
def fake_exists(path: str) -> bool:
if path.endswith("/dist/portal-shell.html"):
return False
return path.endswith("/dist/src/portal-shell/index.html")
monkeypatch.setattr("os.path.exists", fake_exists)
client = app.test_client()
response = client.get("/portal-shell")
assert response.status_code == 200
html = response.data.decode("utf-8")
assert "/static/dist/portal-shell.js" in html
assert "/static/dist/portal-shell.css" in html
assert "/static/dist/tailwind.css" in html
def test_portal_navigation_non_admin_visibility_matches_release_only():
app = create_app("testing")
app.config["TESTING"] = True
client = app.test_client()
response = client.get("/api/portal/navigation")
assert response.status_code == 200
payload = json.loads(response.data.decode("utf-8"))
assert payload["is_admin"] is False
assert payload["admin_user"] is None
all_routes = {
page["route"]
for drawer in payload["drawers"]
for page in drawer["pages"]
}
# Non-admin baseline from current config.
assert "/wip-overview" in all_routes
assert "/resource" in all_routes
assert "/qc-gate" in all_routes
assert "/resource-history" in all_routes
assert "/job-query" in all_routes
assert "/admin/pages" not in all_routes
assert "/excel-query" not in all_routes
def test_portal_navigation_admin_includes_admin_drawer_routes():
app = create_app("testing")
app.config["TESTING"] = True
client = app.test_client()
_login_as_admin(client)
response = client.get("/api/portal/navigation")
assert response.status_code == 200
payload = json.loads(response.data.decode("utf-8"))
assert payload["is_admin"] is True
assert payload["admin_user"]["displayName"] == "Admin"
all_routes = {
page["route"]
for drawer in payload["drawers"]
for page in drawer["pages"]
}
assert "/admin/pages" in all_routes
assert "/admin/performance" in all_routes
assert "/excel-query" in all_routes
def test_wrapper_telemetry_endpoint_removed_after_wrapper_decommission():
app = create_app("testing")
app.config["TESTING"] = True
client = app.test_client()
response = client.post(
"/api/portal/wrapper-telemetry",
json={
"route": "/job-query",
"event_type": "wrapper_loaded",
},
)
assert response.status_code == 404
def test_navigation_drawer_and_page_order_deterministic_non_admin():
app = create_app("testing")
app.config["TESTING"] = True
client = app.test_client()
response = client.get("/api/portal/navigation")
assert response.status_code == 200
payload = json.loads(response.data.decode("utf-8"))
drawer_ids = [drawer["id"] for drawer in payload["drawers"]]
assert drawer_ids == ["reports", "drawer-2", "drawer"]
reports_routes = [page["route"] for page in payload["drawers"][0]["pages"]]
assert reports_routes == ["/wip-overview", "/resource", "/qc-gate"]
def test_navigation_mixed_release_dev_visibility_admin_vs_non_admin():
app = create_app("testing")
app.config["TESTING"] = True
non_admin_client = app.test_client()
non_admin_resp = non_admin_client.get("/api/portal/navigation")
assert non_admin_resp.status_code == 200
non_admin_payload = json.loads(non_admin_resp.data.decode("utf-8"))
non_admin_routes = {
page["route"]
for drawer in non_admin_payload["drawers"]
for page in drawer["pages"]
}
assert "/hold-overview" not in non_admin_routes
assert "/hold-history" not in non_admin_routes
admin_client = app.test_client()
_login_as_admin(admin_client)
admin_resp = admin_client.get("/api/portal/navigation")
assert admin_resp.status_code == 200
admin_payload = json.loads(admin_resp.data.decode("utf-8"))
admin_routes = {
page["route"]
for drawer in admin_payload["drawers"]
for page in drawer["pages"]
}
assert "/hold-overview" in admin_routes
assert "/hold-history" in admin_routes
def test_legacy_wrapper_routes_are_reachable():
app = create_app("testing")
app.config["TESTING"] = True
client = app.test_client()
_login_as_admin(client)
for route in ["/job-query", "/excel-query", "/query-tool", "/tmtt-defect"]:
response = client.get(route)
assert response.status_code == 200, f"{route} should be reachable"

View File

@@ -6,6 +6,7 @@ required core JavaScript resources.
"""
import unittest
import os
from unittest.mock import patch
from mes_dashboard.app import create_app
@@ -21,12 +22,20 @@ class TestTemplateIntegration(unittest.TestCase):
"""Test that all templates properly extend _base.html."""
def setUp(self):
self._old_portal_spa = os.environ.get("PORTAL_SPA_ENABLED")
os.environ["PORTAL_SPA_ENABLED"] = "false"
db._ENGINE = None
self.app = create_app('testing')
self.app.config['TESTING'] = True
self.client = self.app.test_client()
_login_as_admin(self.client)
def tearDown(self):
if self._old_portal_spa is None:
os.environ.pop("PORTAL_SPA_ENABLED", None)
else:
os.environ["PORTAL_SPA_ENABLED"] = self._old_portal_spa
def test_portal_includes_base_scripts(self):
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
@@ -113,13 +122,21 @@ class TestPortalDynamicDrawerRendering(unittest.TestCase):
"""Test dynamic portal drawer rendering."""
def setUp(self):
self._old_portal_spa = os.environ.get("PORTAL_SPA_ENABLED")
os.environ["PORTAL_SPA_ENABLED"] = "false"
db._ENGINE = None
self.app = create_app('testing')
self.app.config['TESTING'] = True
self.client = self.app.test_client()
_login_as_admin(self.client)
def test_portal_uses_navigation_config_for_sidebar_and_iframes(self):
def tearDown(self):
if self._old_portal_spa is None:
os.environ.pop("PORTAL_SPA_ENABLED", None)
else:
os.environ["PORTAL_SPA_ENABLED"] = self._old_portal_spa
def test_portal_uses_navigation_config_for_sidebar_links_without_iframe(self):
drawers = [
{
"id": "custom",
@@ -132,8 +149,6 @@ class TestPortalDynamicDrawerRendering(unittest.TestCase):
"name": "自訂首頁",
"status": "released",
"order": 1,
"frame_id": "customFrame",
"tool_src": None,
}
],
},
@@ -148,8 +163,6 @@ class TestPortalDynamicDrawerRendering(unittest.TestCase):
"name": "頁面管理",
"status": "dev",
"order": 1,
"frame_id": "toolFrame",
"tool_src": "/admin/pages",
}
],
},
@@ -160,10 +173,11 @@ class TestPortalDynamicDrawerRendering(unittest.TestCase):
self.assertEqual(response.status_code, 200)
html = response.data.decode("utf-8")
self.assertIn("自訂分類", html)
self.assertIn('data-target="customFrame"', html)
self.assertIn('id="customFrame"', html)
self.assertIn('data-tool-src="/admin/pages"', html)
self.assertIn('id="toolFrame"', html)
self.assertIn('href="/wip-overview"', html)
self.assertIn('data-route="/wip-overview"', html)
self.assertIn('href="/admin/pages"', html)
self.assertIn('data-route="/admin/pages"', html)
self.assertNotIn("<iframe", html)
def test_portal_hides_admin_only_drawer_for_non_admin(self):
client = self.app.test_client()
@@ -179,8 +193,6 @@ class TestPortalDynamicDrawerRendering(unittest.TestCase):
"name": "自訂首頁",
"status": "released",
"order": 1,
"frame_id": "customFrame",
"tool_src": None,
}
],
},
@@ -195,8 +207,6 @@ class TestPortalDynamicDrawerRendering(unittest.TestCase):
"name": "頁面管理",
"status": "dev",
"order": 1,
"frame_id": "toolFrame",
"tool_src": "/admin/pages",
}
],
},
@@ -208,19 +218,28 @@ class TestPortalDynamicDrawerRendering(unittest.TestCase):
html = response.data.decode("utf-8")
self.assertIn("自訂分類", html)
self.assertNotIn("開發工具", html)
self.assertNotIn('data-tool-src="/admin/pages"', html)
self.assertNotIn('href="/admin/pages"', html)
self.assertNotIn("<iframe", html)
class TestToastCSSIntegration(unittest.TestCase):
"""Test that Toast CSS styles are included in pages."""
def setUp(self):
self._old_portal_spa = os.environ.get("PORTAL_SPA_ENABLED")
os.environ["PORTAL_SPA_ENABLED"] = "false"
db._ENGINE = None
self.app = create_app('testing')
self.app.config['TESTING'] = True
self.client = self.app.test_client()
_login_as_admin(self.client)
def tearDown(self):
if self._old_portal_spa is None:
os.environ.pop("PORTAL_SPA_ENABLED", None)
else:
os.environ["PORTAL_SPA_ENABLED"] = self._old_portal_spa
def test_portal_includes_toast_css(self):
response = self.client.get('/')
html = response.data.decode('utf-8')