Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: ❤️ Tests

on:
pull_request:
branches: ["main", "master", "develop"]
push:
branches: ["main", "master", "develop"]

jobs:
test:
name: Run Tests
runs-on: ubuntu-latest

strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]

steps:
- name: 📥 Checkout code
uses: actions/checkout@v4

- name: 🐍 Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: ⚙️ Install dependencies
run: |
python -m pip install --upgrade pip
pip install poetry
poetry install

- name: ❤️ Run tests
run: poetry run pytest --tb=short -v
3 changes: 2 additions & 1 deletion mkdocs_dracula_theme/assets/css/mkdocs.min.css

Large diffs are not rendered by default.

248 changes: 243 additions & 5 deletions poetry.lock

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,19 @@ mkdocs = { version = ">=1.6.1,<2.0.0" }

[tool.poetry.group.dev.dependencies]
build = "^1.3.0"
pytest = "^8.0"
pytest-cov = "^5.0"
pyyaml = "^6.0"

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.poetry.plugins."mkdocs.themes"]
dracula = "mkdocs_dracula_theme"
dracula = "mkdocs_dracula_theme"

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
93 changes: 92 additions & 1 deletion template/assets/css/mkdocs.css
Original file line number Diff line number Diff line change
Expand Up @@ -476,4 +476,95 @@ img, svg {
.drac-input-search {
margin-top: 2px;
width: 170px;
}
}

/* =============================================================
Admonitions
============================================================= */

.admonition {
border-left: 4px solid var(--purple);
border-radius: 0.5rem;
background: var(--purple-transparent);
padding: 0.75rem 1rem;
margin: 1.25rem 0;
overflow: hidden;
}

.admonition p:last-child {
margin-bottom: 0;
}

.admonition-title {
font-weight: 700;
margin: -0.75rem -1rem 0.75rem -1rem;
padding: 0.5rem 1rem;
display: flex;
align-items: center;
}

.admonition-title::before {
font-family: FontAwesome;
margin-right: 0.5rem;
content: "\f040";
}

/* note */
.admonition.note { border-color: var(--purple); background: var(--purple-transparent); }
.admonition.note .admonition-title { background: rgba(149,128,255,.15); color: var(--purple); }
.admonition.note .admonition-title::before { content: "\f040"; }

/* abstract / summary / tldr */
.admonition.abstract,.admonition.summary,.admonition.tldr { border-color: var(--cyan); background: var(--cyan-transparent); }
.admonition.abstract .admonition-title,.admonition.summary .admonition-title,.admonition.tldr .admonition-title { background: rgba(128,255,234,.15); color: var(--cyan); }
.admonition.abstract .admonition-title::before,.admonition.summary .admonition-title::before,.admonition.tldr .admonition-title::before { content: "\f0ea"; }

/* info */
.admonition.info { border-color: var(--cyan); background: var(--cyan-transparent); }
.admonition.info .admonition-title { background: rgba(128,255,234,.15); color: var(--cyan); }
.admonition.info .admonition-title::before { content: "\f05a"; }

/* tip / hint / important */
.admonition.tip,.admonition.hint,.admonition.important { border-color: var(--green); background: var(--green-transparent); }
.admonition.tip .admonition-title,.admonition.hint .admonition-title,.admonition.important .admonition-title { background: rgba(138,255,128,.15); color: var(--green); }
.admonition.tip .admonition-title::before,.admonition.hint .admonition-title::before,.admonition.important .admonition-title::before { content: "\f0eb"; }

/* success / check / done */
.admonition.success,.admonition.check,.admonition.done { border-color: var(--green); background: var(--green-transparent); }
.admonition.success .admonition-title,.admonition.check .admonition-title,.admonition.done .admonition-title { background: rgba(138,255,128,.15); color: var(--green); }
.admonition.success .admonition-title::before,.admonition.check .admonition-title::before,.admonition.done .admonition-title::before { content: "\f058"; }

/* question / help / faq */
.admonition.question,.admonition.help,.admonition.faq { border-color: var(--yellow); background: var(--yellow-transparent); }
.admonition.question .admonition-title,.admonition.help .admonition-title,.admonition.faq .admonition-title { background: rgba(255,255,128,.15); color: var(--yellow); }
.admonition.question .admonition-title::before,.admonition.help .admonition-title::before,.admonition.faq .admonition-title::before { content: "\f059"; }

/* warning / caution / attention */
.admonition.warning,.admonition.caution,.admonition.attention { border-color: var(--orange); background: var(--orange-transparent); }
.admonition.warning .admonition-title,.admonition.caution .admonition-title,.admonition.attention .admonition-title { background: rgba(255,202,128,.15); color: var(--orange); }
.admonition.warning .admonition-title::before,.admonition.caution .admonition-title::before,.admonition.attention .admonition-title::before { content: "\f071"; }

