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.
- {webwidgets-1.1.0 → webwidgets-1.1.1}/PKG-INFO +1 -1
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/compilation/test_css_sections.py +1 -0
- webwidgets-1.1.1/tests/utility/sizes/test_size.py +59 -0
- webwidgets-1.1.1/tests/utility/sizes/test_sizes.py +33 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/utility/test_validation.py +29 -2
- webwidgets-1.1.1/tests/widgets/containers/test_box.py +465 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/__init__.py +3 -1
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/css.py +1 -1
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/css_rule.py +3 -1
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/sections/preamble.py +7 -4
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/utility/__init__.py +2 -0
- webwidgets-1.1.1/webwidgets/utility/enums.py +18 -0
- webwidgets-1.1.1/webwidgets/utility/sizes/__init__.py +14 -0
- webwidgets-1.1.1/webwidgets/utility/sizes/size.py +92 -0
- webwidgets-1.1.1/webwidgets/utility/sizes/sizes.py +31 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/utility/validation.py +34 -3
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/widgets/containers/__init__.py +1 -1
- webwidgets-1.1.1/webwidgets/widgets/containers/box.py +138 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/widgets/containers/container.py +7 -2
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/widgets/containers/page.py +1 -1
- webwidgets-1.1.0/tests/widgets/containers/test_box.py +0 -110
- webwidgets-1.1.0/webwidgets/widgets/containers/box.py +0 -70
- {webwidgets-1.1.0 → webwidgets-1.1.1}/.github/workflows/cd.yml +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/.github/workflows/ci-full.yml +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/.github/workflows/ci-quick.yml +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/.gitignore +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/LICENSE +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/README.md +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/pyproject.toml +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/__init__.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/compilation/__init__.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/compilation/test_css.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/compilation/test_css_rule.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/compilation/test_html_node.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/compilation/test_html_tags.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/conftest.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/utility/__init__.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/utility/test_indentation.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/utility/test_representation.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/utility/test_sanitizing.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/website/__init__.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/website/test_website.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/widgets/__init__.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/widgets/conftest.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/widgets/containers/__init__.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/widgets/containers/test_page.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/widgets/render_page.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/widgets/test_render_page.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/tests/wrap_core_css.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/__init__.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/__init__.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/sections/__init__.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/sections/css_section.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/css/sections/rule_section.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/html/__init__.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/html/html_node.py +27 -27
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/compilation/html/html_tags.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/utility/indentation.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/utility/representation.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/utility/sanitizing.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/website/__init__.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/website/compiled_website.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/website/website.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/widgets/__init__.py +0 -0
- {webwidgets-1.1.0 → webwidgets-1.1.1}/webwidgets/widgets/widget.py +0 -0
|
@@ -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,
|
|
19
|
+
validate_css_identifier, validate_css_selector, validate_css_value, \
|
|
20
|
+
validate_html_class
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
class
|
|
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.
|
|
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,
|
|
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
|
|