Files
maskanx_cm_backend/tests/test_html_presentation_deck_skill.py
AFFAANhandClaude Opus 5 19e1e84fb7 Initial commit: MaskanX backend
Independent FastAPI backend for the MaskanX agentic growth platform.

Includes the agent runtime, MCP client integrations (Meta Ads, LinkedIn,
HubSpot, Tavily, Exa, xAI, Citedy, image generation), PostgreSQL storage
for chats and cron jobs, provider and secret management, and the CLI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:28:22 +05:30

1950 lines
52 KiB
Python

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