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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user