Skip to content

Commit

Permalink
[tests] Add basic build test for all builtin themes (#12168)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
chrisjsewell committed Mar 22, 2024
1 parent 982679e commit 66fa790
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 34 deletions.
1 change: 0 additions & 1 deletion .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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/**/*",
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion sphinx/themes/basic/search.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<script src="{{ pathto('_static/language_data.js', 1) }}"></script>
{%- endblock %}
{% block extrahead %}
<script src="{{ pathto('searchindex.js', 1) }}" defer></script>
<script src="{{ pathto('searchindex.js', 1) }}" defer="defer"></script>
<meta name="robots" content="noindex" />
{{ super() }}
{% endblock %}
Expand Down
5 changes: 0 additions & 5 deletions sphinx/themes/bizstyle/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@
<script src="{{ pathto('_static/bizstyle.js', 1) }}"></script>
{%- endblock %}

{# doctype override #}
{%- block doctype %}
<!doctype html>
{%- endblock %}

{%- block extrahead %}
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<!--[if lt IE 9]>
Expand Down
11 changes: 8 additions & 3 deletions tests/test_theming/test_html_theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ def test_theme_options(app, status, warning):
assert 'ENABLE_SEARCH_SHORTCUTS: true' in result


@pytest.mark.sphinx('html', testroot='theming',
confoverrides={'html_theme_options.navigation_with_keys': True,
'html_theme_options.enable_search_shortcuts': False})
@pytest.mark.sphinx(
'html',
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):
app.build()

Expand Down
15 changes: 11 additions & 4 deletions tests/test_theming/test_templating.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,26 @@ def test_autosummary_class_template_overloading(make_app, app_params):
setup_documenters(app)
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 'foobar' not in result


@pytest.mark.sphinx('html', testroot='templating',
confoverrides={'autosummary_context': {'sentence': 'foobar'}})
@pytest.mark.sphinx(
'html',
testroot='templating',
confoverrides={'autosummary_context': {'sentence': 'foobar'}},
)
def test_autosummary_context(make_app, app_params):
args, kwargs = app_params
app = make_app(*args, **kwargs)
setup_documenters(app)
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 'foobar' in result
101 changes: 81 additions & 20 deletions tests/test_theming/test_theming.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""Test the Theme class."""

import os
import shutil
from xml.etree.ElementTree import ParseError

import pytest
from defusedxml.ElementTree import parse as xml_parse

import sphinx.builders.html
from sphinx.errors import ThemeError
Expand All @@ -11,18 +14,40 @@

@pytest.mark.sphinx(
testroot='theming',
confoverrides={'html_theme': 'ziptheme',
'html_theme_options.testopt': 'foo'})
confoverrides={'html_theme': 'ziptheme', 'html_theme_options.testopt': 'foo'},
)
def test_theme_api(app, status, warning):
themes = ['basic', 'default', 'scrolls', 'agogo', 'sphinxdoc', 'haiku',
'traditional', 'epub', 'nature', 'pyramid', 'bizstyle', 'classic', 'nonav',
'test-theme', 'ziptheme', 'staticfiles', 'parent', 'child', 'alabaster']
themes = [
'basic',
'default',
'scrolls',
'agogo',
'sphinxdoc',
'haiku',
'traditional',
'epub',
'nature',
'pyramid',
'bizstyle',
'classic',
'nonav',
'test-theme',
'ziptheme',
'staticfiles',
'parent',
'child',
'alabaster',
]

# test Theme class API
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['staticfiles'] == str(app.srcdir / 'test_theme' / 'staticfiles')
assert app.registry.html_themes['staticfiles'] == str(
app.srcdir / 'test_theme' / 'staticfiles'
)

# test Theme instance API
theme = app.builder.theme
Expand Down Expand Up @@ -65,30 +90,26 @@ def test_double_inheriting_theme(app, status, warning):
app.build() # => not raises TemplateNotFound


@pytest.mark.sphinx(testroot='theming',
confoverrides={'html_theme': 'child'})
@pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'child'})
def test_nested_zipped_theme(app, status, warning):
assert app.builder.theme.name == 'child'
app.build() # => not raises TemplateNotFound


@pytest.mark.sphinx(testroot='theming',
confoverrides={'html_theme': 'staticfiles'})
@pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'staticfiles'})
def test_staticfiles(app, status, warning):
app.build()
assert (app.outdir / '_static' / 'staticimg.png').exists()
assert (app.outdir / '_static' / 'statictmpl.html').exists()
assert (app.outdir / '_static' / 'statictmpl.html').read_text(encoding='utf8') == (
'<!-- testing static templates -->\n'
'<html><project>Python</project></html>'
'<!-- testing static templates -->\n<html><project>Python</project></html>'
)

result = (app.outdir / 'index.html').read_text(encoding='utf8')
assert '<meta name="testopt" content="optdefault" />' 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: '')

Expand All @@ -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',
Expand All @@ -111,9 +132,11 @@ def test_dark_style(app, monkeypatch):

result = (app.outdir / 'index.html').read_text(encoding='utf8')
assert '<link rel="stylesheet" type="text/css" href="_static/pygments.css" />' in result
assert ('<link id="pygments_dark_css" media="(prefers-color-scheme: dark)" '
'rel="stylesheet" type="text/css" '
'href="_static/pygments_dark.css" />') in result
assert (
'<link id="pygments_dark_css" media="(prefers-color-scheme: dark)" '
'rel="stylesheet" type="text/css" '
'href="_static/pygments_dark.css" />'
) in result


@pytest.mark.sphinx(testroot='theming')
Expand All @@ -126,3 +149,41 @@ def test_theme_sidebars(app, status, warning):
assert '<h3>Related Topics</h3>' not in result
assert '<h3>This Page</h3>' not 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}')

0 comments on commit 66fa790

Please sign in to comment.