From a3597cc8afea3bc8ff9ee881d3382d3d1c4289ba Mon Sep 17 00:00:00 2001
From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com>
Date: Fri, 23 Oct 2026 07:33:37 +0200
Subject: [PATCH] chore: gate the second site round
Reads website/dist: page weight and element budgets, sidebar labels,
tips, crumbs, stacked phone tables on reader pages, dates in words, and
the integration items of the round (logo, emulator letters, system
links, the getting-started redirect, Served by in existence mode).
---
tests/test_site_r2_gate.py | 273 +++++++++++++++++++++++++++++++++++++
wiki/testing-guide.md | 2 +-
2 files changed, 274 insertions(+), 1 deletion(-)
create mode 100644 tests/test_site_r2_gate.py
diff --git a/tests/test_site_r2_gate.py b/tests/test_site_r2_gate.py
new file mode 100644
index 00000000..3a08cfa8
--- /dev/null
+++ b/tests/test_site_r2_gate.py
@@ -0,0 +1,273 @@
+"""Gate of the second site round, read from the built site (website/dist).
+
+Each package of the round was checked on its own. These checks hold the
+merged site to what the round set out to fix: no sidebar label a number, no
+"Next" tip, a sidebar label that is the page's own title, a card's link icon
+kept out of the heading a search indexes, crumbs that lead to a tab or a
+section, phone tables stacked where the reading level asks for it, and every
+page under 15,000 elements and 600 KB of HTML. The integration items of the
+round are held too: the platform logo beside the title, a section for every
+letter of the emulators index, system links to the section of the system's
+own name, dates in words and the moved getting-started page.
+
+Skips when the site is not built.
+"""
+
+from __future__ import annotations
+
+import json
+import re
+import sys
+import unittest
+from html import unescape
+from pathlib import Path
+
+import yaml
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(REPO_ROOT / "scripts"))
+
+DIST = REPO_ROOT / "website" / "dist"
+DOCS = REPO_ROOT / "website" / "src" / "content" / "docs"
+SIDEBAR = REPO_ROOT / "website" / "src" / "generated" / "sidebar.json"
+BASE = "/retrobios/"
+
+PAGE_BYTES = 600_000
+PAGE_ELEMENTS = 15_000
+PHONE_COLUMN = 358
+READER_LEVELS = {"start", "setup"}
+# The size from which a table is a list scanned by column and keeps its
+# scroll (website/src/plugins/rehype-stack-tables.mjs, RECORD_ROWS).
+RECORD_ROWS = 20
+
+_SCRIPT = re.compile(r"<(script|style)\b[^>]*>.*?\1>", re.S | re.I)
+_ELEMENT = re.compile(r"<[a-zA-Z][a-zA-Z0-9-]*[\s/>]")
+_TAGS = re.compile(r"<[^>]+>")
+_TABLE = re.compile(r"
", re.S)
+_HEADING = re.compile(r"[2-6])\b[^>]*>(?P.*?)", re.S)
+_CRUMBS = re.compile(r'', re.S)
+_HREF = re.compile(r'href="(?P[^"]*)"')
+_TIP = re.compile(r'', re.S)
+
+
+def _text(markup: str) -> str:
+ return " ".join(unescape(_TAGS.sub(" ", markup)).split())
+
+
+def _pages() -> list[Path]:
+ return sorted(DIST.rglob("*.html"))
+
+
+def _url(path: Path) -> str:
+ rel = path.relative_to(DIST).as_posix()
+ if rel.endswith("index.html"):
+ return BASE + rel[: -len("index.html")]
+ return BASE + rel
+
+
+def _levels() -> dict[str, str]:
+ """Page address -> the level its front matter declares."""
+ levels: dict[str, str] = {}
+ for source in DOCS.rglob("*.md*"):
+ text = source.read_text(encoding="utf-8")
+ if not text.startswith("---\n"):
+ continue
+ front = yaml.safe_load(text[4:text.find("\n---\n", 4)]) or {}
+ stem = str(front.get("slug") or source.relative_to(DOCS).with_suffix("").as_posix())
+ stem = re.sub(r"(^|/)index$", "", stem)
+ levels[BASE + (stem + "/" if stem else "")] = str(front.get("level") or "")
+ return levels
+
+
+@unittest.skipUnless((DIST / "index.html").is_file(), "site not built")
+class EveryBuiltPage(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.pages = {path: path.read_text(encoding="utf-8") for path in _pages()}
+ sidebar = json.loads(SIDEBAR.read_text(encoding="utf-8"))
+ cls.tab_roots = {BASE + topic["link"].strip("/") + ("/" if topic["link"].strip("/") else "")
+ for topic in sidebar["topics"]}
+ cls.group_pages = {group["href"] for group in sidebar["groups"].values() if group["href"]}
+ cls.levels = _levels()
+
+ def test_under_the_page_budgets(self):
+ heavy, crowded = [], []
+ for path, html in self.pages.items():
+ size = len(html.encode("utf-8"))
+ if size >= PAGE_BYTES:
+ heavy.append((path.relative_to(DIST).as_posix(), size))
+ elements = len(_ELEMENT.findall(_SCRIPT.sub("", html)))
+ if elements >= PAGE_ELEMENTS:
+ crowded.append((path.relative_to(DIST).as_posix(), elements))
+ self.assertFalse(heavy, f"pages of {PAGE_BYTES:,} bytes or more")
+ self.assertFalse(crowded, f"pages of {PAGE_ELEMENTS:,} elements or more")
+
+ def test_no_sidebar_label_is_a_number(self):
+ for path, html in self.pages.items():
+ sidebar = html.split('id="starlight__sidebar"', 1)[-1].split("", 1)[0]
+ labels = [_text(m) for m in re.findall(r"]*>(.*?)", sidebar, re.S)]
+ numbers = [label for label in labels if re.fullmatch(r"\d+", label)]
+ with self.subTest(page=path.relative_to(DIST).as_posix()):
+ self.assertFalse(numbers)
+
+ def test_no_tip_titled_next(self):
+ for path, html in self.pages.items():
+ for aside in _TIP.findall(html):
+ title = re.search(r'class="starlight-aside__title"[^>]*>(.*?)', aside, re.S)
+ with self.subTest(page=path.relative_to(DIST).as_posix()):
+ self.assertNotEqual(_text(title[1]) if title else "", "Next")
+
+ def test_the_sidebar_names_a_page_by_its_title(self):
+ for path, html in self.pages.items():
+ url = _url(path)
+ current = _CURRENT.search(html.split('id="starlight__sidebar"', 1)[-1])
+ title = _H1.search(html)
+ # A tab, or a group of the sidebar, opens on its overview.
+ if url in self.tab_roots or url in self.group_pages or not current or not title:
+ continue
+ with self.subTest(page=url):
+ self.assertEqual(_text(current["label"]), _text(title["title"]))
+
+ def test_a_card_link_icon_says_nothing_in_its_heading(self):
+ for path, html in self.pages.items():
+ for heading in _HEADING.finditer(html):
+ for link in re.findall(r']*class="rb-permalink"[^>]*>(.*?)',
+ heading["inner"], re.S):
+ with self.subTest(page=path.relative_to(DIST).as_posix()):
+ self.assertEqual(_text(link), "")
+
+ def test_every_crumb_leads_to_a_tab_or_a_section(self):
+ for path, html in self.pages.items():
+ crumbs = _CRUMBS.search(html)
+ if not crumbs:
+ continue
+ for href in _HREF.findall(crumbs["inner"]):
+ with self.subTest(page=_url(path), href=href):
+ self.assertTrue(href in self.tab_roots or href in self.group_pages
+ or "#" in href, href)
+
+ def test_phone_tables_on_reader_pages_are_stacked(self):
+ """A table of three columns or more, under RECORD_ROWS rows, on a
+ level 0 or 1 page is read one row at a time on a phone: it carries
+ data-rb-stack. A two-column table is a list of label and value."""
+ unstacked = []
+ for path, html in self.pages.items():
+ if self.levels.get(_url(path)) not in READER_LEVELS:
+ continue
+ for table in _TABLE.finditer(html):
+ head = table["body"].split("", 1)[0]
+ columns = head.count("
", 1)[-1].count("
= 2 and rows < RECORD_ROWS and "data-rb-stack" not in table["attrs"]:
+ unstacked.append((_url(path), _text(head)[:60]))
+ self.assertFalse(unstacked)
+
+ def test_no_page_dates_itself_with_a_raw_stamp(self):
+ for path, html in self.pages.items():
+ with self.subTest(page=_url(path)):
+ self.assertNotIn("Generated on", html)
+
+
+@unittest.skipUnless((DIST / "index.html").is_file(), "site not built")
+class TheIntegrationItems(unittest.TestCase):
+
+ def test_a_platform_page_draws_its_logo_beside_the_title(self):
+ from common import load_platform_registry
+
+ registry = load_platform_registry(str(REPO_ROOT / "platforms"))
+ with_logo = [key for key, entry in registry.items() if entry.get("logo")]
+ self.assertTrue(with_logo)
+ for key in with_logo:
+ page = DIST / "platforms" / key / "index.html"
+ if not page.is_file():
+ continue
+ html = page.read_text(encoding="utf-8")
+ with self.subTest(platform=key):
+ block = re.search(r'
.*?
', html, re.S)
+ self.assertIsNotNone(block)
+ self.assertRegex(block[0], r'[^/"#]+)/#(?P[^"]+)"')
+ for page in ("emulators/index.html", "platforms/retroarch/index.html",
+ "platforms/batocera/index.html", "systems/index.html"):
+ html = (DIST / page).read_text(encoding="utf-8")
+ for match in target.finditer(html):
+ with self.subTest(page=page, link=match[0]):
+ self.assertIn(match["anchor"], ids.get(match["page"], set()))
+
+ def test_getting_started_sends_to_which_pack(self):
+ page = DIST / "wiki" / "getting-started" / "index.html"
+ self.assertTrue(page.is_file())
+ self.assertIn('url=/retrobios/which-pack/"', page.read_text(encoding="utf-8"))
+ self.assertFalse((REPO_ROOT / "wiki" / "getting-started.md").exists())
+
+ def test_existence_mode_pages_name_the_file_served(self):
+ for key in ("retroarch", "lakka", "retropie"):
+ page = DIST / "platforms" / key / "index.html"
+ if not page.is_file():
+ continue
+ with self.subTest(platform=key):
+ self.assertIn("Served by", page.read_text(encoding="utf-8"))
+
+
+class TheRules(unittest.TestCase):
+ """What the gate reads without a build."""
+
+ def test_the_logo_reaches_the_front_matter(self):
+ import sitestarlight
+
+ _target, text = sitestarlight.finalize_page(
+ "platforms/x.md", "# X\n\nBody.\n", level="setup", weight=None,
+ browser_title="X", description="d", structured={}, terms={},
+ logo="assets/logos/x.svg")
+ self.assertIn("logo: assets/logos/x.svg", text)
+
+ def test_a_large_data_directory_is_listed_on_pages_of_its_own(self):
+ """Dolphin's Sys tree put 16,900 elements on the Dolphin page. Its
+ files are found where they moved."""
+ import generate_site
+
+ members = {f"Sys/{i:05d}.ini": {"sha1": f"{i:040x}", "size": 1}
+ for i in range(generate_site.DATA_TREE_ROWS_PER_PAGE + 1)}
+ pages = generate_site.data_tree_pages("dolphin-sys", members)
+ self.assertEqual([stem for stem, _names in pages], ["data-dolphin-sys", "data-dolphin-sys-2"])
+ self.assertEqual(generate_site.data_tree_pages("small", dict(list(members.items())[:3])), [])
+ items = generate_site.file_location_items(
+ {"files": {}}, {"dolphin": {"data_directories": [{"ref": "dolphin-sys"}]}},
+ {"dolphin-sys": members})
+ last = f"{generate_site.DATA_TREE_ROWS_PER_PAGE:040x}"
+ self.assertIn({"sha1": last, "url": f"emulators/data-dolphin-sys-2/#sha1-{last[:12]}"}, items)
+
+ def test_no_float_rule_for_right_aligned_images(self):
+ css = (REPO_ROOT / "website" / "src" / "styles" / "retrobios.css").read_text(encoding="utf-8")
+ self.assertNotIn("img[align='right']", css)
+ self.assertIn(":is(h2, h3) small", css)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md
index abdbeb0d..158e1673 100644
--- a/wiki/testing-guide.md
+++ b/wiki/testing-guide.md
@@ -55,7 +55,7 @@ Every module in `tests/`, by the area it guards.
| Handheld port | stubbed PortMaster dialogs, fake installer | `test_port` |
| Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_data_dir_readers`, `test_truth_diff`, `test_truth_layout` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_check_freshness`, `test_dedup`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_profile_sync`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_scraper_contract`, `test_upstream`, `test_upstream_index` |
-| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_pages`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_schema_semantic_gate`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
+| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_pages`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_schema_semantic_gate`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
## Test architecture