assert-element 0.3.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. assert_element-0.5.0/.github/workflows/main.yml +104 -0
  2. assert_element-0.5.0/HISTORY.rst +31 -0
  3. {assert_element-0.3.0 → assert_element-0.5.0}/MANIFEST.in +1 -0
  4. assert_element-0.5.0/PKG-INFO +207 -0
  5. assert_element-0.5.0/README.rst +140 -0
  6. {assert_element-0.3.0 → assert_element-0.5.0}/assert_element/__init__.py +1 -1
  7. assert_element-0.5.0/assert_element/assert_element.py +78 -0
  8. assert_element-0.5.0/assert_element.egg-info/PKG-INFO +207 -0
  9. {assert_element-0.3.0 → assert_element-0.5.0}/requirements_test.txt +1 -1
  10. {assert_element-0.3.0 → assert_element-0.5.0}/setup.cfg +1 -4
  11. {assert_element-0.3.0 → assert_element-0.5.0}/tests/settings.py +0 -2
  12. assert_element-0.5.0/tests/test_models.py +334 -0
  13. assert_element-0.5.0/tox.ini +31 -0
  14. assert_element-0.3.0/.github/workflows/main.yml +0 -66
  15. assert_element-0.3.0/HISTORY.rst +0 -19
  16. assert_element-0.3.0/PKG-INFO +0 -135
  17. assert_element-0.3.0/README.rst +0 -92
  18. assert_element-0.3.0/assert_element/assert_element.py +0 -28
  19. assert_element-0.3.0/assert_element.egg-info/PKG-INFO +0 -135
  20. assert_element-0.3.0/tests/test_models.py +0 -58
  21. assert_element-0.3.0/tox.ini +0 -16
  22. {assert_element-0.3.0 → assert_element-0.5.0}/.coveragerc +0 -0
  23. {assert_element-0.3.0 → assert_element-0.5.0}/.editorconfig +0 -0
  24. {assert_element-0.3.0 → assert_element-0.5.0}/.github/ISSUE_TEMPLATE.md +0 -0
  25. {assert_element-0.3.0 → assert_element-0.5.0}/.gitignore +0 -0
  26. {assert_element-0.3.0 → assert_element-0.5.0}/AUTHORS.rst +0 -0
  27. {assert_element-0.3.0 → assert_element-0.5.0}/CONTRIBUTING.rst +0 -0
  28. {assert_element-0.3.0 → assert_element-0.5.0}/LICENSE +0 -0
  29. {assert_element-0.3.0 → assert_element-0.5.0}/assert_element.egg-info/SOURCES.txt +0 -0
  30. {assert_element-0.3.0 → assert_element-0.5.0}/assert_element.egg-info/dependency_links.txt +0 -0
  31. {assert_element-0.3.0 → assert_element-0.5.0}/assert_element.egg-info/not-zip-safe +0 -0
  32. {assert_element-0.3.0 → assert_element-0.5.0}/assert_element.egg-info/requires.txt +0 -0
  33. {assert_element-0.3.0 → assert_element-0.5.0}/assert_element.egg-info/top_level.txt +0 -0
  34. {assert_element-0.3.0 → assert_element-0.5.0}/docs/Makefile +0 -0
  35. {assert_element-0.3.0 → assert_element-0.5.0}/docs/authors.rst +0 -0
  36. {assert_element-0.3.0 → assert_element-0.5.0}/docs/conf.py +0 -0
  37. {assert_element-0.3.0 → assert_element-0.5.0}/docs/contributing.rst +0 -0
  38. {assert_element-0.3.0 → assert_element-0.5.0}/docs/history.rst +0 -0
  39. {assert_element-0.3.0 → assert_element-0.5.0}/docs/index.rst +0 -0
  40. {assert_element-0.3.0 → assert_element-0.5.0}/docs/installation.rst +0 -0
  41. {assert_element-0.3.0 → assert_element-0.5.0}/docs/make.bat +0 -0
  42. {assert_element-0.3.0 → assert_element-0.5.0}/docs/readme.rst +0 -0
  43. {assert_element-0.3.0 → assert_element-0.5.0}/docs/usage.rst +0 -0
  44. {assert_element-0.3.0 → assert_element-0.5.0}/manage.py +0 -0
  45. {assert_element-0.3.0 → assert_element-0.5.0}/mypy.ini +0 -0
  46. {assert_element-0.3.0 → assert_element-0.5.0}/requirements.txt +0 -0
  47. {assert_element-0.3.0 → assert_element-0.5.0}/requirements_dev.txt +0 -0
  48. {assert_element-0.3.0 → assert_element-0.5.0}/runtests.py +0 -0
  49. {assert_element-0.3.0 → assert_element-0.5.0}/setup.py +0 -0
  50. {assert_element-0.3.0 → assert_element-0.5.0}/tasks.py +0 -0
  51. {assert_element-0.3.0 → assert_element-0.5.0}/tests/README.md +0 -0
  52. {assert_element-0.3.0 → assert_element-0.5.0}/tests/__init__.py +0 -0
  53. {assert_element-0.3.0 → assert_element-0.5.0}/tests/requirements.txt +0 -0
  54. {assert_element-0.3.0 → assert_element-0.5.0}/tests/urls.py +0 -0
  55. {assert_element-0.3.0 → assert_element-0.5.0}/tests/wsgi.py +0 -0
