webwidgets 1.1.0__tar.gz → 1.1.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 (65) hide show
  1. {webwidgets-1.1.0 → webwidgets-1.1.1}/PKG-INFO +1 -1
  2. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/compilation/test_css_sections.py +1 -0
  3. webwidgets-1.1.1/tests/utility/sizes/test_size.py +59 -0
  4. webwidgets-1.1.1/tests/utility/sizes/test_sizes.py +33 -0
  5. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/utility/test_validation.py +29 -2
  6. webwidgets-1.1.1/tests/widgets/containers/test_box.py +465 -0
  7. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/__init__.py +3 -1
  8. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/css.py +1 -1
  9. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/css_rule.py +3 -1
  10. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/sections/preamble.py +7 -4
  11. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/utility/__init__.py +2 -0
  12. webwidgets-1.1.1/webwidgets/utility/enums.py +18 -0
  13. webwidgets-1.1.1/webwidgets/utility/sizes/__init__.py +14 -0
  14. webwidgets-1.1.1/webwidgets/utility/sizes/size.py +92 -0
  15. webwidgets-1.1.1/webwidgets/utility/sizes/sizes.py +31 -0
  16. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/utility/validation.py +34 -3
  17. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/widgets/containers/__init__.py +1 -1
  18. webwidgets-1.1.1/webwidgets/widgets/containers/box.py +138 -0
  19. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/widgets/containers/container.py +7 -2
  20. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/widgets/containers/page.py +1 -1
  21. webwidgets-1.1.0/tests/widgets/containers/test_box.py +0 -110
  22. webwidgets-1.1.0/webwidgets/widgets/containers/box.py +0 -70
  23. {webwidgets-1.1.0 → webwidgets-1.1.1}/.github/workflows/cd.yml +0 -0
  24. {webwidgets-1.1.0 → webwidgets-1.1.1}/.github/workflows/ci-full.yml +0 -0
  25. {webwidgets-1.1.0 → webwidgets-1.1.1}/.github/workflows/ci-quick.yml +0 -0
  26. {webwidgets-1.1.0 → webwidgets-1.1.1}/.gitignore +0 -0
  27. {webwidgets-1.1.0 → webwidgets-1.1.1}/LICENSE +0 -0
  28. {webwidgets-1.1.0 → webwidgets-1.1.1}/README.md +0 -0
  29. {webwidgets-1.1.0 → webwidgets-1.1.1}/pyproject.toml +0 -0
  30. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/__init__.py +0 -0
  31. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/compilation/__init__.py +0 -0
  32. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/compilation/test_css.py +0 -0
  33. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/compilation/test_css_rule.py +0 -0
  34. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/compilation/test_html_node.py +0 -0
  35. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/compilation/test_html_tags.py +0 -0
  36. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/conftest.py +0 -0
  37. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/utility/__init__.py +0 -0
  38. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/utility/test_indentation.py +0 -0
  39. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/utility/test_representation.py +0 -0
  40. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/utility/test_sanitizing.py +0 -0
  41. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/website/__init__.py +0 -0
  42. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/website/test_website.py +0 -0
  43. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/widgets/__init__.py +0 -0
  44. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/widgets/conftest.py +0 -0
  45. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/widgets/containers/__init__.py +0 -0
  46. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/widgets/containers/test_page.py +0 -0
  47. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/widgets/render_page.py +0 -0
  48. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/widgets/test_render_page.py +0 -0
  49. {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/wrap_core_css.py +0 -0
  50. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/__init__.py +0 -0
  51. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/__init__.py +0 -0
  52. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/sections/__init__.py +0 -0
  53. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/sections/css_section.py +0 -0
  54. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/sections/rule_section.py +0 -0
  55. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/html/__init__.py +0 -0
  56. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/html/html_node.py +27 -27
  57. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/html/html_tags.py +0 -0
  58. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/utility/indentation.py +0 -0
  59. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/utility/representation.py +0 -0
  60. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/utility/sanitizing.py +0 -0
  61. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/website/__init__.py +0 -0
  62. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/website/compiled_website.py +0 -0
  63. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/website/website.py +0 -0
  64. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/widgets/__init__.py +0 -0
  65. {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/widgets/widget.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webwidgets
3
- Version: 1.1.0
3
+ Version: 1.1.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
@@ -32,6 +32,7 @@ class TestPreamble:
32
32
  " box-sizing: border-box;",
33
33
  " margin: 0;",
34
34
  " padding: 0;",
35
+ " overflow: hidden;",
35
36
  "}"
36
37
  ])
