2Tests for `chives.static_site_tests`.
6from pathlib import Path
9from typing import Any, TypeVar
12from pytest import Pytester
14from chives import dates
20 subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()
25def site_root(tmp_path: Path) -> Path:
27 Return a temp directory to use as a site root.
34 site_root: Path | None = None,
37 paths_in_metadata: set[Path] | None = None,
38 tags_in_metadata: set[str] | None = None,
39 date_formats: list[str] | None = None,
40 known_similar_tags: set[tuple[str, str]] | None = None,
43 Create a new instance of `pytest.Pytester` which is ready to run
44 a test suite based on StaticSiteTestSuite.
46 default_date_formats = [
53 from collections.abc import Iterator
54 from pathlib import Path, PosixPath
55 from typing import Any
59 from chives.static_site_tests import (
62 pytest_generate_tests,
66 class TestSuite(StaticSiteTestSuite[Any]):
68 def get_site_root(self) -> Path:
69 return Path({str(site_root or pytester.path)!r})
72 def metadata(self, site_root: Path) -> Any:
73 return {repr(metadata)}
75 def list_paths_in_metadata(self, metadata: Any) -> set[Path]:
76 return {repr(paths_in_metadata or set())}
78 def list_tags_in_metadata(self, metadata: Any) -> Iterator[str]:
79 yield from {repr(tags_in_metadata or set())}
81 date_formats = {repr(date_formats or default_date_formats)}
83 known_similar_tags = {repr(known_similar_tags or set())}
88def test_paths_saved_locally_match_metadata(
89 pytester: Pytester, site_root: Path
92 The tests check that the set of paths saved locally match the metadata.
94 # Create a series of paths in tmp_path.
104 p = site_root / filename
105 p.parent.mkdir(exist_ok=True)
108 metadata = [Path("media/cat.jpg"), Path("media/dog.png"), Path("media/emu.gif")]
110 create_pyfile(pytester, site_root, metadata, paths_in_metadata=set(metadata))
113 "test_every_file_in_metadata_is_saved_locally or "
114 "test_every_local_file_is_in_metadata"
116 pytester.runpytest("-k", keyword).assert_outcomes(passed=2)
118 # Add a new file locally, and check the test starts failing.
119 (site_root / "media/fish.tiff").write_text("test")
120 pytester.runpytest("-k", keyword).assert_outcomes(passed=1, failed=1)
121 (site_root / "media/fish.tiff").unlink()
123 # Delete one of the local files, and check the test starts failing.
124 (site_root / "media/cat.jpg").unlink()
125 pytester.runpytest("-k", keyword).assert_outcomes(passed=1, failed=1)
128def test_checks_for_git_changes(pytester: Pytester, site_root: Path) -> None:
130 The tests check that there are no uncommitted Git changes.
132 create_pyfile(pytester, site_root)
134 keyword = "test_no_uncommitted_git_changes"
136 # Initially this should fail, because there isn't a Git repo in
138 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
140 # Create a Git repo, add a file, and commit it.
141 (site_root / "README.md").write_text("hello world")
142 subprocess.check_call(["git", "init"], cwd=site_root)
143 subprocess.check_call(["git", "add", "README.md"], cwd=site_root)
144 subprocess.check_call(["git", "commit", "-m", "initial commit"], cwd=site_root)
146 # Check there are no uncommitted Git changes
147 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
149 # Make a new change, and check it's spotted
150 (site_root / "README.md").write_text("a different hello world")
151 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
154def test_checks_for_url_safe_paths(pytester: Pytester, site_root: Path) -> None:
156 The tests check for URL-safe paths.
158 create_pyfile(pytester, site_root)
160 keyword = "test_every_path_is_url_safe"
162 # This should pass trivially when the site is empty.
163 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
165 # Now write some files with URL-safe names, and check it's still okay.
171 (site_root / filename).write_text("test")
173 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
175 # Write another file with a URL-unsafe name, and check it's caught
177 (site_root / "a#b#c").write_text("test")
178 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
181def test_checks_for_av1_videos(pytester: Pytester, site_root: Path) -> None:
183 The tests check for AV1-encoded videos.
185 create_pyfile(pytester, site_root)
187 keyword = "test_no_videos_are_av1"
189 # This should pass trivially when the site is empty.
190 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
192 # Copy in an H.264-encoded video, and check it's not flagged.
194 GIT_ROOT / "tests/fixtures/media/Sintel_360_10s_1MB_H264.mp4",
195 site_root / "Sintel_360_10s_1MB_H264.mp4",
197 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
199 # Copy in an AV1-encoded video, and check it's caught by the test
201 GIT_ROOT / "tests/fixtures/media/Sintel_360_10s_1MB_AV1.mp4",
202 site_root / "Sintel_360_10s_1MB_AV1.mp4",
204 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
207class TestAllTimestampsAreConsistent:
209 Tests for the `test_all_timestamps_are_consistent` method.
212 @pytest.mark.parametrize(
215 {"date_saved": "2025-12-06"},
216 {"date_saved": dates.now()},
219 def test_allows_correct_date_formats(
220 self, pytester: Pytester, metadata: Any
223 The tests pass if all the dates are in the correct format.
225 create_pyfile(pytester, metadata=metadata)
227 keyword = "test_all_timestamps_are_consistent"
229 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
231 @pytest.mark.parametrize("metadata", [{"date_saved": "AAAA-BB-CC"}])
232 def test_rejects_incorrect_date_formats(
233 self, pytester: Pytester, site_root: Path, metadata: Any
236 The tests fail if the metadata has inconsistent date formats.
238 create_pyfile(pytester, metadata=metadata)
240 keyword = "test_all_timestamps_are_consistent"
242 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
244 def test_can_override_date_formats(self, pytester: Pytester) -> None:
246 A previously-blocked date format is allowed if you add it to
247 the `date_formats` list.
249 metadata = {"date_saved": "2025"}
250 keyword = "test_all_timestamps_are_consistent"
252 # It fails with the default settings
253 create_pyfile(pytester, metadata=metadata)
254 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
256 # It passes if we add the format to `date_formats`
257 create_pyfile(pytester, metadata=metadata, date_formats=["%Y"])
258 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
261def test_checks_for_similar_tags(pytester: Pytester) -> None:
263 The tests check for similar and misspelt tags.
265 keyword = "test_no_similar_tags"
267 # Check a site with distinct tags.
268 create_pyfile(pytester, tags_in_metadata={"red", "green", "blue"})
269 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
271 # Check a site with similar tags.
272 create_pyfile(pytester, tags_in_metadata={"red robot", "rod robot", "rid robot"})
273 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
275 # Check a site with similar tags, but marked as known-similar.
278 tags_in_metadata={"red robot", "rod robot", "green", "blue"},
279 known_similar_tags={("red robot", "rod robot")},
281 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
285 "SKIP_PLAYWRIGHT" in os.environ, reason="skip slow Playwright tests"
287class TestLoadsPageCorrectly:
289 Tests for `test_loads_page_correctly`.
292 def test_okay(self, pytester: Pytester, site_root: Path) -> None:
294 If the page contains valid HTML, the test passes.
296 keyword = "test_loads_page_correctly"
298 subprocess.check_call(["playwright", "install", "webkit"], cwd=pytester.path)
300 (site_root / "index.html").write_text("<p>Hello world!</p>")
302 create_pyfile(pytester, site_root)
303 pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
305 def test_noexist(self, pytester: Pytester) -> None:
307 If the page doesn't exist, the test fails.
309 keyword = "test_loads_page_correctly"
311 subprocess.check_call(["playwright", "install", "webkit"], cwd=pytester.path)
313 create_pyfile(pytester)
314 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
316 @pytest.mark.parametrize(
320 "<script>???</script>",
322 # Valid JavaScript that logs an error.
323 "<script>console.error('boom!')</script>",
326 def test_bad_js(self, pytester: Pytester, site_root: Path, html: str) -> None:
328 If the page loads but the JavaScript errors, the test fails.
330 keyword = "test_loads_page_correctly"
332 subprocess.check_call(["playwright", "install", "webkit"], cwd=pytester.path)
334 (site_root / "index.html").write_text(html)
336 create_pyfile(pytester, site_root)
337 pytester.runpytest("-k", keyword).assert_outcomes(failed=1)