Files
maskanx_cm_backend/tests/test_html_presentation_deck_skill.py
T

1950 lines
52 KiB
Python
Raw Normal View History

2026-08-01 10:28:22 +05:30
import importlib.util
import re
import subprocess
import sys
from pathlib import Path
import pytest
from adclaw.agents.skill_scanner import SkillSecurityScanner
SKILL_DIR = Path(__file__).resolve().parents[1] / "src/adclaw/agents/skills/html-presentation-deck"
def _load_validate_html_deck_module():
spec = importlib.util.spec_from_file_location(
"validate_html_deck",
SKILL_DIR / "scripts" / "validate_html_deck.py",
)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
IDEOGRAPH_RE = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]")
def _term(*codes: int) -> str:
return "".join(chr(code) for code in codes)
BANNED_TERMS = [
_term(103, 117, 105, 122, 97, 110, 103),
_term(111, 112, 55, 52, 49, 56),
_term(112, 112, 116, 45, 115, 107, 105, 108, 108),
_term(122, 104, 45, 67, 78),
_term(78, 111, 116, 111, 32, 83, 97, 110, 115, 32, 83, 67),
_term(78, 111, 116, 111, 32, 83, 101, 114, 105, 102, 32, 83, 67),
]
BANNED_RE = re.compile(
"|".join(
rf"(?<![A-Za-z0-9]){re.escape(term)}(?![A-Za-z0-9])"
for term in BANNED_TERMS
),
re.IGNORECASE,
)
PRODUCT_GRID_TEST_CSS = (
".deck{display:flex}.slide{display:block}.stage{display:block}"
".progress{display:block}.nav{display:block}.index{display:block}"
)
def test_html_presentation_deck_has_no_non_english_markers_or_banned_terms():
for path in SKILL_DIR.rglob("*"):
if path.is_file() and path.suffix.lower() in {".md", ".html", ".py", ".json", ".txt"}:
text = path.read_text(encoding="utf-8")
assert not IDEOGRAPH_RE.search(text), f"Non-English ideograph found in {path.relative_to(SKILL_DIR)}"
assert not BANNED_RE.search(text), f"Banned term found in {path.relative_to(SKILL_DIR)}"
def test_html_presentation_deck_skill_docs_use_portable_validator_commands():
skill_md = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8")
checklist = (SKILL_DIR / "references" / "checklist.md").read_text(encoding="utf-8")
hardcoded = "src/adclaw/agents/skills/html-presentation-deck/scripts/validate"
for label, text in (("SKILL.md", skill_md), ("checklist.md", checklist)):
for line in text.splitlines():
if "python3" in line:
assert hardcoded not in line, f"{label} must not hardcode MaskanX repo validator path: {line}"
assert "<skill-dir>/scripts/validate_html_deck.py" in skill_md
assert "<skill-dir>/scripts/validate_deck_quality.py" in skill_md
assert ".codex/skills/html-presentation-deck" in skill_md
assert "active_skills/html-presentation-deck" in skill_md
def test_html_presentation_deck_assets_exist():
expected = [
"assets/screenshot-backgrounds/editorial/ink-paper.webp",
"assets/screenshot-backgrounds/editorial/indigo-porcelain.webp",
"assets/screenshot-backgrounds/editorial/forest-ledger.webp",
"assets/screenshot-backgrounds/editorial/warm-archive.webp",
"assets/screenshot-backgrounds/editorial/sand-gallery.webp",
"assets/screenshot-backgrounds/clean-grid/blue-anchor.webp",
"assets/screenshot-backgrounds/clean-grid/lemon-signal.webp",
"assets/screenshot-backgrounds/clean-grid/lime-circuit.webp",
"assets/screenshot-backgrounds/clean-grid/orange-marker.webp",
]
for rel_path in expected:
path = SKILL_DIR / rel_path
assert path.exists(), f"Missing generated asset: {rel_path}"
assert path.stat().st_size > 1024, f"Generated asset is unexpectedly small: {rel_path}"
def test_html_presentation_deck_referenced_assets_resolve():
reference = SKILL_DIR / "references/screenshot-framing.md"
text = reference.read_text(encoding="utf-8")
referenced_assets = sorted(set(re.findall(r"`(screenshot-backgrounds/[^`]+\.webp)`", text)))
assert len(referenced_assets) == 9
for rel_path in referenced_assets:
path = (reference.parent / "../assets" / rel_path).resolve()
assert path.is_file(), f"Broken screenshot background reference: {rel_path}"
def test_html_presentation_deck_static_scan_is_safe():
result = SkillSecurityScanner().scan_skill(SKILL_DIR)
assert result.safe is True
assert result.critical_count == 0
def test_html_deck_validator_accepts_single_quoted_html_and_generic_region_word(tmp_path):
deck = tmp_path / "deck.html"
regional_word = "".join(chr(code) for code in [83, 119, 105, 115, 115])
deck.write_text(
f"""<!doctype html>
<html lang='en'>
<head><title>Runtime test</title></head>
<body>
<section class='slide'><h1>{regional_word} market readout</h1></section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_html_deck_validator_handles_invalid_path_cleanly(tmp_path):
directory = tmp_path / "deck.html"
directory.mkdir()
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(directory),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 2
assert "Invalid HTML deck path" in result.stderr
def test_html_deck_validator_merges_inline_tokens_with_root_for_contrast(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --muted: #777777; --paper: #ffffff; --panel: #f5f5f5; --accent: #ff5500; }
</style>
</head>
<body>
<section class="slide" style="--panel:#151515">
<p class="muted">Low contrast body</p>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts" / "validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "muted text on panel" in result.stderr
def test_html_deck_contrast_helpers():
mod = _load_validate_html_deck_module()
assert mod._luminance("#ffffff") > mod._luminance("#000000")
assert mod._contrast("#000000", "#ffffff") == 21.0
def test_html_deck_css_variable_contexts_nested_root_and_shorthand_hex():
mod = _load_validate_html_deck_module()
html = """
<style>
:root {
--paper: #ffffff;
--muted: #888888;
--accent-text: #ffcc00;
@media (min-width: 1px) { --panel: #111111; }
}
</style>
<section class="slide" style="--panel:#222222"></section>
"""
contexts = mod._css_variable_contexts(html)
assert any(name.startswith(":root") for name, _ in contexts)
root_contexts = [vars for name, vars in contexts if name.startswith(":root")]
assert len(root_contexts) == 1
assert "--paper" in root_contexts[0]
inline_vars = next(vars for name, vars in contexts if name.startswith("inline"))
assert inline_vars["--muted"].rgb == "#888888"
assert inline_vars["--panel"].rgb == "#222222"
shorthand_only = mod._parse_css_variables(":root { --paper: #fff; }")
assert shorthand_only == {}
def test_html_deck_parses_slide_theme_rule_tokens():
mod = _load_validate_html_deck_module()
html = """
<style>
:root { --ink: #090b0f; --paper: #f7f7f1; --accent: #165cff; --muted: #59605a; --panel: #ecefe6; }
.slide.theme-dark {
--muted: rgba(247,247,241,180);
--panel: rgba(247,247,241,18);
}
.slide.theme-dark {
--muted: #cccccc;
--panel: #dddddd;
}
</style>
<section class="slide theme-dark"></section>
"""
contexts = mod._css_variable_contexts(html)
dark = [c for n, c in contexts if "theme-dark" in n][0]
assert dark["--ink"].rgb == "#090b0f"
assert dark["--muted"].rgb == "#f7f7f1"
assert dark["--muted"].alpha == pytest.approx(1.0)
bad = [c for n, c in contexts if "theme-dark" in n][1]
errors = mod._validate_contrast(
"slide theme rule 2 (.slide.theme-dark)",
bad,
)
assert any("muted text on panel" in error for error in errors)
def test_html_deck_validator_checks_every_grouped_theme_selector(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --ink: #000000; --paper: #ffffff; --yellow: #fff200; --accent: #165cff; --accent-on: #ffffff; --muted: #777777; --panel: #ffffff; }
.slide.theme-dark, .slide.theme-yellow { --muted: #000000; --panel: #000000; }
</style>
</head>
<body><section class="slide theme-dark"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "theme-dark" in result.stderr
assert "muted text on slide background" in result.stderr
def test_html_deck_validator_accepts_theme_class_before_slide_class(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --ink: #000000; --paper: #ffffff; --accent: #165cff; --accent-on: #ffffff; --muted: #777777; --panel: #ffffff; }
.theme-dark.slide { --muted: #000000; --panel: #000000; }
</style>
</head>
<body><section class="slide theme-dark"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "theme-dark" in result.stderr
assert "muted text on slide background" in result.stderr
def test_html_deck_validator_handles_grouped_root_selector_lists(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
html, :root { --paper: #ffffff; --muted: #ffffff; --panel: #eeeeee; }
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "muted text on paper" in result.stderr
def test_html_deck_validator_ignores_descendant_root_scoped_tokens(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --ink: #111111; --paper: #ffffff; --muted: #555555; --panel: #eeeeee; }
:root .component { --muted: #ffffff; }
</style>
</head>
<body><section class="slide"><div class="component"></div></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_html_deck_validator_checks_primary_text_on_paper(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --ink: #ffffff; --paper: #ffffff; --muted: #555555; --panel: #eeeeee; }
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "primary text on paper" in result.stderr
def test_html_deck_validator_rechecks_themes_under_conditional_root(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --ink: #000000; --paper: #ffffff; --accent: #165cff; --accent-on: #ffffff; --muted: #777777; --panel: #ffffff; }
.slide.theme-dark { --muted: #dddddd; --panel: #222222; }
@media (max-width: 760px) { :root { --ink: #ffffff; } }
</style>
</head>
<body><section class="slide theme-dark"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "conditional :root theme" in result.stderr
assert "primary text on theme background" in result.stderr
def test_html_deck_validator_ignores_descendant_theme_component_tokens(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --ink: #000000; --paper: #ffffff; --accent: #165cff; --accent-on: #ffffff; --muted: #555555; --panel: #eeeeee; }
.slide.theme-dark { --muted: #dddddd; --panel: #222222; }
.slide.theme-dark .panel { --muted: #000000; --panel: #ffffff; }
</style>
</head>
<body><section class="slide theme-dark"><div class="panel"></div></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_html_deck_root_parser_ignores_nested_quoted_declarations():
mod = _load_validate_html_deck_module()
html = """
<style>
:root {
@media (min-width: 1px) { .note::before { content: "--muted: #ffffff;"; } }
--paper: #ffffff;
--muted: #555555;
--panel: #eeeeee;
}
</style>
<section class="slide"></section>
"""
root_context = [vars for name, vars in mod._css_variable_contexts(html) if name.startswith(":root")][0]
assert root_context["--paper"].rgb == "#ffffff"
assert root_context["--muted"].rgb == "#555555"
assert root_context["--panel"].rgb == "#eeeeee"
def test_html_deck_parses_rgb_tokens_for_contrast(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root {
--paper: rgb(255,255,255);
--muted: rgb(250,250,250);
}
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "muted text on paper" in result.stderr
def test_html_deck_contrast_composites_translucent_root_tokens(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root {
--paper: #ffffff;
--muted: rgba(0,0,0,.2);
}
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "muted text on paper" in result.stderr
def test_html_deck_theme_rules_accumulate_and_composite_translucent_tokens():
mod = _load_validate_html_deck_module()
html = """
<style>
:root { --ink: #000000; --paper: #ffffff; --muted: #111111; --panel: #ffffff; }
.slide.theme-dark { --muted: rgba(255,255,255,.2); --panel: rgba(255,255,255,.08); }
.slide.theme-dark { --panel: rgba(255,255,255,.4); }
</style>
<section class="slide theme-dark"></section>
"""
contexts = mod._css_variable_contexts(html)
second = [c for n, c in contexts if "theme-dark" in n][1]
assert second["--muted"].alpha == pytest.approx(0.2)
assert second["--panel"].alpha == pytest.approx(0.4)
errors = mod._validate_contrast("slide theme rule 2 (.slide.theme-dark)", second)
assert any("muted text on slide background" in error for error in errors)
assert any("muted text on panel" in error for error in errors)
def test_html_deck_inline_tokens_inherit_slide_theme_context():
mod = _load_validate_html_deck_module()
html = """
<style>
:root { --ink: #000000; --paper: #ffffff; --muted: #111111; --panel: #ffffff; }
.slide.theme-dark { --muted: rgba(255,255,255,.2); --panel: rgba(255,255,255,.08); }
</style>
<section class="slide theme-dark" style="--panel: rgba(255,255,255,.4)"></section>
"""
contexts = mod._css_variable_contexts(html)
inline = [c for n, c in contexts if n.startswith("inline")][0]
assert inline["--muted"].alpha == pytest.approx(0.2)
assert inline["--panel"].alpha == pytest.approx(0.4)
errors = mod._validate_contrast("slide theme rule 1 (.slide.theme-dark)", inline)
assert any("muted text on slide background" in error for error in errors)
assert any("muted text on panel" in error for error in errors)
def test_html_deck_nested_inline_tokens_inherit_ancestor_slide_theme_context():
mod = _load_validate_html_deck_module()
html = """
<style>
:root { --ink: #000000; --paper: #ffffff; --muted: #111111; --panel: #ffffff; }
.slide.theme-dark { --muted: rgba(255,255,255,.2); --panel: rgba(255,255,255,.08); }
</style>
<section class="slide theme-dark"><div class="panel" style="--panel: #f7f7f1"></div></section>
"""
contexts = mod._css_variable_contexts(html)
inline = [c for n, c in contexts if n.startswith("inline")][0]
assert inline["--muted"].alpha == pytest.approx(0.2)
assert inline["--panel"].rgb == "#f7f7f1"
errors = mod._validate_contrast("inline style 1 (.theme-dark)", inline)
assert any("muted text on panel" in error for error in errors)
def test_html_deck_nested_inline_tokens_accept_ancestor_class_order():
mod = _load_validate_html_deck_module()
html = """
<style>
:root { --ink: #000000; --paper: #ffffff; --muted: #111111; --panel: #ffffff; }
.slide.theme-dark { --muted: rgba(255,255,255,.2); --panel: rgba(255,255,255,.08); }
</style>
<section class="theme-dark slide"><div class="panel" style="--panel: #f7f7f1"></div></section>
"""
contexts = mod._css_variable_contexts(html)
inline = [c for n, c in contexts if n.startswith("inline")][0]
assert inline["--muted"].alpha == pytest.approx(0.2)
assert inline["--panel"].rgb == "#f7f7f1"
errors = mod._validate_contrast("inline style 1 (.theme-dark)", inline)
assert any("muted text on panel" in error for error in errors)
def test_html_deck_root_parser_ignores_conditional_tokens_and_css_braces():
mod = _load_validate_html_deck_module()
html = """
<style>
@media (prefers-color-scheme: dark) { :root { --muted: #ffffff; --panel: #ffffff; } }
:root {
--paper: #ffffff;
--muted: #555555;
--asset: url("image{1}.png");
/* { ignored } */
@media (min-width: 1px) { --muted: #ffffff; --panel: #ffffff; }
--accent-text: #111111;
--panel: #eeeeee;
}
</style>
<section class="slide" style="--panel: 'decorative'; --muted: #222222"></section>
"""
contexts = mod._css_variable_contexts(html)
assert len([name for name, _ in contexts if name.startswith(":root")]) == 1
root_context = [vars for name, vars in contexts if name.startswith(":root")][0]
assert root_context["--muted"].rgb == "#555555"
assert root_context["--panel"].rgb == "#eeeeee"
inline_context = [vars for name, vars in contexts if name.startswith("inline")][0]
assert inline_context["--muted"].rgb == "#222222"
def test_html_deck_css_variable_contexts_ignore_visible_css_snippets():
mod = _load_validate_html_deck_module()
html = """
<pre>:root { --paper: #000000; --muted: #000000; }</pre>
<style>
:root { --paper: #ffffff; --muted: #555555; --panel: #eeeeee; }
</style>
<section class="slide"></section>
"""
contexts = mod._css_variable_contexts(html)
root_contexts = [vars for name, vars in contexts if name.startswith(":root")]
assert len(root_contexts) == 1
assert root_contexts[0]["--paper"].rgb == "#ffffff"
assert root_contexts[0]["--muted"].rgb == "#555555"
def test_html_deck_css_variable_contexts_ignore_commented_style_blocks(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<!-- <style>:root { --paper: #ffffff; --muted: #ffffff; --panel: #ffffff; }</style> -->
<style>:root { --paper: #ffffff; --muted: #555555; --panel: #eeeeee; }</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_html_deck_resolves_var_token_references_for_contrast():
mod = _load_validate_html_deck_module()
html = """
<style>
:root {
--paper: #ffffff;
--muted-base: #777777;
--muted: var(--muted-base);
--panel-base: #eeeeee;
--panel: var(--panel-base);
}
</style>
<section class="slide"></section>
"""
context = [vars for name, vars in mod._css_variable_contexts(html) if name.startswith(":root")][0]
errors = mod._validate_contrast(":root block 1", context)
assert any("muted text on paper" in error for error in errors)
assert any("muted text on panel" in error for error in errors)
def test_html_deck_contrast_composites_percentage_alpha_tokens(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root {
--paper: #ffffff;
--muted: rgba(0,0,0,20%);
}
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "muted text on paper" in result.stderr
def test_html_deck_validator_composites_translucent_panel_over_paper(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root {
--paper: #ffffff;
--panel: rgba(0,0,0,.05);
--muted: #555555;
}
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_html_deck_validator_checks_muted_text_on_panel_2(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root {
--paper: #ffffff;
--panel: #eeeeee;
--panel-2: #555555;
--muted: #555555;
}
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "muted text on panel-2" in result.stderr
def test_html_deck_validator_clamps_numeric_rgba_alpha_above_one(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root {
--paper: #ffffff;
--panel: #eeeeee;
--muted: rgba(0,0,0,2);
}
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_html_deck_validator_ignores_malformed_rgba_alpha_without_crashing(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root {
--paper: #ffffff;
--muted: rgba(0,0,0,1.2.3);
}
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
assert "Traceback" not in result.stderr
def test_html_deck_validator_checks_only_final_root_cascade(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --paper: #ffffff; --panel: #eeeeee; --muted: #ffffff; }
:root { --muted: #555555; }
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_html_deck_validator_checks_conditional_root_cascade(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --paper: #ffffff; --panel: #eeeeee; --muted: #555555; }
@media (max-width: 760px) { :root { --muted: #ffffff; } }
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "conditional :root" in result.stderr
assert "muted text on paper" in result.stderr
def test_html_deck_conditional_root_respects_later_base_cascade(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
@media (max-width: 760px) { :root { --muted: #ffffff; } }
:root { --paper: #ffffff; --panel: #eeeeee; --muted: #555555; }
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_html_deck_conditional_root_cascades_sibling_rules(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --paper: #ffffff; --panel: #eeeeee; --muted: #555555; }
@media (max-width: 760px) { :root { --paper: #000000; --panel: #000000; --muted: #ffffff; } }
@media (max-width: 760px) { :root { --muted: #333333; } }
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "conditional :root" in result.stderr
assert "muted text on paper" in result.stderr
def test_html_deck_conditional_root_does_not_merge_distinct_media_rules(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --paper: #ffffff; --panel: #eeeeee; --muted: #555555; }
@media (max-width: 760px) { :root { --paper: #000000; --panel: #000000; --muted: #ffffff; } }
@media (min-width: 1200px) { :root { --muted: #333333; } }
</style>
</head>
<body><section class="slide"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_html_deck_slide_theme_validates_primary_text_contrast():
mod = _load_validate_html_deck_module()
html = """
<style>
:root {
--accent: #165cff;
--accent-on: #ffffff;
--muted: #555555;
--panel: #eeeeee;
}
.slide.theme-accent {
--accent-on: #165cff;
--muted: #ffffff;
--panel: rgba(255,255,255,.08);
}
</style>
<section class="slide theme-accent"></section>
"""
theme_context = [
variables
for name, variables in mod._css_variable_contexts(html)
if "theme-accent" in name
][0]
errors = mod._validate_contrast("slide theme rule 1 (.slide.theme-accent)", theme_context)
assert any("primary text on theme background" in error for error in errors)
def test_html_deck_conditional_theme_respects_later_base_cascade(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --accent: #165cff; --accent-on: #ffffff; --muted: #555555; --panel: #eeeeee; }
@media (max-width: 760px) { .slide.theme-accent { --accent-on: #165cff; } }
.slide.theme-accent { --accent-on: #ffffff; --muted: #ffffff; --panel: rgba(255,255,255,.08); }
</style>
</head>
<body><section class="slide theme-accent"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_html_deck_conditional_theme_cascades_sibling_rules(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --accent: #ffffff; --accent-on: #333333; --accent-text: #333333; --muted: #555555; --panel: #eeeeee; }
.slide.theme-accent { --muted: #555555; --panel: rgba(0,0,0,.05); }
@media (max-width: 760px) { .slide.theme-accent { --accent: #000000; --accent-on: #ffffff; --muted: #ffffff; --panel: rgba(255,255,255,.08); } }
@media (max-width: 760px) { .slide.theme-accent { --accent-on: #333333; } }
</style>
</head>
<body><section class="slide theme-accent"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "conditional slide theme" in result.stderr
assert "primary text on theme background" in result.stderr
def test_html_deck_conditional_theme_does_not_merge_distinct_media_rules(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root { --accent: #ffffff; --accent-on: #333333; --accent-text: #333333; --muted: #555555; --panel: #eeeeee; }
.slide.theme-accent { --muted: #555555; --panel: rgba(0,0,0,.05); }
@media (max-width: 760px) { .slide.theme-accent { --accent: #000000; --accent-on: #ffffff; --muted: #ffffff; --panel: rgba(255,255,255,.08); } }
@media (min-width: 1200px) { .slide.theme-accent { --accent-on: #333333; } }
</style>
</head>
<body><section class="slide theme-accent"></section></body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_html_deck.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_html_deck_slide_theme_validates_accent_component_contrast():
mod = _load_validate_html_deck_module()
html = """
<style>
:root {
--ink: #000000;
--paper: #ffffff;
--accent: #165cff;
--accent-on: #ffffff;
--muted: #555555;
--panel: #eeeeee;
}
.slide.theme-dark {
--accent: #ffffff;
--accent-on: #ffffff;
--muted: #ffffff;
--panel: rgba(255,255,255,.08);
}
</style>
<section class="slide theme-dark"></section>
"""
theme_context = [
variables
for name, variables in mod._css_variable_contexts(html)
if "theme-dark" in name
][0]
errors = mod._validate_contrast("slide theme rule 1 (.slide.theme-dark)", theme_context)
assert any("text on accent background" in error for error in errors)
def test_html_deck_product_grid_theme_tokens_pass_contrast():
mod = _load_validate_html_deck_module()
template = SKILL_DIR / "assets" / "template-product-grid.html"
html = template.read_text(encoding="utf-8")
theme_contexts = [
(name, variables)
for name, variables in mod._css_variable_contexts(html)
if name.startswith("slide theme rule")
]
assert len(theme_contexts) == 3
for name, variables in theme_contexts:
assert mod._validate_slide_theme_contrast(name, variables) == []
def test_html_deck_root_contexts_use_cumulative_aggregate():
mod = _load_validate_html_deck_module()
html = """
<style>
:root { --muted: #595959; --paper: #ffffff; }
:root { --panel: #f0f0f0; }
</style>
"""
contexts = mod._css_variable_contexts(html)
second_root = [vars for name, vars in contexts if name == ":root block 2"][0]
assert second_root["--muted"].rgb == "#595959"
assert second_root["--panel"].rgb == "#f0f0f0"
def test_html_deck_validate_contrast_flags_accent_text_on_paper():
mod = _load_validate_html_deck_module()
color = mod.CssColor
errors = mod._validate_contrast(
"test",
{
"--accent-text": color("#cccccc"),
"--paper": color("#ffffff"),
"--muted": color("#777777"),
"--panel": color("#f0f0f0"),
},
)
assert any("accent text on paper" in error for error in errors)
def test_html_deck_validator_parses_nested_root_block(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<style>
:root {
--muted: #595959;
--paper: #ffffff;
--panel: #f0f0f0;
--accent-text: #0b5d1e;
@supports (display: grid) { --panel: #e8e8e8; }
}
</style>
</head>
<body>
<section class="slide"><p>ok</p></section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[sys.executable, str(SKILL_DIR / "scripts/validate_html_deck.py"), str(deck)],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_product_grid_quality_validator_ignores_xmlns_urls(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage"><svg xmlns="http://www.w3.org/2000/svg"></svg></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_product_grid_quality_validator_rejects_external_and_unregistered_image_slots(tmp_path):
images = tmp_path / "images"
images.mkdir()
(images / "local.png").write_bytes(b"png")
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage">
<img src="https://example.com/remote.png" alt="Remote">
<img src="images/local.png" alt="Local" data-image-slot="pg04-main-16x10">
</div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "external http(s) references" in result.stderr
assert "image slot pg04-main-16x10 is not allowed for PG02" in result.stderr
def test_product_grid_quality_validator_rejects_blank_image_src(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage">
<img src="" alt="Empty source" data-image-slot="pg02-media-16x10">
</div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1, result.stdout
assert "image 1 has blank src" in result.stderr
def test_product_grid_quality_validator_does_not_warn_when_fewer_than_five_slides(tmp_path):
deck = tmp_path / "deck.html"
slides = "\n".join(
f"""<section class="slide" data-system="product-grid" data-layout="{layout}">
<div class="stage"><p>Slide {idx}</p></div>
</section>"""
for idx, layout in enumerate(["PG02", "PG03", "PG05", "PG06"], start=1)
)
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
{slides}
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
assert "Decks under 8 slides should use at least 5 distinct layouts" not in result.stdout
def test_product_grid_quality_validator_allows_responsive_template_css(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}.title{{font-size:clamp(32px,5vw,72px)}}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage"><p>Responsive template CSS is allowed.</p></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_product_grid_quality_validator_rejects_deck_local_css_classes(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head><style>.stage{display:block}.one-off{color:red}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage one-off"><p>Custom classes are not allowed.</p></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "Deck defines unregistered CSS class(es): one-off" in result.stderr
def test_product_grid_quality_validator_rejects_missing_copied_template_css(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head><style>.stage{display:block}</style></head>
<body>
<main class="deck" id="deck">
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage"><p>Template CSS must be copied.</p></div>
</section>
</main>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "missing copied Product Grid template CSS class(es)" in result.stderr
def test_product_grid_quality_validator_rejects_uppercase_external_url_schemes(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage"><img src="HTTPS://example.com/remote.png" alt="Remote"></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "external http(s) references" in result.stderr
def test_product_grid_quality_validator_rejects_nested_sections(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage"><section><p>Nested section</p></section></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "nested <section> elements are not allowed" in result.stderr
def test_product_grid_quality_validator_rejects_remote_base_href(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
"""<!doctype html>
<html lang="en">
<head>
<base href="https://cdn.example/">
<style>.stage{display:block}</style>
</head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage"><p>Remote base URLs are not allowed.</p></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "external http(s) references" in result.stderr
def test_product_grid_quality_validator_rejects_protocol_relative_urls(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage"><img src="//cdn.example.com/remote.png" alt="Remote"></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "external http(s) references" in result.stderr
def test_product_grid_quality_validator_rejects_svg_image_href(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage"><svg><image href="https://example.com/remote.png"></image></svg></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "external http(s) references" in result.stderr
def test_product_grid_quality_validator_checks_dot_prefixed_local_images(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG01">
<div class="stage"><img src="./images/hero.png" alt="Hero"></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "missing local image images/hero.png" in result.stderr
assert "local image 1 missing data-image-slot" in result.stderr
def test_product_grid_quality_validator_rejects_non_images_local_paths(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG01">
<div class="stage"><img src="assets/hero.png" alt="Hero" data-image-slot="pg01-hero-16x9"></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "image 1 must use a local images/ path" in result.stderr
def test_product_grid_quality_validator_allows_font_size_in_style_blocks(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}.stage{{font-size:18px}}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage"><p>Template CSS owns type sizing.</p></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_product_grid_quality_validator_allows_filter_as_body_copy(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage"><p class="body">Filter: tenant knowledge</p></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_product_grid_quality_validator_rejects_slide_level_inline_style(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02" style="font-size:10px;height:100vh">
<div class="stage"><p>Slide-level inline style is not allowed.</p></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "inline font-size override" in result.stderr
assert "fixed vh height" in result.stderr
def test_product_grid_quality_validator_rejects_yellow_section_blue_accent(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide theme-yellow" data-system="product-grid" data-layout="PG02" style="color:#165cff">
<div class="stage"><p>Yellow slides cannot use blue local accents.</p></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "yellow theme contains slide-local blue accent styling" in result.stderr
def test_product_grid_quality_validator_allows_yellow_section_var_accent(tmp_path):
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide theme-yellow" data-system="product-grid" data-layout="PG02" style="color:var(--accent)">
<div class="stage"><p>Yellow theme remaps accent to ink.</p></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SKILL_DIR / "scripts/validate_deck_quality.py"),
str(deck),
],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_product_grid_quality_validator_fails_when_template_classes_are_unavailable(tmp_path, monkeypatch):
module_path = SKILL_DIR / "scripts/validate_deck_quality.py"
spec = importlib.util.spec_from_file_location("validate_deck_quality_for_test", module_path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
deck = tmp_path / "deck.html"
deck.write_text(
f"""<!doctype html>
<html lang="en">
<head><style>{PRODUCT_GRID_TEST_CSS}</style></head>
<body>
<section class="slide" data-system="product-grid" data-layout="PG02">
<div class="stage"><p>Template class registry must load.</p></div>
</section>
</body>
</html>
""",
encoding="utf-8",
)
monkeypatch.setattr(module, "TEMPLATE_PATH", tmp_path / "missing-template.html")
errors, warnings = module.check_file(deck)
assert not warnings
assert any("Template CSS source not found" in error for error in errors)
def test_product_grid_template_requires_horizontal_touch_intent():
template = (SKILL_DIR / "assets/template-product-grid.html").read_text(encoding="utf-8")
assert "event.touches[0].clientY" in template
assert "Math.abs(deltaX) > Math.abs(deltaY)" in template
def test_product_grid_template_uses_horizontal_wheel_direction_for_horizontal_intent():
template = (SKILL_DIR / "assets/template-product-grid.html").read_text(encoding="utf-8")
assert "const direction = horizontalIntent ? event.deltaX : event.deltaY;" in template
assert "event.deltaX > 0 || event.deltaY > 0" not in template