diff --git a/tests/test_site_r5_search.py b/tests/test_site_r5_search.py new file mode 100644 index 000000000..07f2b1b1c --- /dev/null +++ b/tests/test_site_r5_search.py @@ -0,0 +1,288 @@ +"""Round 5, package 3: search, sidebar and head. + +The get/ download pages sat in no tab, and the theme then numbered the tabs +0 to 4. "snes bios" found NES.emu and never the SNES page, which answers +"No file needed" and so never says BIOS where the search reads. An emulator +crumb ran through its letter group ("Emulators / D"). The home page's +structured data named the page and called it part of itself, and an +emulator's source repository published a commit id as its version beside a +placeholder description. These tests read the generator and, where it is +built, the site. +""" + +from __future__ import annotations + +import ast +import json +import re +import sys +import unittest +from html.parser import HTMLParser +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, load_emulator_profiles # noqa: E402 +from tests.test_site_r2_nav import ANCHORS, _nav, _present # noqa: E402 + + +def _gate_queries() -> dict[str, tuple[tuple[str, ...], int]]: + """The gate's QUERIES, read from website/tools/search_gate.py: a tool + script, not a module the suite installs.""" + tree = ast.parse((REPO_ROOT / "website" / "tools" / "search_gate.py").read_text(encoding="utf-8")) + node = next(node for node in tree.body if isinstance(node, ast.AnnAssign) + and isinstance(node.target, ast.Name) and node.target.id == "QUERIES") + return ast.literal_eval(node.value) + + +QUERIES = _gate_queries() +DIST = REPO_ROOT / "website" / "dist" +ROUTE = REPO_ROOT / "website" / "src" / "routeData.ts" +PLATFORMS = str(REPO_ROOT / "platforms") + + +def _sidebar_with_get_pages() -> tuple[dict, list[str]]: + nav = _nav() + names = list_registered_platforms(PLATFORMS, include_archived=True) + get_pages = ["get/index.md", *(f"get/{name}.md" for name in names)] + return sitestarlight.sidebar(nav, _present(nav) | set(get_pages), ANCHORS), get_pages + + +class GetPagesSitInYourSetup(unittest.TestCase): + """3.1: a folder the navigation lists nothing of takes the tab of the + pages of the same name, and nothing is excluded from every tab.""" + + def test_every_get_page_joins_your_setup(self): + side, get_pages = _sidebar_with_get_pages() + self.assertEqual(side["options"]["exclude"], []) + setup = set(side["options"]["topics"].get("your-setup", [])) + for rel in get_pages: + with self.subTest(page=rel): + self.assertIn("/" + sitestarlight._sidebar_link(rel).strip("/"), setup) + + def test_no_page_is_left_out_of_every_tab(self): + nav = _nav() + present = _present(nav) | {"orphans/alone.md"} + side = sitestarlight.sidebar(nav, present, ANCHORS) + self.assertEqual(side["options"]["exclude"], []) + placed = {path for paths in side["options"]["topics"].values() for path in paths} + self.assertIn("/orphans/alone", placed) + + +class ConsoleWordsReachTheConsole(unittest.TestCase): + """3.2: " BIOS" for each console the page names, from its own + `consoles` field, never a word typed in the generator.""" + + def test_each_console_with_bios(self): + self.assertEqual(sitestarlight.console_words(["SNES", "Super Famicom"]), + ["SNES BIOS", "Super Famicom BIOS"]) + + def test_the_terms_field_carries_them(self): + _target, text = sitestarlight.finalize_page( + "systems/nintendo-snes.md", "# SNES\n\nNo file needed.\n", level="0", + weight=None, browser_title="SNES", description="d", + structured={"@type": "TechArticle"}, terms={}, + authored={"consoles": ["SNES"]}) + self.assertIn('data-pagefind-meta="consoles:SNES"', text) + self.assertIn('data-pagefind-meta="terms:SNES BIOS"', text) + + def test_no_search_word_is_typed_in_the_generator(self): + tree = ast.parse((REPO_ROOT / "scripts" / "sitestarlight.py").read_text(encoding="utf-8")) + docstrings = {id(node.value) for node in ast.walk(tree) if isinstance(node, ast.Expr)} + strings = {node.value.casefold() for node in ast.walk(tree) + if isinstance(node, ast.Constant) and isinstance(node.value, str) + and id(node) not in docstrings} + typed = {text for text in strings for query in QUERIES + if re.search(rf"(? None: + super().__init__() + self.ld: list[dict] = [] + self.sidebar: list[str] = [] + self.tabs_current = 0 + self._ld: list[str] | None = None + self._label: list[str] | None = None + self._tabs = False + + def handle_starttag(self, tag: str, attrs: list) -> None: + a = dict(attrs) + if tag == "script" and a.get("type") == "application/ld+json": + self._ld = [] + if tag == "summary": + self._label = [] + if tag == "nav" and a.get("aria-label") == "Sections": + self._tabs = True + elif self._tabs and tag == "a" and a.get("aria-current"): + self.tabs_current += 1 + + def handle_endtag(self, tag: str) -> None: + if tag == "nav": + self._tabs = False + if tag == "script" and self._ld is not None: + self.ld.append(json.loads("".join(self._ld))) + self._ld = None + if tag == "summary" and self._label is not None: + self.sidebar.append(" ".join("".join(self._label).split())) + self._label = None + + def handle_data(self, data: str) -> None: + for buffer in (self._ld, self._label): + if buffer is not None: + buffer.append(data) + + +def _head(path: Path) -> _Head: + page = _Head() + page.feed(path.read_text(encoding="utf-8")) + return page + + +@unittest.skipUnless((DIST / "index.html").is_file(), "site not built") +class BuiltSite(unittest.TestCase): + def test_no_sidebar_group_is_named_by_a_number(self): + for path in sorted(DIST.rglob("*.html")): + labels = _head(path).sidebar + with self.subTest(page=str(path.relative_to(DIST))): + self.assertFalse([label for label in labels if re.fullmatch(r"\d+", label)]) + + def test_every_get_page_has_one_current_tab(self): + pages = sorted((DIST / "get").rglob("index.html")) + self.assertTrue(pages) + for path in [*pages, DIST / "404.html", DIST / "index.html"]: + with self.subTest(page=str(path.relative_to(DIST))): + self.assertEqual(_head(path).tabs_current, 1) + + def test_the_home_page_is_the_site(self): + ld = _head(DIST / "index.html").ld + self.assertEqual(len(ld), 1) + self.assertEqual(ld[0]["@type"], "WebSite") + self.assertEqual(ld[0]["name"], "RetroBIOS") + self.assertNotIn("isPartOf", ld[0]) + + def test_no_source_repository_states_a_commit_or_a_description(self): + pins = {str(value).casefold()[:7] + for profile in load_emulator_profiles(str(REPO_ROOT / "emulators")).values() + for field in ("source_commit", "upstream_commit") + for value in (profile[field].values() if isinstance(profile.get(field), dict) + else [profile.get(field)]) + if value} + self.assertTrue(pins) + seen = 0 + for path in sorted((DIST / "emulators").rglob("index.html")): + for data in _head(path).ld: + about = data.get("about") or [] + for entry in about if isinstance(about, list) else [about]: + seen += 1 + with self.subTest(page=str(path.relative_to(DIST)), repo=entry.get("codeRepository")): + self.assertNotIn("description", entry) + runs = re.findall(r"[0-9a-f]{7,40}", str(entry.get("version", "")).casefold()) + self.assertFalse([run for run in runs if run[:7] in pins]) + self.assertFalse(generate_site._COMMIT_ID.search(str(entry.get("version", "")))) + self.assertTrue(seen) + + +if __name__ == "__main__": + unittest.main() diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index 9091f6571..7ba0d734b 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -57,7 +57,7 @@ Every module in `tests/`, by the area it guards. | Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_app`, `test_install_from`, `test_install_offline`, `test_install_selection`, `test_install_tui` | | Handheld launchers | a pty as the console, fake installer and firmware tools, stubbed PortMaster dialogs | `test_handheld_launchers`, `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_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `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_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `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_r3_collection`, `test_site_r3_components`, `test_site_r3_css`, `test_site_r3_emulators`, `test_site_r3_entry`, `test_site_r3_nav`, `test_site_r3_p0`, `test_site_r3_reference`, `test_site_r3_systems`, `test_site_r3_wiki`, `test_site_r4_components`, `test_site_r4_css`, `test_site_r4_emulators`, `test_site_r4_entry`, `test_site_r4_merge`, `test_site_r4_p0`, `test_site_r4_reference`, `test_site_r4_search`, `test_site_r4_systems`, `test_site_r4_wiki`, `test_site_r5_components`, `test_site_r5_css`, `test_site_r5_emulators`, `test_site_r5_entry`, `test_site_r5_reference`, `test_site_r5_systems`, `test_site_r5_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` | +| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `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_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `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_r3_collection`, `test_site_r3_components`, `test_site_r3_css`, `test_site_r3_emulators`, `test_site_r3_entry`, `test_site_r3_nav`, `test_site_r3_p0`, `test_site_r3_reference`, `test_site_r3_systems`, `test_site_r3_wiki`, `test_site_r4_components`, `test_site_r4_css`, `test_site_r4_emulators`, `test_site_r4_entry`, `test_site_r4_merge`, `test_site_r4_p0`, `test_site_r4_reference`, `test_site_r4_search`, `test_site_r4_systems`, `test_site_r4_wiki`, `test_site_r5_components`, `test_site_r5_css`, `test_site_r5_emulators`, `test_site_r5_entry`, `test_site_r5_reference`, `test_site_r5_search`, `test_site_r5_systems`, `test_site_r5_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` | ## Test architecture