"""Long lists read as a table first, details on demand (density round). MAME's card pages read 13,000 to 16,700 words, card after card, and GroovyMAME 14,400 before its last entry. The TOSEC wanted list was 46 closed blocks, one per DAT, opened one by one to find a name. Each entry is now a table row that opens to its card, and each wanted list one filtered table under its counts. Nothing the pages said is lost: the tests below read every name and hash back. """ from __future__ import annotations import re import sys import unittest from html.parser import HTMLParser from pathlib import Path import yaml REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT / "scripts")) import generate_site as gs # noqa: E402 import siteentries # noqa: E402 import validate_site # noqa: E402 EMPTY_DB = {"files": {}, "indexes": {"by_name": {}}} DIST = REPO_ROOT / "website" / "dist" def _archive(i: int, members: int = 3, **fields: object) -> dict: return { "name": f"set{i:04d}.zip", "required": True, "category": "bios_zip", "contents": [{"name": f"m{i}_{k}.rom", "size": 1024, "crc32": f"{i:04x}{k:04x}", "sha1": f"{i:04x}{k:04x}" * 5} for k in range(members)], **fields, } class TheEntryTable(unittest.TestCase): def _rows(self, count: int) -> list[siteentries.Row]: return [siteentries.Row(f'

n{i}

', [f"- Size: {i} bytes"], [str(i), "", f"MD5 {i}"]) for i in range(count)] def test_a_long_table_has_its_filter_box_and_stacks(self): lines = siteentries.entry_table(self._rows(25), gs.ENTRY_HEADS, "rb-entry-filter-x", "Required") text = "\n".join(lines) self.assertRegex(text, r'
25 rows
\n', text) # The Status column is empty on every row: not written. self.assertIn('' '', text) self.assertNotIn("Status", text) self.assertEqual(text.count('
'), 25) # Labels are written by site.ts on a table this long, as on a Markdown one. self.assertNotIn("data-label", text) def test_a_card_opens_under_its_name_as_markdown(self): lines = siteentries.entry_table(self._rows(2), gs.ENTRY_HEADS, "x", "Required") at = lines.index('

n0

') # A blank line after the summary: the body is read as Markdown. self.assertEqual(lines[at + 1:at + 4], ["", "- Size: 0 bytes", ""]) self.assertNotIn("rb-table-filter", "\n".join(lines)) self.assertIn('
', lines) def test_a_card_loses_only_its_wrapper(self): card = ['
', "", '

x

', "", "Desc.", "", '
', "", "- SHA1: s", "", "
", "", "
", ""] title, body = siteentries.card_title_and_body(card) self.assertEqual(title, '

x

') self.assertEqual(body, ["Desc.", "", '
', "", "- SHA1: s", "", "
"]) def test_the_placeholder_reads_like_the_plugin(self): self.assertEqual(siteentries.filter_placeholder(["File", "Contents", "Status", "Hash"]), "File, contents, status, hash...") self.assertEqual(siteentries.filter_placeholder(["Name", "DAT", "Description", "SHA1"]), "Name, DAT, description, SHA1...") class ASplitEmulatorListsItsCardsAsRows(unittest.TestCase): def setUp(self): self.files = [_archive(i, members=1 + i % 4) for i in range(gs.EMULATOR_CARDS_PER_PAGE * 2)] self.pages = gs.emulator_pages( "big", {"emulator": "Big", "type": "standalone", "files": self.files}, EMPTY_DB) def test_parts_hold_at_most_a_part_of_cards(self): parts = gs.emulator_parts("big", self.files) self.assertEqual(len(parts), -(-len(self.files) // gs.EMULATOR_CARDS_PER_PART)) self.assertLessEqual(max(len(entries) for _, entries in parts), gs.EMULATOR_CARDS_PER_PART) def test_every_name_and_member_hash_is_on_its_part(self): parts = {stem: page for stem, page in self.pages.items() if stem != "big"} anchors = gs.emulator_file_anchors(self.files) for stem, entries in gs.emulator_parts("big", self.files): page = parts[stem] self.assertIn('
FileContentsHash1
', page) self.assertNotIn('class="rb-file-entry"', page) for i in entries: f = self.files[i] self.assertIn(f'