@@ -0,0 +1,104 @@
1
+ # This is a basic workflow to help you get started with Actions
2
+
3
+ name: CI
4
+
5
+ # Controls when the workflow will run
6
+ on:
7
+ # Triggers the workflow on push or pull request events but only for the master branch
8
+ push:
9
+ branches: [ master ]
10
+ pull_request:
11
+ branches: [ '**' ]
12
+
13
+ # Allows you to run this workflow manually from the Actions tab
14
+ workflow_dispatch:
15
+
16
+ # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17
+ jobs:
18
+ # This workflow contains a single job called "build"
19
+ tests:
20
+ # The type of runner that the job will run on
21
+ runs-on: ubuntu-latest
22
+
23
+ strategy:
24
+ matrix:
25
+ python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
26
+ DJANGO_VERSION: ['4.0.*', '4.1.*', '4.2.*', '5.0.*', '5.1.*', '5.2.*']
27
+ exclude:
28
+ # Django 4.0 incompatibilities
29
+ - python-version: '3.11'
30
+ DJANGO_VERSION: '4.0.*'
31
+ - python-version: '3.12'
32
+ DJANGO_VERSION: '4.0.*'
33
+ - python-version: '3.13'
34
+ DJANGO_VERSION: '4.0.*'
35
+
36
+ # Django 4.1 incompatibilities
37
+ - python-version: '3.12'
38
+ DJANGO_VERSION: '4.1.*'
39
+ - python-version: '3.13'
40
+ DJANGO_VERSION: '4.1.*'
41
+
42
+ # Django 4.2 incompatibilities
43
+ - python-version: '3.13'
44
+ DJANGO_VERSION: '4.2.*'
45
+
46
+ # Django 5.0 incompatibilities
47
+ - python-version: '3.8'
48
+ DJANGO_VERSION: '5.0.*'
49
+ - python-version: '3.9'
50
+ DJANGO_VERSION: '5.0.*'
51
+ - python-version: '3.13'
52
+ DJANGO_VERSION: '5.0.*'
53
+
54
+ # Django 5.1 incompatibilities
55
+ - python-version: '3.8'
56
+ DJANGO_VERSION: '5.1.*'
57
+ - python-version: '3.9'
58
+ DJANGO_VERSION: '5.1.*'
59
+ - python-version: '3.13'
60
+ DJANGO_VERSION: '5.1.*'
61
+
62
+ # Django 5.2 incompatibilities
63
+ - python-version: '3.8'
64
+ DJANGO_VERSION: '5.2.*'
65
+ - python-version: '3.9'
66
+ DJANGO_VERSION: '5.2.*'
67
+ fail-fast: false
68
+
69
+ steps:
70
+ - uses: actions/checkout@v2
71
+
72
+ - name: Set up Python ${{ matrix.python-version }}
73
+ uses: actions/setup-python@v2
74
+ with:
75
+ python-version: ${{ matrix.python-version }}
76
+ - uses: actions/cache@v4
77
+ with:
78
+ path: ~/.cache/pip
79
+ key: ${{ hashFiles('setup.py') }}-${{ hashFiles('demoproject/requirements.txt') }}-${{ hashFiles('requirements.txt') }}-${{ matrix.DJANGO_VERSION }}
80
+
81
+ - name: Install
82
+ run: |
83
+ pip install -e .
84
+ pip install tox codecov
85
+
86
+ - name: Testing
87
+ run: |
88
+ tox -e py$(echo ${{ matrix.python-version }} | sed 's/\.//g')-django$(echo ${{ matrix.DJANGO_VERSION }} | cut -d'.' -f1-2 | sed 's/\.//g')
89
+ codecov
90
+ lint:
91
+ runs-on: ubuntu-latest
92
+ steps:
93
+ - uses: actions/checkout@v2
94
+ - name: Install
95
+ run: |
96
+ pip install -r requirements_test.txt
97
+ - name: Running Flake8
98
+ run: flake8
99
+ - name: Running isort
100
+ run: python -m isort . --check-only --diff
101
+ - name: Running black
102
+ run: black --check .
103
+ - name: Running mypy
104
+ run: mypy .
@@ -0,0 +1,31 @@
1
+ .. :changelog:
2
+
3
+ History
4
+ -------
5
+
6
+ 0.5.0 (2025-08-15)
7
+ ++++++++++++++++++
8
+
9
+ * improved whitespace sanitization with aggressive normalization
10
+ * enhanced test coverage for semantically meaningful whitespace differences
11
+ * updated documentation with detailed whitespace normalization behavior
12
+
13
+ 0.4.0 (2023-07-21)
14
+ ++++++++++++++++++
15
+
16
+ * more readable output when assertion fails
17
+
18
+ 0.3.0 (2022-09-16)
19
+ ++++++++++++++++++
20
+
21
+ * more tolerance in whitespace differences
22
+
23
+ 0.2.0 (2022-09-01)
24
+ ++++++++++++++++++
25
+
26
+ * first attribute can be response or content itself
27
+
28
+ 0.1.0 (2022-08-21)
29
+ ++++++++++++++++++
30
+
31
+ * First release on PyPI.
@@ -3,4 +3,5 @@ include CONTRIBUTING.rst
3
3
  include HISTORY.rst
