From 9e1079f4a14c49c9d81c08f4150ab12b45c7b107 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:36:09 +0200 Subject: [PATCH] chore: gate round 3 navigation and search --- tests/test_site_funnel.py | 14 ++- tests/test_site_r3_nav.py | 217 +++++++++++++++++++++++++++++++++++ website/tools/search_gate.py | 21 +++- wiki/testing-guide.md | 2 +- 4 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 tests/test_site_r3_nav.py diff --git a/tests/test_site_funnel.py b/tests/test_site_funnel.py index 2a58bfa41..ee4a1bb77 100644 --- a/tests/test_site_funnel.py +++ b/tests/test_site_funnel.py @@ -209,10 +209,15 @@ class DeepLinksCarryALabel(unittest.TestCase): if level not in ("start", "setup"): continue text = FENCED.sub("", page.read_text(encoding="utf-8")) - heading = "" + # The headings the line sits under, by depth: a system's h3 sits + # in the "## Reference: every file by system" section. + headings: dict[int, str] = {} for line in text.splitlines(): - if re.match(r"#{1,3} ", line): - heading = line.lstrip("#").strip() + level_match = re.match(r"(#{1,3}) ", line) + if level_match: + depth = len(level_match[1]) + headings = {d: h for d, h in headings.items() if d < depth} + headings[depth] = line.lstrip("#").strip() for match in LINK.finditer(line): target = by_url.get(match.group(1)) if target not in levels: @@ -221,7 +226,8 @@ class DeepLinksCarryALabel(unittest.TestCase): if ORDER.index(deeper) - ORDER.index(level) < 2: continue label = f"{generate_site.LEVELS[deeper]}:" - if label not in line[: match.start()] and not heading.startswith(label): + if label not in line[: match.start()] and not any( + heading.startswith(label) for heading in headings.values()): offenders.append(f"{page.relative_to(DOCS)}: {match.group(0)}") self.assertEqual(offenders, [], "\n".join(offenders[:30])) diff --git a/tests/test_site_r3_nav.py b/tests/test_site_r3_nav.py new file mode 100644 index 000000000..9b9890c3c --- /dev/null +++ b/tests/test_site_r3_nav.py @@ -0,0 +1,217 @@ +"""Navigation and search (round 3, package 3). + +"playstation bios" found five setups' lists for the console before its own +page: the pages a setup's list moves a console to weighed 1.5, three times a +reference page. The file names a system page holds were the text of a hidden +span, so the search showed them as the excerpt. Start here looped: First +install's "Next steps" pointed back to Get your files, Is it legal? to Common +questions, and both pages had the same page as previous and next. A part +page had no crumb to the page it belongs to and nothing marked in the +sidebar. "psx" or "dc" found nothing, though every platform files the +console under that name. These tests read the generator and, where they +are built, the pages and the site. +""" + +from __future__ import annotations + +import ast +import re +import sys +import unittest +from html import unescape +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +import generate_site # noqa: E402 +import sitestarlight # noqa: E402 +from common import list_registered_platforms # noqa: E402 + +SEARCH_GATE = REPO_ROOT / "website" / "tools" / "search_gate.py" +DOCS = REPO_ROOT / "website" / "src" / "content" / "docs" +DIST = REPO_ROOT / "website" / "dist" +ROUTE = REPO_ROOT / "website" / "src" / "routeData.ts" +CONTENT_CONFIG = REPO_ROOT / "website" / "src" / "content.config.ts" +START_HERE = [ + "/retrobios/", "/retrobios/wiki/what-are-bios-files/", "/retrobios/which-pack/", + "/retrobios/wiki/first-install/", "/retrobios/wiki/troubleshooting/", + "/retrobios/wiki/faq/", "/retrobios/wiki/legal/", "/retrobios/wiki/glossary/", + "/retrobios/platforms/", +] + + +def _front(text: str) -> str: + end = text.find("\n---\n", 4) + return text[4:end] if text.startswith("---\n") and end != -1 else "" + + +def _moved_platform_pages() -> list[Path]: + platforms = set(list_registered_platforms(str(REPO_ROOT / "platforms"), include_archived=True)) + folder = DOCS / "platforms" + if not folder.is_dir(): + return [] + return [path for path in sorted(folder.glob("*.md*")) + if path.stem not in platforms and path.stem != "index"] + + +class MetaFields(unittest.TestCase): + def test_file_names_are_an_attribute_not_text(self) -> None: + body = ('

scph5501.bin

' + '

a,b.bin

') + meta = sitestarlight.file_names_meta(body) + self.assertIn('data-pagefind-meta="files:a b.bin scph5501.bin"', meta) + self.assertRegex(meta, r"]*hidden>") + + def test_a_field_escapes_its_value(self) -> None: + meta = sitestarlight.search_field("aliases", ['a"b', "c&d"]) + self.assertIn('data-pagefind-meta="aliases:a"b c&d"', meta) + + def test_an_empty_field_writes_nothing(self) -> None: + self.assertEqual(sitestarlight.search_field("aliases", []), "") + self.assertEqual(sitestarlight.file_names_meta("

