Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions web-testbed/tests/assertions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from pytest import approx

TRANSPARENT = "transparent"


def assert_background_color(actual, expected):
# For platforms where alpha blending is manually implemented, the
# probe.background_color property returns a tuple consisting of:
# - The widget's background color
# - The widget's parent's background color
# - The widget's original alpha value - Required for deblending
if isinstance(actual, tuple):
actual_widget_bg, actual_parent_bg, actual_widget_bg_alpha = actual
if actual_widget_bg_alpha == 0:
# Since a color having an alpha value of 0 cannot be deblended.
# So, the deblended widget color would be equal to the parent color.
deblended_actual_widget_bg = actual_parent_bg
else:
deblended_actual_widget_bg = actual_widget_bg.unblend_over(
actual_parent_bg, actual_widget_bg_alpha
)
if isinstance(expected, tuple):
expected_widget_bg, expected_parent_bg, expected_widget_bg_alpha = expected
if expected_widget_bg_alpha == 0:
# Since a color having an alpha value of 0 cannot be deblended.
# So, the deblended widget color would be equal to the parent color.
deblended_expected_widget_bg = expected_parent_bg
else:
deblended_expected_widget_bg = expected_widget_bg.unblend_over(
expected_parent_bg, expected_widget_bg_alpha
)
assert_color(deblended_actual_widget_bg, deblended_expected_widget_bg)
# For comparison when expected is a single value object
else:
if (expected == TRANSPARENT) or (
expected.a == 0
# Since a color having an alpha value of 0 cannot be deblended to
# get the exact original color, as deblending in such cases would
# lead to a division by zero error. So, just check that widget and
# parent have the same color.
):
assert_color(actual_widget_bg, actual_parent_bg)
elif expected.a != 1:
assert_color(deblended_actual_widget_bg, expected)
else:
assert_color(actual_widget_bg, expected)
# For other platforms
else:
assert_color(actual, expected)


def assert_color(actual, expected):
if expected in {None, TRANSPARENT}:
assert expected == actual
else:
if actual in {None, TRANSPARENT}:
assert expected == actual
else:
assert (actual.r, actual.g, actual.b, actual.a) == (
expected.r,
expected.g,
expected.b,
approx(expected.a, abs=(1 / 255)),
)
8 changes: 4 additions & 4 deletions web-testbed/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@

from .tests_backend.playwright_page import BackgroundPage

# In future, would only need to be AppProxy, MainWindowProxy, ProxyBase and
# a SimpleProbe/BaseProbe.
# Possibly only ProxyBase and SimpleProbe/BaseProbe.
# In future, would only need to be ExprProxy and SimpleProbe/BaseProbe.
from .tests_backend.proxies.app_proxy import AppProxy
from .tests_backend.proxies.base_proxy import BaseProxy
from .tests_backend.proxies.box_proxy import BoxProxy
from .tests_backend.proxies.expr_proxy import ExprProxy
from .tests_backend.proxies.main_window_proxy import MainWindowProxy
from .tests_backend.widgets.button import ButtonProbe

Expand All @@ -22,13 +21,14 @@
@pytest.fixture(scope="session")
def page():
p = BackgroundPage()
yield p
return p


@pytest.fixture(scope="session", autouse=True)
def _wire_page(page):
BaseProxy.page_provider = staticmethod(lambda: page)
BoxProxy.page_provider = staticmethod(lambda: page)
ExprProxy.page_provider = staticmethod(lambda: page)
MainWindowProxy.page_provider = staticmethod(lambda: page)
ButtonProbe.page_provider = staticmethod(lambda: page)

Expand Down
17 changes: 10 additions & 7 deletions web-testbed/tests/tests_backend/playwright_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

class BackgroundPage:
def __init__(self):
if getattr(self, "_init", False):
return
self._init = True
self._ready = threading.Event()
self._loop = None
Expand Down Expand Up @@ -37,15 +35,20 @@ async def _bootstrap(self):
self._context = await self._browser.new_context()
self._page = await self._context.new_page()

await self._page.goto("http://localhost:8080/")
# await self._page.goto(
# "http://localhost:8080", wait_until="load", timeout=30_000
# )
await self._page.wait_for_timeout(7000)
await self._page.goto(
"http://localhost:8080", wait_until="load", timeout=30_000
)

await self._page.wait_for_function(
"() => typeof window.test_cmd === 'function'"
)

await self._page.evaluate(
"(code) => window.test_cmd(code)", "self.my_widgets = {}"
)
await self._page.evaluate(
"(code) => window.test_cmd(code)", "self.my_objs = {}"
)

self._alock = asyncio.Lock()
except Exception:
Expand Down
7 changes: 7 additions & 0 deletions web-testbed/tests/tests_backend/proxies/attribute_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .expr_proxy import ExprProxy


class AttributeProxy(ExprProxy):
def __init__(self, owner: ExprProxy, name: str):
ref_expr = f"getattr({owner.js_ref}, {repr(name)})"
super().__init__(ref_expr)
88 changes: 15 additions & 73 deletions web-testbed/tests/tests_backend/proxies/base_proxy.py
Original file line number Diff line number Diff line change
@@ -1,86 +1,28 @@
class BaseProxy:
from .expr_proxy import ExprProxy