{len(f['contents'])}", page) def test_the_section_badge_is_not_repeated_on_each_row(self): page = self.pages["big-files-1"] self.assertIn("## Required", page) self.assertNotIn(">required", page) self.assertIn(">missing", page) class ALongEmulatorPageIsOneTable(unittest.TestCase): def test_rows_replace_the_index_and_the_cards(self): files = ([_archive(i) for i in range(gs.EMULATOR_TABLE_MIN - 10)] + [{"name": f"opt{i}.bin", "md5": f"{i:032x}"} for i in range(10)]) page = gs.generate_emulator_page( "long", {"emulator": "Long", "type": "standalone", "files": files}, EMPTY_DB, None, set()) self.assertNotIn("| File |", page) self.assertNotIn('class="rb-file-entry"', page) self.assertEqual(page.count('
'), len(files)) optional = page[page.index("## Optional"):] # Hash column: the first 12 characters, the whole value in the title # and in the card. md5 = f"{9:032x}" self.assertIn(f'MD5 {md5[:12]}', optional) self.assertRegex(optional, rf'MD5: ]*>{md5}') def test_a_shorter_page_keeps_its_cards(self): files = [_archive(i) for i in range(gs.EMULATOR_TABLE_MIN - 1)] page = gs.generate_emulator_page( "short", {"emulator": "Short", "type": "standalone", "files": files}, EMPTY_DB, None, set()) self.assertIn('class="rb-file-entry"', page) self.assertNotIn('class="rb-entries"', page) class TheWantedListIsOneFilteredTable(unittest.TestCase): def setUp(self): dats = ["Sony - PlayStation 2 - BIOS Images", "Nintendo - GameCube - BIOS Images"] self.missing = [{"name": f"bios{i}.bin", "sha1": f"{i:040x}", "description": f"Dump {i}", "dat": dats[i % 2]} for i in range(30)] self.missing.append({"name": "both.bin", "sha1": "f" * 40, "dats": dats}) report = {"redump": { "imported_at": "2026-01-01", "dats": {}, "matched": 12, "out_of_scope": 7, "covered_dats": dats + ["Sega - Dreamcast - BIOS Images"], "missing": [dict(entry) for entry in self.missing] + [ {"name": "made.bin", "sha1": "a" * 40, "dat": dats[0]}], }} moved: dict[str, str] = {} self.main = gs.generate_provenance_page( {"files": {}}, report, {"a" * 40: {"from": ["bios/x.bin"], "transform": "half"}}, moved) self.page = moved["provenance-redump"] def test_the_counts_come_first(self): head = self.page[:self.page.index(".*?\n]*>\n' r"All 31 wanted dumps\n

