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