#!/usr/bin/env python3 """ Characterization tests for tools/build_content.py (U2). The core guarantee (plan R5): rendering every content/ entry reproduces the CURRENT live detail page, compared semantically (whitespace- and attribute-order-insensitive). This is what protects against SEO/layout regressions when the build takes over page generation. Run directly: python tools/tests/test_build_content.py Or with pytest: pytest tools/tests/test_build_content.py """ import os import sys import unittest TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOT = os.path.dirname(TOOLS) sys.path.insert(0, TOOLS) import build_content as bc # noqa: E402 def _render(out_rel): """Render the content entry whose output path ends with out_rel.""" for meta, body, _path in bc.iter_entries(): if bc.output_path(meta).replace(os.sep, "/").endswith(out_rel): base = bc._template_html(meta) return meta, bc.build_entry(meta, body, base) raise AssertionError(f"no content entry maps to {out_rel}") def _current(out_rel): with open(os.path.join(ROOT, out_rel), encoding="utf-8") as f: return f.read() class ReproducesEverySite(unittest.TestCase): def test_all_pages_semantically_equivalent(self): """Every rendered page must equal the current page (semantic compare).""" _built, mismatches = bc.run(check=True, write=False) self.assertEqual(mismatches, [], f"pages diverged from the live site: {mismatches}") class TitleSuffixFaithful(unittest.TestCase): def test_cyrillic_suffix_preserved(self): # MK pages use ' - МСОС' (Cyrillic); it must survive, not be dropped. meta, rendered = _render("mk/projects/macedonian-student-night-in-ljubljana/index.html") self.assertIn("- МСОС", rendered) # - МСОС def test_no_suffix_stays_absent(self): # Some event pages have no ' - MSOS' suffix; we must not invent one. _meta, rendered = _render("en/projects/morning-coffee-in-front-of-ctk/index.html") self.assertNotIn("- MSOS", rendered) class RichContentSurvives(unittest.TestCase): def test_event_gallery_and_lightbox_render(self): _meta, rendered = _render("en/projects/paint-and-wine-at-sunset/index.html") self.assertIn("event-gallery", rendered) self.assertIn("eg-thumb", rendered) def test_blog_guidebox_and_scrollspy_render(self): _meta, rendered = _render("en/blog/how-to-open-slovenian-bank-account/index.html") self.assertIn("guide-box", rendered) self.assertIn('class="scrollspy-nav"', rendered) class ScrollspyPreservesCuratedLabels(unittest.TestCase): def test_hand_shortened_labels_not_overwritten(self): # The body H2 is long; the sidebar label was shortened by hand. The # rendered page must keep the curated short label, not the H2 text. _meta, rendered = _render("en/projects/macedonian-student-night-in-ljubljana/index.html") self.assertIn(">Idea to Movement", rendered) class HeroPathDepth(unittest.TestCase): def test_hero_src_uses_relative_depth(self): _meta, rendered = _render("en/blog/how-to-open-slovenian-bank-account/index.html") # Hero must resolve from the page's depth, not a root-absolute path. self.assertIn('src="../../../images/', rendered) self.assertNotIn('src="/images/', rendered) class NewsStructure(unittest.TestCase): def test_news_uses_news_article_body(self): _meta, rendered = _render("en/news/proof-of-means-updated-2026/index.html") self.assertIn('class="news-article-body"', rendered) self.assertIn('class="news-article-date"', rendered) if __name__ == "__main__": unittest.main(verbosity=2)