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.
- {webwidgets-0.1.0 → webwidgets-0.2.1}/PKG-INFO +1 -1
- webwidgets-0.2.1/tests/compilation/test_css.py +552 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/tests/compilation/test_html_node.py +110 -1
- webwidgets-0.2.1/tests/compilation/test_html_tags.py +27 -0
- webwidgets-0.2.1/tests/utility/test_representation.py +118 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/tests/utility/test_sanitizing.py +3 -3
- webwidgets-0.2.1/tests/utility/test_validation.py +218 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/webwidgets/__init__.py +1 -1
- {webwidgets-0.1.0 → webwidgets-0.2.1}/webwidgets/compilation/__init__.py +1 -0
- {webwidgets-0.1.0/webwidgets/compilation/html → webwidgets-0.2.1/webwidgets/compilation/css}/__init__.py +2 -1
- webwidgets-0.2.1/webwidgets/compilation/css/css.py +229 -0
- webwidgets-0.2.1/webwidgets/compilation/html/__init__.py +14 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/webwidgets/compilation/html/html_node.py +71 -11
- webwidgets-0.2.1/webwidgets/compilation/html/html_tags.py +40 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/webwidgets/utility/__init__.py +2 -0
- webwidgets-0.2.1/webwidgets/utility/representation.py +34 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/webwidgets/utility/sanitizing.py +2 -2
- webwidgets-0.2.1/webwidgets/utility/validation.py +97 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/.github/workflows/cd.yml +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/.github/workflows/ci-full.yml +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/.github/workflows/ci-quick.yml +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/.gitignore +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/LICENSE +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/README.md +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/pyproject.toml +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/tests/__init__.py +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/tests/compilation/__init__.py +0 -0
- {webwidgets-0.1.0 → webwidgets-0.2.1}/tests/utility/__init__.py +0 -0
@@ -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"
|