31 rows', box.group(0)) self.assertEqual(self.page.count("", self.page) self.assertIn(f'', self.page) self.assertIn("Sony - PlayStation 2 - BIOS Images, Nintendo - GameCube - BIOS Images", self.page) self.assertNotIn("made.bin", self.page) class TheBrowserSide(unittest.TestCase): CSS = (REPO_ROOT / "website" / "src" / "styles" / "retrobios.css").read_text(encoding="utf-8") TS = (REPO_ROOT / "website" / "src" / "scripts" / "site.ts").read_text(encoding="utf-8") def test_a_filtered_row_stays_hidden_on_a_phone(self): # A stacked row is `display: block`, which outweighed `hidden`. self.assertIn(".sl-markdown-content table[data-rb-stack] tr[hidden] { display: none; }", self.CSS) def test_a_box_above_a_closed_block_opens_it(self): self.assertIn("block?.previousElementSibling?.matches('.rb-table-filter')", self.TS) self.assertIn("if (block && query) block.open = true;", self.TS) def test_a_link_to_a_folded_card_opens_its_row(self): start = self.TS.index("const openTargetBlock = () =>") body = self.TS[start:self.TS.index("};", start)] self.assertIn("closest('details')", body) self.assertIn("block.open = true;", body) self.assertIn("addEventListener('hashchange', openTargetBlock);", self.TS) def test_an_open_card_takes_the_table_width(self): self.assertIn(".sl-markdown-content .rb-entries-wrap { container-type: inline-size; }", self.CSS) self.assertIn("details.rb-entry[open] > :not(summary) { width: 100cqi; }", self.CSS) class _Visible(HTMLParser): """Words in
outside closed blocks (their summary shows).""" VOID = frozenset({"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"}) def __init__(self) -> None: super().__init__(convert_charrefs=True) self.stack: list[tuple[str, str]] = [] self.hidden = self.main = self.skip = 0 self.words = 0 def handle_starttag(self, tag, attrs): if tag in self.VOID: return effect = "" if tag == "summary" and self.stack and self.stack[-1][1] == "hide": self.hidden -= 1 effect = "show" elif tag == "details" and "open" not in dict(attrs): self.hidden += 1 effect = "hide" if tag in ("script", "style", "template"): self.skip += 1 if tag == "main": self.main += 1 self.stack.append((tag, effect)) def handle_endtag(self, tag): if tag in self.VOID: return while self.stack: name, effect = self.stack.pop() self.hidden += {"hide": -1, "show": 1}.get(effect, 0) self.skip -= name in ("script", "style", "template") self.main -= name == "main" if name == tag: break def handle_data(self, data): if self.main and not self.skip and not self.hidden: self.words += len(data.split()) def _visible_words(page: str) -> int: parser = _Visible() parser.feed((DIST / page / "index.html").read_text(encoding="utf-8")) return parser.words # Words a visitor sees with every block closed, by page type: the largest # measured after the change plus 5 %, never above the largest before it. # Before: MAME parts 16,680, long emulator pages 14,396 (groovymame), # wanted lists 346 (tosec). After: 1,612, 2,000 (yaps2), 118. VISIBLE_CEILINGS = { "emulator card parts": (r"emulators/[a-z0-9-]+-files-\d+$", 1_693), "long emulator pages": (r"emulators/(groovymame|yaps2|bizhawk|bluemsx|fbneo)$", 2_100), "wanted lists": (r"provenance-[a-z-]+$", 124), } @unittest.skipUnless((DIST / "emulators" / "mame-files-1" / "index.html").exists(), "website/dist is not built") class TheBuiltPagesStayShort(unittest.TestCase): def _pages(self, pattern: str) -> list[str]: found = [path.parent.relative_to(DIST).as_posix() for path in DIST.glob("**/index.html")] return sorted(page for page in found if re.fullmatch(pattern, page)) def test_visible_words_under_each_ceiling(self): for kind, (pattern, ceiling) in VISIBLE_CEILINGS.items(): pages = self._pages(pattern) self.assertTrue(pages, kind) for page in pages: with self.subTest(page=page): self.assertLessEqual(_visible_words(page), ceiling, kind) def test_under_the_element_budget(self): for pattern, _ceiling in VISIBLE_CEILINGS.values(): for page in self._pages(pattern): with self.subTest(page=page): parsed = validate_site._PageParser(DIST / page / "index.html") parsed.feed((DIST / page / "index.html").read_text(encoding="utf-8")) self.assertLessEqual(parsed.page.elements, validate_site.MAX_PAGE_ELEMENTS) def test_every_mame_set_and_member_is_on_a_part(self): profile = yaml.safe_load((REPO_ROOT / "emulators" / "mame.yml").read_text(encoding="utf-8")) html = "".join((DIST / page / "index.html").read_text(encoding="utf-8") for page in self._pages(r"emulators/mame-files-\d+")) for f in profile["files"]: with self.subTest(name=f["name"]): self.assertIn(f"{f['name']}", html) for member in f.get("contents") or []: if member.get("crc32"): self.assertIn(str(member["crc32"]), html) if member.get("sha1"): self.assertIn(str(member["sha1"]), html) if __name__ == "__main__": unittest.main()
"), 31) for entry in self.missing: self.assertIn(f"
{entry['name']}{entry["sha1"]}