Problem
Every assertion in HTMLContentAssertionsMixin (assert_see, assert_dont_see, assert_see_text, assert_dont_see_text, assert_see_in_order) is page-global — it searches the entire decoded response body. There is no way to scope an assertion to a specific section of the HTML.
This breaks down on any response that contains:
- Multiple
<table> elements where you want to assert on rows or content of one specific table.
- Sections where the same value (an email, an ID, a label) legitimately appears in more than one place, so
assert_see / assert_dont_see cannot distinguish them.
- Paginated or truncated tables where the test needs to assert the rendered row count rather than just "this string is somewhere on the page".
- Nested sections where assertions on a child should not match content from a sibling.
The current workaround is raw substring slicing on response.content, which is not fluent, not chainable, not discoverable in IDEs, and reinvented in every test that needs it.
Requirements
A scoped HTML assertion API for pyssertive should:
- Be fluent and chainable.
- Reuse all existing HTML assertions inside the scope — no parallel API.
- Bound the scope by closure, like Laravel's fluent JSON testing. No explicit cleanup, no leaked state.
- Return to the outer page scope after the closure exits, so the outer chain can continue with page-global assertions.
- Support nesting — a section's assertions should themselves be able to open a sub-section.
- Add at least one section-specific assertion that page-global assertions cannot express, such as counting
<tr> rows inside the scoped table.
- Expose the scoped object as a real class with IDE auto-completion and proper typing.
Proposed API
client.get(url).assert_ok().assert_section(
"Section Heading Text",
lambda section: (
section
.assert_see_text("Expected text inside the section")
.assert_row_count(100)
.assert_dont_see_text("Text that must not appear inside the section")
),
).assert_see_text("Text outside the section, on the outer page")
The closure receives a scoped object that exposes the same HTML assertions as the outer client, but bound to the section slice. After the closure returns, assert_section returns the outer fluent client so the chain continues at page scope.
Code draft
class HTMLContentAssertionsMixin:
_response: HttpResponse
def _get_content(self) -> str:
return self._response.content.decode("utf-8", errors="replace")
def assert_see(self, text: str) -> Self:
body = html.unescape(self._get_content())
body = re.sub(r"\s+", " ", body).strip()
assert text in body, f"Expected to see '{text}', got: {body}"
return self
# ... existing assertions refactored to use _get_content() instead of
# decoding self._response.content directly.
def assert_section(
self,
heading: str,
callback: Callable[["HtmlSection"], Any],
*,
end_marker: str = "</table>",
) -> Self:
content = self._get_content()
start = content.find(heading)
assert start != -1, f"Section heading '{heading}' not found in response"
end = content.find(end_marker, start)
assert end != -1, f"End marker '{end_marker}' not found after section '{heading}'"
section_html = content[start : end + len(end_marker)]
callback(HtmlSection(section_html, heading))
return self
class HtmlSection(HTMLContentAssertionsMixin):
"""Scoped HTML assertions over a slice of a response. Created by assert_section."""
def __init__(self, html_content: str, heading: str) -> None:
self._html = html_content
self._heading = heading
def _get_content(self) -> str:
return self._html
def assert_row_count(self, expected: int) -> Self:
actual = self._html.count("<tr>")
assert actual == expected, (
f"Expected {expected} <tr> rows in section '{self._heading}', got {actual}"
)
return self
Problem
Every assertion in
HTMLContentAssertionsMixin(assert_see,assert_dont_see,assert_see_text,assert_dont_see_text,assert_see_in_order) is page-global — it searches the entire decoded response body. There is no way to scope an assertion to a specific section of the HTML.This breaks down on any response that contains:
<table>elements where you want to assert on rows or content of one specific table.assert_see/assert_dont_seecannot distinguish them.The current workaround is raw substring slicing on
response.content, which is not fluent, not chainable, not discoverable in IDEs, and reinvented in every test that needs it.Requirements
A scoped HTML assertion API for pyssertive should:
<tr>rows inside the scoped table.Proposed API
The closure receives a scoped object that exposes the same HTML assertions as the outer client, but bound to the section slice. After the closure returns,
assert_sectionreturns the outer fluent client so the chain continues at page scope.Code draft