From e3f7ebd4bc1e54c3325037f70281b83e0f1675ef Mon Sep 17 00:00:00 2001
From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com>
Date: Fri, 23 Oct 2026 10:47:09 +0200
Subject: [PATCH] chore: tests for the round 3 site components
---
tests/test_site_r2_components.py | 18 +-
tests/test_site_r2_gate.py | 2 +-
tests/test_site_r3_components.py | 390 +++++++++++++++++++++++++++++++
wiki/testing-guide.md | 2 +-
4 files changed, 402 insertions(+), 10 deletions(-)
create mode 100644 tests/test_site_r3_components.py
diff --git a/tests/test_site_r2_components.py b/tests/test_site_r2_components.py
index 6aad12ed..449e2142 100644
--- a/tests/test_site_r2_components.py
+++ b/tests/test_site_r2_components.py
@@ -84,8 +84,10 @@ class TablesStackWhenBuilt(unittest.TestCase):
"action": {"labels": ["A", "B", "C"], "rows": 40, "action": True},
"pair": {"labels": ["Where", "BIOS folder"], "rows": 3, "action": False},
"short-start": {"labels": ["System", "Runs", "BIOS folder"], "rows": 8, "action": False, "level": "start"},
- "long-setup": {"labels": ["System", "Files", "Status", "Emulators"], "rows": 97, "action": False, "level": "setup"},
+ "long-setup": {"labels": ["System", "Files", "Status"], "rows": 97, "action": False, "level": "setup"},
"reference": {"labels": ["A", "B", "C"], "rows": 5, "action": False, "level": "reference"},
+ # Four columns do not fit a phone column, on any page (round 3).
+ "wide": {"labels": ["File", "Region", "Required", "Hash"], "rows": 105, "action": False, "level": "reference"},
}
script = (
f"import {{ stackKind }} from {json.dumps(PLUGIN.as_uri())};"
@@ -98,7 +100,7 @@ class TablesStackWhenBuilt(unittest.TestCase):
)
self.assertEqual(json.loads(result.stdout), {
"zip-header": "true", "action": "true", "pair": "kv",
- "short-start": "true", "long-setup": None, "reference": None,
+ "short-start": "true", "long-setup": None, "reference": None, "wide": "true",
})
@@ -142,7 +144,7 @@ class SearchAnswersAHashAndShowsNoFileDump(unittest.TestCase):
self.assertIn("Look up this hash", source)
self.assertIn("files/?hash=${value}", source)
self.assertIn("endsWith('#_top')", source)
- self.assertIn("tag.dataset.pagefindUiMeta === 'files'", source)
+ self.assertIn("UNSHOWN_FIELDS.has(tag.dataset.pagefindUiMeta ?? '')", source)
# Pagefind's display rules beat the hidden attribute without this.
self.assertIn("#starlight__search [hidden] { display: none !important; }", _read(COMPONENTS / "Header.astro"))
@@ -167,8 +169,8 @@ def _title_block(html: str) -> str:
class LevelBadgeOnlyWhereItAddsSomething(unittest.TestCase):
def test_badge_follows_the_crumb(self):
- # A tab root has no crumb and keeps its badge.
- self.assertIn('class="rb-level rb-level-setup', _title_block(_built("platforms")))
+ # A tab root: the header tab already says it (round 3, 2.1).
+ self.assertNotIn('class="rb-level ', _title_block(_built("platforms")))
# Under the "Start here" or "Your setup" crumb the badge repeated it.
for page in ("which-pack", "platforms/batocera"):
with self.subTest(page=page):
@@ -181,7 +183,7 @@ class TitleLogo(unittest.TestCase):
def test_front_matter_field(self):
self.assertIn("logo: z.string().optional()", _read(WEBSITE / "src" / "content.config.ts"))
title = _read(COMPONENTS / "PageTitle.astro")
- self.assertIn('
', title)
+ self.assertIn('
', title)
class FooterSaysWhoAndWhen(unittest.TestCase):
@@ -200,10 +202,10 @@ class FooterSaysWhoAndWhen(unittest.TestCase):
class FindAFileGivesAVerdict(unittest.TestCase):
def test_lookup_renderer(self):
source = _read(SITE_TS)
- self.assertIn("', held in the collection at '", source)
+ self.assertIn("'Held in the collection at '", source)
self.assertIn("['Setup', 'Name as declared', 'Destination', 'Hash matched', 'Required']", source)
self.assertIn("api/v1/platforms/${item.owner}.json", source)
- self.assertIn("api/v1/systems/${slug}.json", source)
+ self.assertIn("api/v1/systems/${words.slice(0, size).join('-')}.json", source)
style = _read(COMPONENTS / "MarkdownContent.astro")
self.assertIn("max-width: 40rem;", style)
self.assertIn("width: 100%;", style)
diff --git a/tests/test_site_r2_gate.py b/tests/test_site_r2_gate.py
index 3a08cfa8..a83dffa2 100644
--- a/tests/test_site_r2_gate.py
+++ b/tests/test_site_r2_gate.py
@@ -192,7 +192,7 @@ class TheIntegrationItems(unittest.TestCase):
with self.subTest(platform=key):
block = re.search(r'
.*?
', html, re.S)
self.assertIsNotNone(block)
- self.assertRegex(block[0], r'
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 _title_block(html: str) -> str:
+ """What PageTitle draws: from the main column to the end of the h1."""
+ end = html.index("", html.index('id="_top"'))
+ return html[html.rfind(" list[str]:
+ return re.findall(r"", html, re.S)
+
+
+def _headers(table: str) -> list[str]:
+ head = re.search(r"(.*?)", table, re.S)
+ cells = re.findall(r"| ]*>(.*?) | ", head.group(1), re.S) if head else []
+ return [re.sub(r"<[^>]+>", "", cell).strip() for cell in cells]
+
+
+def _node_module(script: str) -> str:
+ node = shutil.which("node")
+ if not node:
+ raise unittest.SkipTest("node is missing")
+ result = subprocess.run(
+ [node, "--input-type=module-typescript", "-e", script],
+ capture_output=True, text=True, check=True, cwd=WEBSITE,
+ )
+ return result.stdout
+
+
+def _webp_size(data: bytes) -> tuple[int, int]:
+ """Width and height from a WebP header (VP8, VP8L or VP8X)."""
+ if data[:4] != b"RIFF" or data[8:12] != b"WEBP":
+ raise ValueError("not a WebP file")
+ chunk = data[12:16]
+ if chunk == b"VP8 ":
+ width, height = struct.unpack("> 14) & 0x3FFF) + 1
+ if chunk == b"VP8X":
+ return int.from_bytes(data[24:27], "little") + 1, int.from_bytes(data[27:30], "little") + 1
+ raise ValueError(f"unknown WebP chunk {chunk!r}")
+
+
+class TitleLogoOnAPlate(unittest.TestCase):
+ """The RetroArch and Batocera marks are black: beside the title they
+ vanished on the dark theme, and the image had no width (Lighthouse
+ unsized-images, V3, T-F3)."""
+
+ def test_the_component_draws_the_tile_square(self):
+ title = _read(COMPONENTS / "PageTitle.astro")
+ self.assertIn('width={MARK} height={MARK}', title)
+ self.assertIn("const MARK = 40;", title)
+ style = title[title.index(".rb-title-mark {"):]
+ style = style[:style.index("}")]
+ for rule in ("width: 40px;", "height: 40px;", "padding: 3px;", "border: 1px solid var(--rb-border);",
+ "border-radius: 6px;", "background: #fff;", "object-fit: contain;"):
+ with self.subTest(rule=rule):
+ self.assertIn(rule, style)
+
+ def test_every_setup_page_has_a_sized_mark(self):
+ index = json.loads(_read(WEBSITE / "public" / "data" / "picker" / "index.json"))
+ for entry in index:
+ with self.subTest(setup=entry["platform"]):
+ block = _title_block(_built(f"platforms/{entry['platform']}"))
+ self.assertIn('class="rb-title-mark', block)
+ for image in re.findall(r"
]*>", block):
+ self.assertIn('width="40"', image)
+ self.assertIn('height="40"', image)
+
+ def test_a_setup_with_no_logo_gets_its_initials(self):
+ block = _title_block(_built("platforms/myminui"))
+ self.assertNotIn("
]*>.*?>MM")
+
+
+class NoLevelBadgeOnATabRoot(unittest.TestCase):
+ def test_tab_roots(self):
+ for page in TAB_ROOTS:
+ with self.subTest(page=page or "home"):
+ self.assertNotIn('class="rb-level ', _title_block(_built(page)))
+
+
+class FooterLicenceMatchesTheLicenceFile(unittest.TestCase):
+ """The footer said MIT covers the tooling. LICENSE names the metadata too."""
+
+ def test_footer(self):
+ self.assertIn("metadata", _read(REPO_ROOT / "LICENSE").split("MIT License")[0])
+ html = _built("data")
+ footer = re.sub(r"<[^>]+>", "", html[html.index('class="rb-footer'):])
+ self.assertIn("MIT for the tooling and metadata.", footer)
+
+
+class WideTablesStackAndFiltersAreBuilt(unittest.TestCase):
+ """Level 2 and 3 tables overflowed a phone (emulator summary, gaps,
+ verification modes, emulators index, data: V5, J8). The filter box,
+ added after paint, pushed the table down: CLS 0.066 on
+ gaps-files-to-source (T-F10)."""
+
+ def test_every_wide_table_stacks(self):
+ for page in ("emulators/duckstation", "gaps", "gaps-files-to-source", "emulators", "data",
+ "wiki/verification-modes"):
+ html = (DIST / page / "index.html")
+ if not html.is_file():
+ continue
+ for table in _tables(_read(html)):
+ if len(_headers(table)) < 4:
+ continue
+ with self.subTest(page=page, header=_headers(table)):
+ self.assertIn('data-rb-stack="true"', table.split(">", 1)[0])
+
+ def test_long_tables_get_their_labels_at_runtime(self):
+ """Labels on every cell of a long stacked table were 122 KB of
+ emulators/mame and took it over the 600,000-byte page budget."""
+ self.assertIn("if (kind === 'kv' || rows.length >= RECORD_ROWS) return;", _read(PLUGIN))
+ source = _read(SITE_TS)
+ self.assertIn("const labelCells = (table: HTMLTableElement) => {", source)
+ self.assertIn("labelCells(table);", source)
+ html = _built("emulators/mame")
+ self.assertLess(len(html.encode()), 600_000)
+
+ def test_the_filter_box_is_in_the_html(self):
+ html = _built("gaps-files-to-source")
+ boxes = re.findall(r'(.*?)
\s*Filter [^<]+')
+ self.assertRegex(box, r'[\d,]+ rows')
+ source = _read(SITE_TS)
+ self.assertNotIn("controls.className = 'rb-table-filter'", source)
+ self.assertIn("controls?.matches('.rb-table-filter')", source)
+
+ def test_every_long_table_has_its_box(self):
+ html = _built("systems")
+ for table in _tables(html):
+ rows = len(re.findall(r"", 1)[-1]))
+ if rows < 20:
+ continue
+ start = html.index(table)
+ self.assertIn('class="rb-table-filter"', html[max(0, start - 600):start])
+
+ def test_the_placeholder_names_the_columns(self):
+ """Every filter offered "Name, platform, system, hash...", over the
+ console list too, which has no hash (V small)."""
+ self.assertNotIn("Name, platform, system, hash", _read(SITE_TS))
+ cases = [["File", "Region", "Required", "Hash"], ["Console", "Files"], ["System", "MD5", "Size"]]
+ out = _node_module(
+ f"import {{ filterPlaceholder }} from {json.dumps(PLUGIN.as_uri())};"
+ f"console.log(JSON.stringify({json.dumps(cases)}.map(filterPlaceholder)));"
+ )
+ self.assertEqual(json.loads(out), ["File, region, required, hash...", "Console, files...", "System, MD5, size..."])
+ html = _built("systems")
+ self.assertRegex(html, r'placeholder="Console[^"]*\.\.\."')
+ self.assertNotRegex(html, r'placeholder="[^"]*hash[^"]*"')
+
+
+class HashLookupGivesOneVerdict(unittest.TestCase):
+ """The lookup read "24 matches", "Setups that list it (12)" over 17 rows,
+ and emulators by id. ?md5= showed the empty form (N8, J7)."""
+
+ def test_parameters(self):
+ source = _read(SITE_TS)
+ self.assertIn("const HASH_PARAMS = ['hash', 'md5', 'crc32', 'sha256'];", source)
+ self.assertIn("HASH_PARAMS.map((key) => params.get(key))", source)
+
+ def test_counts_say_what_they_count(self):
+ source = _read(SITE_TS)
+ self.assertNotIn("'match' : 'matches'", source)
+ self.assertIn("count(platforms.length, 'declaration', 'declarations')} by ${count(setups.size, 'setup', 'setups')}", source)
+ self.assertIn("count(setups.size, 'setup lists', 'setups list')", source)
+ self.assertIn("'file in the collection', 'files in the collection'", source)
+
+ def test_emulators_by_name(self):
+ source = _read(SITE_TS)
+ self.assertIn("json(`api/v1/emulators/${owner}.json`)", source)
+ self.assertIn("profile.emulator || profile.display_name || owner", source)
+ profile = json.loads(_read(DIST / "api" / "v1" / "emulators" / "duckstation.json")) \
+ if (DIST / "api" / "v1" / "emulators" / "duckstation.json").is_file() else None
+ if profile is None:
+ self.skipTest("no built site")
+ self.assertEqual(profile["items"][0]["profile"]["emulator"], "DuckStation")
+
+ def test_a_console_page_card_reads_its_manufacturer_file(self):
+ """Every collection match on a console page (systems/sony-playstation/)
+ asked for api/v1/systems/sony-playstation.json, which does not exist:
+ the verdict lost the file's path."""
+ source = _read(SITE_TS)
+ self.assertIn("api/v1/systems/${words.slice(0, size).join('-')}.json", source)
+ api = DIST / "api" / "v1" / "systems"
+ if not api.is_dir():
+ self.skipTest("no built site")
+ self.assertFalse((api / "sony-playstation.json").exists())
+ self.assertTrue((api / "sony.json").is_file())
+
+ def test_no_match_leads_somewhere(self):
+ source = _read(SITE_TS)
+ self.assertIn("link('contributing/', 'add it')", source)
+ anchor = re.search(r"const WANTED = 'provenance/#([^']+)';", source)
+ self.assertIsNotNone(anchor)
+ self.assertIn(f'id="{anchor[1]}"', _built("provenance"))
+ self.assertNotIn("No file the site knows has this hash.", source)
+
+
+class HashAFileHere(unittest.TestCase):
+ """Find a file asked for a hash only: hashing meant another tool (J7)."""
+
+ def test_the_chooser(self):
+ source = _read(SITE_TS)
+ self.assertIn("input.type = 'file';", source)
+ self.assertIn("It is hashed on this device. Nothing is uploaded.", source)
+ self.assertIn("crypto.subtle.digest(name, buffer)", source)
+ self.assertIn("['SHA-1', 'SHA-256']", source)
+ self.assertIn("lookUp(status, result, [found.sha1, found.md5, found.crc32, found.sha256])", source)
+
+ def test_md5_and_crc32_match_python(self):
+ source = _read(SITE_TS)
+ block = source[source.index("// Hashing a file on this device"):source.index("type FileHashes")]
+ sizes = [0, 1, 55, 56, 63, 64, 65, 119, 120, 128, 1000, 70_001]
+ out = _node_module(block + (
+ f"console.log(JSON.stringify({json.dumps(sizes)}.map((n) => {{"
+ "const bytes = new Uint8Array(n);"
+ "for (let i = 0; i < n; i += 1) bytes[i] = (i * 31 + 7) & 0xff;"
+ "return [md5(bytes), crc32(bytes)]; })));"
+ ))
+ for size, (md5, crc) in zip(sizes, json.loads(out)):
+ data = bytes((i * 31 + 7) & 0xFF for i in range(size))
+ with self.subTest(size=size):
+ self.assertEqual(md5, hashlib.md5(data).hexdigest())
+ self.assertEqual(crc, f"{zlib.crc32(data):08x}")
+
+
+class SearchFoldsPagesUnderAHash(unittest.TestCase):
+ """Under "Look up this hash" came 14 unrelated pages: Magnavox for
+ E490.WAV, Philips for cdi490a.zip (N8)."""
+
+ def test_fold(self):
+ source = _read(SITE_TS)
+ self.assertIn("toggle.textContent = 'Pages that mention these characters';", source)
+ self.assertIn("toggle.setAttribute('aria-expanded', 'false');", source)
+ header = _read(COMPONENTS / "Header.astro")
+ self.assertIn(
+ ".pagefind-ui__drawer[data-rb-hash]:not([data-rb-pages-shown]) .pagefind-ui__results-area { display: none; }",
+ header,
+ )
+
+ def test_search_fields_are_weighted_and_never_shown(self):
+ config = _read(CONFIG)
+ self.assertIn("metaWeights: { title: 10, files: 0.5, aliases: 5, devices: 5 },", config)
+ self.assertIn("const UNSHOWN_FIELDS = new Set(['files', 'aliases', 'devices']);", _read(SITE_TS))
+
+
+class OsTabsKeepTheChoiceInView(unittest.TestCase):
+ def test_the_row_scrolls_not_the_page(self):
+ source = _read(SITE_TS)
+ self.assertIn("list.scrollLeft", source)
+ self.assertNotIn("scrollIntoView", source)
+ self.assertIn("osTabs();", source)
+
+
+class CommandsNeverBreakInsideAWord(unittest.TestCase):
+ """`--` / `platform retroarch` at 1440 px, `--` / `system` and
+ `nintendo-` / `64dd` on a phone: a browser breaks a line after a hyphen,
+ whatever overflow-wrap says."""
+
+ def test_the_words_are_wrapped_and_the_text_kept(self):
+ # Shiki cuts `--platform` into two tokens: the span has to hold both.
+ line = {"type": "element", "tagName": "div", "properties": {"className": ["code"]}, "children": [
+ {"type": "element", "tagName": "span", "properties": {"style": "a"}, "children": [
+ {"type": "text", "value": "sh -s "}]},
+ {"type": "element", "tagName": "span", "properties": {"style": "b"}, "children": [
+ {"type": "text", "value": "--"}]},
+ {"type": "element", "tagName": "span", "properties": {"style": "c"}, "children": [
+ {"type": "text", "value": "platform retroarch --system nintendo-64dd"}]},
+ ]}
+ out = json.loads(_node_module(
+ f"import {{ unbreakWords }} from {json.dumps(UNBROKEN.as_uri())};"
+ f"console.log(JSON.stringify(unbreakWords({json.dumps(line)})));"
+ ))
+
+ def text(node: dict) -> str:
+ return node.get("value", "") + "".join(text(child) for child in node.get("children", []))
+
+ self.assertEqual("".join(text(n) for n in out), "sh -s --platform retroarch --system nintendo-64dd")
+ words = [text(n) for n in out if n.get("properties", {}).get("className") == ["rb-nb"]]
+ self.assertEqual(words, ["-s", "--platform", "--system", "nintendo-64dd"])
+ # The colours of each token survive the cut.
+ platform = next(n for n in out if text(n) == "--platform")
+ self.assertEqual([c["properties"]["style"] for c in platform["children"]], ["b", "c"])
+
+ def test_registered(self):
+ config = _read(CONFIG)
+ self.assertIn("import unbrokenTokens from './src/plugins/ec-unbroken-tokens.mjs';", config)
+ self.assertIn("plugins: [unbrokenTokens()],", config)
+ self.assertIn("baseStyles: `.${CLASS} { white-space: nowrap; }`", _read(UNBROKEN))
+ picker = _read(COMPONENTS / "FilePicker.astro")
+ self.assertIn("showCommand(cmd, `${INSTALL} --platform", picker)
+ self.assertIn(".rb-picker .rb-pick-cmd .rb-nb { white-space: nowrap; }", picker)
+ self.assertIn("navigator.clipboard?.writeText(cmd.textContent ?? '')", picker)
+
+ def test_copy_gives_the_rendered_text(self):
+ """The copy button's text is the block's text: the spans add nothing.
+ Expressive Code leaves a leading comment line out of the copy."""
+ import html as html_lib
+
+ page = _built("platforms/retroarch")
+ figures = re.findall(r"", page, re.S)
+ self.assertTrue(figures)
+ self.assertIn('', page)
+ for figure in figures:
+ copied = re.search(r'data-code="([^"]*)"', figure)
+ if not copied:
+ continue
+ lines = re.findall(r'(.*?)
', figure, re.S)
+ shown = "\n".join(html_lib.unescape(re.sub(r"<[^>]+>", "", line)) for line in lines)
+ code = html_lib.unescape(copied[1]).replace("\x7f", "\n")
+ with self.subTest(code=code[:60]):
+ self.assertTrue(shown.endswith(code), (shown, code))
+
+
+class LogosAtTheirDisplaySize(unittest.TestCase):
+ """136 KiB of the home page was 96 px PNG logos shown at 40 px (T-F7)."""
+
+ def test_raster_logos_are_small_webp(self):
+ manifest = json.loads(_read(LOGOS / "sources.json"))["images"]
+ self.assertTrue(manifest)
+ for entry in manifest:
+ path = LOGOS / entry["file"]
+ with self.subTest(file=entry["file"]):
+ self.assertTrue(path.is_file())
+ self.assertIn(path.suffix, (".webp", ".svg"))
+ if path.suffix == ".webp":
+ self.assertEqual(_webp_size(path.read_bytes()), (LOGO_PIXELS, LOGO_PIXELS))
+ self.assertEqual(sorted(p.name for p in LOGOS.glob("*.png")), [])
+ script = _read(REPO_ROOT / "scripts" / "fetch_site_images.py")
+ self.assertIn(f"LOGO_SIZE = {LOGO_PIXELS}", script)
+ self.assertIn('square_image(data, LOGO_SIZE, "WEBP")', script)
+
+ def test_pages_point_at_the_copies(self):
+ html = _built("platforms/emudeck")
+ self.assertIn('src="/retrobios/assets/logos/emudeck.webp"', html)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md
index 2e67ef63..e7d37756 100644
--- a/wiki/testing-guide.md
+++ b/wiki/testing-guide.md
@@ -54,7 +54,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_offline`, `test_install_selection` |
| 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_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_p0`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `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_p0`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
## Test architecture