/* failure / fail / missing */
.admonition.failure,.admonition.fail,.admonition.missing { border-color: var(--pink); background: var(--pink-transparent); }
.admonition.failure .admonition-title,.admonition.fail .admonition-title,.admonition.missing .admonition-title { background: rgba(255,128,191,.15); color: var(--pink); }
.admonition.failure .admonition-title::before,.admonition.fail .admonition-title::before,.admonition.missing .admonition-title::before { content: "\f057"; }

/* danger / error */
.admonition.danger,.admonition.error { border-color: var(--red); background: var(--red-transparent); }
.admonition.danger .admonition-title,.admonition.error .admonition-title { background: rgba(255,149,128,.15); color: var(--red); }
.admonition.danger .admonition-title::before,.admonition.error .admonition-title::before { content: "\f0e7"; }

/* bug */
.admonition.bug { border-color: var(--red); background: var(--red-transparent); }
.admonition.bug .admonition-title { background: rgba(255,149,128,.15); color: var(--red); }
.admonition.bug .admonition-title::before { content: "\f188"; }

/* example */
.admonition.example { border-color: var(--purple); background: var(--purple-transparent); }
.admonition.example .admonition-title { background: rgba(149,128,255,.15); color: var(--purple); }
.admonition.example .admonition-title::before { content: "\f03a"; }

/* quote / cite */
.admonition.quote,.admonition.cite { border-color: var(--grey); background: rgba(65,69,88,.2); }
.admonition.quote .admonition-title,.admonition.cite .admonition-title { background: rgba(65,69,88,.3); color: var(--white); }
.admonition.quote .admonition-title::before,.admonition.cite .admonition-title::before { content: "\f10d"; }
Empty file added tests/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pathlib import Path

THEME_DIR = Path(__file__).parent.parent / "mkdocs_dracula_theme"
CSS_SOURCE = Path(__file__).parent.parent / "template" / "assets" / "css" / "mkdocs.css"
CSS_MIN = THEME_DIR / "assets" / "css" / "mkdocs.min.css"
155 changes: 155 additions & 0 deletions tests/test_admonitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""Tests for PR #33 — admonition styles with Dracula colors and Font Awesome icons."""
import re
from pathlib import Path

CSS_SOURCE = Path(__file__).parent.parent / "template" / "assets" / "css" / "mkdocs.css"
CSS_MIN = Path(__file__).parent.parent / "mkdocs_dracula_theme" / "assets" / "css" / "mkdocs.min.css"

ADMONITION_TYPES = [
"note",
"abstract", "summary", "tldr",
"info",
"tip", "hint", "important",
"success", "check", "done",
"question", "help", "faq",
"warning", "caution", "attention",
"failure", "fail", "missing",
"danger", "error",
"bug",
"example",
"quote", "cite",
]

# Expected Font Awesome 4.7 icon codepoints per type
EXPECTED_ICONS = {
"note": r"\f040",
"abstract": r"\f0ea", "summary": r"\f0ea", "tldr": r"\f0ea",
"info": r"\f05a",
"tip": r"\f0eb", "hint": r"\f0eb", "important": r"\f0eb",
"success": r"\f058", "check": r"\f058", "done": r"\f058",
"question": r"\f059", "help": r"\f059", "faq": r"\f059",
"warning": r"\f071", "caution": r"\f071", "attention": r"\f071",
"failure": r"\f057", "fail": r"\f057", "missing": r"\f057",
"danger": r"\f0e7", "error": r"\f0e7",
"bug": r"\f188",
"example": r"\f03a",
"quote": r"\f10d", "cite": r"\f10d",
}

# Expected Dracula CSS variable per admonition type
EXPECTED_COLORS = {
"note": "--purple", "example": "--purple",
"abstract": "--cyan", "summary": "--cyan",
"tldr": "--cyan", "info": "--cyan",
"tip": "--green", "hint": "--green", "important": "--green",
"success": "--green", "check": "--green", "done": "--green",
"question": "--yellow", "help": "--yellow", "faq": "--yellow",
"warning": "--orange", "caution": "--orange", "attention": "--orange",
"failure": "--pink", "fail": "--pink", "missing": "--pink",
"danger": "--red", "error": "--red", "bug": "--red",
"quote": "--grey", "cite": "--grey",
}


