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
1 change: 0 additions & 1 deletion web-testbed/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
name = "testbed"
version = "0.0.1"

#[dependency-groups]
[project.optional-dependencies]
test = [
"briefcase",
Expand Down
27 changes: 27 additions & 0 deletions web-testbed/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,34 @@

import pytest

from .tests_backend.page_singleton import BackgroundPage

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


# With this page injection method, we could possibly extend so that
# multiple pages can be created and be running at once (if it is ever
# needed in the future). Would need to add a method/fixture to store
# and switch between them.
@pytest.fixture(scope="session")
def page():
p = BackgroundPage()
yield p


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


@pytest.fixture(scope="session")
Expand Down
18 changes: 3 additions & 15 deletions web-testbed/tests/tests_backend/page_singleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,13 @@


class BackgroundPage:
_inst = None
_lock = threading.Lock()

def __new__(cls):
with cls._lock:
if cls._inst is None:
cls._inst = super().__new__(cls)
return cls._inst

@classmethod
def get(cls):
return cls()

def __init__(self):
if getattr(self, "_init", False):
return
self._init = True
self._ready = threading.Event()
self._loop = None
self._thread = threading.Thread(target=self._run, name="PageLoop", daemon=True)
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
self._ready.wait()

Expand Down Expand Up @@ -62,8 +49,9 @@ async def _bootstrap(self):

self._alock = asyncio.Lock()
except Exception:
self._alock = asyncio.Lock()
raise
finally:
self._alock = asyncio.Lock()
self._ready.set()

async def _eval(self, js, *args):
Expand Down
41 changes: 41 additions & 0 deletions web-testbed/tests/tests_backend/proxies/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
class ProxyBase:
page_provider = staticmethod(lambda: None)

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

# Extend to also hold other objects, including app and main_window?
def __str__(self):
widget_id = object.__getattribute__(self, "id")
return f"self.my_widgets[{widget_id!r}]"

def __setattr__(self, name, value):
# METHOD 1 (working)
literal = (
repr(str(value))
if not isinstance(value, (int, float, bool, type(None)))
else repr(value)
)

# METHOD 2 (working)
# try:
# literal = json.dumps(value)
# except TypeError:
# literal = json.dumps(str(value))

# METHOD 3 (working)
# if name == "text":
# literal = repr(str(value))
# else:
# try:
# literal = json.dumps(value)
# except TypeError:
# literal = repr(value)

code = f"{str(self)}.{name} = {literal}"
self._page().eval_js("(code) => window.test_cmd(code)", code)

def __getattr__(self, name):
code = f"result = {str(self)}.{name}"

return self._page().eval_js("(code) => window.test_cmd(code)", code)
14 changes: 7 additions & 7 deletions web-testbed/tests/tests_backend/proxies/box_proxy.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from ..page_singleton import BackgroundPage


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

page_provider = staticmethod(lambda: None)

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

def __init__(self, children=None):
# Create box object remotely
self.id = self._create_remote_box()
Expand All @@ -19,15 +21,13 @@ def _from_id(cls, box_id: str):
return obj

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

def add(self, widget):
page = BackgroundPage.get()
code = f"self.my_widgets['{self.id}'].add(self.my_widgets['{widget.id}'])"
page.eval_js("(code) => window.test_cmd(code)", code)
self._page().eval_js("(code) => window.test_cmd(code)", code)
56 changes: 6 additions & 50 deletions web-testbed/tests/tests_backend/proxies/button_proxy.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,16 @@
from ..page_singleton import BackgroundPage
from .base import ProxyBase


class ButtonProxy:
class ButtonProxy(ProxyBase):
def __init__(self):
object.__setattr__(self, "_inited", False)

button_id = self.setup()
object.__setattr__(self, "id", button_id)

object.__setattr__(self, "_inited", True)

def __setattr__(self, name, value):
page = BackgroundPage.get()
widget_id = object.__getattribute__(self, "id")

# METHOD 1 (working)

literal = (
repr(str(value))
if not isinstance(value, (int, float, bool, type(None)))
else repr(value)
)

# METHOD 2 (working)
"""
try:
literal = json.dumps(value)
except TypeError:
literal = json.dumps(str(value))
"""
# METHOD 3 (working)
"""
if name == "text":
literal = repr(str(value))
else:
try:
literal = json.dumps(value)
except TypeError:
literal = repr(value)
"""

code = f"self.my_widgets[{widget_id!r}].{name} = {literal}"
page.eval_js("(code) => window.test_cmd(code)", code)

def __getattr__(self, name):
page = BackgroundPage.get()

code = f"result = self.my_widgets['{self.id}'].{name}"

return page.eval_js("(code) => window.test_cmd(code)", code)

def setup(self):
page = BackgroundPage.get()
code = (
"new_widget = toga.Button('Hello')\n"
"self.my_widgets[new_widget.id] = new_widget\n"
"result = new_widget.id"
)
return page.eval_js("(code) => window.test_cmd(code)", code)
widget_id = self._page().eval_js("(code) => window.test_cmd(code)", code)

object.__setattr__(self, "id", widget_id)
object.__setattr__(self, "_inited", True)
12 changes: 7 additions & 5 deletions web-testbed/tests/tests_backend/proxies/main_window_proxy.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from ..page_singleton import BackgroundPage
from .box_proxy import BoxProxy


class MainWindowProxy:
"""Proxy that can get/set content. Content must be a BoxProxy."""

page_provider = staticmethod(lambda: None)

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

@property
def content(self):
page = BackgroundPage.get()
code = "result = self.main_window.content.id"
box_id = page.eval_js("(code) => window.test_cmd(code)", code)
box_id = self._page().eval_js("(code) => window.test_cmd(code)", code)
if box_id is None:
return BoxProxy()
proxy = BoxProxy.__new__(BoxProxy)
Expand All @@ -18,6 +21,5 @@ def content(self):

@content.setter
def content(self, box_proxy):
page = BackgroundPage.get()
code = f"self.main_window.content = self.my_widgets['{box_proxy.id}']"
page.eval_js("(code) => window.test_cmd(code)", code)
self._page().eval_js("(code) => window.test_cmd(code)", code)
17 changes: 10 additions & 7 deletions web-testbed/tests/tests_backend/widgets/button.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
from ..page_singleton import BackgroundPage
class ButtonProbe:
page_provider = staticmethod(lambda: None)

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

class ButtonProbe:
def __init__(self, widget):
object.__setattr__(self, "id", widget.id)
object.__setattr__(self, "dom_id", f"toga_{widget.id}")

def __getattr__(self, name):
page = BackgroundPage.get()
page = self._page()

match name:
case "text":
# was inner_text, but it trims leading/trailing spaces and removes only
# Was inner_text, but it trims leading/trailing spaces and removes only
# whitespace.
return page.run_coro(
lambda page: page.locator(f"#{self.dom_id}").text_content()
lambda p: p.locator(f"#{self.dom_id}").text_content()
)
case "height":
box = page.run_coro(
lambda page: page.locator(f"#{self.dom_id}").bounding_box()
lambda p: p.locator(f"#{self.dom_id}").bounding_box()
)
return None if box is None else box["height"]

return "No match"
# raise AttributeError(name)

""" ALTERNATIVE METHOD - Keep just in case
# Alternate Method - Keep just in case
"""
sel = f"#{self.dom_id}"

if name == "text":
Expand Down
10 changes: 3 additions & 7 deletions web-testbed/tests/widgets/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@

# import toga
from probe import get_probe

# from .probe import get_probe
from tests.tests_backend.proxies.box_proxy import BoxProxy

# from ..tests_backend.proxies.box_proxy import BoxProxy

# TODO: Don't enable until below is implemented.
# @pytest.fixture
# async def widget():
# raise NotImplementedError("test modules must define a `widget` fixture")
@pytest.fixture
async def widget():
raise NotImplementedError("test modules must define a `widget` fixture")


@pytest.fixture
Expand Down