Simple TestCase assertion that finds element based on it's path and check if it equals with given content.
Project description
Simple TestCase assertion that finds element based on its CSS selector and checks if it equals the given content. In case the content is not matching it outputs nice and clean diff of the two compared HTML pieces.
This is more useful than the default Django self.assertContains(response, ..., html=True) because it will find the element and show differences if something changed.
Why Use assertElementContains?
Primary Benefits:
Targeted Error Messages - Shows only the relevant element on failure, not the entire HTML response
Precise Testing - Forces you to write specific, maintainable tests with CSS selectors
Less Brittle - Tests focus on semantic structure, not exact formatting
Better Debugging - Failures point directly to the problematic element
Comparison:
# ❌ BAD - Dumps entire HTML response on failure (thousands of lines)
self.assertContains(response, "Company Dashboard")
self.assertIn("Company Dashboard", response.content.decode())
# ✅ GOOD - Shows only the relevant element (clean, focused)
self.assertElementContains(response, "h1", "<h1>Company Dashboard</h1>")
Whitespace Normalization
The library uses aggressive whitespace normalization to focus on HTML semantic meaning rather than cosmetic formatting differences:
Normalizes cosmetic differences: Multiple spaces, tabs, newlines, and attribute spacing
Handles structural variations: Self-closing vs explicit tags (<br/> vs <br></br>)
Preserves semantic meaning: Only fails when HTML content actually differs in meaning
Browser-consistent: Mimics how browsers treat whitespace (collapsed to single spaces)
This prevents false positive test failures caused by insignificant whitespace variations while still catching genuine HTML content differences.
How it works: The normalization uses BeautifulSoup to parse and normalize HTML structure, then collapses whitespace to single spaces (as browsers do), making tests resilient to formatting changes.
Example: These assertions would pass because the differences are cosmetic:
# All of these work - whitespace is normalized automatically:
self.assertElementContains(response, 'p', '<p>hello world</p>')
self.assertElementContains(response, 'p', '<p>hello world</p>') # Multiple spaces
self.assertElementContains(response, 'p', '<p>hello\tworld</p>') # Tab
self.assertElementContains(response, 'p', '<p>\n hello world \n</p>') # Newlines
Complete Element Matching
Critical: assertElementContains does exact element matching, not content checking. You must include the element’s own tags in the expected string.
# Given: <button class="submit-btn">Save</button>
self.assertElementContains(
response,
'button.submit-btn',
'<button class="submit-btn">Save</button>'
) # ✓ Correct
self.assertElementContains(
response,
'button.submit-btn',
'Save'
) # ✗ Wrong - missing element tags
CSS Selectors Only
Use CSS selectors (not XPath) to target elements:
# ✅ Good - CSS selectors
self.assertElementContains(response, '#page-title', '<h1 id="page-title">Dashboard</h1>')
self.assertElementContains(response, '.invoice-number', '<span class="invoice-number">123</span>')
self.assertElementContains(response, 'button.submit-btn', '<button class="submit-btn">Save</button>')
# ❌ Bad - XPath not supported
# self.assertElementContains(response, '//div[@class="invoice"]', ...) # Won't work
Single Element Requirement
The CSS selector must match exactly one element. If multiple elements match, assertElementContains will raise an exception with details about all matching elements.
Error when multiple elements found:
Exception: More than one element found (3): .member-email
Found elements:
1. <span class="member-email">user1@example.com</span>
2. <span class="member-email">user2@example.com</span>
3. <span class="member-email">user3@example.com</span>
Solutions:
Solution 1: Add Semantic Classes/IDs (Recommended)
Add semantic classes or IDs to your templates to make selectors unique:
<!-- Template -->
<div class="empty-state-message">No team members yet.</div>
<a href="/invite/" class="invite-team-link">Invite Team Members</a>
# Test
self.assertElementContains(
response,
'.empty-state-message',
'<div class="empty-state-message">No team members yet.</div>'
)
Benefits:
Makes templates more semantic and maintainable
Improves accessibility (IDs/classes can be used by screen readers)
Makes tests self-documenting
Solution 2: Use nth-child Selectors
When you need to target a specific element in a list:
# Target first row in a table
self.assertElementContains(
response,
'tbody tr:nth-child(1) .invoice-number',
'<span class="invoice-number">1/FV/12/2025</span>'
)
Solution 3: Make Selector More Specific
Combine selectors to narrow down to a unique element:
# Instead of just '.member-email', use:
self.assertElementContains(
response,
'.business-plan-members .member-email',
'<span class="member-email">user@example.com</span>'
)
Common Patterns
Dynamic Content
Extract dynamic values from models, don’t hardcode:
def test_with_dynamic_content(self):
invoice = Invoice.objects.create(
number="1/FV/12/2025",
total=Decimal("100.00")
)
response = self.client.get(f'/invoices/{invoice.id}/')
# Build expected HTML with dynamic values from the model
expected = f'<span class="invoice-number">{invoice.full_number}</span>'
self.assertElementContains(response, '.invoice-number', expected)
Testing Form Errors
# Template has: <div class="invalid-feedback d-block email-field-error">...</div>
self.assertElementContains(
response,
'.email-field-error',
'<div class="invalid-feedback d-block email-field-error">',
)
Testing Empty States
self.assertElementContains(
response,
'.empty-state-message',
'<p class="text-muted empty-state-message">No team members yet.</p>'
)
Testing Modal Content
self.assertElementContains(
response,
'.upgrade-plan-id',
f'<input type="hidden" name="plan_id" value="{plan.id}" class="upgrade-plan-id">'
)
Best Practices
Use Semantic Selectors - Prefer .invoice-number over .table tbody tr:first-child td:nth-child(2)
Extract Dynamic Data - Get UUIDs/timestamps from models, don’t hardcode
Modify Templates for Testability - Add semantic classes/IDs when needed
Keep Selectors Simple - Avoid overly complex CSS selectors
Common Pitfalls
Missing Element Tags - Must include the selected element’s own tags, not just inner text
Multiple Element Selection - Selector must target exactly one element (use solutions above)
Hardcoded Dynamic Content - Extract UUIDs/timestamps from models, don’t hardcode
Overly Complex Selectors - Prefer semantic classes over deep CSS selectors
Other similar projects
I released this package just to realize after few days, that there are some other very similar projects:
Documentation
The full documentation is at https://assert_element.readthedocs.io.
Quickstart
Install by:
pip install assert-element
Usage in tests:
from assert_element import AssertElementMixin
class MyTestCase(AssertElementMixin, TestCase):
def test_something(self):
response = self.client.get(address)
self.assertElementContains(
response,
'div[id="my-div"]',
'<div id="my-div">My div</div>',
)
The first attribute can be response or content string. Second attribute is the CSS selector to the element. Third attribute is the expected content.
Error Output Example: If response = <html><div id="my-div">Myy div</div></html> the error output of the assertElementContains looks like this:
======================================================================
FAIL: test_element_differs (tests.test_models.MyTestCase.test_element_differs)
Element not found raises Exception
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/petr/soubory/programovani/blenderkit/django-assert-element/assert_element/tests/test_models.py", line 53, in test_element_differs
self.assertElementContains(
File "/home/petr/soubory/programovani/blenderkit/django-assert-element/assert_element/assert_element/assert_element.py", line 58, in assertElementContains
self.assertEqual(element_txt, soup_1_txt)
AssertionError: '<div\n id="my-div"\n>\n Myy div \n</div>' != '<div\n id="my-div"\n>\n My div \n</div>'
<div
id="my-div"
>
- Myy div
? -
+ My div
</div>
which is much cleaner than the original django assertContains() output.
API Reference
assertElementContains(request, html_element, element_text)
Parameters:
request - Django response object or HTML string
html_element - CSS selector string (e.g., '#id', '.class', 'button.submit-btn')
element_text - Expected full element HTML string (must include element’s own tags)
Raises:
Exception - If no element found, multiple elements found, or element doesn’t match
Example:
self.assertElementContains(
response,
'h1#page-title',
'<h1 id="page-title">Dashboard</h1>'
)
How It Works
Parses HTML using BeautifulSoup
Selects element(s) using CSS selector
Validates exactly one element found
Normalizes both actual and expected HTML (whitespace, structure)
Compares normalized HTML strings
The normalization process:
Uses BeautifulSoup for structural normalization
Collapses consecutive whitespace to single spaces
Normalizes line endings
Preserves semantic structure while ignoring cosmetic formatting
Running Tests
Does the code actually work?
source <YOURVIRTUALENV>/bin/activate (myenv) $ pip install tox (myenv) $ tox
Development commands
pip install -r requirements_dev.txt invoke -l
Credits
Tools used in rendering this package:
History
0.6.1 (2025-12-18)
fixed whitespace normalization in attribute values (regression fix)
attribute values now properly normalize multi-line formatting to single line
improved tolerance for cosmetic whitespace differences in srcset, style, and other attributes
added comprehensive test coverage for real-world HTML formatting variations
0.6.0 (2025-12-11)
added element preview in error messages for easier debugging
improved HTML formatting with proper self-closing tag handling
normalized boolean attribute handling for consistent comparisons
added pre-commit configuration for unified CI checks
enhanced README with comprehensive examples and documentation
0.5.0 (2025-08-15)
improved whitespace sanitization with aggressive normalization
enhanced test coverage for semantically meaningful whitespace differences
updated documentation with detailed whitespace normalization behavior
0.4.0 (2023-07-21)
more readable output when assertion fails
0.3.0 (2022-09-16)
more tolerance in whitespace differences
0.2.0 (2022-09-01)
first attribute can be response or content itself
0.1.0 (2022-08-21)
First release on PyPI.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file assert_element-0.6.1.tar.gz.
File metadata
- Download URL: assert_element-0.6.1.tar.gz
- Upload date:
- Size: 30.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72aa7af604e86bf6ab0a7eec5f352d2ff8cb0cd2d7940331daad48ebbde3cb86
|
|
| MD5 |
fefcbb26439a876ff3eadf5a7d92393b
|
|
| BLAKE2b-256 |
900244beaf5f0db3d2fb0f1a27332223b97a410e64ab6ed07be59e27f1e55668
|
File details
Details for the file assert_element-0.6.1-py2.py3-none-any.whl.
File metadata
- Download URL: assert_element-0.6.1-py2.py3-none-any.whl
- Upload date:
- Size: 9.0 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f4313d13f2d1802df9d3a0691d46ba846af88c4628e8dba3e24903b9d06ff31
|
|
| MD5 |
efc8cca537425a1fee8c031ba36ed183
|
|
| BLAKE2b-256 |
1a28045727674d74c875b0aa8c53fd7e71381ce81128603ad4898a9aa1769eab
|