#!/usr/bin/env python3 """Adversarial tests for verify-motion.py and its canonical template.""" from __future__ import annotations import importlib.util import re import subprocess import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent VERIFIER = ROOT / "scripts/verify-motion.py" SEMANTIC_VERIFIER = ROOT / "scripts/verify-semantic-motion.py" TEMPLATE = ROOT / "skills/diagram-design/assets/template-motion.html" EXAMPLE = ROOT / "skills/diagram-design/assets/example-policy-trace-animated.html" def load_verifier(): spec = importlib.util.spec_from_file_location("verify_motion", VERIFIER) if spec is None or spec.loader is None: raise AssertionError("could not load verify-motion.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def load_semantic_verifier(): spec = importlib.util.spec_from_file_location("verify_semantic_motion", SEMANTIC_VERIFIER) if spec is None or spec.loader is None: raise AssertionError("could not load verify-semantic-motion.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def main() -> int: module = load_verifier() semantic_module = load_semantic_verifier() source = TEMPLATE.read_text(encoding="utf-8") with tempfile.TemporaryDirectory(prefix="verify-motion-") as temporary: directory = Path(temporary) def check(name: str, html: str, expected: str | None) -> None: path = directory / f"{name}.html" path.write_text(html, encoding="utf-8") errors = module.verify(path) if expected is None: if errors: raise AssertionError(f"{name}: expected pass, got {errors}") print(f"OK: {name} accepted") else: if not any(expected in error for error in errors): raise AssertionError(f"{name}: expected {expected!r}, got {errors}") print(f"OK: {name} rejected — {expected}") check("canonical-template", source, None) script_free_none = re.sub( r"", "", source, flags=re.IGNORECASE | re.DOTALL ) script_free_none = re.sub( r"
", "", script_free_none, flags=re.DOTALL ) script_free_none = re.sub( r"\sdata-motion-item\sdata-step=\"\d+\"", "", script_free_none ) script_free_none = script_free_none.replace( 'data-motion-mode="step" data-step-count="5"', 'data-motion-mode="none" data-step-count="0"', 1, ) check("script-free-none", script_free_none, None) none_with_controls = script_free_none.replace( "", '
', 1 ) check( "none-with-controls", none_with_controls, "none mode must not expose playback controls or live status", ) loop_two = script_free_none.replace( 'data-motion-mode="none" data-step-count="0"', 'data-motion-mode="loop" data-step-count="2"', 1, ).replace( "", '' '', 1, ) check( "loop-two-semantic-items", loop_two, "loop mode allows at most one semantic item", ) example_errors = module.verify(EXAMPLE) if example_errors: raise AssertionError(f"shipped example: expected pass, got {example_errors}") print("OK: shipped animated example accepted") shipped = module.shipped_motion_files() if shipped != sorted([TEMPLATE, EXAMPLE], key=lambda path: path.as_posix()): raise AssertionError(f"unexpected shipped motion inventory: {shipped}") print("OK: shipped motion inventory is parsed and complete") check( "bad-mode", source.replace('data-motion-mode="step"', 'data-motion-mode="cinematic"', 1), "data-motion-mode must be one of", ) check( "too-many-steps", source.replace('data-step-count="5"', 'data-step-count="9"', 1), "semantic step count must be 1..8", ) check( "step-gap", source.replace('data-step="4" aria-label="Step 4', 'data-step="3" aria-label="Step 4', 1), "semantic steps must be contiguous", ) check( "missing-pause", source.replace('data-motion-action="pause"', 'data-motion-action="stop"', 1), "missing actions: pause", ) check( "bad-live-region", source.replace('aria-live="polite"', 'aria-live="assertive"', 1), "motion status needs role=status", ) check( "hidden-fallback", source.replace('data-motion-item data-step="1"', 'data-motion-item data-step="1" style="opacity:0"', 1), "fallback must be visible", ) check( "decorative-focus", source.replace('data-motion-decorative aria-hidden="true" focusable="false"', 'data-motion-decorative aria-hidden="false" focusable="true"', 1), "needs aria-hidden=true and focusable=false", ) check( "missing-reduced-motion", source.replace("prefers-reduced-motion", "user-reduced-motion"), "missing reduced-motion CSS fallback", ) check( "reduced-motion-controls-visible", source.replace( "[data-motion-controls] { display: none !important; }", "[data-motion-controls] { display: flex !important; }", 1, ), "missing reduced-motion hidden controls", ) check( "hidden-controls-overridden", source.replace("[data-motion-controls][hidden] { display: none !important; }", "", 1), "missing hidden control override", ) check( "missing-static-override", source.replace('data-motion="static"', 'data-motion="still"'), "missing deterministic static override", ) check( "late-title", source.replace( '