diff --git a/pyproject.toml b/pyproject.toml
index e3418ab98a2..15b3fe8a20a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -124,6 +124,7 @@ test = [
"defusedxml>=0.7.1", # for secure XML/HTML parsing
"setuptools>=70.0", # for Cython compilation
"typing_extensions>=4.9", # for typing_extensions.Unpack
+ "beautifulsoup4>=4.12", # for HTML parsing in tests
]
translations = [
"babel>=2.13",
diff --git a/tests/test_builders/test_build_changes.py b/tests/test_builders/test_build_changes.py
index f0328e36e77..c29cde19a1b 100644
--- a/tests/test_builders/test_build_changes.py
+++ b/tests/test_builders/test_build_changes.py
@@ -2,35 +2,82 @@
from __future__ import annotations
+import re
from typing import TYPE_CHECKING
import pytest
+from bs4 import BeautifulSoup
if TYPE_CHECKING:
+ from typing import Any
+
from sphinx.testing.util import SphinxTestApp
@pytest.mark.sphinx('changes', testroot='changes')
def test_build(app: SphinxTestApp) -> None:
+ """Test the 'changes' builder and resolve TODO for better HTML checking."""
app.build()
- # TODO: Use better checking of html content
htmltext = (app.outdir / 'changes.html').read_text(encoding='utf8')
- assert 'Added in version 0.6: Some funny stuff.' in htmltext
- assert 'Changed in version 0.6: Even more funny stuff.' in htmltext
- assert 'Deprecated since version 0.6: Boring stuff.' in htmltext
+ soup = BeautifulSoup(htmltext, 'html.parser')
+
+ # Get all
items up front
+ all_items = soup.find_all('li')
+ assert len(all_items) >= 5, 'Did not find all 5 change items'
+
+ def find_change_item(type_text: str, version: str, content: str) -> dict[str, Any]:
+ """Helper to find and validate change items."""
+ found_item = None
+ # Loop through all tags
+ for item in all_items:
+ # Check if the content is in the item's *full text*
+ if re.search(re.escape(content), item.text):
+ found_item = item
+ break # Found it!
+
+ # This is the assertion that was failing
+ assert found_item is not None, (
+ f"Could not find change item containing '{content}'"
+ )
+
+ type_elem = found_item.find('i')
+ assert type_elem is not None, f"Missing type indicator for '{content}'"
+ assert type_text in type_elem.text.lower(), (
+ f"Expected type '{type_text}' for '{content}'"
+ )
+
+ assert f'version {version}' in found_item.text, (
+ f'Version {version} not found in {content!r}'
+ )
+
+ return {'item': found_item, 'type': type_elem}
+
+ # Test simple changes
+ changes = [
+ ('added', '0.6', 'Some funny stuff.'),
+ ('changed', '0.6', 'Even more funny stuff.'),
+ ('deprecated', '0.6', 'Boring stuff.'),
+ ]
+
+ for change_type, version, content in changes:
+ find_change_item(change_type, version, content)
- path_html = (
- 'Path: deprecated: Deprecated since version 0.6:'
- ' So, that was a bad idea it turns out.'
+ # Test Path deprecation (Search by unique text)
+ path_change = find_change_item(
+ 'deprecated',
+ '0.6',
+ 'So, that was a bad idea it turns out.',
)
- assert path_html in htmltext
+ assert path_change['item'].find('b').text == 'Path'
- malloc_html = (
- 'void *Test_Malloc(size_t n): changed: Changed in version 0.6:'
- ' Can now be replaced with a different allocator.'
+ # Test Malloc function change (Search by unique text)
+ malloc_change = find_change_item(
+ 'changed',
+ '0.6',
+ 'Can now be replaced with a different allocator.',
)
- assert malloc_html in htmltext
+ assert malloc_change['item'].find('b').text == 'void *Test_Malloc(size_t n)'
@pytest.mark.sphinx(