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

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

  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.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.6.1.tar.gz (30.1 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.6.1-py2.py3-none-any.whl (9.0 kB view details)

Uploaded Python 2Python 3

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

Hashes for assert_element-0.6.1.tar.gz
Algorithm Hash digest
SHA256 72aa7af604e86bf6ab0a7eec5f352d2ff8cb0cd2d7940331daad48ebbde3cb86
MD5 fefcbb26439a876ff3eadf5a7d92393b
BLAKE2b-256 900244beaf5f0db3d2fb0f1a27332223b97a410e64ab6ed07be59e27f1e55668

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for assert_element-0.6.1-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 6f4313d13f2d1802df9d3a0691d46ba846af88c4628e8dba3e24903b9d06ff31
MD5 efc8cca537425a1fee8c031ba36ed183
BLAKE2b-256 1a28045727674d74c875b0aa8c53fd7e71381ce81128603ad4898a9aa1769eab

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