4
4
  include LICENSE
5
5
  include README.rst
6
+ include requirements.txt
6
7
  recursive-include assert_element *.html *.png *.gif *js *.css *jpg *jpeg *svg *py
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.4
2
+ Name: assert_element
3
+ Version: 0.5.0
4
+ Summary: Simple TestCase assertion that finds element based on it's path and check if it equals with given content.
5
+ Home-page: https://github.com/PetrDlouhy/django-assert-element
6
+ Author: Petr Dlouhý
7
+ Author-email: petr.dlouhy@email.cz
8
+ License: MIT
9
+ Keywords: assert_element
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: Django :: 1.11
12
+ Classifier: Framework :: Django :: 2.1
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: BSD License
15
+ Classifier: Natural Language :: English
16
+ Classifier: Programming Language :: Python :: 2
17
+ Classifier: Programming Language :: Python :: 2.7
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.5
20
+ Classifier: Programming Language :: Python :: 3.6
21
+ License-File: LICENSE
22
+ License-File: AUTHORS.rst
23
+ Requires-Dist: beautifulsoup4
24
+ Dynamic: author
25
+ Dynamic: author-email
26
+ Dynamic: classifier
27
+ Dynamic: description
28
+ Dynamic: home-page
29
+ Dynamic: keywords
30
+ Dynamic: license
31
+ Dynamic: license-file
32
+ Dynamic: requires-dist
33
+ Dynamic: summary
34
+
35
+ =============================
36
+ Django assert element
37
+ =============================
38
+
39
+ .. image:: https://badge.fury.io/py/assert_element.svg
40
+ :target: https://badge.fury.io/py/assert_element
41
+
42
+ .. image:: https://codecov.io/gh/PetrDlouhy/assert_element/branch/master/graph/badge.svg
43
+ :target: https://codecov.io/gh/PetrDlouhy/assert_element
44
+
45
+ .. image:: https://github.com/PetrDlouhy/django-assert-element/actions/workflows/main.yml/badge.svg?event=registry_package
46
+ :target: https://github.com/PetrDlouhy/django-assert-element/actions/workflows/main.yml
47
+
48
+ Simple ``TestCase`` assertion that finds element based on it's xpath and check if it equals with given content.
49
+ In case the content is not matching it outputs nice and clean diff of the two compared HTML pieces.
50
+
51
+ This is more useful than the default Django ``self.assertContains(response, ..., html=True)``
52
+ because it will find the element and show differences if something changed.
53
+
54
+ Whitespace Normalization
55
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
56
+
57
+ The library uses aggressive whitespace normalization to focus on HTML semantic meaning
58
+ rather than cosmetic formatting differences:
59
+
60
+ * **Normalizes cosmetic differences**: Multiple spaces, tabs, newlines, and attribute spacing
61
+ * **Handles structural variations**: Self-closing vs explicit tags (``<br/>`` vs ``<br></br>``)
62
+ * **Preserves semantic meaning**: Only fails when HTML content actually differs in meaning
63
+ * **Browser-consistent**: Mimics how browsers treat whitespace (collapsed to single spaces)
64
+
65
+ This prevents false positive test failures caused by insignificant whitespace variations
66
+ while still catching genuine HTML content differences.
67
+
68
+ Other similar projects
69
+ ----------------------
70
+
71
+ I released this package just to realize after few days, that there are some other very similar projects:
72
+
73
+ * https://pypi.org/project/django_html_assertions/
74
+ * https://django-with-asserts.readthedocs.io/en/latest/
75
+ * https://github.com/robjohncox/python-html-assert
76
+
77
+ Documentation
78
+ -------------
79
+
80
+ The full documentation is at https://assert_element.readthedocs.io.
81
+
82
+ Quickstart
83
+ ----------
84
+
85
+ Install by:
86
+
87
+ .. code-block:: bash
88
+
89
+ pip install assert-element
90
+
91
+ Usage in tests:
92
+
93
+ .. code-block:: python
94
+
95
+ from assert_element import AssertElementMixin
96
+
97
+ class MyTestCase(AssertElementMixin, TestCase):
98
+ def test_something(self):
99
+ response = self.client.get(address)
100
+ self.assertElementContains(
101
+ response,
102
+ 'div[id="my-div"]',
103
+ '<div id="my-div">My div</div>',
104
+ )
105
+
106
+ The first attribute can be response or content string.
107
+ Second attribute is the xpath to the element.
108
+ Third attribute is the expected content.
109
+
110
+ **Error Output Example**: If response = `<html><div id="my-div">Myy div</div></html>` the error output of the `assertElementContains` looks like this:
111
+
112
+ .. code-block:: console
113
+
114
+ ======================================================================
115
+ FAIL: test_element_differs (tests.test_models.MyTestCase.test_element_differs)
116
+ Element not found raises Exception
117
+ ----------------------------------------------------------------------
118
+ Traceback (most recent call last):
119
+ File "/home/petr/soubory/programovani/blenderkit/django-assert-element/assert_element/tests/test_models.py", line 53, in test_element_differs
120
+ self.assertElementContains(
121
+ File "/home/petr/soubory/programovani/blenderkit/django-assert-element/assert_element/assert_element/assert_element.py", line 58, in assertElementContains
122
+ self.assertEqual(element_txt, soup_1_txt)
123
+ AssertionError: '<div\n id="my-div"\n>\n Myy div \n</div>' != '<div\n id="my-div"\n>\n My div \n</div>'
124
+ <div
125
+ id="my-div"
126
+ >
127
+ - Myy div
128
+ ? -
129
+ + My div
130
+ </div>
131
+
132
+ which is much cleaner than the original django ``assertContains()`` output.
133
+
134
+ **Whitespace Example**: These assertions would pass because the differences are cosmetic:
135
+
136
+ .. code-block:: python
137
+
138
+ # These are all equivalent due to whitespace normalization:
139
+ self.assertElementContains(response, 'p', '<p>hello world</p>')
140
+ self.assertElementContains(response, 'p', '<p>hello world</p>') # Multiple spaces
141
+ self.assertElementContains(response, 'p', '<p>hello\tworld</p>') # Tab
142
+ self.assertElementContains(response, 'p', '<p>\n hello world \n</p>') # Newlines
143
+
144
+ Running Tests
145
+ -------------
146
+
147
+ Does the code actually work?
148
+
149
+ ::
150
+
151
+ source <YOURVIRTUALENV>/bin/activate
152
+ (myenv) $ pip install tox
153
+ (myenv) $ tox
154
+
155
+
156
+ Development commands
157
+ ---------------------
158
+
159
+ ::
160
+
161
+ pip install -r requirements_dev.txt
162
+ invoke -l
163
+
164
+
165
+ Credits
166
+ -------
167
+
168
+ Tools used in rendering this package:
169
+
170
+ * Cookiecutter_
171
+ * `cookiecutter-djangopackage`_
172
+
173
+ .. _Cookiecutter: https://github.com/audreyr/cookiecutter
174
+ .. _`cookiecutter-djangopackage`: https://github.com/pydanny/cookiecutter-djangopackage
175
+
176
+
177
+
178
+
179
+ History
180
+ -------
181
+
182
+ 0.5.0 (2025-08-15)
183
+ ++++++++++++++++++
184
+
185
+ * improved whitespace sanitization with aggressive normalization
186
+ * enhanced test coverage for semantically meaningful whitespace differences
187
+ * updated documentation with detailed whitespace normalization behavior
188
+
189
+ 0.4.0 (2023-07-21)
190
+ ++++++++++++++++++
191
+
192
+ * more readable output when assertion fails
193
+
194
+ 0.3.0 (2022-09-16)
195
+ ++++++++++++++++++
196
+
197
+ * more tolerance in whitespace differences
198
+
199
+ 0.2.0 (2022-09-01)
200
+ ++++++++++++++++++
201
+
202
+ * first attribute can be response or content itself
203
+
204
+ 0.1.0 (2022-08-21)
205
+ ++++++++++++++++++
206
+
207
+ * First release on PyPI.
@@ -0,0 +1,140 @@
1
+ =============================
2
+ Django assert element
3
+ =============================
4
+
5
+ .. image:: https://badge.fury.io/py/assert_element.svg
6
+ :target: https://badge.fury.io/py/assert_element
7
+
8
+ .. image:: https://codecov.io/gh/PetrDlouhy/assert_element/branch/master/graph/badge.svg
9
+ :target: https://codecov.io/gh/PetrDlouhy/assert_element
10
+
11
+ .. image:: https://github.com/PetrDlouhy/django-assert-element/actions/workflows/main.yml/badge.svg?event=registry_package
12
+ :target: https://github.com/PetrDlouhy/django-assert-element/actions/workflows/main.yml
13
+
14
+ Simple ``TestCase`` assertion that finds element based on it's xpath and check if it equals with given content.
15
+ In case the content is not matching it outputs nice and clean diff of the two compared HTML pieces.
16
+
17
+ This is more useful than the default Django ``self.assertContains(response, ..., html=True)``
18
+ because it will find the element and show differences if something changed.
19
+
20
+ Whitespace Normalization
21
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
22
+
23
+ The library uses aggressive whitespace normalization to focus on HTML semantic meaning
24
+ rather than cosmetic formatting differences:
25
+
26
+ * **Normalizes cosmetic differences**: Multiple spaces, tabs, newlines, and attribute spacing
27
+ * **Handles structural variations**: Self-closing vs explicit tags (``<br/>`` vs ``<br></br>``)
28
+ * **Preserves semantic meaning**: Only fails when HTML content actually differs in meaning
29
+ * **Browser-consistent**: Mimics how browsers treat whitespace (collapsed to single spaces)
30
+
31
+ This prevents false positive test failures caused by insignificant whitespace variations
32
+ while still catching genuine HTML content differences.
33
+
34
+ Other similar projects
35
+ ----------------------
36
+
37
+ I released this package just to realize after few days, that there are some other very similar projects:
38
+
39
+ * https://pypi.org/project/django_html_assertions/
40
+ * https://django-with-asserts.readthedocs.io/en/latest/
41
+ * https://github.com/robjohncox/python-html-assert
42
+
43
+ Documentation
44
+ -------------
45
+
46
+ The full documentation is at https://assert_element.readthedocs.io.
47
+
48
+ Quickstart
49
+ ----------
50
+
51
+ Install by:
52
+
53
+ .. code-block:: bash
54
+
55
+ pip install assert-element
56
+
57
+ Usage in tests:
58
+
59
+ .. code-block:: python
60
+
61
+ from assert_element import AssertElementMixin
62
+
63
+ class MyTestCase(AssertElementMixin, TestCase):
64
+ def test_something(self):
65
+ response = self.client.get(address)
66
+ self.assertElementContains(
67
+ response,
68
+ 'div[id="my-div"]',
69
+ '<div id="my-div">My div</div>',
70
+ )
71
+
72
+ The first attribute can be response or content string.
73
+ Second attribute is the xpath to the element.
74
+ Third attribute is the expected content.
75
+
76
+ **Error Output Example**: If response = `<html><div id="my-div">Myy div</div></html>` the error output of the `assertElementContains` looks like this:
77
+
78
+ .. code-block:: console
79
+
80
+ ======================================================================
81
+ FAIL: test_element_differs (tests.test_models.MyTestCase.test_element_differs)
82
+ Element not found raises Exception
83
+ ----------------------------------------------------------------------
84
+ Traceback (most recent call last):
85
+ File "/home/petr/soubory/programovani/blenderkit/django-assert-element/assert_element/tests/test_models.py", line 53, in test_element_differs
86
+ self.assertElementContains(
87
+ File "/home/petr/soubory/programovani/blenderkit/django-assert-element/assert_element/assert_element/assert_element.py", line 58, in assertElementContains
88
+ self.assertEqual(element_txt, soup_1_txt)
89
+ AssertionError: '<div\n id="my-div"\n>\n Myy div \n</div>' != '<div\n id="my-div"\n>\n My div \n</div>'
90
+ <div
91
+ id="my-div"
92
+ >
93
+ - Myy div
94
+ ? -
95
+ + My div
96
+ </div>
97
+
98
+ which is much cleaner than the original django ``assertContains()`` output.
99
+
100
+ **Whitespace Example**: These assertions would pass because the differences are cosmetic:
101
+
102
+ .. code-block:: python
103
+
104
+ # These are all equivalent due to whitespace normalization:
105
+ self.assertElementContains(response, 'p', '<p>hello world</p>')
106
+ self.assertElementContains(response, 'p', '<p>hello world</p>') # Multiple spaces
107
+ self.assertElementContains(response, 'p', '<p>hello\tworld</p>') # Tab
108
+ self.assertElementContains(response, 'p', '<p>\n hello world \n</p>') # Newlines
109
+
110
+ Running Tests
111
+ -------------
112
+
113
+ Does the code actually work?
114
+
115
+ ::
116
+
117
+ source <YOURVIRTUALENV>/bin/activate
118
+ (myenv) $ pip install tox
119
+ (myenv) $ tox
120
+
121
+
122
+ Development commands
123
+ ---------------------
124
+
125
+ ::
126
+
127
+ pip install -r requirements_dev.txt
128
+ invoke -l
129
+
130
+
131
+ Credits
132
+ -------
133
+
134
+ Tools used in rendering this package:
135
+
136
+ * Cookiecutter_
137
+ * `cookiecutter-djangopackage`_
138
+
139
+ .. _Cookiecutter: https://github.com/audreyr/cookiecutter
140
+ .. _`cookiecutter-djangopackage`: https://github.com/pydanny/cookiecutter-djangopackage
@@ -1,2 +1,2 @@
1
- __version__ = "0.3.0"
1
+ __version__ = "0.5.0"
2
2
  from .assert_element import AssertElementMixin # noqa
@@ -0,0 +1,78 @@
1
+ import html.parser
2
+ import re
3
+
4
+ import bs4 as bs
5
+
6
+
7
+ class MyHTMLFormatter(html.parser.HTMLParser):
8
+ def __init__(self, *args, **kwargs):
9
+ super().__init__(*args, **kwargs)
10
+ self.result = []
11
+
12
+ def handle_starttag(self, tag, attrs):
13
+ self.result.append(f"<{tag}")
14
+ for attr in attrs:
15
+ self.result.append(f' {attr[0]}="{attr[1]}"')
16
+ self.result.append(">")
17
+
18
+ def handle_endtag(self, tag):
19
+ self.result.append(f"</{tag}>")
20
+
21
+ def handle_data(self, data):
22
+ self.result.append(data)
23
+
24
+ def prettify(self):
25
+ return "\n".join(self.result)
26
+
27
+
28
+ def pretty_print_html(html_str):
29
+ """Pretty print HTML string"""
30
+ formatter = MyHTMLFormatter()
31
+ formatter.feed(html_str)
32
+ return formatter.prettify()
33
+
34
+
35
+ def sanitize_html(html_str):
36
+ """
37
+ Sanitize HTML string for reliable comparison.
38
+
39
+ Aggressively normalizes cosmetic whitespace differences (multiple spaces,
40
+ tabs, newlines, attribute spacing) while preserving semantically meaningful
41
+ structural differences. Focuses on HTML meaning rather than formatting.
42
+ """
43
+ # First, handle self-closing vs explicit closing tag normalization
44
+ # Use BeautifulSoup for structural normalization
45
+ soup = bs.BeautifulSoup(html_str, "html.parser")
46
+ structure_normalized = str(soup)
47
+
48
+ # Apply aggressive whitespace normalization for cosmetic differences
49
+ # Most whitespace variations are cosmetic and should be normalized
50
+
51
+ # Normalize line endings
52
+ normalized = structure_normalized.replace("\r\n", "\n").replace("\r", "\n")
53
+
54
+ # Use the original aggressive approach but be smarter about it
55
+ # Collapse all consecutive whitespace to single spaces, as browsers do
56
+ collapsed = re.sub(r"[\n\r \t]+", " ", normalized)
57
+
58
+ return pretty_print_html(collapsed.strip())
59
+
60
+
61
+ class AssertElementMixin:
62
+ def assertElementContains( # noqa
63
+ self,
64
+ request,
65
+ html_element="",
66
+ element_text="",
67
+ ):
68
+ content = request.content if hasattr(request, "content") else request
69
+ soup = bs.BeautifulSoup(content, "html.parser")
70
+ element = soup.select(html_element)
71
+ if len(element) == 0:
72
+ raise Exception(f"No element found: {html_element}")
73
+ if len(element) > 1:
74
+ raise Exception(f"More than one element found: {html_element}")
75
+ soup_1 = bs.BeautifulSoup(element_text, "html.parser")
76
+ element_txt = sanitize_html(element[0].prettify())
77
+ soup_1_txt = sanitize_html(soup_1.prettify())
78
+ self.assertEqual(element_txt, soup_1_txt)