Skip to main content

Simple TestCase assertion that finds element based on it's path and check if it equals with given content.

Project description

https://badge.fury.io/py/assert_element.svg https://codecov.io/gh/PetrDlouhy/assert_element/branch/master/graph/badge.svg https://github.com/PetrDlouhy/django-assert-element/actions/workflows/main.yml/badge.svg?event=registry_package

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:

  1. Targeted Error Messages - Shows only the relevant element on failure, not the entire HTML response

  2. Precise Testing - Forces you to write specific, maintainable tests with CSS selectors

  3. Less Brittle - Tests focus on semantic structure, not exact formatting

  4. 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 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

  1. Use Semantic Selectors - Prefer .invoice-number over .table tbody tr:first-child td:nth-child(2)

  2. Extract Dynamic Data - Get UUIDs/timestamps from models, don’t hardcode

  3. Modify Templates for Testability - Add semantic classes/IDs when needed

  4. Keep Selectors Simple - Avoid overly complex CSS selectors

Common Pitfalls

  1. Missing Element Tags - Must include the selected element’s own tags, not just inner text

  2. Multiple Element Selection - Selector must target exactly one element (use solutions above)

  3. Hardcoded Dynamic Content - Extract UUIDs/timestamps from models, don’t hardcode

  4. 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:

  1. Per-assertion parameter (highest priority)

  2. Class attribute

  3. Django setting

  4. 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

  1. Parses HTML using BeautifulSoup

  2. Selects element(s) using CSS selector

  3. Validates exactly one element found

  4. Normalizes both actual and expected HTML (whitespace, structure)

  5. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

assert_element-0.7.2.tar.gz (28.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

assert_element-0.7.2-py2.py3-none-any.whl (12.5 kB view details)

Uploaded Python 2Python 3

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

Hashes for assert_element-0.7.2.tar.gz
Algorithm Hash digest
SHA256 394f98c7c3d3e3c8576b8f4db88b357f66ae93fea4d2dc480c5943a522e1c6d6
MD5 d50190827ca17dcce3518747778f5981
BLAKE2b-256 3d7116278dc578392a016c47a252de09fda24dffc9a4863a2d57110bc3674daa

See more details on using hashes here.

File details

Details for the file assert_element-0.7.2-py2.py3-none-any.whl.

File metadata

File hashes

Hashes for assert_element-0.7.2-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 b67aefc2e932ae72cd3c4e6fd133c1b0c947fb3c5462536844d87a344066ecaa
MD5 d443e50717f93c811ec262ee58e8f78e
BLAKE2b-256 d360d484cea421d2219c62613044865552c1f9a9696b54f46a0248b3b6a79864

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page