"""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) @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 " 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