[tests] Add basic build test for all builtin themes (#12168)

Add `tests/test_theming/test_theming.py::test_theme_builds`, which is a parametrized test against all builtin sphinx HTML themes, that tests:

1. that the themes builds without warnings for a basic project, and
2. that all `.html` files it produces are valid XML (see https://html.spec.whatwg.org/)

https://pypi.org/project/defusedxml/ was added to the test dependencies, in order to safely parse the XML

This required one fix for `sphinx/themes/basic/search.html`, and one for `sphinx/themes/bizstyle/layout.html`

Also, `tests/test_theming` was removed from the `ruff format` exclude list
This commit is contained in:
Chris Sewell 2024-03-22 12:57:34 +01:00 committed by GitHub
parent 982679eeee
commit 66fa790b3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 102 additions and 34 deletions

View File

@ -465,7 +465,6 @@ exclude = [
"tests/test_quickstart.py", "tests/test_quickstart.py",
"tests/test_roles.py", "tests/test_roles.py",
"tests/test_search.py", "tests/test_search.py",
"tests/test_theming/**/*",
"tests/test_toctree.py", "tests/test_toctree.py",
"tests/test_transforms/**/*", "tests/test_transforms/**/*",
"tests/test_util/**/*", "tests/test_util/**/*",

View File

@ -92,6 +92,7 @@ lint = [
test = [ test = [
"pytest>=6.0", "pytest>=6.0",
"html5lib", "html5lib",
"defusedxml>=0.7.1", # for secure XML/HTML parsing
"cython>=3.0", "cython>=3.0",
"setuptools>=67.0", # for Cython compilation "setuptools>=67.0", # for Cython compilation
"filelock", "filelock",

View File

@ -15,7 +15,7 @@
<script src="{{ pathto('_static/language_data.js', 1) }}"></script> <script src="{{ pathto('_static/language_data.js', 1) }}"></script>
{%- endblock %} {%- endblock %}
{% block extrahead %} {% block extrahead %}
<script src="{{ pathto('searchindex.js', 1) }}" defer></script> <script src="{{ pathto('searchindex.js', 1) }}" defer="defer"></script>
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
{{ super() }} {{ super() }}
{% endblock %} {% endblock %}

View File

@ -14,11 +14,6 @@
<script src="{{ pathto('_static/bizstyle.js', 1) }}"></script> <script src="{{ pathto('_static/bizstyle.js', 1) }}"></script>
{%- endblock %} {%- endblock %}
{# doctype override #}
{%- block doctype %}
<!doctype html>
{%- endblock %}
{%- block extrahead %} {%- block extrahead %}
<meta name="viewport" content="width=device-width,initial-scale=1.0" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
<!--[if lt IE 9]> <!--[if lt IE 9]>

View File

@ -10,9 +10,14 @@ def test_theme_options(app, status, warning):
assert 'ENABLE_SEARCH_SHORTCUTS: true' in result assert 'ENABLE_SEARCH_SHORTCUTS: true' in result
@pytest.mark.sphinx('html', testroot='theming', @pytest.mark.sphinx(
confoverrides={'html_theme_options.navigation_with_keys': True, 'html',
'html_theme_options.enable_search_shortcuts': False}) testroot='theming',
confoverrides={
'html_theme_options.navigation_with_keys': True,
'html_theme_options.enable_search_shortcuts': False,
},
)
def test_theme_options_with_override(app, status, warning): def test_theme_options_with_override(app, status, warning):
app.build() app.build()

View File

@ -23,19 +23,26 @@ def test_autosummary_class_template_overloading(make_app, app_params):
setup_documenters(app) setup_documenters(app)
app.build() app.build()
result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text(encoding='utf8') result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text(
encoding='utf8'
)
assert 'autosummary/class.rst method block overloading' in result assert 'autosummary/class.rst method block overloading' in result
assert 'foobar' not in result assert 'foobar' not in result
@pytest.mark.sphinx('html', testroot='templating', @pytest.mark.sphinx(
confoverrides={'autosummary_context': {'sentence': 'foobar'}}) 'html',
testroot='templating',
confoverrides={'autosummary_context': {'sentence': 'foobar'}},
)
def test_autosummary_context(make_app, app_params): def test_autosummary_context(make_app, app_params):
args, kwargs = app_params args, kwargs = app_params
app = make_app(*args, **kwargs) app = make_app(*args, **kwargs)
setup_documenters(app) setup_documenters(app)
app.build() app.build()
result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text(encoding='utf8') result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text(
encoding='utf8'
)
assert 'autosummary/class.rst method block overloading' in result assert 'autosummary/class.rst method block overloading' in result
assert 'foobar' in result assert 'foobar' in result

View File

@ -1,8 +1,11 @@
"""Test the Theme class.""" """Test the Theme class."""
import os import os
import shutil
from xml.etree.ElementTree import ParseError
import pytest import pytest
from defusedxml.ElementTree import parse as xml_parse
import sphinx.builders.html import sphinx.builders.html
from sphinx.errors import ThemeError from sphinx.errors import ThemeError
@ -11,18 +14,40 @@ from sphinx.theming import _load_theme_conf
@pytest.mark.sphinx( @pytest.mark.sphinx(
testroot='theming', testroot='theming',
confoverrides={'html_theme': 'ziptheme', confoverrides={'html_theme': 'ziptheme', 'html_theme_options.testopt': 'foo'},
'html_theme_options.testopt': 'foo'}) )
def test_theme_api(app, status, warning): def test_theme_api(app, status, warning):
themes = ['basic', 'default', 'scrolls', 'agogo', 'sphinxdoc', 'haiku', themes = [
'traditional', 'epub', 'nature', 'pyramid', 'bizstyle', 'classic', 'nonav', 'basic',
'test-theme', 'ziptheme', 'staticfiles', 'parent', 'child', 'alabaster'] 'default',
'scrolls',
'agogo',
'sphinxdoc',
'haiku',
'traditional',
'epub',
'nature',
'pyramid',
'bizstyle',
'classic',
'nonav',
'test-theme',
'ziptheme',
'staticfiles',
'parent',
'child',
'alabaster',
]
# test Theme class API # test Theme class API
assert set(app.registry.html_themes.keys()) == set(themes) assert set(app.registry.html_themes.keys()) == set(themes)
assert app.registry.html_themes['test-theme'] == str(app.srcdir / 'test_theme' / 'test-theme') assert app.registry.html_themes['test-theme'] == str(
app.srcdir / 'test_theme' / 'test-theme'
)
assert app.registry.html_themes['ziptheme'] == str(app.srcdir / 'ziptheme.zip') assert app.registry.html_themes['ziptheme'] == str(app.srcdir / 'ziptheme.zip')
assert app.registry.html_themes['staticfiles'] == str(app.srcdir / 'test_theme' / 'staticfiles') assert app.registry.html_themes['staticfiles'] == str(
app.srcdir / 'test_theme' / 'staticfiles'
)
# test Theme instance API # test Theme instance API
theme = app.builder.theme theme = app.builder.theme
@ -65,30 +90,26 @@ def test_double_inheriting_theme(app, status, warning):
app.build() # => not raises TemplateNotFound app.build() # => not raises TemplateNotFound
@pytest.mark.sphinx(testroot='theming', @pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'child'})
confoverrides={'html_theme': 'child'})
def test_nested_zipped_theme(app, status, warning): def test_nested_zipped_theme(app, status, warning):
assert app.builder.theme.name == 'child' assert app.builder.theme.name == 'child'
app.build() # => not raises TemplateNotFound app.build() # => not raises TemplateNotFound
@pytest.mark.sphinx(testroot='theming', @pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'staticfiles'})
confoverrides={'html_theme': 'staticfiles'})
def test_staticfiles(app, status, warning): def test_staticfiles(app, status, warning):
app.build() app.build()
assert (app.outdir / '_static' / 'staticimg.png').exists() assert (app.outdir / '_static' / 'staticimg.png').exists()
assert (app.outdir / '_static' / 'statictmpl.html').exists() assert (app.outdir / '_static' / 'statictmpl.html').exists()
assert (app.outdir / '_static' / 'statictmpl.html').read_text(encoding='utf8') == ( assert (app.outdir / '_static' / 'statictmpl.html').read_text(encoding='utf8') == (
'<!-- testing static templates -->\n' '<!-- testing static templates -->\n<html><project>Python</project></html>'
'<html><project>Python</project></html>'
) )
result = (app.outdir / 'index.html').read_text(encoding='utf8') result = (app.outdir / 'index.html').read_text(encoding='utf8')
assert '<meta name="testopt" content="optdefault" />' in result assert '<meta name="testopt" content="optdefault" />' in result
@pytest.mark.sphinx(testroot='theming', @pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'test-theme'})
confoverrides={'html_theme': 'test-theme'})
def test_dark_style(app, monkeypatch): def test_dark_style(app, monkeypatch):
monkeypatch.setattr(sphinx.builders.html, '_file_checksum', lambda o, f: '') 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] css_file, properties = app.registry.css_files[0]
assert css_file == 'pygments_dark.css' assert css_file == 'pygments_dark.css'
assert "media" in properties assert 'media' in properties
assert properties["media"] == '(prefers-color-scheme: dark)' assert properties['media'] == '(prefers-color-scheme: dark)'
assert sorted(f.filename for f in app.builder._css_files) == [ assert sorted(f.filename for f in app.builder._css_files) == [
'_static/classic.css', '_static/classic.css',
@ -111,9 +132,11 @@ def test_dark_style(app, monkeypatch):
result = (app.outdir / 'index.html').read_text(encoding='utf8') result = (app.outdir / 'index.html').read_text(encoding='utf8')
assert '<link rel="stylesheet" type="text/css" href="_static/pygments.css" />' in result assert '<link rel="stylesheet" type="text/css" href="_static/pygments.css" />' in result
assert ('<link id="pygments_dark_css" media="(prefers-color-scheme: dark)" ' assert (
'<link id="pygments_dark_css" media="(prefers-color-scheme: dark)" '
'rel="stylesheet" type="text/css" ' 'rel="stylesheet" type="text/css" '
'href="_static/pygments_dark.css" />') in result 'href="_static/pygments_dark.css" />'
) in result
@pytest.mark.sphinx(testroot='theming') @pytest.mark.sphinx(testroot='theming')
@ -126,3 +149,41 @@ def test_theme_sidebars(app, status, warning):
assert '<h3>Related Topics</h3>' not in result assert '<h3>Related Topics</h3>' not in result
assert '<h3>This Page</h3>' not in result assert '<h3>This Page</h3>' not in result
assert '<h3 id="searchlabel">Quick search</h3>' in result assert '<h3 id="searchlabel">Quick search</h3>' 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}')