"""Round 5, package 2: components and runtime. Source checks always run. Checks on the built site read website/dist and skip when no build is there. The rules written in a build plugin run under node and skip without it. """ from __future__ import annotations import html import json import re import shutil import subprocess import sys import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT / "scripts")) from common import list_release_pack_platforms # noqa: E402 WEBSITE = REPO_ROOT / "website" DIST = WEBSITE / "dist" COMPONENTS = WEBSITE / "src" / "components" SITE_TS = WEBSITE / "src" / "scripts" / "site.ts" PLUGINS = WEBSITE / "src" / "plugins" SHORT_CODE = PLUGINS / "rehype-short-code.mjs" STACK_TABLES = PLUGINS / "rehype-stack-tables.mjs" ENTITY = re.compile(r"&(?:#\d+|#x[0-9a-f]+|[a-z]+);", re.I) def _read(path: Path) -> str: return path.read_text(encoding="utf-8") def _built(page: str) -> str: path = DIST / page / "index.html" if not path.is_file(): raise unittest.SkipTest(f"no built site at {path}") return _read(path) def _node(script: str) -> str: node = shutil.which("node") if not node: raise unittest.SkipTest("node is missing") result = subprocess.run( [node, "--input-type=module", "-e", script], capture_output=True, text=True, check=True, cwd=WEBSITE, env={"NODE_DISABLE_COMPILE_CACHE": "1", "PATH": str(Path(node).parent)}, ) return result.stdout def _text(page: str) -> str: """A built page's visible words: tags dropped, entities read.""" return html.unescape(re.sub(r"<[^>]+>", "", page)) def _picker_packs() -> set[str]: """The setups the built picker offers a release ZIP line for.""" page = _built("which-pack") match = re.search(r'data-release="([^"]*)"', page) if not match: raise AssertionError("which-pack has no picker release data") return set(json.loads(html.unescape(match[1]))["packs"]) class PickerFollowsTheRegistry(unittest.TestCase): """2.1: the picker said "The ZIP of release v2026.10.10 holds 5,804 files" for ROCKNIX while its setup page and get/ page said "No ZIP for ROCKNIX" (N2, J5). The picker read the raw release.json.""" def test_source(self): source = _read(COMPONENTS / "FilePicker.astro") self.assertIn("import release from '../generated/release-advertised.json';", source) self.assertNotIn("release.json'", source) def test_picker_offers_only_registry_packs(self): offered = _picker_packs() self.assertTrue(offered) released = set(list_release_pack_platforms(str(REPO_ROOT / "platforms"), include_archived=True)) self.assertLessEqual(offered, released) def test_picker_setup_and_get_pages_agree(self): offered = _picker_packs() setups = json.loads(_read(DIST / "data" / "picker" / "index.json")) self.assertTrue(setups) for entry in setups: name = entry["platform"] with self.subTest(platform=name): get_page = _text(_built(f"get/{name}")) setup_page = _text(_built(f"platforms/{name}")) no_zip = f"No ZIP for {entry['name']}." self.assertEqual(name in offered, no_zip not in get_page) self.assertEqual(name in offered, no_zip not in setup_page) class PathsBreakAtSeparators(unittest.TestCase): """2.2: `~/.var/app/org.libretro.RetroArch/config/r` then `etroarch/system/` on platforms/retroarch/ at 390 px (V8).""" def test_the_rule(self): uri = json.dumps(SHORT_CODE.as_uri()) out = _node( f"import {{ markRawCode, withBreaks, breaksAtSeparators }} from {uri};" "console.log(JSON.stringify([" "markRawCode('
~/.config/retroarch/system/aaaaaaaaaaa/ bios/a.binC:\\\\RetroArch-Win64\\\\system\\\\aaaaaaaaaaaaa~/.config/retroarch/system/aaaaaaaaaaa/ '
' bios/a.binC:\\RetroArch-Win64\\system\\aaaaaaaaaaaaa aaaaaaaaaa/bbbbbbbbbb/cccccccccc/d';"
"console.log(JSON.stringify([BREAK_BUDGET, markRawCode(path, room), markRawCode(path, room), room.left]));"
)
budget, first, second, left = json.loads(out)
self.assertEqual(first, "aaaaaaaaaa/bbbbbbbbbb/cccccccccc/d ")
self.assertEqual(second, "aaaaaaaaaa/bbbbbbbbbb/cccccccccc/d")
self.assertEqual(left, 0)
if not DIST.is_dir():
self.skipTest("no built site")
# Link text the generator breaks itself (a forge address) is not code.
code = re.compile(r"]*)?>(?:[^<]|)* ")
over = [str(path.relative_to(DIST)) for path in DIST.rglob("index.html")
if sum(found.count("", "", page[page.index("sl-markdown-content"):], flags=re.S)
for text in re.findall(r"]*)?>((?:[^<]|<(?!/code>))*)", body):
plain = ENTITY.sub("_", re.sub(r"", "", text))
if len(plain) > 30 and re.search(r"[/\\]", plain[:-1]) \
and "" not in text and "<" in text:
unbroken.append(f"{path.relative_to(DIST)}: {text}")
self.assertEqual(unbroken, [])
def test_crumb_labels_in_a_span(self):
source = _read(COMPONENTS / "PageTitle.astro")
self.assertIn("{crumb.label} : {crumb.label}", source)
crumbs = _built("platforms/retroarch")
crumbs = crumbs[crumbs.index('class="rb-breadcrumbs'):]
crumbs = crumbs[:crumbs.index("")]
items = re.findall(r"]*>(.*?) ", crumbs)
self.assertTrue(items)
for item in items:
self.assertRegex(item, r"^(?:]*>)?]*>[^<]+")
class ShortStackedLabels(unittest.TestCase):
"""2.3: "Name as declared" and "On its BIOS list (destinations)" wrap
to two lines in the stacked label column (V9)."""
def test_the_rule(self):
uri = json.dumps(STACK_TABLES.as_uri())
out = _node(
f"import {{ stackedLabel }} from {uri};"
"console.log(JSON.stringify([stackedLabel('Declared as', 'Name as declared'),"
"stackedLabel(undefined, 'Setup'), stackedLabel(' ', 'Setup')]));"
)
self.assertEqual(json.loads(out), ["Declared as", "Setup", "Setup"])
def test_plugin_and_runtime_read_the_title(self):
plugin = _read(STACK_TABLES)
self.assertIn("stackedLabel(cell.properties?.title, labels[column])", plugin)
self.assertIn("ctx.setProperty(cell, 'dataLabel', stacked[column])", plugin)
runtime = _read(SITE_TS)
self.assertIn("cell.getAttribute('title')?.trim() || (cell.textContent?.trim() ?? '')", runtime)
self.assertIn("[...table.querySelectorAll('thead th')].map(stackedLabel)", runtime)
self.assertIn("'Name as declared': 'Declared as'", runtime)
self.assertIn("cell.dataset.label = stackedLabel(head.cells[column]);", runtime)
class LookupShowsTheDeclaredHash(unittest.TestCase):
"""2.4: with the CRC32 pasted, Batocera's row read "Hash matched: CRC32",
though Batocera declares an MD5 (J9, N10)."""
def test_source(self):
source = _read(SITE_TS)
self.assertIn("declared?: string;", source)
self.assertIn("const declaredHash = (item: Item) => (item.declared || item.algorithm).toUpperCase();", source)
table = source[source.index("const setupTable"):source.index("const distinct")]
self.assertIn("declaredHash(item),", table)
self.assertNotIn("item.algorithm", table)
if __name__ == "__main__":
unittest.main()