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.
Files changed (26) hide show
  1. {webwidgets-0.1.0 → webwidgets-0.2.0}/PKG-INFO +1 -1
  2. webwidgets-0.2.0/tests/compilation/test_css.py +477 -0
  3. {webwidgets-0.1.0 → webwidgets-0.2.0}/tests/compilation/test_html_node.py +110 -1
  4. webwidgets-0.2.0/tests/compilation/test_html_tags.py +27 -0
  5. {webwidgets-0.1.0 → webwidgets-0.2.0}/tests/utility/test_sanitizing.py +3 -3
  6. webwidgets-0.2.0/tests/utility/test_validation.py +154 -0
  7. {webwidgets-0.1.0 → webwidgets-0.2.0}/webwidgets/__init__.py +1 -1
  8. {webwidgets-0.1.0 → webwidgets-0.2.0}/webwidgets/compilation/__init__.py +1 -0
  9. webwidgets-0.2.0/webwidgets/compilation/css/__init__.py +13 -0
  10. webwidgets-0.2.0/webwidgets/compilation/css/css.py +171 -0
  11. {webwidgets-0.1.0 → webwidgets-0.2.0}/webwidgets/compilation/html/__init__.py +2 -1
  12. {webwidgets-0.1.0 → webwidgets-0.2.0}/webwidgets/compilation/html/html_node.py +68 -10
  13. webwidgets-0.2.0/webwidgets/compilation/html/html_tags.py +40 -0
  14. {webwidgets-0.1.0 → webwidgets-0.2.0}/webwidgets/utility/__init__.py +1 -0
  15. {webwidgets-0.1.0 → webwidgets-0.2.0}/webwidgets/utility/sanitizing.py +2 -2
  16. webwidgets-0.2.0/webwidgets/utility/validation.py +97 -0
  17. {webwidgets-0.1.0 → webwidgets-0.2.0}/.github/workflows/cd.yml +0 -0
  18. {webwidgets-0.1.0 → webwidgets-0.2.0}/.github/workflows/ci-full.yml +0 -0
  19. {webwidgets-0.1.0 → webwidgets-0.2.0}/.github/workflows/ci-quick.yml +0 -0
  20. {webwidgets-0.1.0 → webwidgets-0.2.0}/.gitignore +0 -0
  21. {webwidgets-0.1.0 → webwidgets-0.2.0}/LICENSE +0 -0
  22. {webwidgets-0.1.0 → webwidgets-0.2.0}/README.md +0 -0
  23. {webwidgets-0.1.0 → webwidgets-0.2.0}/pyproject.toml +0 -0
  24. {webwidgets-0.1.0 → webwidgets-0.2.0}/tests/__init__.py +0 -0
  25. {webwidgets-0.1.0 → webwidgets-0.2.0}/tests/compilation/__init__.py +0 -0
  26. {webwidgets-0.1.0 → webwidgets-0.2.0}/tests/utility/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webwidgets
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A Python package for designing web UIs.
5
5
  Project-URL: Source code, https://github.com/mlaasri/WebWidgets
6
6
  Author: mlaasri
@@ -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, no_end_tag, RawText
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 _, e in CHAR_TO_HTML_ENTITIES.items())
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 test_html_entity_names(self, name):
25
+ def test_known_html_entities(self, name):
26
26
  assert name in HTML_ENTITIES
27
27
 
28
- def test_html_entities_inverted(self):
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;'