migration: preserve frontend shell pages and styles

This commit is contained in:
leefer
2026-07-31 15:08:57 +08:00
parent 26e67b3a92
commit 66c22a5449
64 changed files with 9918 additions and 9443 deletions
+1
View File
@@ -0,0 +1 @@
"""Test support package for the preserved application."""
+75
View File
@@ -0,0 +1,75 @@
from __future__ import annotations
import hashlib
import re
from pathlib import Path
APP_ROOT = Path(__file__).resolve().parents[1]
ORIGINAL_ROOT = APP_ROOT.parent
FRONTEND_ROOT = APP_ROOT / "frontend"
ORIGINAL_STATIC = ORIGINAL_ROOT / "static"
SOURCE_RANGE = re.compile(
r"/\* PRESERVATION-SOURCE-BEGIN app\.js:(\d+)-(\d+) \*/\n"
r"(.*?)"
r"/\* PRESERVATION-SOURCE-END app\.js:\1-\2 \*/\n?",
re.DOTALL,
)
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def reassembled_frontend_runtime() -> str:
chunks: dict[tuple[int, int], str] = {}
for path in FRONTEND_ROOT.rglob("*.js"):
for match in SOURCE_RANGE.finditer(path.read_text(encoding="utf-8")):
source_range = (int(match.group(1)), int(match.group(2)))
if source_range in chunks:
raise AssertionError(f"duplicate app.js source range: {source_range}")
chunks[source_range] = match.group(3)
assembled: list[str] = []
next_line = 1
for (start, end), content in sorted(chunks.items()):
if start != next_line:
raise AssertionError(
f"app.js source coverage gap: expected line {next_line}, got {start}"
)
if len(content.splitlines(keepends=True)) != end - start + 1:
raise AssertionError(f"app.js line count changed in range {start}-{end}")
assembled.append(content)
next_line = end + 1
original_line_count = len(
(ORIGINAL_STATIC / "app.js").read_text(encoding="utf-8").splitlines()
)
if next_line != original_line_count + 1:
raise AssertionError(
f"app.js source coverage ended at {next_line - 1}, expected {original_line_count}"
)
return "".join(assembled)
def assert_moved_asset_matches(
testcase,
original_relative: str,
frontend_relative: str | None = None,
) -> None:
target_relative = frontend_relative or original_relative
testcase.assertEqual(
sha256(FRONTEND_ROOT / target_relative),
sha256(ORIGINAL_STATIC / original_relative),
original_relative,
)
def assert_page_prefix_matches(testcase, page_relative: str) -> None:
original = (ORIGINAL_STATIC / page_relative).read_text(encoding="utf-8")
migrated = (FRONTEND_ROOT / page_relative).read_text(encoding="utf-8")
testcase.assertTrue(
migrated.startswith(original.rstrip("\n") + "\n\n"),
page_relative,
)
+13 -13
View File
@@ -6,14 +6,14 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
STATIC = ROOT / "static"
STATIC = ROOT / "frontend"
TOKENS = STATIC / "shared" / "tokens.css"
LEGACY_STYLESHEETS = (
"styles.css",
"renovation.css",
"redesign-v2.css",
"design-system.css",
"theme.css",
"styles/styles.css",
"styles/renovation.css",
"styles/redesign-v2.css",
"styles/design-system.css",
"styles/theme.css",
)
@@ -26,12 +26,12 @@ class CssGovernanceTests(unittest.TestCase):
def test_token_layer_loads_before_application_styles(self) -> None:
expected_order = (
"/shared/tokens.css",
"/styles.css",
"/renovation.css",
"/redesign-v2.css",
"/design-system.css",
"/theme.css",
"/wentian-v2.css",
"/styles/styles.css",
"/styles/renovation.css",
"/styles/redesign-v2.css",
"/styles/design-system.css",
"/styles/theme.css",
"/pages/heaven/page.css",
)
positions = [self.html.index(path) for path in expected_order]
self.assertEqual(positions, sorted(positions))
@@ -63,7 +63,7 @@ class CssGovernanceTests(unittest.TestCase):
def test_wentian_tokens_remain_isolated(self) -> None:
self.assertNotRegex(self.tokens, r"--wt-[a-z0-9-]+\s*:")
wentian = (STATIC / "wentian-v2.css").read_text(encoding="utf-8")
wentian = (STATIC / "pages" / "heaven" / "page.css").read_text(encoding="utf-8")
self.assertRegex(wentian, r"--wt-[a-z0-9-]+\s*:")
def test_compatibility_aliases_cover_historical_layers(self) -> None:
+8 -6
View File
@@ -5,9 +5,11 @@ import re
import unittest
from pathlib import Path
from tests.preservation_helpers import reassembled_frontend_runtime
ROOT = Path(__file__).resolve().parents[1]
STATIC = ROOT / "static"
STATIC = ROOT / "frontend"
class FrontendBoundaryTests(unittest.TestCase):
@@ -22,7 +24,7 @@ class FrontendBoundaryTests(unittest.TestCase):
def test_shared_dependencies_load_before_application(self) -> None:
html = (STATIC / "index.html").read_text(encoding="utf-8")
ui_position = html.index('/ui-core.js')
ui_position = html.index('/shared/ui-core.js')
components_position = html.index('/shared/components.js')
pages_position = html.index('/pages.config.js')
runtime_position = html.index('/pages/runtime.js')
@@ -40,7 +42,7 @@ class FrontendBoundaryTests(unittest.TestCase):
self.assertLess(shell_position, app_position)
def test_application_state_is_created_through_shared_boundary(self) -> None:
app = (STATIC / "app.js").read_text(encoding="utf-8")
app = reassembled_frontend_runtime()
self.assertIn("const state = window.XiaobaiState.create({", app)
self.assertNotIn("const state = {", app)
@@ -70,7 +72,7 @@ class FrontendBoundaryTests(unittest.TestCase):
self.assertEqual(actual, expected)
def test_shell_owns_navigation_and_page_mounting(self) -> None:
app = (STATIC / "app.js").read_text(encoding="utf-8")
app = reassembled_frontend_runtime()
shell = (STATIC / "shared" / "shell.js").read_text(encoding="utf-8")
self.assertNotIn("function syncNavigationState", app)
self.assertNotIn("function initializeApplicationShell", app)
@@ -109,7 +111,7 @@ class FrontendBoundaryTests(unittest.TestCase):
self.assertEqual(actual, expected)
def test_page_lifecycle_is_owned_outside_application_monolith(self) -> None:
app = (STATIC / "app.js").read_text(encoding="utf-8")
app = reassembled_frontend_runtime()
runtime = (STATIC / "pages" / "runtime.js").read_text(encoding="utf-8")
start = app.index("function openView(")
end = app.index("\nfunction initializeAutoTableSorting", start)
@@ -122,7 +124,7 @@ class FrontendBoundaryTests(unittest.TestCase):
def test_shared_empty_state_component_is_used_by_multiple_features(self) -> None:
components = (STATIC / "shared" / "components.js").read_text(encoding="utf-8")
app = (STATIC / "app.js").read_text(encoding="utf-8")
app = reassembled_frontend_runtime()
self.assertIn("function emptyStateHtml(message, options = {})", components)
self.assertIn("function renderEmptyState(target, message, options = {})", components)
self.assertGreaterEqual(app.count("renderEmptyState("), 8)
+11 -9
View File
@@ -5,8 +5,10 @@ import unittest
from html.parser import HTMLParser
from pathlib import Path
from tests.preservation_helpers import reassembled_frontend_runtime
STATIC_DIR = Path(__file__).resolve().parents[1] / "static"
STATIC_DIR = Path(__file__).resolve().parents[1] / "frontend"
class IdCollector(HTMLParser):
@@ -22,11 +24,11 @@ class FrontendContractTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.html = (STATIC_DIR / "index.html").read_text(encoding="utf-8")
cls.script = (STATIC_DIR / "app.js").read_text(encoding="utf-8")
cls.script = reassembled_frontend_runtime()
cls.shell = (STATIC_DIR / "shared" / "shell.js").read_text(encoding="utf-8")
cls.ui_core = (STATIC_DIR / "ui-core.js").read_text(encoding="utf-8")
cls.design_system = (STATIC_DIR / "design-system.css").read_text(encoding="utf-8")
cls.theme = (STATIC_DIR / "theme.css").read_text(encoding="utf-8")
cls.ui_core = (STATIC_DIR / "shared" / "ui-core.js").read_text(encoding="utf-8")
cls.design_system = (STATIC_DIR / "styles" / "design-system.css").read_text(encoding="utf-8")
cls.theme = (STATIC_DIR / "styles" / "theme.css").read_text(encoding="utf-8")
cls.tokens = (STATIC_DIR / "shared" / "tokens.css").read_text(encoding="utf-8")
collector = IdCollector()
collector.feed(cls.html)
@@ -158,7 +160,7 @@ class FrontendContractTests(unittest.TestCase):
self.assertIn("必需数据已完整,本日没有股票同时满足", self.script)
def test_dialogs_and_dark_table_hover_have_shared_safety_constraints(self):
redesign = (STATIC_DIR / "redesign-v2.css").read_text(encoding="utf-8")
redesign = (STATIC_DIR / "styles" / "redesign-v2.css").read_text(encoding="utf-8")
self.assertIn(".settings-dialog:not(.heaven-reading-dialog)[open] { margin: auto; }", redesign)
self.assertIn("max-height: min(760px, calc(100dvh - 28px));", redesign)
self.assertIn('#reviewWorkspaceView .data-table tbody tr:hover td', self.theme)
@@ -166,8 +168,8 @@ class FrontendContractTests(unittest.TestCase):
self.assertIn('#screenerView .screener-result-frame tbody tr:hover td:last-child', self.theme)
def test_global_toast_has_one_owner_and_cannot_stretch_between_insets(self):
styles = (STATIC_DIR / "styles.css").read_text(encoding="utf-8")
wentian = (STATIC_DIR / "wentian-v2.css").read_text(encoding="utf-8")
styles = (STATIC_DIR / "styles" / "styles.css").read_text(encoding="utf-8")
wentian = (STATIC_DIR / "pages" / "heaven" / "page.css").read_text(encoding="utf-8")
self.assertIn("#toast.toast {", styles)
self.assertIn("top: auto;", styles)
self.assertIn("left: auto;", styles)
@@ -183,7 +185,7 @@ class FrontendContractTests(unittest.TestCase):
def test_shared_ui_core_loads_before_application(self):
self.assertLess(
self.html.index('<script src="/ui-core.js"'),
self.html.index('<script src="/shared/ui-core.js"'),
self.html.index('<script src="/app.js'),
)
for function_name in (
+3 -2
View File
@@ -4,6 +4,7 @@ import unittest
from pathlib import Path
from server import DashboardService
from tests.preservation_helpers import reassembled_frontend_runtime
class SearchDatabaseStub:
@@ -77,9 +78,9 @@ class GlobalSearchTests(unittest.TestCase):
)
def test_frontend_reuses_full_stock_detail_and_renders_market_daily_k(self):
static_dir = Path(__file__).resolve().parents[1] / "static"
static_dir = Path(__file__).resolve().parents[1] / "frontend"
html = (static_dir / "index.html").read_text(encoding="utf-8")
script = (static_dir / "app.js").read_text(encoding="utf-8")
script = reassembled_frontend_runtime()
self.assertIn('id="globalSearchButton"', html)
self.assertIn('id="globalSearchDialog"', html)
+1 -1
View File
@@ -23,7 +23,7 @@ class AccountSliceStructureTests(unittest.TestCase):
def test_runtime_paths_still_point_at_app_root(self) -> None:
self.assertEqual(APP_DIR, Path(__file__).resolve().parents[1])
self.assertEqual(STATIC_DIR, APP_DIR / "static")
self.assertEqual(STATIC_DIR, APP_DIR / "frontend")
def test_account_persistence_and_http_transport_have_single_owners(self) -> None:
for method in (
+100
View File
@@ -0,0 +1,100 @@
from __future__ import annotations
import re
import unittest
from backend.bootstrap.config import APP_DIR, STATIC_DIR
from tests.preservation_helpers import (
FRONTEND_ROOT,
ORIGINAL_STATIC,
assert_moved_asset_matches,
assert_page_prefix_matches,
reassembled_frontend_runtime,
)
class FrontendPreservationSliceTests(unittest.TestCase):
def test_split_runtime_reassembles_to_the_exact_original(self) -> None:
self.assertEqual(
reassembled_frontend_runtime(),
(ORIGINAL_STATIC / "app.js").read_text(encoding="utf-8"),
)
def test_index_diff_is_limited_to_asset_relocation_and_split_loading(self) -> None:
migrated = (FRONTEND_ROOT / "index.html").read_text(encoding="utf-8")
restored = migrated
for current, original in (
("/styles/styles.css", "/styles.css"),
("/styles/renovation.css", "/renovation.css"),
("/styles/redesign-v2.css", "/redesign-v2.css"),
("/styles/design-system.css", "/design-system.css"),
("/styles/theme.css", "/theme.css"),
("/pages/heaven/page.css", "/wentian-v2.css"),
("/shared/ui-core.js", "/ui-core.js"),
("/pages/heaven/loading-v2.js", "/heaven-loading-v2.js"),
):
restored = restored.replace(current, original)
restored = restored.replace(
' <script src="/pages/market/runtime.js?v=20260731-1" defer></script>\n',
"",
)
restored = restored.replace(
' <script src="/shared/export.js?v=20260731-1" defer></script>\n',
"",
)
self.assertEqual(
restored,
(ORIGINAL_STATIC / "index.html").read_text(encoding="utf-8"),
)
def test_complete_stylesheet_stack_is_byte_identical_after_relocation(self) -> None:
for original, migrated in (
("shared/tokens.css", "shared/tokens.css"),
("styles.css", "styles/styles.css"),
("renovation.css", "styles/renovation.css"),
("redesign-v2.css", "styles/redesign-v2.css"),
("design-system.css", "styles/design-system.css"),
("theme.css", "styles/theme.css"),
("wentian-v2.css", "pages/heaven/page.css"),
):
with self.subTest(asset=original):
assert_moved_asset_matches(self, original, migrated)
def test_shared_vendor_and_animation_assets_are_byte_identical(self) -> None:
for original, migrated in (
("ui-core.js", "shared/ui-core.js"),
("heaven-loading-v2.js", "pages/heaven/loading-v2.js"),
("heaven-loading.js", "heaven-loading.js"),
("vendor/lucide.min.js", "vendor/lucide.min.js"),
("pages.config.js", "pages.config.js"),
("pages/runtime.js", "pages/runtime.js"),
("shared/api.js", "shared/api.js"),
("shared/components.js", "shared/components.js"),
("shared/shell.js", "shared/shell.js"),
("shared/state.js", "shared/state.js"),
):
with self.subTest(asset=original):
assert_moved_asset_matches(self, original, migrated)
def test_original_page_registration_prefixes_are_preserved(self) -> None:
for path in sorted((ORIGINAL_STATIC / "pages").glob("*/page.js")):
relative = path.relative_to(ORIGINAL_STATIC).as_posix()
with self.subTest(page=relative):
assert_page_prefix_matches(self, relative)
def test_frontend_is_the_only_served_static_root(self) -> None:
self.assertEqual(STATIC_DIR, APP_DIR / "frontend")
self.assertFalse((APP_DIR / "static").exists())
def test_shared_api_remains_the_only_fetch_exit(self) -> None:
consumers = []
for path in FRONTEND_ROOT.rglob("*.js"):
if "vendor" in path.parts:
continue
if re.search(r"\bfetch\s*\(", path.read_text(encoding="utf-8")):
consumers.append(path.relative_to(FRONTEND_ROOT).as_posix())
self.assertEqual(consumers, ["shared/api.js"])
if __name__ == "__main__":
unittest.main()
@@ -5,6 +5,13 @@ import hashlib
import unittest
from pathlib import Path
from tests.preservation_helpers import (
ORIGINAL_STATIC,
assert_moved_asset_matches,
assert_page_prefix_matches,
reassembled_frontend_runtime,
)
APP_ROOT = Path(__file__).resolve().parents[1]
ORIGINAL_ROOT = APP_ROOT.parent
@@ -73,19 +80,17 @@ class LadderRotationSliceSourceEquivalenceTests(unittest.TestCase):
)
def test_api_and_frontend_assets_are_unchanged(self) -> None:
for relative in (
"config/api.config.json",
"static/index.html",
"static/app.js",
"static/styles.css",
"static/pages/ladder/page.js",
"static/pages/rotation/page.js",
):
self.assertEqual(
sha256(APP_ROOT / relative),
sha256(ORIGINAL_ROOT / relative),
relative,
)
self.assertEqual(
sha256(APP_ROOT / "config/api.config.json"),
sha256(ORIGINAL_ROOT / "config/api.config.json"),
)
self.assertEqual(
reassembled_frontend_runtime(),
(ORIGINAL_STATIC / "app.js").read_text(encoding="utf-8"),
)
assert_moved_asset_matches(self, "styles.css", "styles/styles.css")
for page in ("pages/ladder/page.js", "pages/rotation/page.js"):
assert_page_prefix_matches(self, page)
if __name__ == "__main__":
+17 -14
View File
@@ -13,6 +13,11 @@ from backend.data import realtime
from backend.data.providers import ifind_client as canonical_ifind
from backend.data.providers import tushare_client as canonical_tushare
from backend.features.market import charts
from tests.preservation_helpers import (
ORIGINAL_STATIC,
assert_moved_asset_matches,
reassembled_frontend_runtime,
)
APP_ROOT = Path(__file__).resolve().parents[1]
@@ -146,21 +151,19 @@ class MarketSliceSourceEquivalenceTests(unittest.TestCase):
top_level_definitions(APP_ROOT / "backend/features/market/charts.py"),
)
def test_unchanged_frontend_assets_match_the_original(self) -> None:
for relative in (
"index.html",
"app.js",
"styles.css",
"renovation.css",
"redesign-v2.css",
"theme.css",
"wentian-v2.css",
def test_relocated_frontend_preserves_original_market_runtime_and_styles(self) -> None:
self.assertEqual(
reassembled_frontend_runtime(),
(ORIGINAL_STATIC / "app.js").read_text(encoding="utf-8"),
)
for original, migrated in (
("styles.css", "styles/styles.css"),
("renovation.css", "styles/renovation.css"),
("redesign-v2.css", "styles/redesign-v2.css"),
("theme.css", "styles/theme.css"),
("wentian-v2.css", "pages/heaven/page.css"),
):
self.assertEqual(
sha256(APP_ROOT / "static" / relative),
sha256(ORIGINAL_ROOT / "static" / relative),
relative,
)
assert_moved_asset_matches(self, original, migrated)
if __name__ == "__main__":
@@ -7,6 +7,12 @@ from pathlib import Path
import market_insights
from backend.features.market import insights as canonical_insights
from tests.preservation_helpers import (
ORIGINAL_STATIC,
assert_moved_asset_matches,
assert_page_prefix_matches,
reassembled_frontend_runtime,
)
APP_ROOT = Path(__file__).resolve().parents[1]
@@ -174,21 +180,22 @@ class MarketInsightsSliceSourceEquivalenceTests(unittest.TestCase):
self.assertEqual(migrated[name], original[name], name)
def test_api_and_frontend_assets_are_unchanged(self) -> None:
for relative in (
"config/api.config.json",
"static/index.html",
"static/app.js",
"static/styles.css",
"static/pages/auction/page.js",
"static/pages/themes/page.js",
"static/pages/popularity/page.js",
"static/pages/dragon-tiger/page.js",
self.assertEqual(
sha256(APP_ROOT / "config/api.config.json"),
sha256(ORIGINAL_ROOT / "config/api.config.json"),
)
self.assertEqual(
reassembled_frontend_runtime(),
(ORIGINAL_STATIC / "app.js").read_text(encoding="utf-8"),
)
assert_moved_asset_matches(self, "styles.css", "styles/styles.css")
for page in (
"pages/auction/page.js",
"pages/themes/page.js",
"pages/popularity/page.js",
"pages/dragon-tiger/page.js",
):
self.assertEqual(
sha256(APP_ROOT / relative),
sha256(ORIGINAL_ROOT / relative),
relative,
)
assert_page_prefix_matches(self, page)
if __name__ == "__main__":
+12 -11
View File
@@ -11,6 +11,12 @@ import screener
import strategy_tracking
from backend.features.screener import compiler, engine, strategies, tracking
from backend.features.screener import service as screener_service
from tests.preservation_helpers import (
ORIGINAL_STATIC,
assert_moved_asset_matches,
assert_page_prefix_matches,
reassembled_frontend_runtime,
)
APP_ROOT = Path(__file__).resolve().parents[1]
@@ -182,17 +188,12 @@ class ScreenerSliceSourceEquivalenceTests(unittest.TestCase):
)
def test_screener_frontend_assets_are_unchanged(self) -> None:
for relative in (
"static/index.html",
"static/app.js",
"static/styles.css",
"static/pages/screener/page.js",
):
self.assertEqual(
sha256(APP_ROOT / relative),
sha256(ORIGINAL_ROOT / relative),
relative,
)
self.assertEqual(
reassembled_frontend_runtime(),
(ORIGINAL_STATIC / "app.js").read_text(encoding="utf-8"),
)
assert_moved_asset_matches(self, "styles.css", "styles/styles.css")
assert_page_prefix_matches(self, "pages/screener/page.js")
if __name__ == "__main__":
@@ -7,6 +7,12 @@ from pathlib import Path
import sentiment_engine
from backend.features.sentiment import engine as canonical_engine
from tests.preservation_helpers import (
ORIGINAL_STATIC,
assert_moved_asset_matches,
assert_page_prefix_matches,
reassembled_frontend_runtime,
)
APP_ROOT = Path(__file__).resolve().parents[1]
@@ -95,19 +101,17 @@ class SentimentPoolSliceSourceEquivalenceTests(unittest.TestCase):
self.assertIs(sentiment_engine, canonical_engine)
def test_api_and_frontend_assets_are_unchanged(self) -> None:
for relative in (
"config/api.config.json",
"static/index.html",
"static/app.js",
"static/styles.css",
"static/pages/sentiment/page.js",
"static/pages/pools/page.js",
):
self.assertEqual(
sha256(APP_ROOT / relative),
sha256(ORIGINAL_ROOT / relative),
relative,
)
self.assertEqual(
sha256(APP_ROOT / "config/api.config.json"),
sha256(ORIGINAL_ROOT / "config/api.config.json"),
)
self.assertEqual(
reassembled_frontend_runtime(),
(ORIGINAL_STATIC / "app.js").read_text(encoding="utf-8"),
)
assert_moved_asset_matches(self, "styles.css", "styles/styles.css")
for page in ("pages/sentiment/page.js", "pages/pools/page.js"):
assert_page_prefix_matches(self, page)
if __name__ == "__main__":