class TestAdmonitionCoverage:
def test_source_css_covers_all_types(self):
"""Source CSS must define rules for every MkDocs admonition type."""
content = CSS_SOURCE.read_text()
missing = [t for t in ADMONITION_TYPES if f".admonition.{t}" not in content]
assert not missing, f"Missing admonition rules in source CSS: {missing}"

def test_min_css_covers_all_types(self):
"""Minified CSS must define rules for every MkDocs admonition type."""
content = CSS_MIN.read_text()
missing = [t for t in ADMONITION_TYPES if f".admonition.{t}" not in content]
assert not missing, f"Missing admonition rules in minified CSS: {missing}"

def test_source_and_min_in_sync(self):
"""Source and minified CSS must cover the exact same set of admonition types."""
source = CSS_SOURCE.read_text()
minified = CSS_MIN.read_text()
for t in ADMONITION_TYPES:
in_src = f".admonition.{t}" in source
in_min = f".admonition.{t}" in minified
assert in_src == in_min, (
f".admonition.{t}: source={in_src}, minified={in_min} — files are out of sync"
)


class TestAdmonitionIcons:
def test_tip_uses_lightbulb_not_fire_in_source(self):
"""tip/hint/important must use fa-lightbulb-o (\\f0eb), not fa-fire (\\f06d)."""
content = CSS_SOURCE.read_text()
assert r"\f0eb" in content, r"Source CSS missing lightbulb icon (\f0eb) for tip/hint/important"
assert r"\f06d" not in content, r"Source CSS must not use fire icon (\f06d)"

def test_tip_uses_lightbulb_not_fire_in_min(self):
"""Minified CSS: tip/hint/important must use fa-lightbulb-o (\\f0eb)."""
content = CSS_MIN.read_text()
assert r"\f0eb" in content, r"Minified CSS missing lightbulb icon (\f0eb)"
assert r"\f06d" not in content, r"Minified CSS must not use fire icon (\f06d)"

def test_all_icon_codepoints_present_in_source(self):
"""Each admonition type must reference its expected Font Awesome codepoint."""
content = CSS_SOURCE.read_text()
seen_codepoints = set()
for admonition_type, codepoint in EXPECTED_ICONS.items():
if codepoint not in seen_codepoints:
assert codepoint in content, (
f"Icon codepoint {codepoint} for .admonition.{admonition_type} "
f"not found in source CSS"
)
seen_codepoints.add(codepoint)


class TestAdmonitionColors:
def test_source_css_uses_dracula_variables(self):
"""Admonition styles must use CSS variables, not hardcoded hex values."""
content = CSS_SOURCE.read_text()
# Extract only the admonition section
start = content.find(".admonition {")
assert start != -1, "Admonition base rule not found"
admonition_section = content[start:]
# Must not contain hex colors in the admonition section
assert not re.search(r":\s*#[0-9a-fA-F]{3,6}", admonition_section), \
"Admonition styles must use CSS variables (var(--color)), not hardcoded hex values"

def test_expected_color_variables_present(self):
"""Each admonition type must reference its expected Dracula color variable."""
content = CSS_SOURCE.read_text()
for admonition_type, color_var in EXPECTED_COLORS.items():
pattern = rf"\.admonition\.{admonition_type}[^{{]*\{{[^}}]*{re.escape(color_var)}"
assert re.search(pattern, content), (
f".admonition.{admonition_type} should reference {color_var}"
)


class TestMinifiedCSSFormat:
def test_admonition_section_has_no_comments(self):
"""The admonition block in mkdocs.min.css must not contain CSS comments."""
content = CSS_MIN.read_text()
admonition_start = content.find(".admonition{")
assert admonition_start != -1, "Admonition CSS not found in minified file"
admonition_section = content[admonition_start:]
assert "/*" not in admonition_section, \
"Minified CSS must not contain comments — strip them before appending to .min.css"

def test_admonition_section_has_no_indentation(self):
"""The admonition block in mkdocs.min.css must not contain indented lines."""
content = CSS_MIN.read_text()
admonition_start = content.find(".admonition{")
assert admonition_start != -1
admonition_section = content[admonition_start:]
assert "\n " not in admonition_section, \
"Minified CSS must not contain indented lines"

def test_admonition_uses_shorthand_values(self):
"""Minified CSS should use shorthand values (e.g. .5rem not 0.5rem)."""
content = CSS_MIN.read_text()
admonition_start = content.find(".admonition{")
assert admonition_start != -1
admonition_section = content[admonition_start:]
assert "0.5rem" not in admonition_section, \
"Minified CSS should use .5rem instead of 0.5rem"
assert "0.75rem" not in admonition_section, \
"Minified CSS should use .75rem instead of 0.75rem"
Loading