Replace hardcoded sidebar drawer configuration with admin-manageable dynamic system. Extend page_status.json with drawer definitions and page assignments, add drawer CRUD API endpoints, render portal sidebar via Jinja2 loops, and extend /admin/pages UI with drawer management. Fix multi-worker cache invalidation via mtime-based staleness detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
372 lines
13 KiB
Python
372 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Unit tests for template integration with _base.html.
|
|
|
|
Verifies that all templates properly extend _base.html and include
|
|
required core JavaScript resources.
|
|
"""
|
|
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from mes_dashboard.app import create_app
|
|
import mes_dashboard.core.database as db
|
|
|
|
|
|
def _login_as_admin(client):
|
|
with client.session_transaction() as sess:
|
|
sess['admin'] = {'displayName': 'Test Admin', 'employeeNo': 'A001'}
|
|
|
|
|
|
class TestTemplateIntegration(unittest.TestCase):
|
|
"""Test that all templates properly extend _base.html."""
|
|
|
|
def setUp(self):
|
|
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_includes_base_scripts(self):
|
|
response = self.client.get('/')
|
|
self.assertEqual(response.status_code, 200)
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('toast.js', html)
|
|
self.assertIn('mes-api.js', html)
|
|
self.assertIn('mes-toast-container', html)
|
|
|
|
def test_wip_overview_includes_base_scripts(self):
|
|
response = self.client.get('/wip-overview')
|
|
self.assertEqual(response.status_code, 200)
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('toast.js', html)
|
|
self.assertIn('mes-api.js', html)
|
|
self.assertIn('mes-toast-container', html)
|
|
|
|
def test_wip_detail_includes_base_scripts(self):
|
|
response = self.client.get('/wip-detail')
|
|
self.assertEqual(response.status_code, 200)
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('toast.js', html)
|
|
self.assertIn('mes-api.js', html)
|
|
self.assertIn('mes-toast-container', html)
|
|
|
|
def test_tables_page_includes_base_scripts(self):
|
|
response = self.client.get('/tables')
|
|
self.assertEqual(response.status_code, 200)
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('toast.js', html)
|
|
self.assertIn('mes-api.js', html)
|
|
self.assertIn('mes-toast-container', html)
|
|
|
|
def test_resource_page_includes_base_scripts(self):
|
|
response = self.client.get('/resource')
|
|
self.assertEqual(response.status_code, 200)
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('toast.js', html)
|
|
self.assertIn('mes-api.js', html)
|
|
self.assertIn('mes-toast-container', html)
|
|
|
|
def test_excel_query_page_includes_base_scripts(self):
|
|
response = self.client.get('/excel-query')
|
|
self.assertEqual(response.status_code, 200)
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('toast.js', html)
|
|
self.assertIn('mes-api.js', html)
|
|
self.assertIn('mes-toast-container', html)
|
|
|
|
def test_query_tool_page_includes_base_scripts(self):
|
|
response = self.client.get('/query-tool')
|
|
self.assertEqual(response.status_code, 200)
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('toast.js', html)
|
|
self.assertIn('mes-api.js', html)
|
|
self.assertIn('mes-toast-container', html)
|
|
|
|
def test_tmtt_defect_page_includes_base_scripts(self):
|
|
response = self.client.get('/tmtt-defect')
|
|
self.assertEqual(response.status_code, 200)
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('toast.js', html)
|
|
self.assertIn('mes-api.js', html)
|
|
self.assertIn('mes-toast-container', html)
|
|
|
|
|
|
class TestPortalDynamicDrawerRendering(unittest.TestCase):
|
|
"""Test dynamic portal drawer rendering."""
|
|
|
|
def setUp(self):
|
|
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):
|
|
drawers = [
|
|
{
|
|
"id": "custom",
|
|
"name": "自訂分類",
|
|
"order": 1,
|
|
"admin_only": False,
|
|
"pages": [
|
|
{
|
|
"route": "/wip-overview",
|
|
"name": "自訂首頁",
|
|
"status": "released",
|
|
"order": 1,
|
|
"frame_id": "customFrame",
|
|
"tool_src": None,
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"id": "dev-tools",
|
|
"name": "開發工具",
|
|
"order": 2,
|
|
"admin_only": True,
|
|
"pages": [
|
|
{
|
|
"route": "/admin/pages",
|
|
"name": "頁面管理",
|
|
"status": "dev",
|
|
"order": 1,
|
|
"frame_id": "toolFrame",
|
|
"tool_src": "/admin/pages",
|
|
}
|
|
],
|
|
},
|
|
]
|
|
with patch("mes_dashboard.app.get_navigation_config", return_value=drawers):
|
|
response = self.client.get("/")
|
|
|
|
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)
|
|
|
|
def test_portal_hides_admin_only_drawer_for_non_admin(self):
|
|
client = self.app.test_client()
|
|
drawers = [
|
|
{
|
|
"id": "custom",
|
|
"name": "自訂分類",
|
|
"order": 1,
|
|
"admin_only": False,
|
|
"pages": [
|
|
{
|
|
"route": "/wip-overview",
|
|
"name": "自訂首頁",
|
|
"status": "released",
|
|
"order": 1,
|
|
"frame_id": "customFrame",
|
|
"tool_src": None,
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"id": "dev-tools",
|
|
"name": "開發工具",
|
|
"order": 2,
|
|
"admin_only": True,
|
|
"pages": [
|
|
{
|
|
"route": "/admin/pages",
|
|
"name": "頁面管理",
|
|
"status": "dev",
|
|
"order": 1,
|
|
"frame_id": "toolFrame",
|
|
"tool_src": "/admin/pages",
|
|
}
|
|
],
|
|
},
|
|
]
|
|
with patch("mes_dashboard.app.get_navigation_config", return_value=drawers):
|
|
response = client.get("/")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
html = response.data.decode("utf-8")
|
|
self.assertIn("自訂分類", html)
|
|
self.assertNotIn("開發工具", html)
|
|
self.assertNotIn('data-tool-src="/admin/pages"', html)
|
|
|
|
|
|
class TestToastCSSIntegration(unittest.TestCase):
|
|
"""Test that Toast CSS styles are included in pages."""
|
|
|
|
def setUp(self):
|
|
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_includes_toast_css(self):
|
|
response = self.client.get('/')
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('.mes-toast-container', html)
|
|
self.assertIn('.mes-toast', html)
|
|
|
|
def test_wip_overview_includes_toast_css(self):
|
|
response = self.client.get('/wip-overview')
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('.mes-toast-container', html)
|
|
self.assertIn('.mes-toast', html)
|
|
|
|
def test_wip_detail_includes_toast_css(self):
|
|
response = self.client.get('/wip-detail')
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('.mes-toast-container', html)
|
|
self.assertIn('.mes-toast', html)
|
|
|
|
|
|
class TestMesApiUsageInTemplates(unittest.TestCase):
|
|
"""Test that templates either inline MesApi usage or load Vite modules."""
|
|
|
|
def setUp(self):
|
|
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_wip_overview_uses_mesapi(self):
|
|
response = self.client.get('/wip-overview')
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertTrue('MesApi.get' in html or '/static/dist/wip-overview.js' in html)
|
|
self.assertNotIn('fetchWithTimeout', html)
|
|
|
|
def test_wip_detail_uses_mesapi(self):
|
|
response = self.client.get('/wip-detail')
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertTrue('MesApi.get' in html or '/static/dist/wip-detail.js' in html)
|
|
self.assertNotIn('fetchWithTimeout', html)
|
|
|
|
def test_tables_page_uses_mesapi_or_vite_module(self):
|
|
response = self.client.get('/tables')
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertTrue('MesApi.post' in html or '/static/dist/tables.js' in html)
|
|
|
|
def test_resource_page_uses_mesapi_or_vite_module(self):
|
|
response = self.client.get('/resource')
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertTrue(
|
|
'MesApi.post' in html or
|
|
'MesApi.get' in html or
|
|
'/static/dist/resource-status.js' in html
|
|
)
|
|
|
|
def test_query_tool_page_uses_vite_module(self):
|
|
response = self.client.get('/query-tool')
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('/static/dist/query-tool.js', html)
|
|
self.assertIn('type="module"', html)
|
|
|
|
def test_tmtt_defect_page_uses_vite_module(self):
|
|
response = self.client.get('/tmtt-defect')
|
|
html = response.data.decode('utf-8')
|
|
|
|
self.assertIn('/static/dist/tmtt-defect.js', html)
|
|
self.assertIn('type="module"', html)
|
|
|
|
|
|
class TestViteModuleIntegration(unittest.TestCase):
|
|
"""Ensure page templates render Vite module assets."""
|
|
|
|
def setUp(self):
|
|
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_pages_render_vite_module_reference(self):
|
|
endpoints_and_assets = [
|
|
('/wip-overview', 'wip-overview.js'),
|
|
('/wip-detail', 'wip-detail.js'),
|
|
('/hold-detail?reason=test-reason', 'hold-detail.js'),
|
|
('/tables', 'tables.js'),
|
|
('/resource', 'resource-status.js'),
|
|
('/resource-history', 'resource-history.js'),
|
|
('/job-query', 'job-query.js'),
|
|
('/excel-query', 'excel-query.js'),
|
|
('/query-tool', 'query-tool.js'),
|
|
('/tmtt-defect', 'tmtt-defect.js'),
|
|
]
|
|
for endpoint, asset in endpoints_and_assets:
|
|
with patch('mes_dashboard.app.os.path.exists', return_value=False):
|
|
response = self.client.get(endpoint)
|
|
self.assertEqual(response.status_code, 200)
|
|
html = response.data.decode('utf-8')
|
|
self.assertIn(f'/static/dist/{asset}', html)
|
|
self.assertIn('type="module"', html)
|
|
|
|
|
|
class TestStaticFilesServing(unittest.TestCase):
|
|
"""Test that static JavaScript files are served correctly."""
|
|
|
|
def setUp(self):
|
|
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_toast_js_is_served(self):
|
|
response = self.client.get('/static/js/toast.js')
|
|
self.assertEqual(response.status_code, 200)
|
|
content = response.data.decode('utf-8')
|
|
|
|
self.assertIn('Toast', content)
|
|
self.assertIn('info', content)
|
|
self.assertIn('success', content)
|
|
self.assertIn('error', content)
|
|
self.assertIn('loading', content)
|
|
|
|
def test_mes_api_js_is_served(self):
|
|
response = self.client.get('/static/js/mes-api.js')
|
|
self.assertEqual(response.status_code, 200)
|
|
content = response.data.decode('utf-8')
|
|
|
|
self.assertIn('MesApi', content)
|
|
self.assertIn('get', content)
|
|
self.assertIn('post', content)
|
|
self.assertIn('AbortController', content)
|
|
|
|
def test_toast_js_contains_retry_button(self):
|
|
response = self.client.get('/static/js/toast.js')
|
|
content = response.data.decode('utf-8')
|
|
|
|
self.assertIn('retry', content)
|
|
self.assertIn('mes-toast-retry', content)
|
|
|
|
def test_mes_api_js_has_exponential_backoff(self):
|
|
response = self.client.get('/static/js/mes-api.js')
|
|
content = response.data.decode('utf-8')
|
|
|
|
self.assertIn('1000', content)
|
|
self.assertIn('retry', content.lower())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|