webwidgets 0.1.0__tar.gz → 0.2.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.
- {webwidgets-0.1.0 → webwidgets-0.2.0}/PKG-INFO +1 -1
- webwidgets-0.2.0/tests/compilation/test_css.py +477 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/tests/compilation/test_html_node.py +110 -1
- webwidgets-0.2.0/tests/compilation/test_html_tags.py +27 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/tests/utility/test_sanitizing.py +3 -3
- webwidgets-0.2.0/tests/utility/test_validation.py +154 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/webwidgets/__init__.py +1 -1
- {webwidgets-0.1.0 → webwidgets-0.2.0}/webwidgets/compilation/__init__.py +1 -0
- webwidgets-0.2.0/webwidgets/compilation/css/__init__.py +13 -0
- webwidgets-0.2.0/webwidgets/compilation/css/css.py +171 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/webwidgets/compilation/html/__init__.py +2 -1
- {webwidgets-0.1.0 → webwidgets-0.2.0}/webwidgets/compilation/html/html_node.py +68 -10
- webwidgets-0.2.0/webwidgets/compilation/html/html_tags.py +40 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/webwidgets/utility/__init__.py +1 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/webwidgets/utility/sanitizing.py +2 -2
- webwidgets-0.2.0/webwidgets/utility/validation.py +97 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/.github/workflows/cd.yml +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/.github/workflows/ci-full.yml +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/.github/workflows/ci-quick.yml +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/.gitignore +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/LICENSE +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/README.md +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/pyproject.toml +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/tests/__init__.py +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/tests/compilation/__init__.py +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.0}/tests/utility/__init__.py +0 -0
@@ -0,0 +1,477 @@
|
|
1
|
+
# =======================================================================
|
2
|
+
#
|
3
|
+
# This file is part of WebWidgets, a Python package for designing web
|
4
|
+
# UIs.
|
5
|
+
#
|
6
|
+
# You should have received a copy of the MIT License along with
|
7
|
+
# WebWidgets. If not, see <https://opensource.org/license/mit>.
|
8
|
+
#
|
9
|
+
# Copyright(C) 2025, mlaasri
|
10
|
+
#
|
11
|
+
# =======================================================================
|
12
|
+
|
13
|
+
import pytest
|
14
|
+
from webwidgets.compilation.html.html_node import HTMLNode
|
15
|
+
from webwidgets.compilation.html.html_tags import TextNode
|
16
|
+
from webwidgets.compilation.css.css import compile_css, CompiledCSS, apply_css
|
17
|
+
|
18
|
+
|
19
|
+
class TestCompileCSS:
|
20
|
+
def test_argument_type(self):
|
21
|
+
"""Compares compilation when given a node object versus a list of
|
22
|
+
nodes.
|
23
|
+
"""
|
24
|
+
# Create a tree
|
25
|
+
tree = HTMLNode(
|
26
|
+
style={"a": "5", "b": "4"},
|
27
|
+
children=[
|
28
|
+
HTMLNode(style={"a": "5"})
|
29
|
+
]
|
30
|
+
)
|
31
|
+
|
32
|
+
# Define expected compilation results
|
33
|
+
expected_rules = {
|
34
|
+
'r0': {'a': '5'},
|
35
|
+
'r1': {'b': '4'}
|
36
|
+
}
|
37
|
+
expected_mapping = {
|
38
|
+
id(tree): ['r0', 'r1'],
|
39
|
+
id(tree.children[0]): ['r0']
|
40
|
+
}
|
41
|
+
|
42
|
+
# Compile tree as single node object
|
43
|
+
compiled_css = compile_css(tree)
|
44
|
+
|
45
|
+
# Check results of compilation
|
46
|
+
assert compiled_css.trees == [tree]
|
47
|
+
assert [id(t) for t in compiled_css.trees] == [id(tree)]
|
48
|
+
assert compiled_css.rules == expected_rules
|
49
|
+
assert compiled_css.mapping == expected_mapping
|
50
|
+
|
51
|
+
# Compile tree as list of one node
|
52
|
+
compiled_css2 = compile_css([tree])
|
53
|
+
|
54
|
+
# Check results of compilation again (should be unchanged)
|
55
|
+
assert compiled_css2.trees == [tree]
|
56
|
+
assert [id(t) for t in compiled_css2.trees] == [id(tree)]
|
57
|
+
assert compiled_css2.rules == expected_rules
|
58
|
+
assert compiled_css2.mapping == expected_mapping
|
59
|
+
|
60
|
+
def test_basic_compilation(self):
|
61
|
+
# Create some HTML nodes with different styles
|
62
|
+
node1 = HTMLNode(style={"margin": "0", "padding": "0"})
|
63
|
+
node2 = HTMLNode(style={"margin": "0", "color": "blue"})
|
64
|
+
node3 = HTMLNode(style={"margin": "0", "padding": "0"})
|
65
|
+
|
66
|
+
# Compile the CSS for the trees
|
67
|
+
compiled_css = compile_css([node1, node2, node3])
|
68
|
+
|
69
|
+
# Check that the trees are correctly saved in the result
|
70
|
+
assert compiled_css.trees == [node1, node2, node3]
|
71
|
+
assert [id(t) for t in compiled_css.trees] == [
|
72
|
+
id(node1), id(node2), id(node3)]
|
73
|
+
|
74
|
+
# Check that the rules are correctly generated
|
75
|
+
expected_rules = {
|
76
|
+
'r0': {'color': 'blue'},
|
77
|
+
'r1': {'margin': '0'},
|
78
|
+
'r2': {'padding': '0'}
|
79
|
+
}
|
80
|
+
assert compiled_css.rules == expected_rules
|
81
|
+
|
82
|
+
# Check that the mapping is correctly generated
|
83
|
+
expected_mapping = {id(node1): ['r1', 'r2'], id(
|
84
|
+
node2): ['r0', 'r1'], id(node3): ['r1', 'r2']}
|
85
|
+
assert compiled_css.mapping == expected_mapping
|
86
|
+
|
87
|
+
def test_nested_compilation_one_tree(self):
|
88
|
+
# Create some nested HTML nodes
|
89
|
+
tree = HTMLNode(
|
90
|
+
style={"margin": "0", "padding": "0"},
|
91
|
+
children=[
|
92
|
+
TextNode("Hello World!", style={
|
93
|
+
"margin": "5", "color": "blue"}),
|
94
|
+
TextNode("Another text node", style={
|
95
|
+
"padding": "0", "color": "blue"})
|
96
|
+
]
|
97
|
+
)
|
98
|
+
|
99
|
+
# Compile the CSS for the tree
|
100
|
+
compiled_css = compile_css(tree)
|
101
|
+
|
102
|
+
# Check that the tree is correctly saved
|
103
|
+
assert compiled_css.trees == [tree]
|
104
|
+
assert [id(t) for t in compiled_css.trees] == [id(tree)]
|
105
|
+
|
106
|
+
# Check that the rules are correctly generated
|
107
|
+
expected_rules = {
|
108
|
+
'r0': {'color': 'blue'},
|
109
|
+
'r1': {'margin': '0'},
|
110
|
+
'r2': {'margin': '5'},
|
111
|
+
'r3': {'padding': '0'}
|
112
|
+
}
|
113
|
+
assert compiled_css.rules == expected_rules
|
114
|
+
|
115
|
+
# Check that the mapping is correctly generated
|
116
|
+
expected_mapping = {
|
117
|
+
id(tree): ['r1', 'r3'],
|
118
|
+
id(tree.children[0]): ['r0', 'r2'],
|
119
|
+
id(tree.children[1]): ['r0', 'r3'],
|
120
|
+
id(tree.children[0].children[0]): [],
|
121
|
+
id(tree.children[1].children[0]): []
|
122
|
+
}
|
123
|
+
assert compiled_css.mapping == expected_mapping
|
124
|
+
|
125
|
+
def test_nested_compilation_two_trees(self):
|
126
|
+
# Create 2 trees
|
127
|
+
tree1 = HTMLNode(
|
128
|
+
style={"margin": "10", "padding": "0"},
|
129
|
+
children=[
|
130
|
+
HTMLNode(style={"color": "red"})
|
131
|
+
]
|
132
|
+
)
|
133
|
+
tree2 = HTMLNode(
|
134
|
+
style={"margin": "5", "padding": "0"},
|
135
|
+
children=[
|
136
|
+
HTMLNode(style={"margin": "10"})
|
137
|
+
]
|
138
|
+
)
|
139
|
+
|
140
|
+
# Compile the CSS for the trees
|
141
|
+
compiled_css = compile_css([tree1, tree2])
|
142
|
+
|
143
|
+
# Check that the tree is correctly saved
|
144
|
+
assert compiled_css.trees == [tree1, tree2]
|
145
|
+
assert [id(t) for t in compiled_css.trees] == [
|
146
|
+
id(tree1), id(tree2)]
|
147
|
+
|
148
|
+
# Check that the rules are correctly generated
|
149
|
+
expected_rules = {
|
150
|
+
'r0': {'color': 'red'},
|
151
|
+
'r1': {'margin': '10'},
|
152
|
+
'r2': {'margin': '5'},
|
153
|
+
'r3': {'padding': '0'}
|
154
|
+
}
|
155
|
+
assert compiled_css.rules == expected_rules
|
156
|
+
|
157
|
+
# Check that the mapping is correctly generated
|
158
|
+
expected_mapping = {
|
159
|
+
id(tree1): ['r1', 'r3'],
|
160
|
+
id(tree1.children[0]): ['r0'],
|
161
|
+
id(tree2): ['r2', 'r3'],
|
162
|
+
id(tree2.children[0]): ['r1']
|
163
|
+
}
|
164
|
+
assert compiled_css.mapping == expected_mapping
|
165
|
+
|
166
|
+
def test_rules_numbered_in_order(self):
|
167
|
+
"""Test that rules are numbered in lexicographical order"""
|
168
|
+
tree = HTMLNode(
|
169
|
+
style={"a": "5", "b": "4"},
|
170
|
+
children=[
|
171
|
+
HTMLNode(style={"a": "10"}),
|
172
|
+
HTMLNode(style={"b": "10"}),
|
173
|
+
HTMLNode(style={"c": "5"})
|
174
|
+
]
|
175
|
+
)
|
176
|
+
compiled_css = compile_css(tree)
|
177
|
+
expected_rules = {
|
178
|
+
'r0': {'a': '10'},
|
179
|
+
'r1': {'a': '5'},
|
180
|
+
'r2': {'b': '10'},
|
181
|
+
'r3': {'b': '4'},
|
182
|
+
'r4': {'c': '5'}
|
183
|
+
}
|
184
|
+
assert compiled_css.rules == expected_rules
|
185
|
+
|
186
|
+
def test_duplicate_node(self):
|
187
|
+
"""Test that adding the same node twice does not impact compilation"""
|
188
|
+
# Compiling a tree
|
189
|
+
tree = HTMLNode(
|
190
|
+
style={"a": "5", "b": "4"},
|
191
|
+
children=[
|
192
|
+
HTMLNode(style={"a": "5"}),
|
193
|
+
HTMLNode(style={"b": "10"}),
|
194
|
+
]
|
195
|
+
)
|
196
|
+
expected_rules = {
|
197
|
+
'r0': {'a': '5'},
|
198
|
+
'r1': {'b': '10'},
|
199
|
+
'r2': {'b': '4'}
|
200
|
+
}
|
201
|
+
expected_mapping = {
|
202
|
+
id(tree): ['r0', 'r2'],
|
203
|
+
id(tree.children[0]): ['r0'],
|
204
|
+
id(tree.children[1]): ['r1']
|
205
|
+
}
|
206
|
+
compiled_css = compile_css([tree])
|
207
|
+
assert compiled_css.trees == [tree]
|
208
|
+
assert [id(t) for t in compiled_css.trees] == [id(tree)]
|
209
|
+
assert compiled_css.rules == expected_rules
|
210
|
+
assert compiled_css.mapping == expected_mapping
|
211
|
+
|
212
|
+
# Compiling the tree and one of its children, which should already be
|
213
|
+
# included recursively from the tree itself and should not affect the
|
214
|
+
# result
|
215
|
+
compiled_css2 = compile_css([tree, tree.children[0]])
|
216
|
+
assert compiled_css2.trees == [tree, tree.children[0]]
|
217
|
+
assert [id(t) for t in compiled_css2.trees] == [
|
218
|
+
id(tree), id(tree.children[0])]
|
219
|
+
assert compiled_css2.rules == expected_rules
|
220
|
+
assert compiled_css2.mapping == expected_mapping
|
221
|
+
|
222
|
+
|
223
|
+
class TestCompiledCSS:
|
224
|
+
def test_export_custom_compiled_css(self):
|
225
|
+
rules = {
|
226
|
+
"r0": {
|
227
|
+
"margin": "0",
|
228
|
+
"padding": "0"
|
229
|
+
},
|
230
|
+
"r1": {
|
231
|
+
"color": "blue"
|
232
|
+
},
|
233
|
+
"r2": {
|
234
|
+
"background-color": "white",
|
235
|
+
"font-size": "16px"
|
236
|
+
}
|
237
|
+
}
|
238
|
+
compiled_css = CompiledCSS(trees=None,
|
239
|
+
rules=rules,
|
240
|
+
mapping=None)
|
241
|
+
expected_css = '\n'.join([
|
242
|
+
".r0 {",
|
243
|
+
" margin: 0;",
|
244
|
+
" padding: 0;",
|
245
|
+
"}",
|
246
|
+
"",
|
247
|
+
".r1 {",
|
248
|
+
" color: blue;",
|
249
|
+
"}",
|
250
|
+
"",
|
251
|
+
".r2 {",
|
252
|
+
" background-color: white;",
|
253
|
+
" font-size: 16px;",
|
254
|
+
"}"
|
255
|
+
])
|
256
|
+
assert compiled_css.to_css() == expected_css
|
257
|
+
|
258
|
+
def test_export_real_compiled_css(self):
|
259
|
+
tree = HTMLNode(
|
260
|
+
style={"margin": "0", "padding": "0"},
|
261
|
+
children=[
|
262
|
+
TextNode("a", style={"margin": "0", "color": "blue"}),
|
263
|
+
HTMLNode(style={"margin": "0", "color": "green"})
|
264
|
+
]
|
265
|
+
)
|
266
|
+
compiled_css = compile_css(tree)
|
267
|
+
expected_css = '\n'.join([
|
268
|
+
".r0 {",
|
269
|
+
" color: blue;",
|
270
|
+
"}",
|
271
|
+
"",
|
272
|
+
".r1 {",
|
273
|
+
" color: green;",
|
274
|
+
"}",
|
275
|
+
"",
|
276
|
+
".r2 {",
|
277
|
+
" margin: 0;",
|
278
|
+
"}",
|
279
|
+
"",
|
280
|
+
".r3 {",
|
281
|
+
" padding: 0;",
|
282
|
+
"}"
|
283
|
+
])
|
284
|
+
assert compiled_css.to_css() == expected_css
|
285
|
+
|
286
|
+
def test_export_empty_style(self):
|
287
|
+
node = HTMLNode()
|
288
|
+
css = compile_css(node).to_css()
|
289
|
+
assert css == ""
|
290
|
+
other_css = CompiledCSS(trees=None,
|
291
|
+
rules={},
|
292
|
+
mapping=None).to_css()
|
293
|
+
assert other_css == ""
|
294
|
+
|
295
|
+
def test_export_invalid_style(self):
|
296
|
+
node = HTMLNode(style={"marg!in": "0", "padding": "0"})
|
297
|
+
compiled_css = compile_css(node)
|
298
|
+
with pytest.raises(ValueError, match="marg!in"):
|
299
|
+
compiled_css.to_css()
|
300
|
+
|
301
|
+
@pytest.mark.parametrize("indent_size", [0, 2, 4, 8])
|
302
|
+
def test_css_indentation(self, indent_size):
|
303
|
+
node = HTMLNode(style={"a": "0", "b": "1"})
|
304
|
+
expected_css = '\n'.join([
|
305
|
+
".r0 {",
|
306
|
+
f"{' ' * indent_size}a: 0;",
|
307
|
+
"}",
|
308
|
+
"",
|
309
|
+
".r1 {",
|
310
|
+
f"{' ' * indent_size}b: 1;",
|
311
|
+
"}"
|
312
|
+
])
|
313
|
+
css = compile_css(node).to_css(indent_size=indent_size)
|
314
|
+
assert css == expected_css
|
315
|
+
|
316
|
+
|
317
|
+
class TestApplyCSS:
|
318
|
+
@pytest.mark.parametrize("class_in, class_out", [
|
319
|
+
(None, "r0 r1"), # No class attribute
|
320
|
+
("", "r0 r1"), # Empty class
|
321
|
+
("z", "z r0 r1"), # Existing class
|
322
|
+
("r1", "r1 r0"), # Existing rule
|
323
|
+
("z r1", "z r1 r0"), # Existing class and rule
|
324
|
+
("r1 z", "r1 z r0") # Existing rule and class
|
325
|
+
])
|
326
|
+
def test_apply_css_to_node(self, class_in, class_out):
|
327
|
+
tree = HTMLNode(attributes=None if class_in is None else {"class": class_in},
|
328
|
+
style={"a": "0", "b": "1"})
|
329
|
+
apply_css(compile_css(tree), tree)
|
330
|
+
assert tree.attributes["class"] == class_out
|
331
|
+
assert tree.to_html() == f'<htmlnode class="{class_out}"></htmlnode>'
|
332
|
+
|
333
|
+
@pytest.mark.parametrize("cl1_in, cl1_out", [
|
334
|
+
(None, "r2 r3"), # No class attribute
|
335
|
+
("", "r2 r3"), # Empty class
|
336
|
+
("c", "c r2 r3"), # Existing class
|
337
|
+
("r3", "r3 r2"), # Existing rule
|
338
|
+
("c r3", "c r3 r2"), # Existing class and rule
|
339
|
+
("r3 c", "r3 c r2"), # Existing rule and class
|
340
|
+
("rr3", "rr3 r2 r3") # Rule decoy
|
341
|
+
])
|
342
|
+
@pytest.mark.parametrize("cl2_in, cl2_out", [
|
343
|
+
(None, "r1 r2"), # No class attribute
|
344
|
+
("", "r1 r2"), # Empty class
|
345
|
+
("z", "z r1 r2"), # Existing class
|
346
|
+
("r1", "r1 r2"), # Existing rule
|
347
|
+
("z r1", "z r1 r2"), # Existing class and rule
|
348
|
+
("r1 z", "r1 z r2"), # Existing rule and class
|
349
|
+
("rr1", "rr1 r1 r2") # Rule decoy
|
350
|
+
])
|
351
|
+
@pytest.mark.parametrize("mix", [False, True])
|
352
|
+
def test_apply_css_to_tree(self, cl1_in, cl1_out, cl2_in, cl2_out, mix):
|
353
|
+
# Creating a tree with some nodes and styles
|
354
|
+
tree = HTMLNode(
|
355
|
+
attributes=None if cl1_in is None else {"class": cl1_in},
|
356
|
+
style={"margin": "0", "padding": "0"},
|
357
|
+
children=[
|
358
|
+
TextNode("a", style={"margin": "0", "color": "blue"}) if mix
|
359
|
+
else HTMLNode(style={"margin": "0", "color": "blue"}),
|
360
|
+
HTMLNode(attributes=None if cl2_in is None else {"class": cl2_in},
|
361
|
+
style={"margin": "0", "color": "green"})
|
362
|
+
]
|
363
|
+
)
|
364
|
+
|
365
|
+
# Compiling and applying CSS to the tree
|
366
|
+
compiled_css = compile_css(tree)
|
367
|
+
assert compiled_css.rules == {
|
368
|
+
"r0": {"color": "blue"},
|
369
|
+
"r1": {"color": "green"},
|
370
|
+
"r2": {"margin": "0"},
|
371
|
+
"r3": {"padding": "0"}
|
372
|
+
}
|
373
|
+
apply_css(compiled_css, tree)
|
374
|
+
|
375
|
+
# Checking the tree's new classes
|
376
|
+
assert tree.attributes["class"] == cl1_out
|
377
|
+
assert tree.children[0].attributes["class"] == "r0 r2"
|
378
|
+
assert tree.children[1].attributes["class"] == cl2_out
|
379
|
+
|
380
|
+
# Checking the final HTML code
|
381
|
+
mix_node = '<textnode class="r0 r2">a</textnode>' if mix else \
|
382
|
+
'<htmlnode class="r0 r2"></htmlnode>'
|
383
|
+
expected_html = '\n'.join([
|
384
|
+
f'<htmlnode class="{cl1_out}">',
|
385
|
+
f' {mix_node}',
|
386
|
+
f' <htmlnode class="{cl2_out}"></htmlnode>',
|
387
|
+
'</htmlnode>'
|
388
|
+
])
|
389
|
+
assert tree.to_html() == expected_html
|
390
|
+
|
391
|
+
def test_apply_css_without_styles(self):
|
392
|
+
# Compiling and applying CSS to a tree with no styles
|
393
|
+
tree = HTMLNode(
|
394
|
+
children=[
|
395
|
+
TextNode("a"),
|
396
|
+
HTMLNode(attributes={"class": "z"})
|
397
|
+
]
|
398
|
+
)
|
399
|
+
html_before = tree.to_html()
|
400
|
+
compiled_css = compile_css(tree)
|
401
|
+
assert compiled_css.rules == {}
|
402
|
+
apply_css(compiled_css, tree)
|
403
|
+
html_after = tree.to_html()
|
404
|
+
|
405
|
+
# Checking the tree's new classes
|
406
|
+
assert "class" not in tree.attributes
|
407
|
+
assert "class" not in tree.children[0].attributes
|
408
|
+
assert tree.children[1].attributes["class"] == "z"
|
409
|
+
|
410
|
+
# Checking the final HTML code
|
411
|
+
expected_html = '\n'.join([
|
412
|
+
'<htmlnode>',
|
413
|
+
' <textnode>a</textnode>',
|
414
|
+
' <htmlnode class="z"></htmlnode>',
|
415
|
+
'</htmlnode>'
|
416
|
+
])
|
417
|
+
assert html_before == expected_html
|
418
|
+
assert html_after == expected_html
|
419
|
+
|
420
|
+
def test_apply_css_with_partial_styles(self):
|
421
|
+
# Compiling and applying CSS to a tree where some nodes have styles but
|
422
|
+
# others do not
|
423
|
+
tree = HTMLNode(
|
424
|
+
children=[
|
425
|
+
TextNode("a", style={"margin": "0", "color": "blue"}),
|
426
|
+
HTMLNode(attributes={"class": "z"})
|
427
|
+
]
|
428
|
+
)
|
429
|
+
compiled_css = compile_css(tree)
|
430
|
+
apply_css(compiled_css, tree)
|
431
|
+
|
432
|
+
# Checking the tree's new classes
|
433
|
+
assert "class" not in tree.attributes
|
434
|
+
assert tree.children[0].attributes["class"] == "r0 r1"
|
435
|
+
assert tree.children[1].attributes["class"] == "z"
|
436
|
+
|
437
|
+
# Checking the final HTML code
|
438
|
+
expected_html = '\n'.join([
|
439
|
+
'<htmlnode>',
|
440
|
+
' <textnode class="r0 r1">a</textnode>',
|
441
|
+
' <htmlnode class="z"></htmlnode>',
|
442
|
+
'</htmlnode>'
|
443
|
+
])
|
444
|
+
assert tree.to_html() == expected_html
|
445
|
+
|
446
|
+
@pytest.mark.parametrize("class_in, class_out", [
|
447
|
+
(None, "r0 r1"),
|
448
|
+
("", "r0 r1"),
|
449
|
+
("z", "z r0 r1"),
|
450
|
+
("r0", "r0 r1"),
|
451
|
+
("r1", "r1 r0"),
|
452
|
+
])
|
453
|
+
def test_apply_css_multiple_times(self, class_in, class_out):
|
454
|
+
tree = HTMLNode(style={"a": "0", "b": "1"}) if class_in is None else \
|
455
|
+
HTMLNode(attributes={"class": class_in},
|
456
|
+
style={"a": "0", "b": "1"})
|
457
|
+
html_before = '<htmlnode></htmlnode>' if class_in is None else \
|
458
|
+
f'<htmlnode class="{class_in}"></htmlnode>'
|
459
|
+
html_after = f'<htmlnode class="{class_out}"></htmlnode>'
|
460
|
+
|
461
|
+
assert tree.to_html() == html_before
|
462
|
+
compiled_css = compile_css(tree)
|
463
|
+
apply_css(compiled_css, tree)
|
464
|
+
assert tree.attributes["class"] == class_out
|
465
|
+
assert tree.to_html() == html_after
|
466
|
+
apply_css(compiled_css, tree)
|
467
|
+
assert tree.attributes["class"] == class_out
|
468
|
+
assert tree.to_html() == html_after
|
469
|
+
|
470
|
+
def test_empty_style(self):
|
471
|
+
"""Tests that no classes are added if style exists but is empty."""
|
472
|
+
tree = HTMLNode(style={})
|
473
|
+
assert tree.to_html() == '<htmlnode></htmlnode>'
|
474
|
+
compiled_css = compile_css(tree)
|
475
|
+
apply_css(compiled_css, tree)
|
476
|
+
assert "class" not in tree.attributes
|
477
|
+
assert tree.to_html() == '<htmlnode></htmlnode>'
|
@@ -11,7 +11,8 @@
|
|
11
11
|
# =======================================================================
|
12
12
|
|
13
13
|
import pytest
|
14
|
-
from webwidgets.compilation.html.html_node import HTMLNode, no_start_tag,
|
14
|
+
from webwidgets.compilation.html.html_node import HTMLNode, no_start_tag, \
|
15
|
+
no_end_tag, one_line, RawText
|
15
16
|
|
16
17
|
|
17
18
|
class TestHTMLNode:
|
@@ -37,6 +38,10 @@ class TestHTMLNode:
|
|
37
38
|
class OneLineNoStartNode(NoStartNode):
|
38
39
|
one_line = True
|
39
40
|
|
41
|
+
@one_line
|
42
|
+
class OneLineDecoratorNode(HTMLNode):
|
43
|
+
pass
|
44
|
+
|
40
45
|
class KwargsReceiverNode(HTMLNode):
|
41
46
|
def to_html(self, return_lines: bool, message: str,
|
42
47
|
**kwargs):
|
@@ -98,6 +103,15 @@ class TestHTMLNode:
|
|
98
103
|
expected_html = "<noendnode>child1child2"
|
99
104
|
assert node.to_html(force_one_line=True) == expected_html
|
100
105
|
|
106
|
+
def test_one_line_decorator(self):
|
107
|
+
inner_node = HTMLNode(children=[RawText('inner_child')])
|
108
|
+
node = TestHTMLNode.OneLineDecoratorNode(
|
109
|
+
children=[inner_node]
|
110
|
+
)
|
111
|
+
expected_html = "<onelinedecoratornode>" + \
|
112
|
+
"<htmlnode>inner_child</htmlnode></onelinedecoratornode>"
|
113
|
+
assert node.to_html() == expected_html
|
114
|
+
|
101
115
|
def test_recursive_rendering(self):
|
102
116
|
inner_node = HTMLNode(children=[RawText('inner_child')])
|
103
117
|
node = TestHTMLNode.CustomNode(children=[inner_node])
|
@@ -332,3 +346,98 @@ class TestHTMLNode:
|
|
332
346
|
"</htmlnode>"
|
333
347
|
])
|
334
348
|
assert node.to_html(replace_all_entities=True) == expected_html
|
349
|
+
|
350
|
+
def test_get_styles_no_children(self):
|
351
|
+
node = HTMLNode()
|
352
|
+
assert node.get_styles() == {id(node): {}}
|
353
|
+
|
354
|
+
def test_get_styles_no_children_with_style(self):
|
355
|
+
node = HTMLNode(style={"color": "red"})
|
356
|
+
assert node.get_styles() == {id(node): {"color": "red"}}
|
357
|
+
|
358
|
+
def test_get_styles(self):
|
359
|
+
inner_1 = HTMLNode(style={"color": "red"})
|
360
|
+
inner_2 = HTMLNode(style={"margin": "0"})
|
361
|
+
node = HTMLNode(children=[inner_1, inner_2],
|
362
|
+
style={"font-size": "20px"})
|
363
|
+
assert node.get_styles() == {
|
364
|
+
id(inner_1): {"color": "red"},
|
365
|
+
id(inner_2): {"margin": "0"},
|
366
|
+
id(node): {"font-size": "20px"},
|
367
|
+
}
|
368
|
+
|
369
|
+
def test_get_styles_deeper_tree(self):
|
370
|
+
grandchild_1 = HTMLNode(style={"color": "red"})
|
371
|
+
grandchild_2 = HTMLNode(style={"margin": "0"})
|
372
|
+
child_1 = HTMLNode(children=[grandchild_1, grandchild_2],
|
373
|
+
style={"font-size": "20px"})
|
374
|
+
grandchild_3 = HTMLNode(style={"background-color": "blue"})
|
375
|
+
child_2 = HTMLNode(children=[grandchild_3],
|
376
|
+
style={"font-weight": "bold"})
|
377
|
+
node = HTMLNode(children=[child_1, child_2],
|
378
|
+
style={"padding": "5px"})
|
379
|
+
|
380
|
+
assert node.get_styles() == {
|
381
|
+
id(grandchild_1): {"color": "red"},
|
382
|
+
id(grandchild_2): {"margin": "0"},
|
383
|
+
id(child_1): {"font-size": "20px"},
|
384
|
+
id(grandchild_3): {"background-color": "blue"},
|
385
|
+
id(child_2): {"font-weight": "bold"},
|
386
|
+
id(node): {"padding": "5px"},
|
387
|
+
}
|
388
|
+
|
389
|
+
def test_shallow_copy(self):
|
390
|
+
node = HTMLNode(style={"color": "red"})
|
391
|
+
copied_node = node.copy(deep=False)
|
392
|
+
assert id(copied_node) != id(node)
|
393
|
+
assert copied_node.style == {"color": "red"}
|
394
|
+
child = RawText("text")
|
395
|
+
copied_node.children.append(child)
|
396
|
+
assert len(copied_node.children) == 1
|
397
|
+
assert id(copied_node.children[0]) == id(child)
|
398
|
+
assert len(node.children) == 1
|
399
|
+
assert id(node.children[0]) == id(child)
|
400
|
+
|
401
|
+
def test_shallow_copy_nested(self):
|
402
|
+
node = HTMLNode(
|
403
|
+
style={"a": "0"},
|
404
|
+
children=[HTMLNode(style={"a": "1"})]
|
405
|
+
)
|
406
|
+
copied_node = node.copy(deep=False)
|
407
|
+
assert id(copied_node) != id(node)
|
408
|
+
assert copied_node.style == {"a": "0"}
|
409
|
+
assert [id(c) for c in copied_node.children] == [
|
410
|
+
id(c) for c in node.children]
|
411
|
+
copied_node.children[0].style["a"] = "2"
|
412
|
+
assert node.children[0].style == {"a": "2"}
|
413
|
+
|
414
|
+
def test_deep_copy(self):
|
415
|
+
node = HTMLNode(style={"color": "red"})
|
416
|
+
copied_node = node.copy(deep=True)
|
417
|
+
assert id(copied_node) != id(node)
|
418
|
+
assert copied_node.style == {"color": "red"}
|
419
|
+
copied_node.children.append(RawText("text"))
|
420
|
+
assert len(copied_node.children) == 1
|
421
|
+
assert len(node.children) == 0
|
422
|
+
|
423
|
+
def test_deep_copy_nested(self):
|
424
|
+
node = HTMLNode(
|
425
|
+
style={"a": "0"},
|
426
|
+
children=[HTMLNode(style={"a": "1"})]
|
427
|
+
)
|
428
|
+
copied_node = node.copy(deep=True)
|
429
|
+
assert id(copied_node) != id(node)
|
430
|
+
assert copied_node.style == {"a": "0"}
|
431
|
+
assert id(copied_node.children[0]) != id(node.children[0])
|
432
|
+
assert copied_node.children[0].style == {"a": "1"}
|
433
|
+
copied_node.children[0].style["a"] = "2"
|
434
|
+
assert node.children[0].style == {"a": "1"}
|
435
|
+
|
436
|
+
def test_copy_default(self):
|
437
|
+
"""Tests that the default copy is a shallow copy"""
|
438
|
+
node = HTMLNode(style={"color": "red"})
|
439
|
+
copied_node = node.copy()
|
440
|
+
child = RawText("text")
|
441
|
+
copied_node.children.append(child)
|
442
|
+
assert len(node.children) == 1
|
443
|
+
assert id(node.children[0]) == id(child)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# =======================================================================
|
2
|
+
#
|
3
|
+
# This file is part of WebWidgets, a Python package for designing web
|
4
|
+
# UIs.
|
5
|
+
#
|
6
|
+
# You should have received a copy of the MIT License along with
|
7
|
+
# WebWidgets. If not, see <https://opensource.org/license/mit>.
|
8
|
+
#
|
9
|
+
# Copyright(C) 2025, mlaasri
|
10
|
+
#
|
11
|
+
# =======================================================================
|
12
|
+
|
13
|
+
from webwidgets.compilation.html.html_tags import *
|
14
|
+
|
15
|
+
|
16
|
+
class TestHTMLTags:
|
17
|
+
def test_text_node(self):
|
18
|
+
text_node = TextNode("Hello, World!")
|
19
|
+
assert text_node.to_html() == "<textnode>Hello, World!</textnode>"
|
20
|
+
|
21
|
+
def test_text_node_with_attributes(self):
|
22
|
+
text_node = TextNode("Hello, World!",
|
23
|
+
attributes={"class": "my-class", "id": "my-id"})
|
24
|
+
assert text_node.to_html() in [
|
25
|
+
'<textnode class="my-class" id="my-id">Hello, World!</textnode>',
|
26
|
+
'<textnode id="my-id" class="my-class">Hello, World!</textnode>',
|
27
|
+
]
|
@@ -17,15 +17,15 @@ from webwidgets.utility.sanitizing import HTML_ENTITIES, \
|
|
17
17
|
|
18
18
|
class TestSanitizingHTMLText:
|
19
19
|
def test_no_empty_html_entities(self):
|
20
|
-
assert all(e for
|
20
|
+
assert all(e for e in CHAR_TO_HTML_ENTITIES.values())
|
21
21
|
|
22
22
|
@pytest.mark.parametrize("name", [
|
23
23
|
'amp;', 'lt;', 'gt;', 'semi;', 'sol;', 'apos;', 'quot;'
|
24
24
|
])
|
25
|
-
def
|
25
|
+
def test_known_html_entities(self, name):
|
26
26
|
assert name in HTML_ENTITIES
|
27
27
|
|
28
|
-
def
|
28
|
+
def test_char_to_html_entities(self):
|
29
29
|
assert set(CHAR_TO_HTML_ENTITIES['&']) == set((
|
30
30
|
'amp;', 'AMP', 'amp', 'AMP;'))
|
31
31
|
assert CHAR_TO_HTML_ENTITIES['&'][0] == 'amp;'
|