no card

"), "") + + def test_system_pages_carry_the_platforms_names(self) -> None: + page = DOCS / "systems" / "sony-playstation.md" + if not page.is_file(): + self.skipTest("site pages not generated") + text = page.read_text(encoding="utf-8") + match = re.search(r'data-pagefind-meta="aliases:([^"]*)"', text) + self.assertIsNotNone(match, "no aliases field on the PlayStation page") + self.assertIn("psx", unescape(match[1]).split()) + dreamcast = (DOCS / "systems" / "sega-dreamcast.md").read_text(encoding="utf-8") + self.assertRegex(dreamcast, r'data-pagefind-meta="aliases:[^"]*\bdc\b') + + +class MovedPlatformPages(unittest.TestCase): + def test_a_setups_list_for_one_console_is_a_reference(self) -> None: + source = Path(generate_site.__file__).read_text(encoding="utf-8") + block = source[source.index("for stem, page in moved.items():"):] + block = block[: block.index("PAGE_DATA[f\"platforms/{name}.md\"]")] + self.assertIn('SEARCH_DEMOTED.add(f"platforms/{stem}.md")', block) + self.assertIn('level="reference"', block) + self.assertIn('search="exclude"', block) + self.assertIn("parent=", block) + + def test_generated_pages_weigh_as_references_and_name_their_setup(self) -> None: + pages = _moved_platform_pages() + if not pages: + self.skipTest("site pages not generated") + for path in pages: + front = _front(path.read_text(encoding="utf-8")) + with self.subTest(page=path.name): + self.assertIn("\nlevel: reference\n", f"\n{front}\n") + self.assertIn("\nsearchWeight: 0.5\n", f"\n{front}\n") + # The title alone was indexed, and it outranked the console. + self.assertIn("\npagefind: false\n", f"\n{front}\n") + self.assertRegex(front, r"parent:\n label: .+\n link: /retrobios/platforms/[a-z0-9_-]+/\n") + + def test_parent_link_resolves_against_the_page(self) -> None: + _target, text = sitestarlight.finalize_page( + "platforms/retroarch-sony-playstation.md", "# RetroArch: Sony PlayStation\n\nBody.\n", + level="reference", weight=0.5, browser_title="x", description="d", + structured={}, terms={}, parent={"label": "RetroArch", "link": "retroarch.md"}, + ) + self.assertIn("parent:\n label: RetroArch\n link: /retrobios/platforms/retroarch/\n", text) + + def test_the_schema_accepts_parent(self) -> None: + self.assertRegex(CONTENT_CONFIG.read_text(encoding="utf-8"), + r"parent: z\.object\(\{ label: z\.string\(\), link: z\.string\(\) \}\)") + + +class ArcadeGroup(unittest.TestCase): + def test_arcade_is_not_a_console_or_computer(self) -> None: + nav = generate_site._system_nav({"Sony": {}, "Arcade": {}, "Sega": {}}) + groups = {next(iter(entry)): entry[next(iter(entry))] for entry in nav if isinstance(entry, dict)} + self.assertEqual(groups["Arcade"], [{"Arcade": "systems/arcade.md"}]) + self.assertNotIn({"Arcade": "systems/arcade.md"}, groups["Consoles and computers"]) + + +class RouteMiddleware(unittest.TestCase): + def test_next_steps_link_is_kept_only_forward(self) -> None: + source = ROUTE.read_text(encoding="utf-8") + self.assertIn("target <= position", source) + + def test_parent_is_a_crumb_and_marked(self) -> None: + source = ROUTE.read_text(encoding="utf-8") + self.assertIn("route.entry.data.parent", source) + self.assertIn("markCurrent(route.sidebar", source) + + +def _gate_queries() -> dict: + """The search gate's QUERIES, read from its source: the tool is run + with node beside it, not imported as a package.""" + tree = ast.parse(SEARCH_GATE.read_text(encoding="utf-8")) + for node in tree.body: + if isinstance(node, ast.AnnAssign) and getattr(node.target, "id", "") == "QUERIES": + return ast.literal_eval(node.value) + raise AssertionError("no QUERIES in search_gate.py") + + +class SearchGateQueries(unittest.TestCase): + def test_the_audits_queries_are_gated(self) -> None: + for query in ("playstation bios", "dreamcast bios", "neo geo bios", "bios not found", + "offline install", "r36s", "wonderswan color", "dc bios missing", + "which mame version", "psx bios"): + self.assertIn(query, _gate_queries()) + + def test_prefixes_match_a_consoles_own_page(self) -> None: + pages, _rank = _gate_queries()["playstation bios"] + self.assertTrue(any("systems/sony-playstation/".startswith(p) for p in pages)) + + +def _html(url: str) -> str: + rel = url[len("/retrobios/"):] + path = DIST / rel / "index.html" + return path.read_text(encoding="utf-8") if path.is_file() else "" + + +def _pagination(text: str) -> tuple[str | None, str | None]: + start = text.find('