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
With optional strict validation support:
pip install assert-element[strict]
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, html=None)
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)
html - HTML validation mode (optional, priority order):
None (default): Use class attribute → Django setting → True
True: Standard validation (Django’s parse_html)
'strict': Strict HTML5 validation (requires html5lib)
False: No validation
Priority: explicit parameter > class attribute > ASSERT_ELEMENT_HTML_MODE setting > default (True)
Raises:
Exception - If no element found or multiple elements found
AssertionError - If element doesn’t match or HTML validation fails
ImportError - If html='strict' but html5lib is not installed
Example:
self.assertElementContains(
response,
'h1#page-title',
'<h1 id="page-title">Dashboard</h1>'
)
HTML Validation Modes
assertElementContains supports three HTML validation modes to balance strictness with practicality.
Standard Mode (Default)
# Default - catches major structural errors
self.assertElementContains(response, 'div', '<div>...</div>')
# Explicit
self.assertElementContains(response, 'div', '<div>...</div>', html=True)
Uses Django’s HTML parser (same as assertContains). Catches structural errors like wrong closing tags, but allows browser-like auto-closing of tags.
Strict Mode (Optional)
# Requires: pip install assert-element[strict]
self.assertElementContains(response, 'div', '<div>...</div>', html='strict')
Uses html5lib for strict HTML5 validation according to WHATWG specification. Requires html5lib package to be installed.
No Validation
self.assertElementContains(response, 'div', '<div>...</div>', html=False)
Skips HTML validation entirely. Useful for testing HTML fragments or intentionally malformed HTML.
Setting Project-Wide Defaults
There are multiple ways to set defaults, with the following priority order:
Per-assertion parameter (highest priority)
Class attribute
Django setting
Default (True - standard validation)
Option 1: Django Setting (Project-Wide)
Configure validation mode for your entire project in settings.py:
# settings.py
ASSERT_ELEMENT_HTML_MODE = 'strict' # or True, or False
# In test files - uses setting automatically
from assert_element import AssertElementMixin
from django.test import TestCase
class MyTests(AssertElementMixin, TestCase):
def test_something(self):
# Uses 'strict' from Django setting
self.assertElementContains(response, 'div', '<div>...</div>')
# Can still override per-assertion
self.assertElementContains(response, 'p', '<p>...</p>', html=True)
Option 2: Class Attribute (Test Class)
Set validation mode for all assertions in a specific test class:
from assert_element import AssertElementMixin
from django.test import TestCase
class MyTests(AssertElementMixin, TestCase):
assert_element_html_mode = 'strict' # Overrides Django setting
def test_something(self):
# Uses class attribute 'strict'
self.assertElementContains(response, 'div', '<div>...</div>')
# Can still override per-assertion
self.assertElementContains(response, 'p', '<p>...</p>', html=True)
Convenience Class for Strict Validation
from assert_element import StrictAssertElementMixin
from django.test import TestCase
class MyTests(StrictAssertElementMixin, TestCase):
# All assertions use strict validation by default
def test_something(self):
self.assertElementContains(response, 'div', '<div>...</div>')
Creating Project-Wide Base Class
# your_project/tests/base.py
from assert_element import StrictAssertElementMixin
from django.test import TestCase
class BaseTestCase(StrictAssertElementMixin, TestCase):
"""Base test case with strict HTML validation."""
pass
# In test files:
from your_project.tests.base import BaseTestCase
class MyTests(BaseTestCase):
# Inherits strict validation from base class
pass
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.7.2 (2026-02-17)
improved strict HTML5 validation error messages with clear context and location
error messages now show exact line and column numbers where validation fails
added visual context (3 lines before/after) with pointer to exact error location
multiple validation errors now reported individually with their own context
error messages are now actionable instead of showing cryptic stack traces
0.7.1 (2026-02-13)
added Django settings integration for project-wide validation mode configuration
added ASSERT_ELEMENT_HTML_MODE Django setting support
implemented priority chain: parameter > class attribute > Django setting > default
changed default assert_element_html_mode to None (falls back to Django setting)
added 6 new tests for Django settings integration (55 total tests)
updated documentation with Django settings examples
maintained full backward compatibility
0.7.0 (2026-02-12)
added HTML validation modes for catching structural errors
added standard HTML validation (default) using Django’s parse_html
added optional strict HTML5 validation using html5lib (install with: pip install assert-element[strict])
added class-level validation mode configuration via assert_element_html_mode attribute
added StrictAssertElementMixin convenience class for strict-by-default validation
improved backward compatibility - all existing code works unchanged
added 21 new tests for validation modes (49 total tests)
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
Release history Release notifications | RSS feed
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.7.2.tar.gz.
File metadata
- Download URL: assert_element-0.7.2.tar.gz
- Upload date:
- Size: 28.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
394f98c7c3d3e3c8576b8f4db88b357f66ae93fea4d2dc480c5943a522e1c6d6
|
|
| MD5 |
d50190827ca17dcce3518747778f5981
|
|
| BLAKE2b-256 |
3d7116278dc578392a016c47a252de09fda24dffc9a4863a2d57110bc3674daa
|
File details
Details for the file assert_element-0.7.2-py2.py3-none-any.whl.
File metadata
- Download URL: assert_element-0.7.2-py2.py3-none-any.whl
- Upload date:
- Size: 12.5 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 |
b67aefc2e932ae72cd3c4e6fd133c1b0c947fb3c5462536844d87a344066ecaa
|
|
| MD5 |
d443e50717f93c811ec262ee58e8f78e
|
|
| BLAKE2b-256 |
d360d484cea421d2219c62613044865552c1f9a9696b54f46a0248b3b6a79864
|