37
38
  assert preamble.to_css() == expected_css
@@ -0,0 +1,59 @@
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.utility.sizes.size import Size, AbsoluteSize, RelativeSize, \
15
+ with_unit
16
+ import webwidgets as ww
17
+
18
+
19
+ class TestSize:
20
+ @pytest.mark.parametrize("value", [0, 10, 10.0, 12.33])
21
+ def test_size(self, value):
22
+ size = Size(value)
23
+ assert size.value == value
24
+ assert size.unit == "size"
25
+ assert size.to_css() == f"{value}size"
26
+
27
+ @pytest.mark.parametrize("attr_to_test",
28
+ [AbsoluteSize, RelativeSize, Size,
29
+ with_unit])
30
+ def test_size_helpers_not_at_top_level(self, attr_to_test):
31
+ """Tests the visibility of helper classes and functions."""
32
+ # Making sure the class or function exists in the proper size module
33
+ assert hasattr(ww.utility.size, attr_to_test.__name__)
34
+
35
+ # Making sure it is not visible at the top level
36
+ assert not hasattr(ww, attr_to_test.__name__)
37
+
38
+ def test_absolute_size_not_importable_at_top_level(self):
39
+ with pytest.raises(AttributeError, match="AbsoluteSize"):
40
+ ww.AbsoluteSize(5)
41
+
42
+ def test_relative_size_not_importable_at_top_level(self):
43
+ with pytest.raises(AttributeError, match="RelativeSize"):
44
+ ww.RelativeSize(5)
45
+
46
+ def test_size_not_importable_at_top_level(self):
47
+ with pytest.raises(AttributeError, match="Size"):
48
+ ww.Size(5)
49
+
50
+ def test_with_unit_not_importable_at_top_level(self):
51
+ with pytest.raises(AttributeError, match="with_unit"):
52
+ ww.with_unit("10px")
53
+
54
+ @pytest.mark.parametrize("unit", ["m", "cm", "%"])
55
+ def test_with_unit(self, unit):
56
+ @with_unit(unit)
57
+ class CustomUnit(Size):
58
+ pass
59
+ assert CustomUnit(3).to_css() == f"3{unit}"
@@ -0,0 +1,33 @@
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.utility.sizes.size import AbsoluteSize, RelativeSize
15
+ import webwidgets as ww
16
+
17
+
18
+ class TestSizes:
19
+ @pytest.mark.parametrize("value", [0, 10, 10.0, 82.33])
20
+ def test_percent(self, value):
21
+ size = ww.Percent(value)
22
+ assert isinstance(size, RelativeSize)
23
+ assert size.value == value
24
+ assert size.unit == "%"
25
+ assert size.to_css() == f"{value}%"
26
+
27
+ @pytest.mark.parametrize("value", [0, 10, 10.0, 12.33])
28
+ def test_px(self, value):
29
+ size = ww.Px(value)
30
+ assert isinstance(size, AbsoluteSize)
31
+ assert size.value == value
32
+ assert size.unit == "px"
33
+ assert size.to_css() == f"{value}px"
@@ -16,10 +16,11 @@ import re
16
16
  from webwidgets.compilation.css import apply_css, compile_css
17
17
  from webwidgets.compilation.html import HTMLNode
18
18
  from webwidgets.utility.validation import validate_css_comment, \
19
- validate_css_identifier, validate_css_selector, validate_html_class
19
+ validate_css_identifier, validate_css_selector, validate_css_value, \
20
+ validate_html_class
20
21
 
21
22
 
22
- class TestValidate:
23
+ class TestValidateCSS:
23
24
  @pytest.fixture
24
25
  def valid_css_identifiers(self):