class BaseProxy(ExprProxy):
_storage_expr: str = "self.my_widgets"

page_provider = staticmethod(lambda: None)

def _page(self):
return type(self).page_provider()

def __init__(self, widget_id: str):
object.__setattr__(self, "_id", widget_id)
def __init__(self, object_key: str):
object.__setattr__(self, "_id", object_key)
ref_expr = f"{type(self)._storage_expr}[{repr(object_key)}]"
super().__init__(ref_expr)

@property
def id(self) -> str:
return object.__getattribute__(self, "_id")

@property
def js_ref(self) -> str:
return f"self.my_widgets[{repr(self.id)}]"

@classmethod
def from_id(cls, widget_id: str) -> "BaseProxy":
return cls(widget_id)

def _is_function(self, name: str) -> bool:
prop = repr(name)
code = (
f"_obj = {self.js_ref}\n"
f"_attr = getattr(_obj, {prop})\n"
f"result = callable(_attr)"
)
return bool(self._page().eval_js("(code) => window.test_cmd(code)", code))

def _encode_value(self, value) -> str:
# other proxy, pass by reference
if isinstance(value, BaseProxy):
return value.js_ref
# if plain primitives, embed as python literal
if isinstance(value, (str, int, float, bool)) or value is None:
return repr(value)
# everything else use text form (what Toga expects for .text, etc)
return repr(str(value))

def __setattr__(self, name, value):
prop = repr(name)

if name.startswith("_"):
return object.__setattr__(self, name, value)
if name == "id":
raise AttributeError("Proxy 'id' is read-only")

rhs = self._encode_value(value)

code = f"setattr({self.js_ref}, {prop}, {rhs})"
self._page().eval_js("(code) => window.test_cmd(code)", code)

def __getattr__(self, name):
prop = repr(name)

# If it's a function on the remote side, return a Python wrapper
if self._is_function(name):

def _method(*args):
parts = [self._encode_value(a) for a in args]
args_py = ", ".join(parts)
code = (
f"_obj = {self.js_ref}\n"
f"_fn = getattr(_obj, {prop})\n"
f"result = _fn({args_py})"
)
return self._page().eval_js("(code) => window.test_cmd(code)", code)

return _method

# else plain property get
code = f"result = getattr({self.js_ref}, {prop})"
return self._page().eval_js("(code) => window.test_cmd(code)", code)

def add_to_main_window(self):
self._page().eval_js(
"(code) => window.test_cmd(code)",
f"self.main_window.content.add({self.js_ref})",
)
def from_id(cls, object_key: str):
self = object.__new__(cls)
BaseProxy.__init__(self, object_key)
return self

def __repr__(self):
return f"<WidgetProxy id={self.id}>"

def __str__(self):
return f"WidgetProxy({self.id})"
return f"<{type(self).__name__} id={self.id}>"
36 changes: 14 additions & 22 deletions web-testbed/tests/tests_backend/proxies/box_proxy.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,27 @@
class BoxProxy:
# Currently only for use in the 'probe' pytest fixture.
from .widget_proxy import WidgetProxy

"""Proxy for toga.Box(children=[...])."""

page_provider = staticmethod(lambda: None)
class BoxProxy(WidgetProxy):
_ctor_expr = "toga.Box"

def _page(self):
return type(self).page_provider()

def __init__(self, children=None):
# Create box object remotely
self.id = self._create_remote_box()
# If there's children, add them
def __init__(self, children=None, *args, **kwargs):
key = self._create_with_known_id(self._ctor_expr, *args, **kwargs)
super().__init__(key)
if children:
for child in children:
self.add(child)

@classmethod
def _from_id(cls, box_id: str):
obj = cls.__new__(cls)
object.__setattr__(obj, "id", box_id)
WidgetProxy.__init__(obj, box_id)
return obj

def _create_remote_box(self):
code = (
"new_box = toga.Box()\n"
"self.my_widgets[new_box.id] = new_box\n"
"result = new_box.id"
)
return self._page().eval_js("(code) => window.test_cmd(code)", code)

def add(self, widget):
code = f"self.my_widgets['{self.id}'].add(self.my_widgets['{widget.id}'])"
self._page().eval_js("(code) => window.test_cmd(code)", code)
child_js = getattr(widget, "js_ref", None)
if child_js is None:
child_js = f"{type(self)._storage_expr}[{repr(widget)}]"
self._page().eval_js(
"(code) => window.test_cmd(code)",
f"{self.js_ref}.add({child_js})",
)
17 changes: 7 additions & 10 deletions web-testbed/tests/tests_backend/proxies/button_proxy.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from .base_proxy import BaseProxy
from .widget_proxy import WidgetProxy


class ButtonProxy(BaseProxy):
def __init__(self, text="Hello"):
code = (
f"new_widget = toga.Button({repr(text)})\n"
"self.my_widgets[new_widget.id] = new_widget\n"
"result = new_widget.id"
)
wid = self._page().eval_js("(code) => window.test_cmd(code)", code)
super().__init__(wid)
class ButtonProxy(WidgetProxy):
_ctor_expr = "toga.Button"

def __init__(self, *args, **kwargs):
key = self._create_with_known_id(self._ctor_expr, *args, **kwargs)
super().__init__(key)
Loading