1"""
2    test_build_html
3    ~~~~~~~~~~~~~~~
4
5    Test the HTML builder and check output against XPath.
6
7    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
8    :license: BSD, see LICENSE for details.
9"""
10
11import os
12import re
13from distutils.version import LooseVersion
14from itertools import chain, cycle
15from unittest.mock import ANY, call, patch
16
17import pygments
18import pytest
19from html5lib import HTMLParser
20
21from sphinx.builders.html import validate_html_extra_path, validate_html_static_path
22from sphinx.errors import ConfigError
23from sphinx.testing.util import strip_escseq
24from sphinx.util import docutils, md5
25from sphinx.util.inventory import InventoryFile
26
27ENV_WARNINGS = """\
28%(root)s/autodoc_fodder.py:docstring of autodoc_fodder.MarkupError:\\d+: \
29WARNING: Explicit markup ends without a blank line; unexpected unindent.
30%(root)s/index.rst:\\d+: WARNING: Encoding 'utf-8-sig' used for reading included \
31file '%(root)s/wrongenc.inc' seems to be wrong, try giving an :encoding: option
32%(root)s/index.rst:\\d+: WARNING: invalid single index entry ''
33%(root)s/index.rst:\\d+: WARNING: image file not readable: foo.png
34%(root)s/index.rst:\\d+: WARNING: download file not readable: %(root)s/nonexisting.png
35%(root)s/undecodable.rst:\\d+: WARNING: undecodable source characters, replacing \
36with "\\?": b?'here: >>>(\\\\|/)xbb<<<((\\\\|/)r)?'
37"""
38
39HTML_WARNINGS = ENV_WARNINGS + """\
40%(root)s/index.rst:\\d+: WARNING: unknown option: &option
41%(root)s/index.rst:\\d+: WARNING: citation not found: missing
42%(root)s/index.rst:\\d+: WARNING: a suitable image for html builder not found: foo.\\*
43%(root)s/index.rst:\\d+: WARNING: Could not lex literal_block as "c". Highlighting skipped.
44"""
45
46
47etree_cache = {}
48
49
50@pytest.fixture(scope='module')
51def cached_etree_parse():
52    def parse(fname):
53        if fname in etree_cache:
54            return etree_cache[fname]
55        with (fname).open('rb') as fp:
56            etree = HTMLParser(namespaceHTMLElements=False).parse(fp)
57            etree_cache.clear()
58            etree_cache[fname] = etree
59            return etree
60    yield parse
61    etree_cache.clear()
62
63
64def flat_dict(d):
65    return chain.from_iterable(
66        [
67            zip(cycle([fname]), values)
68            for fname, values in d.items()
69        ]
70    )
71
72
73def tail_check(check):
74    rex = re.compile(check)
75
76    def checker(nodes):
77        for node in nodes:
78            if node.tail and rex.search(node.tail):
79                return True
80        assert False, '%r not found in tail of any nodes %s' % (check, nodes)
81    return checker
82
83
84def check_xpath(etree, fname, path, check, be_found=True):
85    nodes = list(etree.findall(path))
86    if check is None:
87        assert nodes == [], ('found any nodes matching xpath '
88                             '%r in file %s' % (path, fname))
89        return
90    else:
91        assert nodes != [], ('did not find any node matching xpath '
92                             '%r in file %s' % (path, fname))
93    if hasattr(check, '__call__'):
94        check(nodes)
95    elif not check:
96        # only check for node presence
97        pass
98    else:
99        def get_text(node):
100            if node.text is not None:
101                # the node has only one text
102                return node.text
103            else:
104                # the node has tags and text; gather texts just under the node
105                return ''.join(n.tail or '' for n in node)
106
107        rex = re.compile(check)
108        if be_found:
109            if any(rex.search(get_text(node)) for node in nodes):
110                return
111        else:
112            if all(not rex.search(get_text(node)) for node in nodes):
113                return
114
115        assert False, ('%r not found in any node matching '
116                       'path %s in %s: %r' % (check, path, fname,
117                                              [node.text for node in nodes]))
118
119
120@pytest.mark.sphinx('html', testroot='warnings')
121def test_html_warnings(app, warning):
122    app.build()
123    html_warnings = strip_escseq(re.sub(re.escape(os.sep) + '{1,2}', '/', warning.getvalue()))
124    html_warnings_exp = HTML_WARNINGS % {
125        'root': re.escape(app.srcdir.replace(os.sep, '/'))}
126    assert re.match(html_warnings_exp + '$', html_warnings), \
127        'Warnings don\'t match:\n' + \
128        '--- Expected (regex):\n' + html_warnings_exp + \
129        '--- Got:\n' + html_warnings
130
131
132@pytest.mark.sphinx('html', confoverrides={'html4_writer': True})
133def test_html4_output(app, status, warning):
134    app.build()
135
136
137@pytest.mark.parametrize("fname,expect", flat_dict({
138    'images.html': [
139        (".//img[@src='_images/img.png']", ''),
140        (".//img[@src='_images/img1.png']", ''),
141        (".//img[@src='_images/simg.png']", ''),
142        (".//img[@src='_images/svgimg.svg']", ''),
143        (".//a[@href='_sources/images.txt']", ''),
144    ],
145    'subdir/images.html': [
146        (".//img[@src='../_images/img1.png']", ''),
147        (".//img[@src='../_images/rimg.png']", ''),
148    ],
149    'subdir/includes.html': [
150        (".//a[@class='reference download internal']", ''),
151        (".//img[@src='../_images/img.png']", ''),
152        (".//p", 'This is an include file.'),
153        (".//pre/span", 'line 1'),
154        (".//pre/span", 'line 2'),
155    ],
156    'includes.html': [
157        (".//pre", 'Max Strauß'),
158        (".//a[@class='reference download internal']", ''),
159        (".//pre/span", '"quotes"'),
160        (".//pre/span", "'included'"),
161        (".//pre/span[@class='s2']", 'üöä'),
162        (".//div[@class='inc-pyobj1 highlight-text notranslate']//pre",
163         r'^class Foo:\n    pass\n\s*$'),
164        (".//div[@class='inc-pyobj2 highlight-text notranslate']//pre",
165         r'^    def baz\(\):\n        pass\n\s*$'),
166        (".//div[@class='inc-lines highlight-text notranslate']//pre",
167         r'^class Foo:\n    pass\nclass Bar:\n$'),
168        (".//div[@class='inc-startend highlight-text notranslate']//pre",
169         '^foo = "Including Unicode characters: üöä"\\n$'),
170        (".//div[@class='inc-preappend highlight-text notranslate']//pre",
171         r'(?m)^START CODE$'),
172        (".//div[@class='inc-pyobj-dedent highlight-python notranslate']//span",
173         r'def'),
174        (".//div[@class='inc-tab3 highlight-text notranslate']//pre",
175         r'-| |-'),
176        (".//div[@class='inc-tab8 highlight-python notranslate']//pre/span",
177         r'-|      |-'),
178    ],
179    'autodoc.html': [
180        (".//dl[@class='py class']/dt[@id='autodoc_target.Class']", ''),
181        (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'\*\*'),
182        (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'kwds'),
183        (".//dd/p", r'Return spam\.'),
184    ],
185    'extapi.html': [
186        (".//strong", 'from class: Bar'),
187    ],
188    'markup.html': [
189        (".//title", 'set by title directive'),
190        (".//p/em", 'Section author: Georg Brandl'),
191        (".//p/em", 'Module author: Georg Brandl'),
192        # created by the meta directive
193        (".//meta[@name='author'][@content='Me']", ''),
194        (".//meta[@name='keywords'][@content='docs, sphinx']", ''),
195        # a label created by ``.. _label:``
196        (".//div[@id='label']", ''),
197        # code with standard code blocks
198        (".//pre", '^some code$'),
199        # an option list
200        (".//span[@class='option']", '--help'),
201        # admonitions
202        (".//p[@class='admonition-title']", 'My Admonition'),
203        (".//div[@class='admonition note']/p", 'Note text.'),
204        (".//div[@class='admonition warning']/p", 'Warning text.'),
205        # inline markup
206        (".//li/p/strong", r'^command\\n$'),
207        (".//li/p/strong", r'^program\\n$'),
208        (".//li/p/em", r'^dfn\\n$'),
209        (".//li/p/kbd", r'^kbd\\n$'),
210        (".//li/p/span", 'File \N{TRIANGULAR BULLET} Close'),
211        (".//li/p/code/span[@class='pre']", '^a/$'),
212        (".//li/p/code/em/span[@class='pre']", '^varpart$'),
213        (".//li/p/code/em/span[@class='pre']", '^i$'),
214        (".//a[@href='https://www.python.org/dev/peps/pep-0008']"
215         "[@class='pep reference external']/strong", 'PEP 8'),
216        (".//a[@href='https://www.python.org/dev/peps/pep-0008']"
217         "[@class='pep reference external']/strong",
218         'Python Enhancement Proposal #8'),
219        (".//a[@href='https://tools.ietf.org/html/rfc1.html']"
220         "[@class='rfc reference external']/strong", 'RFC 1'),
221        (".//a[@href='https://tools.ietf.org/html/rfc1.html']"
222         "[@class='rfc reference external']/strong", 'Request for Comments #1'),
223        (".//a[@href='objects.html#envvar-HOME']"
224         "[@class='reference internal']/code/span[@class='pre']", 'HOME'),
225        (".//a[@href='#with']"
226         "[@class='reference internal']/code/span[@class='pre']", '^with$'),
227        (".//a[@href='#grammar-token-try_stmt']"
228         "[@class='reference internal']/code/span", '^statement$'),
229        (".//a[@href='#some-label'][@class='reference internal']/span", '^here$'),
230        (".//a[@href='#some-label'][@class='reference internal']/span", '^there$'),
231        (".//a[@href='subdir/includes.html']"
232         "[@class='reference internal']/span", 'Including in subdir'),
233        (".//a[@href='objects.html#cmdoption-python-c']"
234         "[@class='reference internal']/code/span[@class='pre']", '-c'),
235        # abbreviations
236        (".//abbr[@title='abbreviation']", '^abbr$'),
237        # version stuff
238        (".//div[@class='versionadded']/p/span", 'New in version 0.6: '),
239        (".//div[@class='versionadded']/p/span",
240         tail_check('First paragraph of versionadded')),
241        (".//div[@class='versionchanged']/p/span",
242         tail_check('First paragraph of versionchanged')),
243        (".//div[@class='versionchanged']/p",
244         'Second paragraph of versionchanged'),
245        # footnote reference
246        (".//a[@class='footnote-reference brackets']", r'1'),
247        # created by reference lookup
248        (".//a[@href='index.html#ref1']", ''),
249        # ``seealso`` directive
250        (".//div/p[@class='admonition-title']", 'See also'),
251        # a ``hlist`` directive
252        (".//table[@class='hlist']/tbody/tr/td/ul/li/p", '^This$'),
253        # a ``centered`` directive
254        (".//p[@class='centered']/strong", 'LICENSE'),
255        # a glossary
256        (".//dl/dt[@id='term-boson']", 'boson'),
257        (".//dl/dt[@id='term-boson']/a", '¶'),
258        # a production list
259        (".//pre/strong", 'try_stmt'),
260        (".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'),
261        # tests for ``only`` directive
262        (".//p", 'A global substitution!'),
263        (".//p", 'In HTML.'),
264        (".//p", 'In both.'),
265        (".//p", 'Always present'),
266        # tests for ``any`` role
267        (".//a[@href='#with']/span", 'headings'),
268        (".//a[@href='objects.html#func_without_body']/code/span", 'objects'),
269        # tests for numeric labels
270        (".//a[@href='#id1'][@class='reference internal']/span", 'Testing various markup'),
271        # tests for smartypants
272        (".//li/p", 'Smart “quotes” in English ‘text’.'),
273        (".//li/p", 'Smart — long and – short dashes.'),
274        (".//li/p", 'Ellipsis…'),
275        (".//li/p/code/span[@class='pre']", 'foo--"bar"...'),
276        (".//p", 'Этот «абзац» должен использовать „русские“ кавычки.'),
277        (".//p", 'Il dit : « C’est “super” ! »'),
278    ],
279    'objects.html': [
280        (".//dt[@id='mod.Cls.meth1']", ''),
281        (".//dt[@id='errmod.Error']", ''),
282        (".//dt/code/span", r'long\(parameter,'),
283        (".//dt/code/span", r'list\)'),
284        (".//dt/code/span", 'another'),
285        (".//dt/code/span", 'one'),
286        (".//a[@href='#mod.Cls'][@class='reference internal']", ''),
287        (".//dl[@class='std userdesc']", ''),
288        (".//dt[@id='userdesc-myobj']", ''),
289        (".//a[@href='#userdesc-myobj'][@class='reference internal']", ''),
290        # docfields
291        (".//a[@class='reference internal'][@href='#TimeInt']/em", 'TimeInt'),
292        (".//a[@class='reference internal'][@href='#Time']", 'Time'),
293        (".//a[@class='reference internal'][@href='#errmod.Error']/strong", 'Error'),
294        # C references
295        (".//span[@class='pre']", 'CFunction()'),
296        (".//a[@href='#c.Sphinx_DoSomething']", ''),
297        (".//a[@href='#c.SphinxStruct.member']", ''),
298        (".//a[@href='#c.SPHINX_USE_PYTHON']", ''),
299        (".//a[@href='#c.SphinxType']", ''),
300        (".//a[@href='#c.sphinx_global']", ''),
301        # test global TOC created by toctree()
302        (".//ul[@class='current']/li[@class='toctree-l1 current']/a[@href='#']",
303         'Testing object descriptions'),
304        (".//li[@class='toctree-l1']/a[@href='markup.html']",
305         'Testing various markup'),
306        # test unknown field names
307        (".//dt[@class='field-odd']", 'Field_name'),
308        (".//dt[@class='field-even']", 'Field_name all lower'),
309        (".//dt[@class='field-odd']", 'FIELD_NAME'),
310        (".//dt[@class='field-even']", 'FIELD_NAME ALL CAPS'),
311        (".//dt[@class='field-odd']", 'Field_Name'),
312        (".//dt[@class='field-even']", 'Field_Name All Word Caps'),
313        (".//dt[@class='field-odd']", 'Field_name'),
314        (".//dt[@class='field-even']", 'Field_name First word cap'),
315        (".//dt[@class='field-odd']", 'FIELd_name'),
316        (".//dt[@class='field-even']", 'FIELd_name PARTial caps'),
317        # custom sidebar
318        (".//h4", 'Custom sidebar'),
319        # docfields
320        (".//dd[@class='field-odd']/p/strong", '^moo$'),
321        (".//dd[@class='field-odd']/p/strong", tail_check(r'\(Moo\) .* Moo')),
322        (".//dd[@class='field-odd']/ul/li/p/strong", '^hour$'),
323        (".//dd[@class='field-odd']/ul/li/p/em", '^DuplicateType$'),
324        (".//dd[@class='field-odd']/ul/li/p/em", tail_check(r'.* Some parameter')),
325        # others
326        (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span",
327         'perl'),
328        (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span",
329         '\\+p'),
330        (".//a[@class='reference internal'][@href='#cmdoption-perl-ObjC']/code/span",
331         '--ObjC\\+\\+'),
332        (".//a[@class='reference internal'][@href='#cmdoption-perl-plugin.option']/code/span",
333         '--plugin.option'),
334        (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-create-auth-token']"
335         "/code/span",
336         'create-auth-token'),
337        (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-arg']/code/span",
338         'arg'),
339        (".//a[@class='reference internal'][@href='#cmdoption-perl-j']/code/span",
340         '-j'),
341        (".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
342         'hg'),
343        (".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
344         'commit'),
345        (".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
346         'git'),
347        (".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
348         'commit'),
349        (".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
350         '-p'),
351    ],
352    'index.html': [
353        (".//meta[@name='hc'][@content='hcval']", ''),
354        (".//meta[@name='hc_co'][@content='hcval_co']", ''),
355        (".//dt[@class='label']/span[@class='brackets']", r'Ref1'),
356        (".//dt[@class='label']", ''),
357        (".//li[@class='toctree-l1']/a", 'Testing various markup'),
358        (".//li[@class='toctree-l2']/a", 'Inline markup'),
359        (".//title", 'Sphinx <Tests>'),
360        (".//div[@class='footer']", 'Georg Brandl & Team'),
361        (".//a[@href='http://python.org/']"
362         "[@class='reference external']", ''),
363        (".//li/p/a[@href='genindex.html']/span", 'Index'),
364        (".//li/p/a[@href='py-modindex.html']/span", 'Module Index'),
365        # custom sidebar only for contents
366        (".//h4", 'Contents sidebar'),
367        # custom JavaScript
368        (".//script[@src='file://moo.js']", ''),
369        # URL in contents
370        (".//a[@class='reference external'][@href='http://sphinx-doc.org/']",
371         'http://sphinx-doc.org/'),
372        (".//a[@class='reference external'][@href='http://sphinx-doc.org/latest/']",
373         'Latest reference'),
374        # Indirect hyperlink targets across files
375        (".//a[@href='markup.html#some-label'][@class='reference internal']/span",
376         '^indirect hyperref$'),
377    ],
378    'bom.html': [
379        (".//title", " File with UTF-8 BOM"),
380    ],
381    'extensions.html': [
382        (".//a[@href='http://python.org/dev/']", "http://python.org/dev/"),
383        (".//a[@href='http://bugs.python.org/issue1000']", "issue 1000"),
384        (".//a[@href='http://bugs.python.org/issue1042']", "explicit caption"),
385    ],
386    'genindex.html': [
387        # index entries
388        (".//a/strong", "Main"),
389        (".//a/strong", "[1]"),
390        (".//a/strong", "Other"),
391        (".//a", "entry"),
392        (".//li/a", "double"),
393    ],
394    'footnote.html': [
395        (".//a[@class='footnote-reference brackets'][@href='#id9'][@id='id1']", r"1"),
396        (".//a[@class='footnote-reference brackets'][@href='#id10'][@id='id2']", r"2"),
397        (".//a[@class='footnote-reference brackets'][@href='#foo'][@id='id3']", r"3"),
398        (".//a[@class='reference internal'][@href='#bar'][@id='id4']/span", r"\[bar\]"),
399        (".//a[@class='reference internal'][@href='#baz-qux'][@id='id5']/span", r"\[baz_qux\]"),
400        (".//a[@class='footnote-reference brackets'][@href='#id11'][@id='id6']", r"4"),
401        (".//a[@class='footnote-reference brackets'][@href='#id12'][@id='id7']", r"5"),
402        (".//a[@class='fn-backref'][@href='#id1']", r"1"),
403        (".//a[@class='fn-backref'][@href='#id2']", r"2"),
404        (".//a[@class='fn-backref'][@href='#id3']", r"3"),
405        (".//a[@class='fn-backref'][@href='#id4']", r"bar"),
406        (".//a[@class='fn-backref'][@href='#id5']", r"baz_qux"),
407        (".//a[@class='fn-backref'][@href='#id6']", r"4"),
408        (".//a[@class='fn-backref'][@href='#id7']", r"5"),
409        (".//a[@class='fn-backref'][@href='#id8']", r"6"),
410    ],
411    'otherext.html': [
412        (".//h1", "Generated section"),
413        (".//a[@href='_sources/otherext.foo.txt']", ''),
414    ]
415}))
416@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
417                    reason='docutils-0.13 or above is required')
418@pytest.mark.sphinx('html', tags=['testtag'],
419                    confoverrides={'html_context.hckey_co': 'hcval_co'})
420@pytest.mark.test_params(shared_result='test_build_html_output')
421def test_html5_output(app, cached_etree_parse, fname, expect):
422    app.build()
423    print(app.outdir / fname)
424    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
425
426
427@pytest.mark.sphinx('html', parallel=2)
428def test_html_parallel(app):
429    app.build()
430
431
432@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
433                    reason='docutils-0.13 or above is required')
434@pytest.mark.sphinx('html')
435@pytest.mark.test_params(shared_result='test_build_html_output')
436def test_html_download(app):
437    app.build()
438
439    # subdir/includes.html
440    result = (app.outdir / 'subdir' / 'includes.html').read_text()
441    pattern = ('<a class="reference download internal" download="" '
442               'href="../(_downloads/.*/img.png)">')
443    matched = re.search(pattern, result)
444    assert matched
445    assert (app.outdir / matched.group(1)).exists()
446    filename = matched.group(1)
447
448    # includes.html
449    result = (app.outdir / 'includes.html').read_text()
450    pattern = ('<a class="reference download internal" download="" '
451               'href="(_downloads/.*/img.png)">')
452    matched = re.search(pattern, result)
453    assert matched
454    assert (app.outdir / matched.group(1)).exists()
455    assert matched.group(1) == filename
456
457
458@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
459                    reason='docutils-0.13 or above is required')
460@pytest.mark.sphinx('html', testroot='roles-download')
461def test_html_download_role(app, status, warning):
462    app.build()
463    digest = md5(b'dummy.dat').hexdigest()
464    assert (app.outdir / '_downloads' / digest / 'dummy.dat').exists()
465    digest_another = md5(b'another/dummy.dat').hexdigest()
466    assert (app.outdir / '_downloads' / digest_another / 'dummy.dat').exists()
467
468    content = (app.outdir / 'index.html').read_text()
469    assert (('<li><p><a class="reference download internal" download="" '
470             'href="_downloads/%s/dummy.dat">'
471             '<code class="xref download docutils literal notranslate">'
472             '<span class="pre">dummy.dat</span></code></a></p></li>' % digest)
473            in content)
474    assert (('<li><p><a class="reference download internal" download="" '
475             'href="_downloads/%s/dummy.dat">'
476             '<code class="xref download docutils literal notranslate">'
477             '<span class="pre">another/dummy.dat</span></code></a></p></li>' %
478             digest_another) in content)
479    assert ('<li><p><code class="xref download docutils literal notranslate">'
480            '<span class="pre">not_found.dat</span></code></p></li>' in content)
481    assert ('<li><p><a class="reference download external" download="" '
482            'href="http://www.sphinx-doc.org/en/master/_static/sphinxheader.png">'
483            '<code class="xref download docutils literal notranslate">'
484            '<span class="pre">Sphinx</span> <span class="pre">logo</span>'
485            '</code></a></p></li>' in content)
486
487
488@pytest.mark.sphinx('html', testroot='build-html-translator')
489def test_html_translator(app):
490    app.build()
491    assert app.builder.docwriter.visitor.depart_with_node == 10
492
493
494@pytest.mark.parametrize("fname,expect", flat_dict({
495    'index.html': [
496        (".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True),
497        (".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True),
498        (".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False),
499        (".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False),
500    ],
501    'foo.html': [
502        (".//h1", 'Foo', True),
503        (".//h2", 'Foo A', True),
504        (".//h3", 'Foo A1', True),
505        (".//h2", 'Foo B', True),
506        (".//h3", 'Foo B1', True),
507
508        (".//h1//span[@class='section-number']", '1. ', True),
509        (".//h2//span[@class='section-number']", '1.1. ', True),
510        (".//h3//span[@class='section-number']", '1.1.1. ', True),
511        (".//h2//span[@class='section-number']", '1.2. ', True),
512        (".//h3//span[@class='section-number']", '1.2.1. ', True),
513
514        (".//div[@class='sphinxsidebarwrapper']//li/a", '1.1. Foo A', True),
515        (".//div[@class='sphinxsidebarwrapper']//li/a", '1.1.1. Foo A1', True),
516        (".//div[@class='sphinxsidebarwrapper']//li/a", '1.2. Foo B', True),
517        (".//div[@class='sphinxsidebarwrapper']//li/a", '1.2.1. Foo B1', True),
518    ],
519    'bar.html': [
520        (".//h1", 'Bar', True),
521        (".//h2", 'Bar A', True),
522        (".//h2", 'Bar B', True),
523        (".//h3", 'Bar B1', True),
524        (".//h1//span[@class='section-number']", '2. ', True),
525        (".//h2//span[@class='section-number']", '2.1. ', True),
526        (".//h2//span[@class='section-number']", '2.2. ', True),
527        (".//h3//span[@class='section-number']", '2.2.1. ', True),
528        (".//div[@class='sphinxsidebarwrapper']//li/a", '2. Bar', True),
529        (".//div[@class='sphinxsidebarwrapper']//li/a", '2.1. Bar A', True),
530        (".//div[@class='sphinxsidebarwrapper']//li/a", '2.2. Bar B', True),
531        (".//div[@class='sphinxsidebarwrapper']//li/a", '2.2.1. Bar B1', False),
532    ],
533    'baz.html': [
534        (".//h1", 'Baz A', True),
535        (".//h1//span[@class='section-number']", '2.1.1. ', True),
536    ],
537}))
538@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
539                    reason='docutils-0.13 or above is required')
540@pytest.mark.sphinx('html', testroot='tocdepth')
541@pytest.mark.test_params(shared_result='test_build_html_tocdepth')
542def test_tocdepth(app, cached_etree_parse, fname, expect):
543    app.build()
544    # issue #1251
545    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
546
547
548@pytest.mark.parametrize("fname,expect", flat_dict({
549    'index.html': [
550        (".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True),
551        (".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True),
552        (".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False),
553        (".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False),
554
555        # index.rst
556        (".//h1", 'test-tocdepth', True),
557
558        # foo.rst
559        (".//h2", 'Foo', True),
560        (".//h3", 'Foo A', True),
561        (".//h4", 'Foo A1', True),
562        (".//h3", 'Foo B', True),
563        (".//h4", 'Foo B1', True),
564        (".//h2//span[@class='section-number']", '1. ', True),
565        (".//h3//span[@class='section-number']", '1.1. ', True),
566        (".//h4//span[@class='section-number']", '1.1.1. ', True),
567        (".//h3//span[@class='section-number']", '1.2. ', True),
568        (".//h4//span[@class='section-number']", '1.2.1. ', True),
569
570        # bar.rst
571        (".//h2", 'Bar', True),
572        (".//h3", 'Bar A', True),
573        (".//h3", 'Bar B', True),
574        (".//h4", 'Bar B1', True),
575        (".//h2//span[@class='section-number']", '2. ', True),
576        (".//h3//span[@class='section-number']", '2.1. ', True),
577        (".//h3//span[@class='section-number']", '2.2. ', True),
578        (".//h4//span[@class='section-number']", '2.2.1. ', True),
579
580        # baz.rst
581        (".//h4", 'Baz A', True),
582        (".//h4//span[@class='section-number']", '2.1.1. ', True),
583    ],
584}))
585@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
586                    reason='docutils-0.13 or above is required')
587@pytest.mark.sphinx('singlehtml', testroot='tocdepth')
588@pytest.mark.test_params(shared_result='test_build_html_tocdepth')
589def test_tocdepth_singlehtml(app, cached_etree_parse, fname, expect):
590    app.build()
591    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
592
593
594@pytest.mark.sphinx('html', testroot='numfig')
595@pytest.mark.test_params(shared_result='test_build_html_numfig')
596def test_numfig_disabled_warn(app, warning):
597    app.build()
598    warnings = warning.getvalue()
599    assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' in warnings
600    assert 'index.rst:56: WARNING: invalid numfig_format: invalid' not in warnings
601    assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' not in warnings
602
603
604@pytest.mark.parametrize("fname,expect", flat_dict({
605    'index.html': [
606        (".//div[@class='figure align-default']/p[@class='caption']/"
607         "span[@class='caption-number']", None, True),
608        (".//table/caption/span[@class='caption-number']", None, True),
609        (".//div[@class='code-block-caption']/"
610         "span[@class='caption-number']", None, True),
611        (".//li/p/code/span", '^fig1$', True),
612        (".//li/p/code/span", '^Figure%s$', True),
613        (".//li/p/code/span", '^table-1$', True),
614        (".//li/p/code/span", '^Table:%s$', True),
615        (".//li/p/code/span", '^CODE_1$', True),
616        (".//li/p/code/span", '^Code-%s$', True),
617        (".//li/p/a/span", '^Section 1$', True),
618        (".//li/p/a/span", '^Section 2.1$', True),
619        (".//li/p/code/span", '^Fig.{number}$', True),
620        (".//li/p/a/span", '^Sect.1 Foo$', True),
621    ],
622    'foo.html': [
623        (".//div[@class='figure align-default']/p[@class='caption']/"
624         "span[@class='caption-number']", None, True),
625        (".//table/caption/span[@class='caption-number']", None, True),
626        (".//div[@class='code-block-caption']/"
627         "span[@class='caption-number']", None, True),
628    ],
629    'bar.html': [
630        (".//div[@class='figure align-default']/p[@class='caption']/"
631         "span[@class='caption-number']", None, True),
632        (".//table/caption/span[@class='caption-number']", None, True),
633        (".//div[@class='code-block-caption']/"
634         "span[@class='caption-number']", None, True),
635    ],
636    'baz.html': [
637        (".//div[@class='figure align-default']/p[@class='caption']/"
638         "span[@class='caption-number']", None, True),
639        (".//table/caption/span[@class='caption-number']", None, True),
640        (".//div[@class='code-block-caption']/"
641         "span[@class='caption-number']", None, True),
642    ],
643}))
644@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
645                    reason='docutils-0.13 or above is required')
646@pytest.mark.sphinx('html', testroot='numfig')
647@pytest.mark.test_params(shared_result='test_build_html_numfig')
648def test_numfig_disabled(app, cached_etree_parse, fname, expect):
649    app.build()
650    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
651
652
653@pytest.mark.sphinx(
654    'html', testroot='numfig',
655    srcdir='test_numfig_without_numbered_toctree_warn',
656    confoverrides={'numfig': True})
657def test_numfig_without_numbered_toctree_warn(app, warning):
658    app.build()
659    # remove :numbered: option
660    index = (app.srcdir / 'index.rst').read_text()
661    index = re.sub(':numbered:.*', '', index)
662    (app.srcdir / 'index.rst').write_text(index)
663    app.builder.build_all()
664
665    warnings = warning.getvalue()
666    assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
667    assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
668    assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
669    assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
670
671
672@pytest.mark.parametrize("fname,expect", flat_dict({
673    'index.html': [
674        (".//div[@class='figure align-default']/p[@class='caption']/"
675         "span[@class='caption-number']", '^Fig. 9 $', True),
676        (".//div[@class='figure align-default']/p[@class='caption']/"
677         "span[@class='caption-number']", '^Fig. 10 $', True),
678        (".//table/caption/span[@class='caption-number']",
679         '^Table 9 $', True),
680        (".//table/caption/span[@class='caption-number']",
681         '^Table 10 $', True),
682        (".//div[@class='code-block-caption']/"
683         "span[@class='caption-number']", '^Listing 9 $', True),
684        (".//div[@class='code-block-caption']/"
685         "span[@class='caption-number']", '^Listing 10 $', True),
686        (".//li/p/a/span", '^Fig. 9$', True),
687        (".//li/p/a/span", '^Figure6$', True),
688        (".//li/p/a/span", '^Table 9$', True),
689        (".//li/p/a/span", '^Table:6$', True),
690        (".//li/p/a/span", '^Listing 9$', True),
691        (".//li/p/a/span", '^Code-6$', True),
692        (".//li/p/code/span", '^foo$', True),
693        (".//li/p/code/span", '^bar_a$', True),
694        (".//li/p/a/span", '^Fig.9 should be Fig.1$', True),
695        (".//li/p/code/span", '^Sect.{number}$', True),
696    ],
697    'foo.html': [
698        (".//div[@class='figure align-default']/p[@class='caption']/"
699         "span[@class='caption-number']", '^Fig. 1 $', True),
700        (".//div[@class='figure align-default']/p[@class='caption']/"
701         "span[@class='caption-number']", '^Fig. 2 $', True),
702        (".//div[@class='figure align-default']/p[@class='caption']/"
703         "span[@class='caption-number']", '^Fig. 3 $', True),
704        (".//div[@class='figure align-default']/p[@class='caption']/"
705         "span[@class='caption-number']", '^Fig. 4 $', True),
706        (".//table/caption/span[@class='caption-number']",
707         '^Table 1 $', True),
708        (".//table/caption/span[@class='caption-number']",
709         '^Table 2 $', True),
710        (".//table/caption/span[@class='caption-number']",
711         '^Table 3 $', True),
712        (".//table/caption/span[@class='caption-number']",
713         '^Table 4 $', True),
714        (".//div[@class='code-block-caption']/"
715         "span[@class='caption-number']", '^Listing 1 $', True),
716        (".//div[@class='code-block-caption']/"
717         "span[@class='caption-number']", '^Listing 2 $', True),
718        (".//div[@class='code-block-caption']/"
719         "span[@class='caption-number']", '^Listing 3 $', True),
720        (".//div[@class='code-block-caption']/"
721         "span[@class='caption-number']", '^Listing 4 $', True),
722    ],
723    'bar.html': [
724        (".//div[@class='figure align-default']/p[@class='caption']/"
725         "span[@class='caption-number']", '^Fig. 5 $', True),
726        (".//div[@class='figure align-default']/p[@class='caption']/"
727         "span[@class='caption-number']", '^Fig. 7 $', True),
728        (".//div[@class='figure align-default']/p[@class='caption']/"
729         "span[@class='caption-number']", '^Fig. 8 $', True),
730        (".//table/caption/span[@class='caption-number']",
731         '^Table 5 $', True),
732        (".//table/caption/span[@class='caption-number']",
733         '^Table 7 $', True),
734        (".//table/caption/span[@class='caption-number']",
735         '^Table 8 $', True),
736        (".//div[@class='code-block-caption']/"
737         "span[@class='caption-number']", '^Listing 5 $', True),
738        (".//div[@class='code-block-caption']/"
739         "span[@class='caption-number']", '^Listing 7 $', True),
740        (".//div[@class='code-block-caption']/"
741         "span[@class='caption-number']", '^Listing 8 $', True),
742    ],
743    'baz.html': [
744        (".//div[@class='figure align-default']/p[@class='caption']/"
745         "span[@class='caption-number']", '^Fig. 6 $', True),
746        (".//table/caption/span[@class='caption-number']",
747         '^Table 6 $', True),
748        (".//div[@class='code-block-caption']/"
749         "span[@class='caption-number']", '^Listing 6 $', True),
750    ],
751}))
752@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
753                    reason='docutils-0.13 or above is required')
754@pytest.mark.sphinx(
755    'html', testroot='numfig',
756    srcdir='test_numfig_without_numbered_toctree',
757    confoverrides={'numfig': True})
758def test_numfig_without_numbered_toctree(app, cached_etree_parse, fname, expect):
759    # remove :numbered: option
760    index = (app.srcdir / 'index.rst').read_text()
761    index = re.sub(':numbered:.*', '', index)
762    (app.srcdir / 'index.rst').write_text(index)
763
764    if not app.outdir.listdir():
765        app.build()
766    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
767
768
769@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True})
770@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
771def test_numfig_with_numbered_toctree_warn(app, warning):
772    app.build()
773    warnings = warning.getvalue()
774    assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
775    assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
776    assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
777    assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
778
779
780@pytest.mark.parametrize("fname,expect", flat_dict({
781    'index.html': [
782        (".//div[@class='figure align-default']/p[@class='caption']/"
783         "span[@class='caption-number']", '^Fig. 1 $', True),
784        (".//div[@class='figure align-default']/p[@class='caption']/"
785         "span[@class='caption-number']", '^Fig. 2 $', True),
786        (".//table/caption/span[@class='caption-number']",
787         '^Table 1 $', True),
788        (".//table/caption/span[@class='caption-number']",
789         '^Table 2 $', True),
790        (".//div[@class='code-block-caption']/"
791         "span[@class='caption-number']", '^Listing 1 $', True),
792        (".//div[@class='code-block-caption']/"
793         "span[@class='caption-number']", '^Listing 2 $', True),
794        (".//li/p/a/span", '^Fig. 1$', True),
795        (".//li/p/a/span", '^Figure2.2$', True),
796        (".//li/p/a/span", '^Table 1$', True),
797        (".//li/p/a/span", '^Table:2.2$', True),
798        (".//li/p/a/span", '^Listing 1$', True),
799        (".//li/p/a/span", '^Code-2.2$', True),
800        (".//li/p/a/span", '^Section.1$', True),
801        (".//li/p/a/span", '^Section.2.1$', True),
802        (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
803        (".//li/p/a/span", '^Sect.1 Foo$', True),
804    ],
805    'foo.html': [
806        (".//div[@class='figure align-default']/p[@class='caption']/"
807         "span[@class='caption-number']", '^Fig. 1.1 $', True),
808        (".//div[@class='figure align-default']/p[@class='caption']/"
809         "span[@class='caption-number']", '^Fig. 1.2 $', True),
810        (".//div[@class='figure align-default']/p[@class='caption']/"
811         "span[@class='caption-number']", '^Fig. 1.3 $', True),
812        (".//div[@class='figure align-default']/p[@class='caption']/"
813         "span[@class='caption-number']", '^Fig. 1.4 $', True),
814        (".//table/caption/span[@class='caption-number']",
815         '^Table 1.1 $', True),
816        (".//table/caption/span[@class='caption-number']",
817         '^Table 1.2 $', True),
818        (".//table/caption/span[@class='caption-number']",
819         '^Table 1.3 $', True),
820        (".//table/caption/span[@class='caption-number']",
821         '^Table 1.4 $', True),
822        (".//div[@class='code-block-caption']/"
823         "span[@class='caption-number']", '^Listing 1.1 $', True),
824        (".//div[@class='code-block-caption']/"
825         "span[@class='caption-number']", '^Listing 1.2 $', True),
826        (".//div[@class='code-block-caption']/"
827         "span[@class='caption-number']", '^Listing 1.3 $', True),
828        (".//div[@class='code-block-caption']/"
829         "span[@class='caption-number']", '^Listing 1.4 $', True),
830    ],
831    'bar.html': [
832        (".//div[@class='figure align-default']/p[@class='caption']/"
833         "span[@class='caption-number']", '^Fig. 2.1 $', True),
834        (".//div[@class='figure align-default']/p[@class='caption']/"
835         "span[@class='caption-number']", '^Fig. 2.3 $', True),
836        (".//div[@class='figure align-default']/p[@class='caption']/"
837         "span[@class='caption-number']", '^Fig. 2.4 $', True),
838        (".//table/caption/span[@class='caption-number']",
839         '^Table 2.1 $', True),
840        (".//table/caption/span[@class='caption-number']",
841         '^Table 2.3 $', True),
842        (".//table/caption/span[@class='caption-number']",
843         '^Table 2.4 $', True),
844        (".//div[@class='code-block-caption']/"
845         "span[@class='caption-number']", '^Listing 2.1 $', True),
846        (".//div[@class='code-block-caption']/"
847         "span[@class='caption-number']", '^Listing 2.3 $', True),
848        (".//div[@class='code-block-caption']/"
849         "span[@class='caption-number']", '^Listing 2.4 $', True),
850    ],
851    'baz.html': [
852        (".//div[@class='figure align-default']/p[@class='caption']/"
853         "span[@class='caption-number']", '^Fig. 2.2 $', True),
854        (".//table/caption/span[@class='caption-number']",
855         '^Table 2.2 $', True),
856        (".//div[@class='code-block-caption']/"
857         "span[@class='caption-number']", '^Listing 2.2 $', True),
858    ],
859}))
860@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
861                    reason='docutils-0.13 or above is required')
862@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True})
863@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
864def test_numfig_with_numbered_toctree(app, cached_etree_parse, fname, expect):
865    app.build()
866    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
867
868
869@pytest.mark.sphinx('html', testroot='numfig', confoverrides={
870    'numfig': True,
871    'numfig_format': {'figure': 'Figure:%s',
872                      'table': 'Tab_%s',
873                      'code-block': 'Code-%s',
874                      'section': 'SECTION-%s'}})
875@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn')
876def test_numfig_with_prefix_warn(app, warning):
877    app.build()
878    warnings = warning.getvalue()
879    assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
880    assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
881    assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
882    assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
883
884
885@pytest.mark.parametrize("fname,expect", flat_dict({
886    'index.html': [
887        (".//div[@class='figure align-default']/p[@class='caption']/"
888         "span[@class='caption-number']", '^Figure:1 $', True),
889        (".//div[@class='figure align-default']/p[@class='caption']/"
890         "span[@class='caption-number']", '^Figure:2 $', True),
891        (".//table/caption/span[@class='caption-number']",
892         '^Tab_1 $', True),
893        (".//table/caption/span[@class='caption-number']",
894         '^Tab_2 $', True),
895        (".//div[@class='code-block-caption']/"
896         "span[@class='caption-number']", '^Code-1 $', True),
897        (".//div[@class='code-block-caption']/"
898         "span[@class='caption-number']", '^Code-2 $', True),
899        (".//li/p/a/span", '^Figure:1$', True),
900        (".//li/p/a/span", '^Figure2.2$', True),
901        (".//li/p/a/span", '^Tab_1$', True),
902        (".//li/p/a/span", '^Table:2.2$', True),
903        (".//li/p/a/span", '^Code-1$', True),
904        (".//li/p/a/span", '^Code-2.2$', True),
905        (".//li/p/a/span", '^SECTION-1$', True),
906        (".//li/p/a/span", '^SECTION-2.1$', True),
907        (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
908        (".//li/p/a/span", '^Sect.1 Foo$', True),
909    ],
910    'foo.html': [
911        (".//div[@class='figure align-default']/p[@class='caption']/"
912         "span[@class='caption-number']", '^Figure:1.1 $', True),
913        (".//div[@class='figure align-default']/p[@class='caption']/"
914         "span[@class='caption-number']", '^Figure:1.2 $', True),
915        (".//div[@class='figure align-default']/p[@class='caption']/"
916         "span[@class='caption-number']", '^Figure:1.3 $', True),
917        (".//div[@class='figure align-default']/p[@class='caption']/"
918         "span[@class='caption-number']", '^Figure:1.4 $', True),
919        (".//table/caption/span[@class='caption-number']",
920         '^Tab_1.1 $', True),
921        (".//table/caption/span[@class='caption-number']",
922         '^Tab_1.2 $', True),
923        (".//table/caption/span[@class='caption-number']",
924         '^Tab_1.3 $', True),
925        (".//table/caption/span[@class='caption-number']",
926         '^Tab_1.4 $', True),
927        (".//div[@class='code-block-caption']/"
928         "span[@class='caption-number']", '^Code-1.1 $', True),
929        (".//div[@class='code-block-caption']/"
930         "span[@class='caption-number']", '^Code-1.2 $', True),
931        (".//div[@class='code-block-caption']/"
932         "span[@class='caption-number']", '^Code-1.3 $', True),
933        (".//div[@class='code-block-caption']/"
934         "span[@class='caption-number']", '^Code-1.4 $', True),
935    ],
936    'bar.html': [
937        (".//div[@class='figure align-default']/p[@class='caption']/"
938         "span[@class='caption-number']", '^Figure:2.1 $', True),
939        (".//div[@class='figure align-default']/p[@class='caption']/"
940         "span[@class='caption-number']", '^Figure:2.3 $', True),
941        (".//div[@class='figure align-default']/p[@class='caption']/"
942         "span[@class='caption-number']", '^Figure:2.4 $', True),
943        (".//table/caption/span[@class='caption-number']",
944         '^Tab_2.1 $', True),
945        (".//table/caption/span[@class='caption-number']",
946         '^Tab_2.3 $', True),
947        (".//table/caption/span[@class='caption-number']",
948         '^Tab_2.4 $', True),
949        (".//div[@class='code-block-caption']/"
950         "span[@class='caption-number']", '^Code-2.1 $', True),
951        (".//div[@class='code-block-caption']/"
952         "span[@class='caption-number']", '^Code-2.3 $', True),
953        (".//div[@class='code-block-caption']/"
954         "span[@class='caption-number']", '^Code-2.4 $', True),
955    ],
956    'baz.html': [
957        (".//div[@class='figure align-default']/p[@class='caption']/"
958         "span[@class='caption-number']", '^Figure:2.2 $', True),
959        (".//table/caption/span[@class='caption-number']",
960         '^Tab_2.2 $', True),
961        (".//div[@class='code-block-caption']/"
962         "span[@class='caption-number']", '^Code-2.2 $', True),
963    ],
964}))
965@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
966                    reason='docutils-0.13 or above is required')
967@pytest.mark.sphinx('html', testroot='numfig',
968                    confoverrides={'numfig': True,
969                                   'numfig_format': {'figure': 'Figure:%s',
970                                                     'table': 'Tab_%s',
971                                                     'code-block': 'Code-%s',
972                                                     'section': 'SECTION-%s'}})
973@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn')
974def test_numfig_with_prefix(app, cached_etree_parse, fname, expect):
975    app.build()
976    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
977
978
979@pytest.mark.sphinx('html', testroot='numfig',
980                    confoverrides={'numfig': True, 'numfig_secnum_depth': 2})
981@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2')
982def test_numfig_with_secnum_depth_warn(app, warning):
983    app.build()
984    warnings = warning.getvalue()
985    assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
986    assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
987    assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
988    assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings
989
990
991@pytest.mark.parametrize("fname,expect", flat_dict({
992    'index.html': [
993        (".//div[@class='figure align-default']/p[@class='caption']/"
994         "span[@class='caption-number']", '^Fig. 1 $', True),
995        (".//div[@class='figure align-default']/p[@class='caption']/"
996         "span[@class='caption-number']", '^Fig. 2 $', True),
997        (".//table/caption/span[@class='caption-number']",
998         '^Table 1 $', True),
999        (".//table/caption/span[@class='caption-number']",
1000         '^Table 2 $', True),
1001        (".//div[@class='code-block-caption']/"
1002         "span[@class='caption-number']", '^Listing 1 $', True),
1003        (".//div[@class='code-block-caption']/"
1004         "span[@class='caption-number']", '^Listing 2 $', True),
1005        (".//li/p/a/span", '^Fig. 1$', True),
1006        (".//li/p/a/span", '^Figure2.1.2$', True),
1007        (".//li/p/a/span", '^Table 1$', True),
1008        (".//li/p/a/span", '^Table:2.1.2$', True),
1009        (".//li/p/a/span", '^Listing 1$', True),
1010        (".//li/p/a/span", '^Code-2.1.2$', True),
1011        (".//li/p/a/span", '^Section.1$', True),
1012        (".//li/p/a/span", '^Section.2.1$', True),
1013        (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
1014        (".//li/p/a/span", '^Sect.1 Foo$', True),
1015    ],
1016    'foo.html': [
1017        (".//div[@class='figure align-default']/p[@class='caption']/"
1018         "span[@class='caption-number']", '^Fig. 1.1 $', True),
1019        (".//div[@class='figure align-default']/p[@class='caption']/"
1020         "span[@class='caption-number']", '^Fig. 1.1.1 $', True),
1021        (".//div[@class='figure align-default']/p[@class='caption']/"
1022         "span[@class='caption-number']", '^Fig. 1.1.2 $', True),
1023        (".//div[@class='figure align-default']/p[@class='caption']/"
1024         "span[@class='caption-number']", '^Fig. 1.2.1 $', True),
1025        (".//table/caption/span[@class='caption-number']",
1026         '^Table 1.1 $', True),
1027        (".//table/caption/span[@class='caption-number']",
1028         '^Table 1.1.1 $', True),
1029        (".//table/caption/span[@class='caption-number']",
1030         '^Table 1.1.2 $', True),
1031        (".//table/caption/span[@class='caption-number']",
1032         '^Table 1.2.1 $', True),
1033        (".//div[@class='code-block-caption']/"
1034         "span[@class='caption-number']", '^Listing 1.1 $', True),
1035        (".//div[@class='code-block-caption']/"
1036         "span[@class='caption-number']", '^Listing 1.1.1 $', True),
1037        (".//div[@class='code-block-caption']/"
1038         "span[@class='caption-number']", '^Listing 1.1.2 $', True),
1039        (".//div[@class='code-block-caption']/"
1040         "span[@class='caption-number']", '^Listing 1.2.1 $', True),
1041    ],
1042    'bar.html': [
1043        (".//div[@class='figure align-default']/p[@class='caption']/"
1044         "span[@class='caption-number']", '^Fig. 2.1.1 $', True),
1045        (".//div[@class='figure align-default']/p[@class='caption']/"
1046         "span[@class='caption-number']", '^Fig. 2.1.3 $', True),
1047        (".//div[@class='figure align-default']/p[@class='caption']/"
1048         "span[@class='caption-number']", '^Fig. 2.2.1 $', True),
1049        (".//table/caption/span[@class='caption-number']",
1050         '^Table 2.1.1 $', True),
1051        (".//table/caption/span[@class='caption-number']",
1052         '^Table 2.1.3 $', True),
1053        (".//table/caption/span[@class='caption-number']",
1054         '^Table 2.2.1 $', True),
1055        (".//div[@class='code-block-caption']/"
1056         "span[@class='caption-number']", '^Listing 2.1.1 $', True),
1057        (".//div[@class='code-block-caption']/"
1058         "span[@class='caption-number']", '^Listing 2.1.3 $', True),
1059        (".//div[@class='code-block-caption']/"
1060         "span[@class='caption-number']", '^Listing 2.2.1 $', True),
1061    ],
1062    'baz.html': [
1063        (".//div[@class='figure align-default']/p[@class='caption']/"
1064         "span[@class='caption-number']", '^Fig. 2.1.2 $', True),
1065        (".//table/caption/span[@class='caption-number']",
1066         '^Table 2.1.2 $', True),
1067        (".//div[@class='code-block-caption']/"
1068         "span[@class='caption-number']", '^Listing 2.1.2 $', True),
1069    ],
1070}))
1071@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
1072                    reason='docutils-0.13 or above is required')
1073@pytest.mark.sphinx('html', testroot='numfig',
1074                    confoverrides={'numfig': True,
1075                                   'numfig_secnum_depth': 2})
1076@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2')
1077def test_numfig_with_secnum_depth(app, cached_etree_parse, fname, expect):
1078    app.build()
1079    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
1080
1081
1082@pytest.mark.parametrize("fname,expect", flat_dict({
1083    'index.html': [
1084        (".//div[@class='figure align-default']/p[@class='caption']/"
1085         "span[@class='caption-number']", '^Fig. 1 $', True),
1086        (".//div[@class='figure align-default']/p[@class='caption']/"
1087         "span[@class='caption-number']", '^Fig. 2 $', True),
1088        (".//table/caption/span[@class='caption-number']",
1089         '^Table 1 $', True),
1090        (".//table/caption/span[@class='caption-number']",
1091         '^Table 2 $', True),
1092        (".//div[@class='code-block-caption']/"
1093         "span[@class='caption-number']", '^Listing 1 $', True),
1094        (".//div[@class='code-block-caption']/"
1095         "span[@class='caption-number']", '^Listing 2 $', True),
1096        (".//li/p/a/span", '^Fig. 1$', True),
1097        (".//li/p/a/span", '^Figure2.2$', True),
1098        (".//li/p/a/span", '^Table 1$', True),
1099        (".//li/p/a/span", '^Table:2.2$', True),
1100        (".//li/p/a/span", '^Listing 1$', True),
1101        (".//li/p/a/span", '^Code-2.2$', True),
1102        (".//li/p/a/span", '^Section.1$', True),
1103        (".//li/p/a/span", '^Section.2.1$', True),
1104        (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
1105        (".//li/p/a/span", '^Sect.1 Foo$', True),
1106        (".//div[@class='figure align-default']/p[@class='caption']/"
1107         "span[@class='caption-number']", '^Fig. 1.1 $', True),
1108        (".//div[@class='figure align-default']/p[@class='caption']/"
1109         "span[@class='caption-number']", '^Fig. 1.2 $', True),
1110        (".//div[@class='figure align-default']/p[@class='caption']/"
1111         "span[@class='caption-number']", '^Fig. 1.3 $', True),
1112        (".//div[@class='figure align-default']/p[@class='caption']/"
1113         "span[@class='caption-number']", '^Fig. 1.4 $', True),
1114        (".//table/caption/span[@class='caption-number']",
1115         '^Table 1.1 $', True),
1116        (".//table/caption/span[@class='caption-number']",
1117         '^Table 1.2 $', True),
1118        (".//table/caption/span[@class='caption-number']",
1119         '^Table 1.3 $', True),
1120        (".//table/caption/span[@class='caption-number']",
1121         '^Table 1.4 $', True),
1122        (".//div[@class='code-block-caption']/"
1123         "span[@class='caption-number']", '^Listing 1.1 $', True),
1124        (".//div[@class='code-block-caption']/"
1125         "span[@class='caption-number']", '^Listing 1.2 $', True),
1126        (".//div[@class='code-block-caption']/"
1127         "span[@class='caption-number']", '^Listing 1.3 $', True),
1128        (".//div[@class='code-block-caption']/"
1129         "span[@class='caption-number']", '^Listing 1.4 $', True),
1130        (".//div[@class='figure align-default']/p[@class='caption']/"
1131         "span[@class='caption-number']", '^Fig. 2.1 $', True),
1132        (".//div[@class='figure align-default']/p[@class='caption']/"
1133         "span[@class='caption-number']", '^Fig. 2.3 $', True),
1134        (".//div[@class='figure align-default']/p[@class='caption']/"
1135         "span[@class='caption-number']", '^Fig. 2.4 $', True),
1136        (".//table/caption/span[@class='caption-number']",
1137         '^Table 2.1 $', True),
1138        (".//table/caption/span[@class='caption-number']",
1139         '^Table 2.3 $', True),
1140        (".//table/caption/span[@class='caption-number']",
1141         '^Table 2.4 $', True),
1142        (".//div[@class='code-block-caption']/"
1143         "span[@class='caption-number']", '^Listing 2.1 $', True),
1144        (".//div[@class='code-block-caption']/"
1145         "span[@class='caption-number']", '^Listing 2.3 $', True),
1146        (".//div[@class='code-block-caption']/"
1147         "span[@class='caption-number']", '^Listing 2.4 $', True),
1148        (".//div[@class='figure align-default']/p[@class='caption']/"
1149         "span[@class='caption-number']", '^Fig. 2.2 $', True),
1150        (".//table/caption/span[@class='caption-number']",
1151         '^Table 2.2 $', True),
1152        (".//div[@class='code-block-caption']/"
1153         "span[@class='caption-number']", '^Listing 2.2 $', True),
1154    ],
1155}))
1156@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
1157                    reason='docutils-0.13 or above is required')
1158@pytest.mark.sphinx('singlehtml', testroot='numfig', confoverrides={'numfig': True})
1159@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
1160def test_numfig_with_singlehtml(app, cached_etree_parse, fname, expect):
1161    app.build()
1162    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
1163
1164
1165@pytest.mark.parametrize("fname,expect", flat_dict({
1166    'index.html': [
1167        (".//div[@class='figure align-default']/p[@class='caption']"
1168         "/span[@class='caption-number']", "Fig. 1", True),
1169        (".//div[@class='figure align-default']/p[@class='caption']"
1170         "/span[@class='caption-number']", "Fig. 2", True),
1171        (".//div[@class='figure align-default']/p[@class='caption']"
1172         "/span[@class='caption-number']", "Fig. 3", True),
1173        (".//div//span[@class='caption-number']", "No.1 ", True),
1174        (".//div//span[@class='caption-number']", "No.2 ", True),
1175        (".//li/p/a/span", 'Fig. 1', True),
1176        (".//li/p/a/span", 'Fig. 2', True),
1177        (".//li/p/a/span", 'Fig. 3', True),
1178        (".//li/p/a/span", 'No.1', True),
1179        (".//li/p/a/span", 'No.2', True),
1180    ],
1181}))
1182@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
1183                    reason='docutils-0.13 or above is required')
1184@pytest.mark.sphinx('html', testroot='add_enumerable_node',
1185                    srcdir='test_enumerable_node')
1186def test_enumerable_node(app, cached_etree_parse, fname, expect):
1187    app.build()
1188    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
1189
1190
1191@pytest.mark.sphinx('html', testroot='html_assets')
1192def test_html_assets(app):
1193    app.builder.build_all()
1194
1195    # exclude_path and its family
1196    assert not (app.outdir / 'static' / 'index.html').exists()
1197    assert not (app.outdir / 'extra' / 'index.html').exists()
1198
1199    # html_static_path
1200    assert not (app.outdir / '_static' / '.htaccess').exists()
1201    assert not (app.outdir / '_static' / '.htpasswd').exists()
1202    assert (app.outdir / '_static' / 'API.html').exists()
1203    assert (app.outdir / '_static' / 'API.html').read_text() == 'Sphinx-1.4.4'
1204    assert (app.outdir / '_static' / 'css' / 'style.css').exists()
1205    assert (app.outdir / '_static' / 'js' / 'custom.js').exists()
1206    assert (app.outdir / '_static' / 'rimg.png').exists()
1207    assert not (app.outdir / '_static' / '_build' / 'index.html').exists()
1208    assert (app.outdir / '_static' / 'background.png').exists()
1209    assert not (app.outdir / '_static' / 'subdir' / '.htaccess').exists()
1210    assert not (app.outdir / '_static' / 'subdir' / '.htpasswd').exists()
1211
1212    # html_extra_path
1213    assert (app.outdir / '.htaccess').exists()
1214    assert not (app.outdir / '.htpasswd').exists()
1215    assert (app.outdir / 'API.html_t').exists()
1216    assert (app.outdir / 'css/style.css').exists()
1217    assert (app.outdir / 'rimg.png').exists()
1218    assert not (app.outdir / '_build' / 'index.html').exists()
1219    assert (app.outdir / 'background.png').exists()
1220    assert (app.outdir / 'subdir' / '.htaccess').exists()
1221    assert not (app.outdir / 'subdir' / '.htpasswd').exists()
1222
1223    # html_css_files
1224    content = (app.outdir / 'index.html').read_text()
1225    assert '<link rel="stylesheet" type="text/css" href="_static/css/style.css" />' in content
1226    assert ('<link media="print" rel="stylesheet" title="title" type="text/css" '
1227            'href="https://example.com/custom.css" />' in content)
1228
1229    # html_js_files
1230    assert '<script src="_static/js/custom.js"></script>' in content
1231    assert ('<script async="async" src="https://example.com/script.js">'
1232            '</script>' in content)
1233
1234
1235@pytest.mark.sphinx('html', testroot='html_assets')
1236def test_assets_order(app):
1237    app.add_css_file('normal.css')
1238    app.add_css_file('early.css', priority=100)
1239    app.add_css_file('late.css', priority=750)
1240    app.add_css_file('lazy.css', priority=900)
1241    app.add_js_file('normal.js')
1242    app.add_js_file('early.js', priority=100)
1243    app.add_js_file('late.js', priority=750)
1244    app.add_js_file('lazy.js', priority=900)
1245
1246    app.builder.build_all()
1247    content = (app.outdir / 'index.html').read_text()
1248
1249    # css_files
1250    expected = ['_static/pygments.css', '_static/alabaster.css', '_static/early.css',
1251                'https://example.com/custom.css', '_static/normal.css', '_static/late.css',
1252                '_static/css/style.css', '_static/lazy.css']
1253    pattern = '.*'.join('href="%s"' % f for f in expected)
1254    assert re.search(pattern, content, re.S)
1255
1256    # js_files
1257    expected = ['_static/early.js', '_static/jquery.js', '_static/underscore.js',
1258                '_static/doctools.js', 'https://example.com/script.js', '_static/normal.js',
1259                '_static/late.js', '_static/js/custom.js', '_static/lazy.js']
1260    pattern = '.*'.join('src="%s"' % f for f in expected)
1261    assert re.search(pattern, content, re.S)
1262
1263
1264@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_copy_source': False})
1265def test_html_copy_source(app):
1266    app.builder.build_all()
1267    assert not (app.outdir / '_sources' / 'index.rst.txt').exists()
1268
1269
1270@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.txt'})
1271def test_html_sourcelink_suffix(app):
1272    app.builder.build_all()
1273    assert (app.outdir / '_sources' / 'index.rst.txt').exists()
1274
1275
1276@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.rst'})
1277def test_html_sourcelink_suffix_same(app):
1278    app.builder.build_all()
1279    assert (app.outdir / '_sources' / 'index.rst').exists()
1280
1281
1282@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': ''})
1283def test_html_sourcelink_suffix_empty(app):
1284    app.builder.build_all()
1285    assert (app.outdir / '_sources' / 'index.rst').exists()
1286
1287
1288@pytest.mark.sphinx('html', testroot='html_entity')
1289def test_html_entity(app):
1290    app.builder.build_all()
1291    valid_entities = {'amp', 'lt', 'gt', 'quot', 'apos'}
1292    content = (app.outdir / 'index.html').read_text()
1293    for entity in re.findall(r'&([a-z]+);', content, re.M):
1294        assert entity not in valid_entities
1295
1296
1297@pytest.mark.sphinx('html', testroot='basic')
1298@pytest.mark.xfail(os.name != 'posix', reason="Not working on windows")
1299def test_html_inventory(app):
1300    app.builder.build_all()
1301    with open(app.outdir / 'objects.inv', 'rb') as f:
1302        invdata = InventoryFile.load(f, 'https://www.google.com', os.path.join)
1303    assert set(invdata.keys()) == {'std:label', 'std:doc'}
1304    assert set(invdata['std:label'].keys()) == {'modindex',
1305                                                'py-modindex',
1306                                                'genindex',
1307                                                'search'}
1308    assert invdata['std:label']['modindex'] == ('Python',
1309                                                '',
1310                                                'https://www.google.com/py-modindex.html',
1311                                                'Module Index')
1312    assert invdata['std:label']['py-modindex'] == ('Python',
1313                                                   '',
1314                                                   'https://www.google.com/py-modindex.html',
1315                                                   'Python Module Index')
1316    assert invdata['std:label']['genindex'] == ('Python',
1317                                                '',
1318                                                'https://www.google.com/genindex.html',
1319                                                'Index')
1320    assert invdata['std:label']['search'] == ('Python',
1321                                              '',
1322                                              'https://www.google.com/search.html',
1323                                              'Search Page')
1324    assert set(invdata['std:doc'].keys()) == {'index'}
1325    assert invdata['std:doc']['index'] == ('Python',
1326                                           '',
1327                                           'https://www.google.com/index.html',
1328                                           'The basic Sphinx documentation for testing')
1329
1330
1331@pytest.mark.sphinx('html', testroot='images', confoverrides={'html_sourcelink_suffix': ''})
1332def test_html_anchor_for_figure(app):
1333    app.builder.build_all()
1334    content = (app.outdir / 'index.html').read_text()
1335    assert ('<p class="caption"><span class="caption-text">The caption of pic</span>'
1336            '<a class="headerlink" href="#id1" title="Permalink to this image">¶</a></p>'
1337            in content)
1338
1339
1340@pytest.mark.sphinx('html', testroot='directives-raw')
1341def test_html_raw_directive(app, status, warning):
1342    app.builder.build_all()
1343    result = (app.outdir / 'index.html').read_text()
1344
1345    # standard case
1346    assert 'standalone raw directive (HTML)' in result
1347    assert 'standalone raw directive (LaTeX)' not in result
1348
1349    # with substitution
1350    assert '<p>HTML: abc def ghi</p>' in result
1351    assert '<p>LaTeX: abc  ghi</p>' in result
1352
1353
1354@pytest.mark.parametrize("fname,expect", flat_dict({
1355    'index.html': [
1356        (".//link[@href='_static/persistent.css']"
1357         "[@rel='stylesheet']", '', True),
1358        (".//link[@href='_static/default.css']"
1359         "[@rel='stylesheet']"
1360         "[@title='Default']", '', True),
1361        (".//link[@href='_static/alternate1.css']"
1362         "[@rel='alternate stylesheet']"
1363         "[@title='Alternate']", '', True),
1364        (".//link[@href='_static/alternate2.css']"
1365         "[@rel='alternate stylesheet']", '', True),
1366        (".//link[@href='_static/more_persistent.css']"
1367         "[@rel='stylesheet']", '', True),
1368        (".//link[@href='_static/more_default.css']"
1369         "[@rel='stylesheet']"
1370         "[@title='Default']", '', True),
1371        (".//link[@href='_static/more_alternate1.css']"
1372         "[@rel='alternate stylesheet']"
1373         "[@title='Alternate']", '', True),
1374        (".//link[@href='_static/more_alternate2.css']"
1375         "[@rel='alternate stylesheet']", '', True),
1376    ],
1377}))
1378@pytest.mark.sphinx('html', testroot='stylesheets')
1379def test_alternate_stylesheets(app, cached_etree_parse, fname, expect):
1380    app.build()
1381    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
1382
1383
1384@pytest.mark.sphinx('html', testroot='html_style')
1385def test_html_style(app, status, warning):
1386    app.build()
1387    result = (app.outdir / 'index.html').read_text()
1388    assert '<link rel="stylesheet" href="_static/default.css" type="text/css" />' in result
1389    assert ('<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />'
1390            not in result)
1391
1392
1393@pytest.mark.sphinx('html', testroot='images')
1394def test_html_remote_images(app, status, warning):
1395    app.builder.build_all()
1396
1397    result = (app.outdir / 'index.html').read_text()
1398    assert ('<img alt="https://www.python.org/static/img/python-logo.png" '
1399            'src="https://www.python.org/static/img/python-logo.png" />' in result)
1400    assert not (app.outdir / 'python-logo.png').exists()
1401
1402
1403@pytest.mark.sphinx('html', testroot='basic')
1404def test_html_sidebar(app, status, warning):
1405    ctx = {}
1406
1407    # default for alabaster
1408    app.builder.build_all()
1409    result = (app.outdir / 'index.html').read_text()
1410    assert ('<div class="sphinxsidebar" role="navigation" '
1411            'aria-label="main navigation">' in result)
1412    assert '<h1 class="logo"><a href="#">Python</a></h1>' in result
1413    assert '<h3>Navigation</h3>' in result
1414    assert '<h3>Related Topics</h3>' in result
1415    assert '<h3 id="searchlabel">Quick search</h3>' in result
1416
1417    app.builder.add_sidebars('index', ctx)
1418    assert ctx['sidebars'] == ['about.html', 'navigation.html', 'relations.html',
1419                               'searchbox.html', 'donate.html']
1420
1421    # only relations.html
1422    app.config.html_sidebars = {'**': ['relations.html']}
1423    app.builder.build_all()
1424    result = (app.outdir / 'index.html').read_text()
1425    assert ('<div class="sphinxsidebar" role="navigation" '
1426            'aria-label="main navigation">' in result)
1427    assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result
1428    assert '<h3>Navigation</h3>' not in result
1429    assert '<h3>Related Topics</h3>' in result
1430    assert '<h3 id="searchlabel">Quick search</h3>' not in result
1431
1432    app.builder.add_sidebars('index', ctx)
1433    assert ctx['sidebars'] == ['relations.html']
1434
1435    # no sidebars
1436    app.config.html_sidebars = {'**': []}
1437    app.builder.build_all()
1438    result = (app.outdir / 'index.html').read_text()
1439    assert ('<div class="sphinxsidebar" role="navigation" '
1440            'aria-label="main navigation">' not in result)
1441    assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result
1442    assert '<h3>Navigation</h3>' not in result
1443    assert '<h3>Related Topics</h3>' not in result
1444    assert '<h3 id="searchlabel">Quick search</h3>' not in result
1445
1446    app.builder.add_sidebars('index', ctx)
1447    assert ctx['sidebars'] == []
1448
1449
1450@pytest.mark.parametrize('fname,expect', flat_dict({
1451    'index.html': [(".//em/a[@href='https://example.com/man.1']", "", True),
1452                   (".//em/a[@href='https://example.com/ls.1']", "", True),
1453                   (".//em/a[@href='https://example.com/sphinx.']", "", True)]
1454
1455}))
1456@pytest.mark.sphinx('html', testroot='manpage_url', confoverrides={
1457    'manpages_url': 'https://example.com/{page}.{section}'})
1458@pytest.mark.test_params(shared_result='test_build_html_manpage_url')
1459def test_html_manpage(app, cached_etree_parse, fname, expect):
1460    app.build()
1461    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
1462
1463
1464@pytest.mark.sphinx('html', testroot='toctree-glob',
1465                    confoverrides={'html_baseurl': 'https://example.com/'})
1466def test_html_baseurl(app, status, warning):
1467    app.build()
1468
1469    result = (app.outdir / 'index.html').read_text()
1470    assert '<link rel="canonical" href="https://example.com/index.html" />' in result
1471
1472    result = (app.outdir / 'qux' / 'index.html').read_text()
1473    assert '<link rel="canonical" href="https://example.com/qux/index.html" />' in result
1474
1475
1476@pytest.mark.sphinx('html', testroot='toctree-glob',
1477                    confoverrides={'html_baseurl': 'https://example.com/subdir',
1478                                   'html_file_suffix': '.htm'})
1479def test_html_baseurl_and_html_file_suffix(app, status, warning):
1480    app.build()
1481
1482    result = (app.outdir / 'index.htm').read_text()
1483    assert '<link rel="canonical" href="https://example.com/subdir/index.htm" />' in result
1484
1485    result = (app.outdir / 'qux' / 'index.htm').read_text()
1486    assert '<link rel="canonical" href="https://example.com/subdir/qux/index.htm" />' in result
1487
1488
1489@pytest.mark.sphinx('html', testroot='basic')
1490def test_default_html_math_renderer(app, status, warning):
1491    assert app.builder.math_renderer_name == 'mathjax'
1492
1493
1494@pytest.mark.sphinx('html', testroot='basic',
1495                    confoverrides={'extensions': ['sphinx.ext.mathjax']})
1496def test_html_math_renderer_is_mathjax(app, status, warning):
1497    assert app.builder.math_renderer_name == 'mathjax'
1498
1499
1500@pytest.mark.sphinx('html', testroot='basic',
1501                    confoverrides={'extensions': ['sphinx.ext.imgmath']})
1502def test_html_math_renderer_is_imgmath(app, status, warning):
1503    assert app.builder.math_renderer_name == 'imgmath'
1504
1505
1506@pytest.mark.sphinx('html', testroot='basic',
1507                    confoverrides={'extensions': ['sphinxcontrib.jsmath',
1508                                                  'sphinx.ext.imgmath']})
1509def test_html_math_renderer_is_duplicated(make_app, app_params):
1510    try:
1511        args, kwargs = app_params
1512        make_app(*args, **kwargs)
1513        assert False
1514    except ConfigError as exc:
1515        assert str(exc) == ('Many math_renderers are registered. '
1516                            'But no math_renderer is selected.')
1517
1518
1519@pytest.mark.sphinx('html', testroot='basic',
1520                    confoverrides={'extensions': ['sphinx.ext.imgmath',
1521                                                  'sphinx.ext.mathjax']})
1522def test_html_math_renderer_is_duplicated2(app, status, warning):
1523    # case of both mathjax and another math_renderer is loaded
1524    assert app.builder.math_renderer_name == 'imgmath'  # The another one is chosen
1525
1526
1527@pytest.mark.sphinx('html', testroot='basic',
1528                    confoverrides={'extensions': ['sphinxcontrib.jsmath',
1529                                                  'sphinx.ext.imgmath'],
1530                                   'html_math_renderer': 'imgmath'})
1531def test_html_math_renderer_is_chosen(app, status, warning):
1532    assert app.builder.math_renderer_name == 'imgmath'
1533
1534
1535@pytest.mark.sphinx('html', testroot='basic',
1536                    confoverrides={'extensions': ['sphinxcontrib.jsmath',
1537                                                  'sphinx.ext.mathjax'],
1538                                   'html_math_renderer': 'imgmath'})
1539def test_html_math_renderer_is_mismatched(make_app, app_params):
1540    try:
1541        args, kwargs = app_params
1542        make_app(*args, **kwargs)
1543        assert False
1544    except ConfigError as exc:
1545        assert str(exc) == "Unknown math_renderer 'imgmath' is given."
1546
1547
1548@pytest.mark.sphinx('html', testroot='basic')
1549def test_html_pygments_style_default(app):
1550    style = app.builder.highlighter.formatter_args.get('style')
1551    assert style.__name__ == 'Alabaster'
1552
1553
1554@pytest.mark.sphinx('html', testroot='basic',
1555                    confoverrides={'pygments_style': 'sphinx'})
1556def test_html_pygments_style_manually(app):
1557    style = app.builder.highlighter.formatter_args.get('style')
1558    assert style.__name__ == 'SphinxStyle'
1559
1560
1561@pytest.mark.sphinx('html', testroot='basic',
1562                    confoverrides={'html_theme': 'classic'})
1563def test_html_pygments_for_classic_theme(app):
1564    style = app.builder.highlighter.formatter_args.get('style')
1565    assert style.__name__ == 'SphinxStyle'
1566
1567
1568@pytest.mark.sphinx('html', testroot='basic')
1569def test_html_dark_pygments_style_default(app):
1570    assert app.builder.dark_highlighter is None
1571
1572
1573@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_extra_path')
1574def test_validate_html_extra_path(app):
1575    (app.confdir / '_static').makedirs()
1576    app.config.html_extra_path = [
1577        '/path/to/not_found',       # not found
1578        '_static',
1579        app.outdir,                 # outdir
1580        app.outdir / '_static',     # inside outdir
1581    ]
1582    validate_html_extra_path(app, app.config)
1583    assert app.config.html_extra_path == ['_static']
1584
1585
1586@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_static_path')
1587def test_validate_html_static_path(app):
1588    (app.confdir / '_static').makedirs()
1589    app.config.html_static_path = [
1590        '/path/to/not_found',       # not found
1591        '_static',
1592        app.outdir,                 # outdir
1593        app.outdir / '_static',     # inside outdir
1594    ]
1595    validate_html_static_path(app, app.config)
1596    assert app.config.html_static_path == ['_static']
1597
1598
1599@pytest.mark.sphinx(testroot='html_scaled_image_link')
1600def test_html_scaled_image_link(app):
1601    app.build()
1602    context = (app.outdir / 'index.html').read_text()
1603
1604    # no scaled parameters
1605    assert re.search('\n<img alt="_images/img.png" src="_images/img.png" />', context)
1606
1607    # scaled_image_link
1608    assert re.search('\n<a class="reference internal image-reference" href="_images/img.png">'
1609                     '<img alt="_images/img.png" src="_images/img.png" style="[^"]+" /></a>',
1610                     context)
1611
1612    # no-scaled-link class disables the feature
1613    assert re.search('\n<img alt="_images/img.png" class="no-scaled-link"'
1614                     ' src="_images/img.png" style="[^"]+" />',
1615                     context)
1616
1617
1618@pytest.mark.sphinx('html', testroot='reST-code-block',
1619                    confoverrides={'html_codeblock_linenos_style': 'table'})
1620def test_html_codeblock_linenos_style_table(app):
1621    app.build()
1622    content = (app.outdir / 'index.html').read_text()
1623
1624    pygments_version = tuple(LooseVersion(pygments.__version__).version)
1625    if pygments_version >= (2, 8):
1626        assert ('<div class="linenodiv"><pre><span class="normal">1</span>\n'
1627                '<span class="normal">2</span>\n'
1628                '<span class="normal">3</span>\n'
1629                '<span class="normal">4</span></pre></div>') in content
1630    else:
1631        assert '<div class="linenodiv"><pre>1\n2\n3\n4</pre></div>' in content
1632
1633
1634@pytest.mark.sphinx('html', testroot='reST-code-block',
1635                    confoverrides={'html_codeblock_linenos_style': 'inline'})
1636def test_html_codeblock_linenos_style_inline(app):
1637    app.build()
1638    content = (app.outdir / 'index.html').read_text()
1639
1640    pygments_version = tuple(LooseVersion(pygments.__version__).version)
1641    if pygments_version > (2, 7):
1642        assert '<span class="linenos">1</span>' in content
1643    else:
1644        assert '<span class="lineno">1 </span>' in content
1645
1646
1647@pytest.mark.sphinx('html', testroot='highlight_options')
1648def test_highlight_options(app):
1649    subject = app.builder.highlighter
1650    with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight:
1651        app.build()
1652
1653        call_args = highlight.call_args_list
1654        assert len(call_args) == 3
1655        assert call_args[0] == call(ANY, 'default', force=False, linenos=False,
1656                                    location=ANY, opts={'default_option': True})
1657        assert call_args[1] == call(ANY, 'python', force=False, linenos=False,
1658                                    location=ANY, opts={'python_option': True})
1659        assert call_args[2] == call(ANY, 'java', force=False, linenos=False,
1660                                    location=ANY, opts={})
1661
1662
1663@pytest.mark.sphinx('html', testroot='highlight_options',
1664                    confoverrides={'highlight_options': {'default_option': True}})
1665def test_highlight_options_old(app):
1666    subject = app.builder.highlighter
1667    with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight:
1668        app.build()
1669
1670        call_args = highlight.call_args_list
1671        assert len(call_args) == 3
1672        assert call_args[0] == call(ANY, 'default', force=False, linenos=False,
1673                                    location=ANY, opts={'default_option': True})
1674        assert call_args[1] == call(ANY, 'python', force=False, linenos=False,
1675                                    location=ANY, opts={})
1676        assert call_args[2] == call(ANY, 'java', force=False, linenos=False,
1677                                    location=ANY, opts={})
1678
1679
1680@pytest.mark.sphinx('html', testroot='basic',
1681                    confoverrides={'html_permalinks': False})
1682def test_html_permalink_disable(app):
1683    app.build()
1684    content = (app.outdir / 'index.html').read_text()
1685
1686    assert '<h1>The basic Sphinx documentation for testing</h1>' in content
1687
1688
1689@pytest.mark.sphinx('html', testroot='basic',
1690                    confoverrides={'html_permalinks_icon': '<span>[PERMALINK]</span>'})
1691def test_html_permalink_icon(app):
1692    app.build()
1693    content = (app.outdir / 'index.html').read_text()
1694
1695    assert ('<h1>The basic Sphinx documentation for testing<a class="headerlink" '
1696            'href="#the-basic-sphinx-documentation-for-testing" '
1697            'title="Permalink to this headline"><span>[PERMALINK]</span></a></h1>' in content)
1698