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.
- assert_element-0.5.0/.github/workflows/main.yml +104 -0
- assert_element-0.5.0/HISTORY.rst +31 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/MANIFEST.in +1 -0
- assert_element-0.5.0/PKG-INFO +207 -0
- assert_element-0.5.0/README.rst +140 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/assert_element/__init__.py +1 -1
- assert_element-0.5.0/assert_element/assert_element.py +78 -0
- assert_element-0.5.0/assert_element.egg-info/PKG-INFO +207 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/requirements_test.txt +1 -1
- {assert_element-0.3.0 → assert_element-0.5.0}/setup.cfg +1 -4
- {assert_element-0.3.0 → assert_element-0.5.0}/tests/settings.py +0 -2
- assert_element-0.5.0/tests/test_models.py +334 -0
- assert_element-0.5.0/tox.ini +31 -0
- assert_element-0.3.0/.github/workflows/main.yml +0 -66
- assert_element-0.3.0/HISTORY.rst +0 -19
- assert_element-0.3.0/PKG-INFO +0 -135
- assert_element-0.3.0/README.rst +0 -92
- assert_element-0.3.0/assert_element/assert_element.py +0 -28
- assert_element-0.3.0/assert_element.egg-info/PKG-INFO +0 -135
- assert_element-0.3.0/tests/test_models.py +0 -58
- assert_element-0.3.0/tox.ini +0 -16
- {assert_element-0.3.0 → assert_element-0.5.0}/.coveragerc +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/.editorconfig +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/.github/ISSUE_TEMPLATE.md +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/.gitignore +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/AUTHORS.rst +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/CONTRIBUTING.rst +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/LICENSE +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/assert_element.egg-info/SOURCES.txt +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/assert_element.egg-info/dependency_links.txt +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/assert_element.egg-info/not-zip-safe +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/assert_element.egg-info/requires.txt +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/assert_element.egg-info/top_level.txt +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/docs/Makefile +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/docs/authors.rst +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/docs/conf.py +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/docs/contributing.rst +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/docs/history.rst +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/docs/index.rst +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/docs/installation.rst +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/docs/make.bat +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/docs/readme.rst +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/docs/usage.rst +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/manage.py +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/mypy.ini +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/requirements.txt +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/requirements_dev.txt +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/runtests.py +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/setup.py +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/tasks.py +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/tests/README.md +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/tests/__init__.py +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/tests/requirements.txt +0 -0
- {assert_element-0.3.0 → assert_element-0.5.0}/tests/urls.py +0 -0
- {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.
|
|
@@ -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.
|
|
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)
|