feat(frontend): convert to live real-time dynamic runtime fetching with force-dynamic and cache no-store

- Set cache: 'no-store' in lib/api.ts to trigger real-time REST API requests on every page load
- Add export const dynamic = 'force-dynamic' across all 6 app pages (/home, /members, /publications, /lectures, /standardization)
- Update REQUIREMENTS.md and E2E test runner to validate live server routes and dynamic runtime fetching
- Pass all 12 quality gates cleanly
This commit is contained in:
2026-08-24 19:25:26 +09:00
parent 9cb6258d3d
commit 5041ab1ea8
10 changed files with 260 additions and 121 deletions
+78 -21
View File
@@ -1,35 +1,94 @@
#!/usr/bin/env python3
"""
Python Unittest Suite for ANL Homepage E2E Verification
Parses prerendered SSG HTML files to assert DOM structure, text content, and theme attributes.
Queries live running Next.js server to assert DOM structure, text content, and theme attributes.
"""
import unittest
import os
import re
import time
import subprocess
import urllib.request
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SERVER_APP_DIR = os.path.join(BASE_DIR, ".next", "server", "app")
if not os.path.exists(SERVER_APP_DIR):
for candidate in [
os.path.join(os.getcwd(), "refer_landing_page", ".next", "server", "app"),
os.path.join(os.getcwd(), "website", ".next", "server", "app"),
os.path.join(os.getcwd(), ".next", "server", "app"),
]:
if os.path.exists(candidate):
SERVER_APP_DIR = candidate
break
TEST_SERVER_URL = os.environ.get("TEST_SERVER_URL", "http://localhost:3000")
class TestANLHomepageE2E(unittest.TestCase):
_server_proc = None
_html_cache = {}
@classmethod
def setUpClass(cls):
# Check if server is already responding at TEST_SERVER_URL
is_ready = False
try:
with urllib.request.urlopen(f"{TEST_SERVER_URL}/", timeout=1) as resp:
if resp.status == 200:
is_ready = True
except Exception:
is_ready = False
if not is_ready:
# Spawn local next start server
cls._server_proc = subprocess.Popen(
["npm", "run", "start"],
cwd=BASE_DIR,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
for _ in range(30):
try:
with urllib.request.urlopen(f"{TEST_SERVER_URL}/", timeout=1) as resp:
if resp.status == 200:
is_ready = True
break
except Exception:
time.sleep(0.5)
if not is_ready:
cls._server_proc.terminate()
raise RuntimeError(f"Next.js server failed to become ready at {TEST_SERVER_URL}")
@classmethod
def tearDownClass(cls):
if cls._server_proc:
cls._server_proc.terminate()
try:
cls._server_proc.wait(timeout=5)
except Exception:
cls._server_proc.kill()
def get_route_html(self, route):
if route in self._html_cache:
return self._html_cache[route]
url = f"{TEST_SERVER_URL}{route}"
req = urllib.request.Request(url, headers={"User-Agent": "E2ETestRunner"})
with urllib.request.urlopen(req, timeout=10) as resp:
html = resp.read().decode("utf-8")
self._html_cache[route] = html
return html
def read_html(self, relative_path):
full_path = os.path.join(SERVER_APP_DIR, relative_path)
self.assertTrue(os.path.exists(full_path), f"File {relative_path} does not exist")
with open(full_path, "r", encoding="utf-8") as f:
return f.read()
route_map = {
"index.html": "/",
"members.html": "/members",
"publications.html": "/publications",
os.path.join("publications", "intl-journal-conf.html"): "/publications/intl-journal-conf",
"publications/intl-journal-conf.html": "/publications/intl-journal-conf",
os.path.join("publications", "domestic-journal-conf.html"): "/publications/domestic-journal-conf",
"publications/domestic-journal-conf.html": "/publications/domestic-journal-conf",
os.path.join("publications", "patent.html"): "/publications/patent",
"publications/patent.html": "/publications/patent",
"lectures.html": "/lectures",
"standardization.html": "/standardization",
}
route = route_map.get(relative_path, f"/{relative_path}")
return self.get_route_html(route)
def test_01_home_page_structure(self):
"""TC-T1-F3: Verify Home page SSG HTML content and semantic structures."""
"""TC-T1-F3: Verify Home page HTML content and semantic structures."""
html = self.read_html("index.html")
# 1. Brand & Header titles
@@ -66,7 +125,7 @@ class TestANLHomepageE2E(unittest.TestCase):
self.assertIn("Project Editor", html)
def test_03_publications_category_routes(self):
"""TC-T1-F5: Verify Publications categories SSG static pages."""
"""TC-T1-F5: Verify Publications categories live dynamic pages."""
cats = [
("intl-journal-conf.html", "International Journal"),
("domestic-journal-conf.html", "Domestic Journal"),
@@ -91,8 +150,6 @@ class TestANLHomepageE2E(unittest.TestCase):
def test_06_theme_css_variables(self):
"""TC-T1-F2: Verify globals.css and built assets contain theme tokens."""
globals_path = os.path.join(BASE_DIR, "app", "globals.css")
if not os.path.exists(globals_path):
globals_path = os.path.join(os.getcwd(), "refer_landing_page", "app", "globals.css")
with open(globals_path, "r", encoding="utf-8") as f:
css = f.read()
@@ -100,7 +157,7 @@ class TestANLHomepageE2E(unittest.TestCase):
self.assertIn(token, css, f"Missing CSS variable {token} in globals.css")
def test_07_research_projects_pageview(self):
"""TC-T1-F7: Verify Research Project PageView content in Home static SSG HTML (AC-27 / AC-31)."""
"""TC-T1-F7: Verify Research Project PageView content in Home HTML."""
html = self.read_html("index.html")
# 1. Project PageView Title & Projects
@@ -118,7 +175,7 @@ class TestANLHomepageE2E(unittest.TestCase):
self.assertIn("IEC TC100: Multimedia Systems and Equipment", html)
self.assertIn("ISO/IEC JTC1/SC6: Reliable Multicasting", html)
# 4. Assert zero placeholder Funder labels (AC-31)
# 4. Assert zero placeholder Funder labels
self.assertNotIn("Funder:", html)
def test_08_publications_highlights_and_category_nav(self):