mdcms/test_phase.py
Claude b626d5e066
Add test_phase.py for Phase 4
- Phase 4 branch points to claude/debug-api-errors-gd730
- Phase 4 EXTRA_FILES checks out app/config.yml and app/pages/home.md
  from the branch (needed for message: key test and callout test cases)
- Updated Phase 4 verify checklist covers all spec requirements:
  basic types, title row, no-title, markdown body, icon override,
  message: key, dark mode
- Added EXTRA_FILES mechanism for per-phase content file checkout
- Improved checklist formatting with section headers

https://claude.ai/code/session_01UP8Wo2CKPNhvvTkzX48CWF
2026-05-17 17:50:01 +00:00

223 lines
8.3 KiB
Python

#!/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()