diff --git a/.ruff.toml b/.ruff.toml
index ba7856b41..c99033d0e 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -465,7 +465,6 @@ exclude = [
"tests/test_quickstart.py",
"tests/test_roles.py",
"tests/test_search.py",
- "tests/test_theming/**/*",
"tests/test_toctree.py",
"tests/test_transforms/**/*",
"tests/test_util/**/*",
diff --git a/pyproject.toml b/pyproject.toml
index 86f805b7b..3327667e3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -92,6 +92,7 @@ lint = [
test = [
"pytest>=6.0",
"html5lib",
+ "defusedxml>=0.7.1", # for secure XML/HTML parsing
"cython>=3.0",
"setuptools>=67.0", # for Cython compilation
"filelock",
diff --git a/sphinx/themes/basic/search.html b/sphinx/themes/basic/search.html
index d10036a2d..8bad82a51 100644
--- a/sphinx/themes/basic/search.html
+++ b/sphinx/themes/basic/search.html
@@ -15,7 +15,7 @@
{%- endblock %}
{% block extrahead %}
-
+
{{ super() }}
{% endblock %}
diff --git a/sphinx/themes/bizstyle/layout.html b/sphinx/themes/bizstyle/layout.html
index 36f6fd3bd..226c7878d 100644
--- a/sphinx/themes/bizstyle/layout.html
+++ b/sphinx/themes/bizstyle/layout.html
@@ -14,11 +14,6 @@
{%- endblock %}
-{# doctype override #}
-{%- block doctype %}
-
-{%- endblock %}
-
{%- block extrahead %}
\n'
- 'Python'
+ '\nPython'
)
result = (app.outdir / 'index.html').read_text(encoding='utf8')
assert '' in result
-@pytest.mark.sphinx(testroot='theming',
- confoverrides={'html_theme': 'test-theme'})
+@pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'test-theme'})
def test_dark_style(app, monkeypatch):
monkeypatch.setattr(sphinx.builders.html, '_file_checksum', lambda o, f: '')
@@ -100,8 +121,8 @@ def test_dark_style(app, monkeypatch):
css_file, properties = app.registry.css_files[0]
assert css_file == 'pygments_dark.css'
- assert "media" in properties
- assert properties["media"] == '(prefers-color-scheme: dark)'
+ assert 'media' in properties
+ assert properties['media'] == '(prefers-color-scheme: dark)'
assert sorted(f.filename for f in app.builder._css_files) == [
'_static/classic.css',
@@ -111,9 +132,11 @@ def test_dark_style(app, monkeypatch):
result = (app.outdir / 'index.html').read_text(encoding='utf8')
assert '' in result
- assert ('') in result
+ assert (
+ ''
+ ) in result
@pytest.mark.sphinx(testroot='theming')
@@ -126,3 +149,41 @@ def test_theme_sidebars(app, status, warning):
assert '
Related Topics
' not in result
assert 'This Page
' not in result
assert 'Quick search
' in result
+
+
+@pytest.mark.parametrize(
+ 'theme_name',
+ [
+ 'alabaster',
+ 'agogo',
+ 'basic',
+ 'bizstyle',
+ 'classic',
+ 'default',
+ 'epub',
+ 'haiku',
+ 'nature',
+ 'nonav',
+ 'pyramid',
+ 'scrolls',
+ 'sphinxdoc',
+ 'traditional',
+ ],
+)
+def test_theme_builds(make_app, rootdir, sphinx_test_tempdir, theme_name):
+ """Test all the themes included with Sphinx build a simple project and produce valid XML."""
+ testroot_path = rootdir / 'test-basic'
+ srcdir = sphinx_test_tempdir / f'test-theme-{theme_name}'
+ shutil.copytree(testroot_path, srcdir)
+
+ app = make_app(srcdir=srcdir, confoverrides={'html_theme': theme_name})
+ app.build()
+ assert not app.warning.getvalue().strip()
+ assert app.outdir.joinpath('index.html').exists()
+
+ # check that the generated HTML files are well-formed (as strict XML)
+ for html_file in app.outdir.rglob('*.html'):
+ try:
+ xml_parse(html_file)
+ except ParseError as exc:
+ pytest.fail(f'Failed to parse {html_file.relative_to(app.outdir)}: {exc}')