168 lines
6.4 KiB
Python
168 lines
6.4 KiB
Python
"""End-to-end tests: Pelican builds small fixture sites with the plugin."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from importlib.resources import files
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import zipfile
|
|
|
|
from conftest import FIXTURES, build_case, normalize
|
|
import pytest
|
|
|
|
from pelican.plugins.mews.mews import CSS_NAME
|
|
from pelican.plugins.mews.subset import MewsSubsetError, parse_allowlist
|
|
|
|
PACKAGE = "pelican.plugins.mews"
|
|
STRICT_OVERRIDES = {"MEWS_STRICT": True}
|
|
|
|
|
|
@pytest.mark.parametrize("case", ["basic", "strip", "cssdir", "structure"])
|
|
def test_written_page_matches_expected(tmp_path, case):
|
|
"""The written page matches the fixture, whatever was stripped."""
|
|
overrides = {"MEWS_CSS_DIR": "style"} if case == "cssdir" else {}
|
|
output = build_case(case, tmp_path, **overrides)
|
|
page = (output / "page.xhtml").read_bytes()
|
|
expected = (FIXTURES / case / "expected.xhtml").read_bytes()
|
|
assert normalize(page) == normalize(expected)
|
|
|
|
|
|
def test_a_site_can_choose_its_own_suffix(tmp_path):
|
|
"""A site that is Mews throughout writes .html, so servers send text/html."""
|
|
output = build_case(
|
|
"basic",
|
|
tmp_path,
|
|
MEWS_SUFFIX=".html",
|
|
PAGE_SAVE_AS="{slug}.html",
|
|
PAGE_URL="{slug}.html",
|
|
# With .html the plugin sees every page Pelican writes, including the
|
|
# index, tags and archives it falls back to when the theme has no
|
|
# template for them. Those fallbacks are HTML, not XML, so a site
|
|
# using .html has to give every direct template a Mews template or
|
|
# switch the lot off, as here.
|
|
DIRECT_TEMPLATES=[],
|
|
)
|
|
page = (output / "page.html").read_bytes()
|
|
assert normalize(page) == normalize(
|
|
(FIXTURES / "basic" / "expected.xhtml").read_bytes()
|
|
)
|
|
|
|
|
|
def test_the_default_suffix_leaves_other_html_alone(tmp_path):
|
|
"""Left at its default the plugin must not touch a plain HTML page."""
|
|
output = build_case(
|
|
"strip",
|
|
tmp_path,
|
|
PAGE_SAVE_AS="{slug}.html",
|
|
PAGE_URL="{slug}.html",
|
|
INDEX_SAVE_AS="",
|
|
)
|
|
# The strip fixture carries a <span>, a class and a comment, all three of
|
|
# which the plugin removes. Untouched, every one of them survives.
|
|
written = (output / "page.html").read_bytes()
|
|
assert b"<span" in written
|
|
assert b'class="big"' in written
|
|
assert b"stray comment" in written
|
|
|
|
|
|
@pytest.mark.parametrize("case", ["basic", "strip", "cssdir", "structure"])
|
|
def test_css_is_copied(tmp_path, case):
|
|
"""The bundled stylesheet lands under the configured directory."""
|
|
overrides = {"MEWS_CSS_DIR": "style"} if case == "cssdir" else {}
|
|
output = build_case(case, tmp_path, **overrides)
|
|
css_dir = "style" if case == "cssdir" else "css"
|
|
bundled = files(PACKAGE).joinpath("static", CSS_NAME).read_bytes()
|
|
assert (output / css_dir / CSS_NAME).read_bytes() == bundled
|
|
|
|
|
|
def test_template_global_points_at_the_css(tmp_path):
|
|
"""Themes see MEWS_CSS as the relative stylesheet path."""
|
|
output = build_case("basic", tmp_path)
|
|
page = (output / "page.xhtml").read_text(encoding="utf-8")
|
|
assert 'href="css/mews-0.1.css"' in page
|
|
|
|
|
|
def test_lenient_mode_logs_removals(tmp_path, caplog):
|
|
"""Default mode strips and logs what it removed."""
|
|
caplog.set_level("WARNING", logger=f"{PACKAGE}.mews")
|
|
build_case("strip", tmp_path)
|
|
assert "<span> is not in the Mews subset" in caplog.text
|
|
assert "em class is not in the Mews subset" in caplog.text
|
|
|
|
|
|
def test_strict_mode_fails_the_build(tmp_path):
|
|
"""MEWS_STRICT turns disallowed content into a build failure."""
|
|
with pytest.raises(MewsSubsetError, match="span"):
|
|
build_case("strict", tmp_path, **STRICT_OVERRIDES)
|
|
|
|
|
|
def test_parallel_output_folder(tmp_path):
|
|
"""MEWS_OUTPUT_PATH keeps the normal output free of Mews files.
|
|
|
|
The processed page and the stylesheet move to the parallel folder,
|
|
keeping the subpath; the directory the move emptied is pruned.
|
|
"""
|
|
parallel = tmp_path / "mews"
|
|
output = build_case(
|
|
"basic",
|
|
tmp_path,
|
|
MEWS_OUTPUT_PATH=str(parallel),
|
|
PAGE_SAVE_AS="posts/{slug}.xhtml",
|
|
PAGE_URL="posts/{slug}.xhtml",
|
|
)
|
|
expected = (FIXTURES / "basic" / "expected.xhtml").read_bytes()
|
|
page = (parallel / "posts" / "page.xhtml").read_bytes()
|
|
assert normalize(page) == normalize(expected)
|
|
assert (parallel / "css" / CSS_NAME).exists()
|
|
assert not (output / "posts").exists()
|
|
assert not (output / "css").exists()
|
|
|
|
|
|
# Section 4.1 of the specification keeps this many elements. The plugin lists
|
|
# none of them itself; the DTD is the only place they are written down.
|
|
SECTION_4_1_ELEMENTS = 51
|
|
|
|
|
|
def test_allowlist_is_read_from_the_dtd():
|
|
"""The subset comes from the DTD, not from code."""
|
|
dtd = files(PACKAGE).joinpath("dtd", "mews.dtd").read_text(encoding="utf-8")
|
|
allowlist = parse_allowlist(dtd)
|
|
assert len(allowlist) == SECTION_4_1_ELEMENTS
|
|
# The common attributes, including the two the profile adds to XHTML-MP.
|
|
assert allowlist["p"] == {"dir", "id", "lang", "title", "xml:lang"}
|
|
# html takes no id or title; section 4.1 gives it these four and no more.
|
|
assert allowlist["html"] == {"dir", "lang", "xml:lang", "xmlns"}
|
|
assert "href" in allowlist["a"]
|
|
# class is permitted on body and nowhere else (section 5.3).
|
|
assert "class" in allowlist["body"]
|
|
assert not [
|
|
name for name, attrs in allowlist.items() if name != "body" and "class" in attrs
|
|
]
|
|
# The parts the placeholder DTD lacked, which the plugin was stripping.
|
|
assert {"table", "td", "ol", "dl", "form", "input", "address"} <= set(allowlist)
|
|
|
|
|
|
@pytest.mark.skipif(shutil.which("uv") is None, reason="uv is not installed")
|
|
def test_wheel_bundles_plugin_data(tmp_path):
|
|
"""The wheel carries the CSS, templates and DTD.
|
|
|
|
The namespace directories ship without __init__.py.
|
|
"""
|
|
root = Path(__file__).resolve().parents[1]
|
|
subprocess.run(
|
|
["uv", "build", "--wheel", "-o", str(tmp_path), str(root)],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
(wheel,) = tmp_path.glob("pelican_mews-*.whl")
|
|
names = set(zipfile.ZipFile(wheel).namelist())
|
|
expected = {
|
|
"pelican/plugins/mews/static/mews-0.1.css",
|
|
"pelican/plugins/mews/templates/mews/head.html",
|
|
"pelican/plugins/mews/dtd/mews.dtd",
|
|
"pelican/plugins/mews/py.typed",
|
|
}
|
|
assert expected <= names
|
|
assert "pelican/__init__.py" not in names
|
|
assert "pelican/plugins/__init__.py" not in names
|