#!/usr/bin/env python3 """ MD-CMS v0.4 phase test runner. Usage: python3 test_phase.py [phase] phase: 1-7 (default: run all phases sequentially) Each phase fetches the corresponding branch, checks out the renderer and content files for that phase, starts a local HTTP server, and opens the browser. Press Enter when done to continue to the next phase. """ import functools import http.server import subprocess import sys import threading import time import webbrowser from pathlib import Path PORT = 8800 PHASES = { 1: ("main", "theme.yml and colour system"), 2: ("v0.4_phase2", "Icon system — local SVGs, no Google Fonts"), 3: ("v0.4_phase3", "Asset validation in mdcms build"), 4: ("claude/debug-api-errors-gd730", "Callout tags"), 5: ("v0.4_phase5", "Table of contents tag"), 6: ("v0.4_phase6", "Offline / fetch-deps"), 7: ("v0.4_phase7", "PWA — service worker and manifest"), } VERIFY = { 1: [ "Existing site renders correctly with theme.yml present", "Missing theme: key falls back gracefully to hardcoded defaults", "Accent colour and dark/light mode colours apply from theme.yml", ], 2: [ "All UI icons render correctly from local SVG files (no Google Icons font)", "Theme toggle, search, hamburger, nav arrows all show icons", "Broken image displays for a missing icon (test by renaming one SVG)", "Icon name normalisation: 'arrow-right' and 'Arrow Right' both resolve", ], 3: [ "Run: python3 mdcms.py build --path app/ (NOT the installed mdcms command)", "Warning printed for assets/images/missing-photo.png (referenced in pages/about.md)", "No warning for assets/images/logo.png (file exists)", "Build continues and completes after warnings", ], 4: [ "── Basic types ─────────────────────────────────────────────────", "callout-info, callout-warning, callout-success, callout-error all render", "Each has: coloured left border + low-opacity background in the right colour", "── Title row ───────────────────────────────────────────────────", "Title row shows: icon (inlined SVG) + bold title text in the accent colour", "Title text is correct for each type: Information / Warning / Success / Error", "── No-title callout ────────────────────────────────────────────", "Callout with no title key: no title row rendered, just the body", "── Markdown body ───────────────────────────────────────────────", "Body renders full markdown: bold, italic, lists, inline code, links", "── Custom icon override ────────────────────────────────────────", "icon: warning on a callout-info renders the warning SVG, not the info SVG", "── Config-defined message: key ─────────────────────────────────", "message: aitranslation resolves title 'PLEASE NOTE:' and body from config.yml", "── Dark mode ───────────────────────────────────────────────────", "Toggle dark mode: all four callout types still look correct", "Colours adapt (border and background tint remain visible on dark background)", ], 5: [ "TOC tag renders a section-grouped page list", "Only pages visible for active category are listed", "Draft pages are excluded", "TOC page itself is excluded from the listing", "Section headings and sort order are correct", ], 6: [ "mdcms fetch-deps downloads JS/CSS to assets/required/vendors/", "Patched index.html makes no external network requests", "Fonts load correctly from local paths", "Site loads fully offline after fetch-deps", ], 7: [ "service-worker.js and manifest.json generated when pwa: yes", "Full site accessible offline after one online visit", "Cache updates correctly on new build deployment", "Offline message displays when cache evicted", ], } # Files checked out from the phase branch on every phase switch. # Renderer + presentational config. Content files (home.md, config.yml) are # added per phase in EXTRA_FILES so the right test content is loaded. RENDERER_FILES = [ "app/index.html", "app/theme.yml", "app/assets/icons", "mdcms.py", ] # Per-phase extra files to check out from the phase branch. # Use this for content or config files that differ between phases. EXTRA_FILES = { 4: [ "app/config.yml", # has callouts: block for message: key test "app/pages/home.md", # has Phase 4 callout test cases ], } def checkout_phase(phase: int) -> bool: branch, _ = PHASES[phase] repo_root = Path(__file__).parent print(f"\n Fetching branch {branch} from origin...") result = subprocess.run( ["git", "fetch", "origin", branch], cwd=repo_root, capture_output=True, text=True, ) if result.returncode != 0: print(f" ERROR fetching: {result.stderr.strip()}") return False files = RENDERER_FILES + EXTRA_FILES.get(phase, []) print(f" Checking out {len(files)} file groups from origin/{branch}...") result = subprocess.run( ["git", "checkout", f"origin/{branch}", "--"] + files, cwd=repo_root, capture_output=True, text=True, ) if result.returncode != 0: print(f" ERROR checking out files: {result.stderr.strip()}") return False print(" Rebuilding nav.yml...") result = subprocess.run( ["python3", "mdcms.py", "build", "--path", "app/"], cwd=repo_root, capture_output=True, text=True, ) if result.returncode != 0: print(f" WARNING: mdcms build failed: {result.stderr.strip()}") else: out = result.stdout.strip() if out: print(f" {out}") print(" Ready.\n") return True def serve(app_dir: Path): handler = functools.partial( http.server.SimpleHTTPRequestHandler, directory=str(app_dir), ) handler.log_message = lambda *a: None with http.server.HTTPServer(("", PORT), handler) as httpd: httpd.serve_forever() def run_phase(phase: int): branch, description = PHASES[phase] print("\n" + "=" * 62) print(f" Phase {phase}: {description}") print(f" Branch: {branch}") print("=" * 62) repo_root = Path(__file__).parent app_dir = repo_root / "app" if phase > 1: if not checkout_phase(phase): print(" Checkout failed — skipping this phase.") return print(f" Serving app/ at http://localhost:{PORT}\n") print(" Checklist:") for item in VERIFY[phase]: if item.startswith("──"): print(f"\n {item}") else: print(f" [ ] {item}") t = threading.Thread(target=serve, args=(app_dir,), daemon=True) t.start() time.sleep(0.3) webbrowser.open(f"http://localhost:{PORT}") print("\n Press Enter when done (Ctrl+C to abort)...") try: input() except KeyboardInterrupt: print("\n Aborted.") def main(): if len(sys.argv) == 2: try: phase = int(sys.argv[1]) except ValueError: print("Usage: python3 test_phase.py [1-7]") sys.exit(1) if phase not in PHASES: print(f"Phase must be 1-7, got {phase}") sys.exit(1) run_phase(phase) else: for phase in sorted(PHASES): run_phase(phase) print(f"\n Phase {phase} done.") if __name__ == "__main__": main()