webwidgets 0.1.0__tar.gz → 0.2.1__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 (28) hide show
  1. {webwidgets-0.1.0 → webwidgets-0.2.1}/PKG-INFO +1 -1
  2. webwidgets-0.2.1/tests/compilation/test_css.py +552 -0
  3. {webwidgets-0.1.0 → webwidgets-0.2.1}/tests/compilation/test_html_node.py +110 -1
  4. webwidgets-0.2.1/tests/compilation/test_html_tags.py +27 -0
  5. webwidgets-0.2.1/tests/utility/test_representation.py +118 -0
  6. {webwidgets-0.1.0 → webwidgets-0.2.1}/tests/utility/test_sanitizing.py +3 -3
  7. webwidgets-0.2.1/tests/utility/test_validation.py +218 -0
  8. {webwidgets-0.1.0 → webwidgets-0.2.1}/webwidgets/__init__.py +1 -1
  9. {webwidgets-0.1.0 → webwidgets-0.2.1}/webwidgets/compilation/__init__.py +1 -0
  10. {webwidgets-0.1.0/webwidgets/compilation/html → webwidgets-0.2.1/webwidgets/compilation/css}/__init__.py +2 -1
  11. webwidgets-0.2.1/webwidgets/compilation/css/css.py +229 -0
  12. webwidgets-0.2.1/webwidgets/compilation/html/__init__.py +14 -0
  13. {webwidgets-0.1.0 → webwidgets-0.2.1}/webwidgets/compilation/html/html_node.py +71 -11
  14. webwidgets-0.2.1/webwidgets/compilation/html/html_tags.py +40 -0
  15. {webwidgets-0.1.0 → webwidgets-0.2.1}/webwidgets/utility/__init__.py +2 -0
  16. webwidgets-0.2.1/webwidgets/utility/representation.py +34 -0
  17. {webwidgets-0.1.0 → webwidgets-0.2.1}/webwidgets/utility/sanitizing.py +2 -2
  18. webwidgets-0.2.1/webwidgets/utility/validation.py +97 -0
  19. {webwidgets-0.1.0 → webwidgets-0.2.1}/.github/workflows/cd.yml +0 -0
  20. {webwidgets-0.1.0 → webwidgets-0.2.1}/.github/workflows/ci-full.yml +0 -0
  21. {webwidgets-0.1.0 → webwidgets-0.2.1}/.github/workflows/ci-quick.yml +0 -0
  22. {webwidgets-0.1.0 → webwidgets-0.2.1}/.gitignore +0 -0
  23. {webwidgets-0.1.0 → webwidgets-0.2.1}/LICENSE +0 -0
  24. {webwidgets-0.1.0 → webwidgets-0.2.1}/README.md +0 -0
  25. {webwidgets-0.1.0 → webwidgets-0.2.1}/pyproject.toml +0 -0
  26. {webwidgets-0.1.0 → webwidgets-0.2.1}/tests/__init__.py +0 -0
  27. {webwidgets-0.1.0 → webwidgets-0.2.1}/tests/compilation/__init__.py +0 -0
  28. {webwidgets-0.1.0 → webwidgets-0.2.1}/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.1
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,552 @@
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 typing import Any, Dict, List
15
+ from webwidgets.compilation.html.html_node import HTMLNode
16
+ from webwidgets.compilation.html.html_tags import TextNode
17
+ from webwidgets.compilation.css.css import compile_css, CSSRule, CompiledCSS, \
18
+ apply_css, default_rule_namer
19
+
20
+
21
+ class TestCompileCSS:
22
+ @staticmethod
23
+ def _serialize_rules(rules: List[CSSRule]) -> List[Dict[str, Any]]:
24
+ """Utility function to convert a list of :py:class:`CSSRule` objects
25
+ into a dictionary that can be used in testing.
26
+
27
+ :param rules: List of :py:class:`CSSRule` objects.
28
+ :type rules: List[CSSRule]
29
+ :return: List of the member variables of each :py:class:`CSSRule`.
30
+ :rtype: Dict[int, Any]
31
+ """
32
+ return [vars(rule) for rule in rules]
33
+
34
+ @staticmethod
35
+ def _serialize_mapping(mapping: Dict[int, List[CSSRule]]) -> Dict[int, List[str]]:
36
+ """Utility function to convert a :py:attr:`CompiledCSS.mapping` object
37
+ into a dictionary that can be used in testing.
38
+
39
+ :param mapping: :py:attr:`CompiledCSS.mapping` object.
40
+ :type mapping: Dict[int, List[CSSRule]]
41
+ :return: Dictionary mapping each node ID to the name of the rules that
42
+ achieve the same style.
43
+ """
44
+ return {i: [r.name for r in rules] for i, rules in mapping.items()}
45
+
46
+ def test_argument_type(self):
47
+ """Compares compilation when given a node object versus a list of
48
+ nodes.
49
+ """
50
+ # Create a tree
51
+ tree = HTMLNode(
52
+ style={"a": "5", "b": "4"},
53
+ children=[
54
+ HTMLNode(style={"a": "5"})
55
+ ]
56
+ )
57
+
58
+ # Define expected compilation results
59
+ expected_rules = [
60
+ {"name": "r0", "declarations": {"a": "5"}},
61
+ {"name": "r1", "declarations": {"b": "4"}}
62
+ ]
63
+ expected_mapping = {
64
+ id(tree): ['r0', 'r1'],
65
+ id(tree.children[0]): ['r0']
66
+ }
67
+
68
+ # Compile tree as single node object
69
+ compiled_css = compile_css(tree)
70
+
71
+ # Check results of compilation
72
+ assert compiled_css.trees == [tree]
73
+ assert [id(t) for t in compiled_css.trees] == [id(tree)]
74
+ assert TestCompileCSS._serialize_rules(
75
+ compiled_css.rules) == expected_rules
76
+ assert TestCompileCSS._serialize_mapping(
77
+ compiled_css.mapping) == expected_mapping
78
+
79
+ # Compile tree as list of one node
80
+ compiled_css2 = compile_css([tree])
81
+
82
+ # Check results of compilation again (should be unchanged)
83
+ assert compiled_css2.trees == [tree]
84
+ assert [id(t) for t in compiled_css2.trees] == [id(tree)]
85
+ assert TestCompileCSS._serialize_rules(
86
+ compiled_css2.rules) == expected_rules
87
+ assert TestCompileCSS._serialize_mapping(
88
+ compiled_css2.mapping) == expected_mapping
89
+
90
+ def test_basic_compilation(self):
91
+ # Create some HTML nodes with different styles
92
+ node1 = HTMLNode(style={"margin": "0", "padding": "0"})
93
+ node2 = HTMLNode(style={"margin": "0", "color": "blue"})
94
+ node3 = HTMLNode(style={"margin": "0", "padding": "0"})
95
+
96
+ # Compile the CSS for the trees
97
+ compiled_css = compile_css([node1, node2, node3])
98
+
99
+ # Check that the trees are correctly saved in the result
100
+ assert compiled_css.trees == [node1, node2, node3]
101
+ assert [id(t) for t in compiled_css.trees] == [
102
+ id(node1), id(node2), id(node3)]
103
+
104
+ # Check that the rules are correctly generated
105
+ expected_rules = [
106
+ {"name": "r0", "declarations": {"color": "blue"}},
107
+ {"name": "r1", "declarations": {"margin": "0"}},
108
+ {"name": "r2", "declarations": {"padding": "0"}}
109
+ ]
110
+ assert TestCompileCSS._serialize_rules(
111
+ compiled_css.rules) == expected_rules
112
+
113
+ # Check that the mapping is correctly generated
114
+ expected_mapping = {id(node1): ['r1', 'r2'], id(
115
+ node2): ['r0', 'r1'], id(node3): ['r1', 'r2']}
116
+ assert TestCompileCSS._serialize_mapping(
117
+ compiled_css.mapping) == expected_mapping
118
+
119
+ def test_nested_compilation_one_tree(self):
120
+ # Create some nested HTML nodes
121
+ tree = HTMLNode(
122
+ style={"margin": "0", "padding": "0"},
123
+ children=[
124
+ TextNode("Hello World!", style={
125
+ "margin": "5", "color": "blue"}),
126
+ TextNode("Another text node", style={
127
+ "padding": "0", "color": "blue"})
128
+ ]
129
+ )
130
+
131
+ # Compile the CSS for the tree
132
+ compiled_css = compile_css(tree)
133
+
134
+ # Check that the tree is correctly saved
135
+ assert compiled_css.trees == [tree]
136
+ assert [id(t) for t in compiled_css.trees] == [id(tree)]
137
+
138
+ # Check that the rules are correctly generated
139
+ expected_rules = [
140
+ {"name": "r0", "declarations": {"color": "blue"}},
141
+ {"name": "r1", "declarations": {"margin": "0"}},
142
+ {"name": "r2", "declarations": {"margin": "5"}},
143
+ {"name": "r3", "declarations": {"padding": "0"}}
144
+ ]
145
+ assert TestCompileCSS._serialize_rules(
146
+ compiled_css.rules) == expected_rules
147
+
148
+ # Check that the mapping is correctly generated
149
+ expected_mapping = {
150
+ id(tree): ['r1', 'r3'],
151
+ id(tree.children[0]): ['r0', 'r2'],
152
+ id(tree.children[1]): ['r0', 'r3'],
153
+ id(tree.children[0].children[0]): [],
154
+ id(tree.children[1].children[0]): []
155
+ }
156
+ assert TestCompileCSS._serialize_mapping(
157
+ compiled_css.mapping) == expected_mapping
158
+
159
+ def test_nested_compilation_two_trees(self):
160
+ # Create 2 trees
161
+ tree1 = HTMLNode(
162
+ style={"margin": "10", "padding": "0"},
163
+ children=[
164
+ HTMLNode(style={"color": "red"})
165
+ ]
166
+ )
167
+ tree2 = HTMLNode(
168
+ style={"margin": "5", "padding": "0"},
169
+ children=[
170
+ HTMLNode(style={"margin": "10"})
171
+ ]
172
+ )
173
+
174
+ # Compile the CSS for the trees
175
+ compiled_css = compile_css([tree1, tree2])
176
+
177
+ # Check that the tree is correctly saved
178
+ assert compiled_css.trees == [tree1, tree2]
179
+ assert [id(t) for t in compiled_css.trees] == [
180
+ id(tree1), id(tree2)]
181
+
182
+ # Check that the rules are correctly generated
183
+ expected_rules = [
184
+ {"name": "r0", "declarations": {"color": "red"}},
185
+ {"name": "r1", "declarations": {"margin": "10"}},
186
+ {"name": "r2", "declarations": {"margin": "5"}},
187
+ {"name": "r3", "declarations": {"padding": "0"}}
188
+ ]
189
+ assert TestCompileCSS._serialize_rules(
190
+ compiled_css.rules) == expected_rules
191
+
192
+ # Check that the mapping is correctly generated
193
+ expected_mapping = {
194
+ id(tree1): ['r1', 'r3'],
195
+ id(tree1.children[0]): ['r0'],
196
+ id(tree2): ['r2', 'r3'],
197
+ id(tree2.children[0]): ['r1']
198
+ }
199
+ assert TestCompileCSS._serialize_mapping(
200
+ compiled_css.mapping) == expected_mapping
201
+
202
+ def test_rules_numbered_in_order(self):
203
+ """Test that rules are numbered in lexicographical order"""
204
+ tree = HTMLNode(
205
+ style={"a": "5", "b": "4"},
206
+ children=[
207
+ HTMLNode(style={"a": "10"}),
208
+ HTMLNode(style={"b": "10"}),
209
+ HTMLNode(style={"c": "5"})
210
+ ]
211
+ )
212
+ compiled_css = compile_css(tree)
213
+ expected_rules = [
214
+ {"name": "r0", "declarations": {"a": "10"}},
215
+ {"name": "r1", "declarations": {"a": "5"}},
216
+ {"name": "r2", "declarations": {"b": "10"}},
217
+ {"name": "r3", "declarations": {"b": "4"}},
218
+ {"name": "r4", "declarations": {"c": "5"}},
219
+ ]
220
+ assert TestCompileCSS._serialize_rules(
221
+ compiled_css.rules) == expected_rules
222
+
223
+ def test_duplicate_node(self):
224
+ """Test that adding the same node twice does not impact compilation"""
225
+ # Compiling a tree
226
+ tree = HTMLNode(
227
+ style={"a": "5", "b": "4"},
228
+ children=[
229
+ HTMLNode(style={"a": "5"}),
230
+ HTMLNode(style={"b": "10"}),
231
+ ]
232
+ )
233
+ expected_rules = [
234
+ {"name": "r0", "declarations": {"a": "5"}},
235
+ {"name": "r1", "declarations": {"b": "10"}},
236
+ {"name": "r2", "declarations": {"b": "4"}}
237
+ ]
238
+ expected_mapping = {
239
+ id(tree): ['r0', 'r2'],
240
+ id(tree.children[0]): ['r0'],
241
+ id(tree.children[1]): ['r1']
242
+ }
243
+ compiled_css = compile_css([tree])
244
+ assert compiled_css.trees == [tree]
245
+ assert [id(t) for t in compiled_css.trees] == [id(tree)]
246
+ assert TestCompileCSS._serialize_rules(
247
+ compiled_css.rules) == expected_rules
248
+ assert TestCompileCSS._serialize_mapping(
249
+ compiled_css.mapping) == expected_mapping
250
+
251
+ # Compiling the tree and one of its children, which should already be
252
+ # included recursively from the tree itself and should not affect the
253
+ # result
254
+ compiled_css2 = compile_css([tree, tree.children[0]])
255
+ assert compiled_css2.trees == [tree, tree.children[0]]
256
+ assert [id(t) for t in compiled_css2.trees] == [
257
+ id(tree), id(tree.children[0])]
258
+ assert TestCompileCSS._serialize_rules(
259
+ compiled_css2.rules) == expected_rules
260
+ assert TestCompileCSS._serialize_mapping(
261
+ compiled_css2.mapping) == expected_mapping
262
+
263
+ @pytest.mark.parametrize("rule_namer, names", [
264
+ (lambda _, i: f"rule{i}", ["rule0", "rule1", "rule2"]),
265
+ (lambda _, i: f"rule-{i + 1}", ["rule-1", "rule-2", "rule-3"]),
266
+ (lambda r, i: f"{list(r[i].declarations.items())[0][0]}{i}", [
267
+ "az0", "bz1", "bz2"]),
268
+ (lambda r, i: f"{list(r[i].declarations.items())[0][0][0]}{i}", [
269
+ "a0", "b1", "b2"]),
270
+ (lambda r, i: f"r{list(r[i].declarations.items())[0][1]}-{i}", [
271
+ "r10-1", "r4-2", "r5-0"]),
272
+ ])
273
+ def test_custom_rule_names(self, rule_namer, names):
274
+ tree = HTMLNode(
275
+ style={"az": "5", "bz": "4"},
276
+ children=[
277
+ HTMLNode(style={"az": "5"}),
278
+ HTMLNode(style={"bz": "10"}),
279
+ ]
280
+ )
281
+ compiled_css = compile_css(tree, rule_namer=rule_namer)
282
+ assert [r.name for r in compiled_css.rules] == names
283
+
284
+
285
+ class TestCompiledCSS:
286
+ def test_export_custom_compiled_css(self):
287
+ rules = [
288
+ CSSRule(name="r0", declarations={"margin": "0", "padding": "0"}),
289
+ CSSRule(name="r1", declarations={"color": "blue"}),
290
+ CSSRule(name="r2", declarations={
291
+ "background-color": "white", "font-size": "16px"})
292
+ ]
293
+ compiled_css = CompiledCSS(trees=None,
294
+ rules=rules,
295
+ mapping=None)
296
+ expected_css = '\n'.join([
297
+ ".r0 {",
298
+ " margin: 0;",
299
+ " padding: 0;",
300
+ "}",
301
+ "",
302
+ ".r1 {",
303
+ " color: blue;",
304
+ "}",
305
+ "",
306
+ ".r2 {",
307
+ " background-color: white;",
308
+ " font-size: 16px;",
309
+ "}"
310
+ ])
311
+ assert compiled_css.to_css() == expected_css
312
+
313
+ def test_export_real_compiled_css(self):
314
+ tree = HTMLNode(
315
+ style={"margin": "0", "padding": "0"},
316
+ children=[
317
+ TextNode("a", style={"margin": "0", "color": "blue"}),
318
+ HTMLNode(style={"margin": "0", "color": "green"})
319
+ ]
320
+ )
321
+ compiled_css = compile_css(tree)
322
+ expected_css = '\n'.join([
323
+ ".r0 {",
324
+ " color: blue;",
325
+ "}",
326
+ "",
327
+ ".r1 {",
328
+ " color: green;",
329
+ "}",
330
+ "",
331
+ ".r2 {",
332
+ " margin: 0;",
333
+ "}",
334
+ "",
335
+ ".r3 {",
336
+ " padding: 0;",
337
+ "}"
338
+ ])
339
+ assert compiled_css.to_css() == expected_css
340
+
341
+ def test_export_empty_style(self):
342
+ node = HTMLNode()
343
+ css = compile_css(node).to_css()
344
+ assert css == ""
345
+ other_css = CompiledCSS(trees=None,
346
+ rules={},
347
+ mapping=None).to_css()
348
+ assert other_css == ""
349
+
350
+ def test_export_invalid_style(self):
351
+ node = HTMLNode(style={"marg!in": "0", "padding": "0"})
352
+ compiled_css = compile_css(node)
353
+ with pytest.raises(ValueError, match="marg!in"):
354
+ compiled_css.to_css()
355
+
356
+ @pytest.mark.parametrize("indent_size", [0, 2, 4, 8])
357
+ def test_css_indentation(self, indent_size):
358
+ node = HTMLNode(style={"a": "0", "b": "1"})
359
+ expected_css = '\n'.join([
360
+ ".r0 {",
361
+ f"{' ' * indent_size}a: 0;",
362
+ "}",
363
+ "",
364
+ ".r1 {",
365
+ f"{' ' * indent_size}b: 1;",
366
+ "}"
367
+ ])
368
+ css = compile_css(node).to_css(indent_size=indent_size)
369
+ assert css == expected_css
370
+
371
+
372
+ class TestApplyCSS:
373
+ @pytest.mark.parametrize("class_in, class_out", [
374
+ (None, "r0 r1"), # No class attribute
375
+ ("", "r0 r1"), # Empty class
376
+ ("z", "z r0 r1"), # Existing class
377
+ ("r1", "r1 r0"), # Existing rule
378
+ ("z r1", "z r1 r0"), # Existing class and rule
379
+ ("r1 z", "r1 z r0") # Existing rule and class
380
+ ])
381
+ def test_apply_css_to_node(self, class_in, class_out):
382
+ tree = HTMLNode(attributes=None if class_in is None else {"class": class_in},
383
+ style={"a": "0", "b": "1"})
384
+ apply_css(compile_css(tree), tree)
385
+ assert tree.attributes["class"] == class_out
386
+ assert tree.to_html() == f'<htmlnode class="{class_out}"></htmlnode>'
387
+
388
+ @pytest.mark.parametrize("cl1_in, cl1_out", [
389
+ (None, "r2 r3"), # No class attribute
390
+ ("", "r2 r3"), # Empty class
391
+ ("c", "c r2 r3"), # Existing class
392
+ ("r3", "r3 r2"), # Existing rule
393
+ ("c r3", "c r3 r2"), # Existing class and rule
394
+ ("r3 c", "r3 c r2"), # Existing rule and class
395
+ ("rr3", "rr3 r2 r3") # Rule decoy
396
+ ])
397
+ @pytest.mark.parametrize("cl2_in, cl2_out", [
398
+ (None, "r1 r2"), # No class attribute
399
+ ("", "r1 r2"), # Empty class
400
+ ("z", "z r1 r2"), # Existing class
401
+ ("r1", "r1 r2"), # Existing rule
402
+ ("z r1", "z r1 r2"), # Existing class and rule
403
+ ("r1 z", "r1 z r2"), # Existing rule and class
404
+ ("rr1", "rr1 r1 r2") # Rule decoy
405
+ ])
406
+ @pytest.mark.parametrize("mix", [False, True])
407
+ def test_apply_css_to_tree(self, cl1_in, cl1_out, cl2_in, cl2_out, mix):
408
+ # Creating a tree with some nodes and styles
409
+ tree = HTMLNode(
410
+ attributes=None if cl1_in is None else {"class": cl1_in},
411
+ style={"margin": "0", "padding": "0"},
412
+ children=[
413
+ TextNode("a", style={"margin": "0", "color": "blue"}) if mix
414
+ else HTMLNode(style={"margin": "0", "color": "blue"}),
415
+ HTMLNode(attributes=None if cl2_in is None else {"class": cl2_in},
416
+ style={"margin": "0", "color": "green"})
417
+ ]
418
+ )
419
+
420
+ # Compiling and applying CSS to the tree
421
+ compiled_css = compile_css(tree)
422
+ assert TestCompileCSS._serialize_rules(compiled_css.rules) == [
423
+ {"name": "r0", "declarations": {"color": "blue"}},
424
+ {"name": "r1", "declarations": {"color": "green"}},
425
+ {"name": "r2", "declarations": {"margin": "0"}},
426
+ {"name": "r3", "declarations": {"padding": "0"}}
427
+ ]
428
+ apply_css(compiled_css, tree)
429
+
430
+ # Checking the tree's new classes
431
+ assert tree.attributes["class"] == cl1_out
432
+ assert tree.children[0].attributes["class"] == "r0 r2"
433
+ assert tree.children[1].attributes["class"] == cl2_out
434
+
435
+ # Checking the final HTML code
436
+ mix_node = '<textnode class="r0 r2">a</textnode>' if mix else \
437
+ '<htmlnode class="r0 r2"></htmlnode>'
438
+ expected_html = '\n'.join([
439
+ f'<htmlnode class="{cl1_out}">',
440
+ f' {mix_node}',
441
+ f' <htmlnode class="{cl2_out}"></htmlnode>',
442
+ '</htmlnode>'
443
+ ])
444
+ assert tree.to_html() == expected_html
445
+
446
+ def test_apply_css_without_styles(self):
447
+ # Compiling and applying CSS to a tree with no styles
448
+ tree = HTMLNode(
449
+ children=[
450
+ TextNode("a"),
451
+ HTMLNode(attributes={"class": "z"})
452
+ ]
453
+ )
454
+ html_before = tree.to_html()
455
+ compiled_css = compile_css(tree)
456
+ assert compiled_css.rules == []
457
+ apply_css(compiled_css, tree)
458
+ html_after = tree.to_html()
459
+
460
+ # Checking the tree's new classes
461
+ assert "class" not in tree.attributes
462
+ assert "class" not in tree.children[0].attributes
463
+ assert tree.children[1].attributes["class"] == "z"
464
+
465
+ # Checking the final HTML code
466
+ expected_html = '\n'.join([
467
+ '<htmlnode>',
468
+ ' <textnode>a</textnode>',
469
+ ' <htmlnode class="z"></htmlnode>',
470
+ '</htmlnode>'
471
+ ])
472
+ assert html_before == expected_html
473
+ assert html_after == expected_html
474
+
475
+ def test_apply_css_with_partial_styles(self):
476
+ # Compiling and applying CSS to a tree where some nodes have styles but
477
+ # others do not
478
+ tree = HTMLNode(
479
+ children=[
480
+ TextNode("a", style={"margin": "0", "color": "blue"}),
481
+ HTMLNode(attributes={"class": "z"})
482
+ ]
483
+ )
484
+ compiled_css = compile_css(tree)
485
+ apply_css(compiled_css, tree)
486
+
487
+ # Checking the tree's new classes
488
+ assert "class" not in tree.attributes
489
+ assert tree.children[0].attributes["class"] == "r0 r1"
490
+ assert tree.children[1].attributes["class"] == "z"
491
+
492
+ # Checking the final HTML code
493
+ expected_html = '\n'.join([
494
+ '<htmlnode>',
495
+ ' <textnode class="r0 r1">a</textnode>',
496
+ ' <htmlnode class="z"></htmlnode>',
497
+ '</htmlnode>'
498
+ ])
499
+ assert tree.to_html() == expected_html
500
+
501
+ @pytest.mark.parametrize("class_in, class_out", [
502
+ (None, "r0 r1"),
503
+ ("", "r0 r1"),
504
+ ("z", "z r0 r1"),
505
+ ("r0", "r0 r1"),
506
+ ("r1", "r1 r0"),
507
+ ])
508
+ def test_apply_css_multiple_times(self, class_in, class_out):
509
+ tree = HTMLNode(style={"a": "0", "b": "1"}) if class_in is None else \
510
+ HTMLNode(attributes={"class": class_in},
511
+ style={"a": "0", "b": "1"})
512
+ html_before = '<htmlnode></htmlnode>' if class_in is None else \
513
+ f'<htmlnode class="{class_in}"></htmlnode>'
514
+ html_after = f'<htmlnode class="{class_out}"></htmlnode>'
515
+
516
+ assert tree.to_html() == html_before
517
+ compiled_css = compile_css(tree)
518
+ apply_css(compiled_css, tree)
519
+ assert tree.attributes["class"] == class_out
520
+ assert tree.to_html() == html_after
521
+ apply_css(compiled_css, tree)
522
+ assert tree.attributes["class"] == class_out
523
+ assert tree.to_html() == html_after
524
+
525
+ def test_empty_style(self):
526
+ """Tests that no classes are added if style exists but is empty."""
527
+ tree = HTMLNode(style={})
528
+ assert tree.to_html() == '<htmlnode></htmlnode>'
529
+ compiled_css = compile_css(tree)
530
+ apply_css(compiled_css, tree)
531
+ assert "class" not in tree.attributes
532
+ assert tree.to_html() == '<htmlnode></htmlnode>'
533
+
534
+
535
+ class TestDefaultRuleNamer:
536
+ def test_default_rule_namer(self):
537
+ rules = [CSSRule(None, {"color": "red"}),
538
+ CSSRule(None, {"margin": "0"})]
539
+ for i, rule in enumerate(rules):
540
+ rule.name = default_rule_namer(rules=rules, index=i)
541
+ assert rules[0].name == "r0"
542
+ assert rules[1].name == "r1"
543
+
544
+ def test_default_rule_namer_override(self):
545
+ rules = [CSSRule("first", {"color": "red"}),
546
+ CSSRule("second", {"margin": "0"})]
547
+ assert rules[0].name == "first"
548
+ assert rules[1].name == "second"
549
+ for i, rule in enumerate(rules):
550
+ rule.name = default_rule_namer(rules=rules, index=i)
551
+ assert rules[0].name == "r0"
552
+ assert rules[1].name == "r1"