25
26
  return [
@@ -160,6 +161,32 @@ class TestValidate:
160
161
  with pytest.raises(ValueError, match=r"Invalid character\(s\).* !"):
161
162
  validate_css_selector(".h!")
162
163
 
164
+ def test_valid_css_values(self):
165
+ """Tests that valid CSS property values are accepted"""
166
+ validate_css_value("abcdefghijklmnopqrstuvwxyz")
167
+ validate_css_value("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
168
+ validate_css_value("0123456789")
169
+ validate_css_value("9876543210px")
170
+ validate_css_value("blue 0 0 5-5 30px")
171
+ validate_css_value("4 orange 56")
172
+ validate_css_value("4 oRAnGe 56")
173
+ validate_css_value("border-box")
174
+ validate_css_value("5 #ff0Az3 space-between auto 10%m")
175
+
176
+ @pytest.mark.parametrize("char1", "!@$^&*()<>?/|\\}{[\":;\']")
177
+ @pytest.mark.parametrize("char2", "}{")
178
+ @pytest.mark.parametrize("use_char2", (False, True))
179
+ def test_invalid_css_value_with_invalid_character(self, char1, char2,
180
+ use_char2):
181
+ """Tests that an invalid CSS value (containing one or two invalid
182
+ characters) raises an exception"""
183
+ chars = ', '.join([char1, char2] if use_char2 else [char1])
184
+ with pytest.raises(ValueError,
185
+ match=fr"Invalid character\(s\).*{re.escape(chars)}"):
186
+ validate_css_value(f"red-{char1} 0{char2 if use_char2 else ''} px")
187
+
188
+
189
+ class TestValidateHTML:
163
190
  def test_valid_html_classes(self):
164
191
  """Test that valid HTML class attributes are accepted"""
165
192
  validate_html_class("")
@@ -0,0 +1,465 @@
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 numpy as np
14
+ import pytest
15
+ from typing import Tuple
16
+ import webwidgets as ww
17
+ from webwidgets.compilation.html import Div
18
+ from webwidgets.widgets.containers.box import BoxItemProperties
19
+
20
+
21
+ class TestBox:
22
+
23
+ # A simple color widget
24
+ class Color(ww.Widget):
25
+ def __init__(self,
26
+ color: Tuple[int, int, int],
27
+ height: str = "100%",
28
+ width: str = "100%"):
29
+ super().__init__()
30
+ self.color = color
31
+ self.height = height
32
+ self.width = width
33
+
34
+ def build(self):
35
+ hex_color = "#%02x%02x%02x" % self.color
36
+ return Div(style={"background-color": hex_color,
37
+ "height": self.height,
38
+ "width": self.width})
39
+
40
+ # A Box that fills the entire viewport
41
+ class FullViewportBox(ww.Box):
42
+ def build(self, *args, **kwargs):
43
+ node = super().build(*args, **kwargs)
44
+ node.style["width"] = "100vw"
45
+ node.style["height"] = "100vh"
46
+ return node
47
+
48
+ # A Box that expands to 100% of its available space
49
+ class FullyExpandedBox(ww.Box):
50
+ def build(self, *args, **kwargs):
51
+ node = super().build(*args, **kwargs)
52
+ node.style["width"] = "100%"
53
+ node.style["height"] = "100%"
54
+ return node
55
+
56
+ @pytest.mark.parametrize("colors", [
57
+ [(255, 0, 0)],
58
+ [(255, 0, 0), (0, 255, 0)],
59
+ [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
60
+ ])
61
+ def test_horizontal_box(self, colors, render_page, web_drivers):
62
+ """Tests the even distribution of multiple colored widgets by a Box."""
63
+ # Creating a page with one box containing widgets with the given colors
64
+ box = TestBox.FullViewportBox(direction=ww.Direction.HORIZONTAL)
65
+ for color in colors:
66
+ box.add(TestBox.Color(color=color))
67
+ page = ww.Page([box])
68
+
69
+ for web_driver in web_drivers:
70
+
71
+ # Rendering the page with the box
72
+ array = render_page(page, web_driver)
73
+
74
+ # Computing the regions where to search for each color. If the
75
+ # colors cannot spread evenly (which happens when the image size is
76
+ # not divisible by the number of colors), we exclude all edges
77
+ # where one color stops and another starts.
78
+ all_indices = np.arange(array.shape[1])
79
+ edges = np.linspace(0, array.shape[1], len(colors) + 1)[1:-1]
80
+ edges = np.floor(edges).astype(np.int32)
81
+ regions = np.split(all_indices, edges)
82
+ if array.shape[1] % len(colors) != 0:
83
+ regions = [r[~np.isin(r, edges)] for r in regions]
84
+
85
+ assert len(regions) == len(colors) # One region per color
86
+ for color, region in zip(colors, regions):
87
+ assert np.all(array[:, region, 0] == color[0])
88
+ assert np.all(array[:, region, 1] == color[1])
89
+ assert np.all(array[:, region, 2] == color[2])
90
+
91
+ @pytest.mark.parametrize("colors", [
92
+ [(255, 0, 0)],
93
+ [(255, 0, 0), (0, 255, 0)],
94
+ [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
95
+ ])
96
+ def test_vertical_box(self, colors, render_page, web_drivers):
97
+ """Tests the even distribution of multiple colored widgets by a Box."""
98
+ # Creating a page with one box containing widgets with the given colors
99
+ box = TestBox.FullViewportBox(direction=ww.Direction.VERTICAL)
100
+ for color in colors:
101
+ box.add(TestBox.Color(color=color))
102
+ page = ww.Page([box])
103
+
104
+ for web_driver in web_drivers:
105
+
106
+ # Rendering the page with the box
107
+ array = render_page(page, web_driver)
108
+
109
+ # Computing the regions where to search for each color. If the
110
+ # colors cannot spread evenly (which happens when the image size is
111
+ # not divisible by the number of colors), we exclude all edges
112
+ # where one color stops and another starts.
113
+ all_indices = np.arange(array.shape[0])
114
+ edges = np.linspace(0, array.shape[0], len(colors) + 1)[1:-1]
115
+ edges = np.floor(edges).astype(np.int32)
116
+ regions = np.split(all_indices, edges)
117
+ if array.shape[0] % len(colors) != 0:
118
+ regions = [r[~np.isin(r, edges)] for r in regions]
119
+
120
+ assert len(regions) == len(colors) # One region per color
121
+ for color, region in zip(colors, regions):
122
+ assert np.all(array[region, :, 0] == color[0])
123
+ assert np.all(array[region, :, 1] == color[1])
124
+ assert np.all(array[region, :, 2] == color[2])
125
+
126
+ def test_nested_boxes(self, render_page, web_drivers):
127
+ """Tests that two nested boxes with orthogonal directions render
128
+ correctly.
129
+ """
130
+ top_box = TestBox.FullyExpandedBox(direction=ww.Direction.HORIZONTAL)
131
+ top_box.add(TestBox.Color(color=(255, 0, 0)))
132
+ top_box.add(TestBox.Color(color=(0, 255, 0)))
133
+ out_box = TestBox.FullViewportBox(direction=ww.Direction.VERTICAL)
134
+ out_box.add(top_box)
135
+ out_box.add(TestBox.Color(color=(0, 0, 255)))
136
+ page = ww.Page([out_box])
137
+
138
+ for web_driver in web_drivers:
139
+ a = render_page(page, web_driver)
140
+ for i, c in enumerate((255, 0, 0)):
141
+ assert np.all(a[:a.shape[0] // 2, :a.shape[1] // 2, i] == c)
142
+ edge_col = a.shape[1] // 2 + (0 if a.shape[1] % 2 == 0 else 1)
143
+ for i, c in enumerate((0, 255, 0)):
144
+ assert np.all(a[:a.shape[0] // 2, edge_col:, i] == c)
145
+ edge_row = a.shape[0] // 2 + (0 if a.shape[0] % 2 == 0 else 1)
146
+ for i, c in enumerate((0, 0, 255)):
147
+ assert np.all(a[edge_row:, :, i] == c)
148
+
149
+ @pytest.mark.parametrize("green_space", [
150
+ 2, 3, 4, # as int
151
+ 2.0, 3.0, 4.0 # as float
152
+ ])
153
+ @pytest.mark.parametrize("explicit_default", [True, False])
154
+ def test_horizontal_box_spacing_two_colors(self, green_space,
155
+ explicit_default, render_page,
156
+ web_drivers):
157
+ # Creating a page with one box containing Color widgets
158
+ box = TestBox.FullViewportBox(direction=ww.Direction.HORIZONTAL)
159
+ if explicit_default:
160
+ box.add(TestBox.Color(color=(255, 0, 0)), space=1)
161
+ else:
162
+ box.add(TestBox.Color(color=(255, 0, 0)))
163
+ box.add(TestBox.Color(color=(0, 255, 0)), space=green_space)
164
+ page = ww.Page([box])
165
+
166
+ for web_driver in web_drivers:
167
+
168
+ # Rendering the page with the box
169
+ array = render_page(page, web_driver)
170
+
171
+ # Computing the expected red and green regions, avoiding the edge
172
+ # if colors cannot spread evenly
173
+ all_indices = np.arange(array.shape[1])
174
+ edge = array.shape[1] // (int(green_space) + 1)
175
+ red, green = np.split(all_indices, [edge])
176
+ if array.shape[1] % (int(green_space) + 1) != 0:
177
+ green = green[green != edge]
178
+
179
+ # Testing than first region is red and second region is green
180
+ assert np.all(array[:, red, 0] == 255)
181
+ assert np.all(array[:, red, 1] == 0)
182
+ assert np.all(array[:, red, 2] == 0)
183
+ assert np.all(array[:, green, 0] == 0)
184
+ assert np.all(array[:, green, 1] == 255)
185
+ assert np.all(array[:, green, 2] == 0)
186
+
187
+ @pytest.mark.parametrize("green_space", [
188
+ 2, 3, 4, # as int
189
+ 2.0, 3.0, 4.0 # as float
190
+ ])
191
+ @pytest.mark.parametrize("explicit_default", [True, False])
192
+ def test_vertical_box_spacing_two_colors(self, green_space,
193
+ explicit_default, render_page,
194
+ web_drivers):
195
+ # Creating a page with one box containing Color widgets
196
+ box = TestBox.FullViewportBox(direction=ww.Direction.VERTICAL)
197
+ if explicit_default:
198
+ box.add(TestBox.Color(color=(255, 0, 0)), space=1)
199
+ else:
200
+ box.add(TestBox.Color(color=(255, 0, 0)))
201
+ box.add(TestBox.Color(color=(0, 255, 0)), space=green_space)
202
+ page = ww.Page([box])
203
+
204
+ for web_driver in web_drivers:
205
+
206
+ # Rendering the page with the box
207
+ array = render_page(page, web_driver)
208
+
209
+ # Computing the expected red and green regions, avoiding the edge
210
+ # if colors cannot spread evenly
211
+ all_indices = np.arange(array.shape[0])
212
+ edge = array.shape[0] // (int(green_space) + 1)
213
+ red, green = np.split(all_indices, [edge])
214
+ if array.shape[0] % (int(green_space) + 1) != 0:
215
+ green = green[green != edge]
216
+
217
+ # Testing than first region is red and second region is green
218
+ assert np.all(array[red, :, 0] == 255)
219
+ assert np.all(array[red, :, 1] == 0)
220
+ assert np.all(array[red, :, 2] == 0)
221
+ assert np.all(array[green, :, 0] == 0)
222
+ assert np.all(array[green, :, 1] == 255)
223
+ assert np.all(array[green, :, 2] == 0)
224
+
225
+ @pytest.mark.parametrize("spaces", [
226
+ (2, 2, 2, 2), (1, 2, 3, 4),
227
+ (1, 2, 2, 0.5), (1, 0.25, 0.75, 3) # Mixed types
228
+ ])
229
+ def test_horizontal_box_spacing_more_colors(self, spaces, render_page,
230
+ web_drivers):
231
+ spaces = np.array(spaces)
232
+
233
+ # Creating a page with one box containing Color widgets
234
+ colors = [
235
+ (255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)
236
+ ]
237
+ box = TestBox.FullViewportBox(direction=ww.Direction.HORIZONTAL)
238
+ for color, space in zip(colors, spaces):
239
+ box.add(TestBox.Color(color=color), space=space.item())
240
+ page = ww.Page([box])
241
+
242
+ for web_driver in web_drivers:
243
+
244
+ # Rendering the page with the box
245
+ array = render_page(page, web_driver)
246
+
247
+ # Computing the expected colored regions, avoiding all edges even
248
+ # if colors spread evenly
249
+ all_indices = np.arange(array.shape[1])
250
+ edges = array.shape[1] * (spaces.cumsum() / spaces.sum())[:-1]
251
+ edges = np.floor(edges).astype(np.int32)
252
+ regions = np.split(all_indices, edges)
253
+ regions = [r[~np.isin(r, edges)] for r in regions]
254
+
255
+ # Testing each region
256
+ for color, region in zip(colors, regions):
257
+ assert np.all(array[:, region, 0] == color[0])
258
+ assert np.all(array[:, region, 1] == color[1])
259
+ assert np.all(array[:, region, 2] == color[2])
260
+
261
+ @pytest.mark.parametrize("spaces", [
262
+ (2, 2, 2, 2), (1, 2, 3, 4),
263
+ (1, 2, 2, 0.5), (1, 0.25, 0.75, 3) # Mixed types
264
+ ])
265
+ def test_vertical_box_spacing_more_colors(self, spaces, render_page,
266
+ web_drivers):
267
+ spaces = np.array(spaces)
268
+
269
+ # Creating a page with one box containing Color widgets
270
+ colors = [
271
+ (255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)
272
+ ]
273
+ box = TestBox.FullViewportBox(direction=ww.Direction.VERTICAL)
274
+ for color, space in zip(colors, spaces):
275
+ box.add(TestBox.Color(color=color), space=space.item())
276
+ page = ww.Page([box])
277
+
278
+ for web_driver in web_drivers:
279
+
280
+ # Rendering the page with the box
281
+ array = render_page(page, web_driver)
282
+
283
+ # Computing the expected colored regions, avoiding all edges even
284
+ # if colors spread evenly
285
+ all_indices = np.arange(array.shape[0])
286
+ edges = array.shape[0] * (spaces.cumsum() / spaces.sum())[:-1]
287
+ edges = np.floor(edges).astype(np.int32)
288
+ regions = np.split(all_indices, edges)
289
+ regions = [r[~np.isin(r, edges)] for r in regions]
290
+
291
+ # Testing each region
292
+ for color, region in zip(colors, regions):
293
+ assert np.all(array[region, :, 0] == color[0])
294
+ assert np.all(array[region, :, 1] == color[1])
295
+ assert np.all(array[region, :, 2] == color[2])
296
+
297
+ def test_nested_boxes_with_uneven_spacing(self, render_page, web_drivers):
298
+ """Tests that two nested boxes with orthogonal directions and uneven
299
+ spacing rules render correctly.
300
+ """
301
+ top_box = TestBox.FullyExpandedBox(direction=ww.Direction.HORIZONTAL)
302
+ top_box.add(TestBox.Color(color=(255, 0, 0)))
303
+ top_box.add(TestBox.Color(color=(0, 255, 0)), space=3)
304
+ out_box = TestBox.FullViewportBox(direction=ww.Direction.VERTICAL)
305
+ out_box.add(top_box, space=0.5)
306
+ out_box.add(TestBox.Color(color=(0, 0, 255)))
307
+ page = ww.Page([out_box])
308
+
309
+ for web_driver in web_drivers:
310
+ a = render_page(page, web_driver)
311
+ for i, c in enumerate((255, 0, 0)):
312
+ assert np.all(a[:a.shape[0] // 3, :a.shape[1] // 4, i] == c)
313
+ edge_col = a.shape[1] // 4 + (0 if a.shape[1] % 4 == 0 else 1)
314
+ for i, c in enumerate((0, 255, 0)):
315
+ assert np.all(a[:a.shape[0] // 3, edge_col:, i] == c)
316
+ edge_row = a.shape[0] // 3 + (0 if a.shape[0] % 3 == 0 else 1)
317
+ for i, c in enumerate((0, 0, 255)):
318
+ assert np.all(a[edge_row:, :, i] == c)
319
+
320
+ @pytest.mark.parametrize("size", (3, 4, 5, 6))
321
+ @pytest.mark.parametrize("position", (0, 1, 2))
322
+ def test_horizontal_box_with_absolute_size(self, size, position,
323
+ render_page, web_drivers):
324
+ """Tests that a widget with absolute size renders with the requested
325
+ size and is not expanded. Multiple sizes and positions within the box
326
+ (first, middle, last) are tested.
327
+ """
328
+ # Creating a page with one box
329
+ colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
330
+ spaces = [1, 1, 1]
331
+ spaces[position] = ww.Px(size)
332
+ box = TestBox.FullViewportBox(direction=ww.Direction.HORIZONTAL)
333
+ for _ in range(3):
334
+ box.add(TestBox.Color(color=colors.pop(0)), space=spaces.pop(0))
335
+ page = ww.Page([box])
336
+
337
+ for web_driver in web_drivers:
338
+
339
+ # Rendering the page with the box
340
+ array = render_page(page, web_driver)
341
+
342
+ # Computing the regions where to search for each color. If the two
343
+ # expanding colors cannot spread evenly (which happens when the
344
+ # remaining space for them has an odd size), we exclude all edges
345
+ # where one color stops and another starts.
346
+ all_indices = np.arange(array.shape[1])
347
+ half_remainder = (array.shape[1] - size) // 2
348
+ edges = [
349
+ [size, size + half_remainder], # position = 0
350
+ [half_remainder, array.shape[1] - half_remainder], # position = 1
351
+ [half_remainder, 2 * half_remainder] # position = 2
352
+ ][position]
353
+ regions = np.split(all_indices, edges)
354
+ if (array.shape[1] - size) % 2 != 0:
355
+ regions = [r[~np.isin(r, edges)] for r in regions]
356
+
357
+ assert len(regions) == 3 # One region per color
358
+ for color, region in zip(((255, 0, 0), (0, 255, 0), (0, 0, 255)),
359
+ regions):
360
+ assert np.all(array[:, region, 0] == color[0])
361
+ assert np.all(array[:, region, 1] == color[1])
362
+ assert np.all(array[:, region, 2] == color[2])
363
+
364
+ @pytest.mark.parametrize("size", (3, 4, 5, 6))
365
+ @pytest.mark.parametrize("position", (0, 1, 2))
366
+ def test_vertical_box_with_absolute_size(self, size, position,
367
+ render_page, web_drivers):
368
+ """Tests that a widget with absolute size renders with the requested
369
+ size and is not expanded. Multiple sizes and positions within the box
370
+ (first, middle, last) are tested.
371
+ """
372
+ # Creating a page with one box
373
+ colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
374
+ spaces = [1, 1, 1]
375
+ spaces[position] = ww.Px(size)
376
+ box = TestBox.FullViewportBox(direction=ww.Direction.VERTICAL)
377
+ for _ in range(3):
378
+ box.add(TestBox.Color(color=colors.pop(0)), space=spaces.pop(0))
379
+ page = ww.Page([box])
380
+
381
+ for web_driver in web_drivers:
382
+
383
+ # Rendering the page with the box
384
+ array = render_page(page, web_driver)
385
+
386
+ # Computing the regions where to search for each color. If the two
387
+ # expanding colors cannot spread evenly (which happens when the
388
+ # remaining space for them has an odd size), we exclude all edges
389
+ # where one color stops and another starts.
390
+ all_indices = np.arange(array.shape[0])
391
+ half_remainder = (array.shape[0] - size) // 2
392
+ edges = [
393
+ [size, size + half_remainder], # position = 0
394
+ [half_remainder, array.shape[0] - half_remainder], # position = 1
395
+ [half_remainder, 2 * half_remainder] # position = 2
396
+ ][position]
397
+ regions = np.split(all_indices, edges)
398
+ if (array.shape[0] - size) % 2 != 0:
399
+ regions = [r[~np.isin(r, edges)] for r in regions]
400
+
401
+ assert len(regions) == 3 # One region per color
402
+ for color, region in zip(((255, 0, 0), (0, 255, 0), (0, 0, 255)),
403
+ regions):
404
+ assert np.all(array[region, :, 0] == color[0])
405
+ assert np.all(array[region, :, 1] == color[1])
406
+ assert np.all(array[region, :, 2] == color[2])
407
+
408
+ @pytest.mark.parametrize("direction", (
409
+ ww.Direction.HORIZONTAL, ww.Direction.VERTICAL
410
+ ))
411
+ def test_large_box_item_is_clipped(self, direction, render_page,
412
+ web_drivers):
413
+ """Tests that a large box item is clipped to respect the spacing rules
414
+ of the box.
415
+ """
416
+ # Defining the box. The large item has either a width of 50vw or a
417
+ # height of 50vh (depending on the direction), which is above the space
418
+ # allocated by the spacing rules (1/3), so it should be clipped to 1/3
419
+ # of the viewport
420
+ box = TestBox.FullViewportBox(direction=direction)
421
+ size_arg = {"width": "50vw"} if direction == ww.Direction.HORIZONTAL \
422
+ else {"height": "50vh"}
423
+ big_item = TestBox.Color(color=(255, 0, 0), **size_arg)
424
+ box.add(big_item, space=1)
425
+ box.add(TestBox.Color(color=(0, 255, 0)), space=2)
426
+
427
+ # Creating a page containing the box
428
+ page = ww.Page([box])
429
+
430
+ # Testing that the large item (colored in red) only occupies 1/3 of the
431
+ # box
432
+ for web_driver in web_drivers:
433
+ a = render_page(page, web_driver)
434
+ for i, c in enumerate((255, 0, 0)):
435
+ if direction == ww.Direction.HORIZONTAL:
436
+ assert np.all(a[:, :a.shape[1] // 3, i] == c)
437
+ else:
438
+ assert np.all(a[:a.shape[0] // 3, :, i] == c)
439
+ axis = 1 if direction == ww.Direction.HORIZONTAL else 0
440
+ edge = a.shape[axis] // 3 + (0 if a.shape[axis] % 3 == 0 else 1)
441
+ for i, c in enumerate((0, 255, 0)):
442
+ if direction == ww.Direction.HORIZONTAL:
443
+ assert np.all(a[:, edge:, i] == c)
444
+ else:
445
+ assert np.all(a[edge:, :, i] == c)
446
+
447
+
448
+ class TestBoxItemProperties:
449
+ @pytest.mark.parametrize("space", [4, 5.1, 0.2])
450
+ def test_to_style_numeric(self, space):
451
+ props = BoxItemProperties(space=space)
452
+ assert props.to_style() == {
453
+ 'flex-basis': "0",
454
+ 'flex-grow': str(space),
455
+ 'flex-shrink': str(space)
456
+ }
457
+
458
+ @pytest.mark.parametrize("space", [ww.Px(4), ww.Px(3.5)])
459
+ def test_to_style_absolute_size(self, space):
460
+ props = BoxItemProperties(space=space)
461
+ assert props.to_style() == {
462
+ 'flex-basis': f"{space.value}px",
463
+ 'flex-grow': "0",
464
+ 'flex-shrink': "0"
465
+ }
@@ -10,9 +10,11 @@
10
10
  #
11
11
  # =======================================================================
12
12
 
13
- __version__ = "1.1.0" # Dynamically set by build backend
13
+ __version__ = "1.1.1" # Dynamically set by build backend
14
14
 
15
15
  from . import compilation
16
16
  from . import utility
17
+ from .utility.enums import *
18
+ from .utility.sizes.sizes import *
17
19
  from .website import *
18
20
  from .widgets import *
@@ -170,7 +170,7 @@ def compile_css(trees: Union[HTMLNode, List[HTMLNode]],
170
170
  Every HTML node present in one or more of the input trees is included
171
171
  in the :py:attr:`CompiledCSS.mapping` attribute, even if the node does
172
172
  not have a style. Rules are alphabetically ordered by class name in the
173
- mapping.
173
+ mapping and in the :py:attr:`CompiledCSS.core` rule section.
174
174
  :rtype: CompiledCSS
175
175
  """
176
176
  # Handling case of a single tree
@@ -13,7 +13,8 @@
13
13
  from typing import Dict
14
14
  from webwidgets.utility.indentation import get_indentation
15
15
  from webwidgets.utility.representation import ReprMixin
16
- from webwidgets.utility.validation import validate_css_identifier, validate_css_selector
16
+ from webwidgets.utility.validation import validate_css_identifier, \
17
+ validate_css_selector, validate_css_value
17
18
 
18
19
 
19
20
  class CSSRule(ReprMixin):
@@ -60,6 +61,7 @@ class CSSRule(ReprMixin):
60
61
  css_code = self.selector + " {\n"
61
62
  for property_name, value in self.declarations.items():
62
63
  validate_css_identifier(property_name)
64
+ validate_css_value(value)
63
65
  css_code += f"{indentation}{property_name}: {value};\n"
64
66
  css_code += "}"
65
67