336 lines
15 KiB
Python
336 lines
15 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from tests.frontend_test_helpers import (
|
|
PAGE_MOUNT_MARKER,
|
|
assembled_frontend_document,
|
|
assembled_frontend_runtime,
|
|
registered_frontend_fragments,
|
|
registered_frontend_runtime_scripts,
|
|
)
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
STATIC = ROOT / "frontend"
|
|
|
|
|
|
class FrontendBoundaryTests(unittest.TestCase):
|
|
def test_shared_api_is_the_only_application_fetch_exit(self) -> None:
|
|
fetch_files = []
|
|
for path in STATIC.rglob("*"):
|
|
if "vendor" in path.parts:
|
|
continue
|
|
if path.suffix not in {".js", ".mjs"}:
|
|
continue
|
|
if re.search(r"\bfetch\s*\(", path.read_text(encoding="utf-8")):
|
|
fetch_files.append(path.relative_to(STATIC).as_posix())
|
|
self.assertEqual(fetch_files, ["bootstrap.js", "shared/api.js"])
|
|
bootstrap = (STATIC / "bootstrap.js").read_text(encoding="utf-8")
|
|
self.assertIn("fetch(fragment.url", bootstrap)
|
|
self.assertNotIn('"/api/', bootstrap)
|
|
|
|
def test_shared_dependencies_load_before_application(self) -> None:
|
|
scripts = registered_frontend_runtime_scripts()
|
|
ui_position = scripts.index('/shared/ui-core.js')
|
|
components_position = next(index for index, value in enumerate(scripts) if value.startswith('/shared/components.js'))
|
|
runtime_position = next(index for index, value in enumerate(scripts) if value.startswith('/pages/runtime.js'))
|
|
state_position = next(index for index, value in enumerate(scripts) if value.startswith('/shared/state.js'))
|
|
api_position = next(index for index, value in enumerate(scripts) if value.startswith('/shared/api.js'))
|
|
shell_position = next(index for index, value in enumerate(scripts) if value.startswith('/shared/shell.js'))
|
|
app_position = next(index for index, value in enumerate(scripts) if value.startswith('/app.js'))
|
|
self.assertLess(ui_position, components_position)
|
|
self.assertLess(components_position, runtime_position)
|
|
self.assertLess(runtime_position, state_position)
|
|
self.assertLess(state_position, api_position)
|
|
self.assertLess(api_position, shell_position)
|
|
self.assertLess(shell_position, app_position)
|
|
|
|
def test_application_state_is_created_through_shared_boundary(self) -> None:
|
|
app = assembled_frontend_runtime()
|
|
self.assertIn("const state = window.XiaobaiState.create({", app)
|
|
self.assertNotIn("const state = {", app)
|
|
|
|
def test_current_runtime_assembly_uses_registered_first_party_sources(self) -> None:
|
|
expected = "\n".join(
|
|
(STATIC / url.split("?", 1)[0].lstrip("/")).read_text(encoding="utf-8")
|
|
for url in registered_frontend_runtime_scripts()
|
|
if not url.startswith("/vendor/")
|
|
)
|
|
app = assembled_frontend_runtime()
|
|
self.assertEqual(app, expected)
|
|
self.assertIn("function openView(", app)
|
|
|
|
def test_historical_frontend_replay_scaffolding_is_retired(self) -> None:
|
|
runtime = "\n".join(
|
|
path.read_text(encoding="utf-8")
|
|
for path in STATIC.rglob("*.js")
|
|
if "vendor" not in path.parts
|
|
)
|
|
helper = (ROOT / "tests" / "frontend_test_helpers.py").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
self.assertNotIn("PRESERVATION-SOURCE-", runtime)
|
|
self.assertNotIn("AUDITED_CSS_RETIREMENTS", helper)
|
|
self.assertNotIn("AUDITED_CSS_REPLACEMENTS", helper)
|
|
self.assertNotIn("AUDITED_FRONTEND_SOURCE_LINE_COUNT", helper)
|
|
self.assertFalse((ROOT / "tools" / "split_frontend_runtime.py").exists())
|
|
|
|
def test_runtime_page_registry_matches_governance_registry(self) -> None:
|
|
expected = json.loads(
|
|
(ROOT / "config" / "pages.config.json").read_text(encoding="utf-8")
|
|
)["pages"]
|
|
runtime = (STATIC / "pages.config.js").read_text(encoding="utf-8")
|
|
rows = re.findall(
|
|
r'^\s*\["([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", (true|false)\],$',
|
|
runtime,
|
|
re.MULTILINE,
|
|
)
|
|
actual = [
|
|
{
|
|
"id": row[0],
|
|
"title": row[1],
|
|
"feature": row[2],
|
|
"group": row[3],
|
|
"access": row[4],
|
|
"default": row[5] == "true",
|
|
"desktop_scroll": "page",
|
|
"mobile_layout": "dedicated",
|
|
}
|
|
for row in rows
|
|
]
|
|
self.assertEqual(actual, expected)
|
|
|
|
def test_shell_owns_navigation_and_page_mounting(self) -> None:
|
|
app = assembled_frontend_runtime()
|
|
entry = (STATIC / "app.js").read_text(encoding="utf-8")
|
|
shell = (STATIC / "shared" / "shell.js").read_text(encoding="utf-8")
|
|
self.assertNotIn("function syncNavigationState", app)
|
|
self.assertNotIn("function initializeApplicationShell", app)
|
|
self.assertIn("function syncNavigation(viewId)", shell)
|
|
self.assertIn("function mount(viewId, mountOptions = {})", shell)
|
|
self.assertIn("function openModalDialog(dialog)", shell)
|
|
self.assertNotIn('document.querySelectorAll(".module-tab").forEach', entry)
|
|
|
|
def test_every_registered_view_has_one_feature_page_module(self) -> None:
|
|
scripts = registered_frontend_runtime_scripts()
|
|
runtime_position = next(index for index, value in enumerate(scripts) if value.startswith('/pages/runtime.js'))
|
|
app_position = next(index for index, value in enumerate(scripts) if value.startswith('/app.js'))
|
|
expected = {
|
|
page["id"]: page["feature"]
|
|
for page in json.loads(
|
|
(ROOT / "config" / "pages.config.json").read_text(encoding="utf-8")
|
|
)["pages"]
|
|
}
|
|
expected["screenerTrackingView"] = "screener"
|
|
actual: dict[str, str] = {}
|
|
for path in (STATIC / "pages").glob("*/page.js"):
|
|
script_url = f'/pages/{path.parent.name}/page.js'
|
|
script_position = next(
|
|
index for index, value in enumerate(scripts) if value.startswith(script_url)
|
|
)
|
|
self.assertLess(runtime_position, script_position)
|
|
self.assertLess(script_position, app_position)
|
|
script = path.read_text(encoding="utf-8")
|
|
for match in re.finditer(
|
|
r'XiaobaiPageModules\.register\("([^"]+)",\s*\[(.*?)\]',
|
|
script,
|
|
re.DOTALL,
|
|
):
|
|
feature = match.group(1)
|
|
for view_id in re.findall(r'"([A-Za-z][A-Za-z0-9]+)"', match.group(2)):
|
|
self.assertNotIn(view_id, actual)
|
|
actual[view_id] = feature
|
|
self.assertEqual(actual, expected)
|
|
|
|
def test_index_is_shell_only_and_has_one_bootstrap_entry(self) -> None:
|
|
html = (STATIC / "index.html").read_text(encoding="utf-8")
|
|
self.assertEqual(html.count(PAGE_MOUNT_MARKER), 1)
|
|
self.assertNotRegex(
|
|
html,
|
|
r'<section id="(?:[A-Za-z][A-Za-z0-9_-]*View|limitPool)" class="workspace-view',
|
|
)
|
|
self.assertEqual(
|
|
re.findall(r'<script[^>]+src="([^"]+)"', html),
|
|
["/bootstrap.js?v=20260803-2"],
|
|
)
|
|
|
|
def test_registered_fragments_are_the_only_page_markup_owners(self) -> None:
|
|
registry = (STATIC / "pages.config.js").read_text(encoding="utf-8")
|
|
rows = re.findall(
|
|
r'^\s*\["([^"]+)", "(/pages/[^"]+/page\.html\?[^\"]+)", \[([^\]]+)\]\],$',
|
|
registry,
|
|
re.MULTILINE,
|
|
)
|
|
self.assertEqual([row[1] for row in rows], registered_frontend_fragments())
|
|
self.assertEqual(len(rows), 12)
|
|
|
|
actual_views = []
|
|
for feature, url, declared_source in rows:
|
|
relative = url.split("?", 1)[0].lstrip("/")
|
|
fragment = (STATIC / relative).read_text(encoding="utf-8")
|
|
self.assertNotIn("<script", fragment, relative)
|
|
declared = re.findall(r'"([A-Za-z][A-Za-z0-9_-]+)"', declared_source)
|
|
roots = re.findall(
|
|
r'<section id="([A-Za-z][A-Za-z0-9_-]*View|limitPool)" class="workspace-view',
|
|
fragment,
|
|
)
|
|
self.assertEqual(roots, declared, feature)
|
|
actual_views.extend(roots)
|
|
|
|
expected = [
|
|
page["id"]
|
|
for page in json.loads(
|
|
(ROOT / "config" / "pages.config.json").read_text(encoding="utf-8")
|
|
)["pages"]
|
|
]
|
|
expected.append("screenerTrackingView")
|
|
self.assertEqual(set(actual_views), set(expected))
|
|
self.assertEqual(len(actual_views), len(set(actual_views)))
|
|
|
|
def test_runtime_script_order_has_one_registered_owner(self) -> None:
|
|
scripts = registered_frontend_runtime_scripts()
|
|
self.assertEqual(len(scripts), len(set(scripts)))
|
|
self.assertEqual(scripts[-1].split("?", 1)[0], "/app.js")
|
|
for url in scripts:
|
|
self.assertTrue((STATIC / url.split("?", 1)[0].lstrip("/")).is_file(), url)
|
|
|
|
bootstrap = (STATIC / "bootstrap.js").read_text(encoding="utf-8")
|
|
self.assertIn("registry.fragments.map(loadPageFragment)", bootstrap)
|
|
self.assertIn("loadRuntimeScripts(registry.runtimeScripts)", bootstrap)
|
|
self.assertIn("script.async = false", bootstrap)
|
|
self.assertIn('document.body.dataset.runtimeReady = "true"', bootstrap)
|
|
self.assertEqual(
|
|
assembled_frontend_document().count('class="workspace-view'),
|
|
17,
|
|
)
|
|
|
|
def test_page_lifecycle_is_owned_outside_application_monolith(self) -> None:
|
|
entry = (STATIC / "app.js").read_text(encoding="utf-8")
|
|
runtime = (STATIC / "pages" / "runtime.js").read_text(encoding="utf-8")
|
|
start = entry.index("function openView(")
|
|
end = entry.index("\n\nif (document.readyState", start)
|
|
open_view = entry[start:end]
|
|
self.assertIn("pageModules.beforeMount(viewId, previousView);", open_view)
|
|
self.assertIn("pageModules.afterMount(viewId, previousView);", open_view)
|
|
self.assertNotRegex(open_view, r'viewId\s*[!=]==?\s*"')
|
|
self.assertIn("function beforeMount(viewId, previousView)", runtime)
|
|
self.assertIn("function afterMount(viewId, previousView)", runtime)
|
|
|
|
def test_application_entry_only_coordinates_startup(self) -> None:
|
|
entry = (STATIC / "app.js").read_text(encoding="utf-8")
|
|
self.assertLessEqual(len(entry.splitlines()), 120)
|
|
for owned_symbol in (
|
|
"XiaobaiState.create",
|
|
"XiaobaiAPI.configure",
|
|
"function loadDashboard(",
|
|
"function openAdminSettings(",
|
|
"function syncThemeControl(",
|
|
"function initializeAutoTableSorting(",
|
|
):
|
|
with self.subTest(owned_symbol=owned_symbol):
|
|
self.assertNotIn(owned_symbol, entry)
|
|
binding = re.search(r"function bindEvents\(\) \{(.*?)\n\}", entry, re.DOTALL)
|
|
self.assertIsNotNone(binding)
|
|
self.assertIn("pageModules.bind();", binding.group(1))
|
|
self.assertNotIn("addEventListener", binding.group(1))
|
|
self.assertNotIn("querySelector", binding.group(1))
|
|
|
|
def test_shared_runtime_responsibilities_have_single_owners(self) -> None:
|
|
owners = {
|
|
"context.js": "XiaobaiState.create",
|
|
"application.js": "XiaobaiAPI.configure",
|
|
"dashboard.js": "function loadDashboard(",
|
|
"admin.js": "function openAdminSettings(",
|
|
"theme.js": "function syncThemeControl(",
|
|
"table.js": "function initializeAutoTableSorting(",
|
|
}
|
|
runtime_sources = {
|
|
path.name: path.read_text(encoding="utf-8")
|
|
for path in (STATIC / "shared").glob("*.js")
|
|
}
|
|
for owner, symbol in owners.items():
|
|
with self.subTest(owner=owner, symbol=symbol):
|
|
self.assertIn(symbol, runtime_sources[owner])
|
|
self.assertEqual(
|
|
[name for name, source in runtime_sources.items() if symbol in source],
|
|
[owner],
|
|
)
|
|
|
|
def test_each_feature_owns_its_control_binding(self) -> None:
|
|
expected = {
|
|
"auction": "bindAuctionEvents",
|
|
"dragon-tiger": "bindDragonTigerEvents",
|
|
"heaven": "bindHeavenEvents",
|
|
"ladder": "bindLadderEvents",
|
|
"mentor": "bindMentorEvents",
|
|
"pools": "bindPoolEvents",
|
|
"popularity": "bindPopularityEvents",
|
|
"review": "bindReviewEvents",
|
|
"rotation": "bindRotationEvents",
|
|
"screener": "bindScreenerEvents",
|
|
"sentiment": "bindSentimentEvents",
|
|
"themes": "bindThemeLibraryEvents",
|
|
}
|
|
for feature, binder in expected.items():
|
|
with self.subTest(feature=feature):
|
|
source = (STATIC / "pages" / feature / "page.js").read_text(encoding="utf-8")
|
|
self.assertIn(f"bind: {binder}", source)
|
|
self.assertIn(f"function {binder}()", source)
|
|
market = (STATIC / "pages" / "market" / "bindings.js").read_text(encoding="utf-8")
|
|
self.assertIn("function bindMarketEvents()", market)
|
|
|
|
def test_market_runtime_has_narrow_registered_owners(self) -> None:
|
|
expected = [
|
|
"/pages/market/breadth.js",
|
|
"/pages/market/charts.js",
|
|
"/pages/market/entity-detail.js",
|
|
"/pages/market/stock-detail.js",
|
|
"/pages/market/preview.js",
|
|
"/pages/market/search.js",
|
|
"/pages/market/bindings.js",
|
|
]
|
|
registered = [
|
|
url.split("?", 1)[0]
|
|
for url in registered_frontend_runtime_scripts()
|
|
if url.startswith("/pages/market/")
|
|
]
|
|
self.assertEqual(registered, expected)
|
|
self.assertFalse((STATIC / "pages" / "market" / "runtime.js").exists())
|
|
owners = {
|
|
"breadth.js": "function renderMarketBreadth(",
|
|
"charts.js": "function currentChartPalette(",
|
|
"entity-detail.js": "function openEntityDetail(",
|
|
"stock-detail.js": "function openStock(",
|
|
"preview.js": "function showStockPreview(",
|
|
"search.js": "function openGlobalSearch(",
|
|
"bindings.js": "function bindMarketEvents(",
|
|
}
|
|
sources = {
|
|
path.name: path.read_text(encoding="utf-8")
|
|
for path in (STATIC / "pages" / "market").glob("*.js")
|
|
}
|
|
for owner, symbol in owners.items():
|
|
self.assertEqual(
|
|
[name for name, source in sources.items() if symbol in source],
|
|
[owner],
|
|
)
|
|
for relative in expected:
|
|
source = (STATIC / relative.lstrip("/")).read_text(encoding="utf-8")
|
|
self.assertLessEqual(len(source.splitlines()), 500, relative)
|
|
|
|
def test_shared_empty_state_component_is_used_by_multiple_features(self) -> None:
|
|
components = (STATIC / "shared" / "components.js").read_text(encoding="utf-8")
|
|
app = assembled_frontend_runtime()
|
|
self.assertIn("function emptyStateHtml(message, options = {})", components)
|
|
self.assertIn("function renderEmptyState(target, message, options = {})", components)
|
|
self.assertGreaterEqual(app.count("renderEmptyState("), 8)
|
|
self.assertGreaterEqual(app.count("emptyStateHtml("), 8)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|