Skip to content
Merged
1 change: 1 addition & 0 deletions positron/src/positron/django_templates/manage.py.tmpl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""

import os
import sys

Expand Down
7 changes: 1 addition & 6 deletions web-testbed/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "testbed"
version = "0.0.1"

[dependency-groups]
[project.optional-dependencies]
test = [
"briefcase",
"playwright == 1.51.0",
Expand Down Expand Up @@ -45,10 +45,5 @@ requires = [
]
style_framework = "Shoelace v2.3"

# Attempted integrating async operations, but it required calls like 'widget.text'
# to need 'await' when calling, which is not the case in Toga test methods.

# Uncomment below if using async.

[tool.pytest.ini_options]
asyncio_mode = "auto"
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.playwright_page 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_proxy import BaseProxy
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):
BaseProxy.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
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 All @@ -50,20 +37,21 @@ 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/")
# await self._page.goto(
# "http://localhost:8080", wait_until="load", timeout=30_000
# )
await self._page.wait_for_timeout(5000)
await self._page.wait_for_timeout(7000)

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

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
86 changes: 86 additions & 0 deletions web-testbed/tests/tests_backend/proxies/base_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
class BaseProxy:
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)

@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 __repr__(self):
return f"<WidgetProxy id={self.id}>"

def __str__(self):
return f"WidgetProxy({self.id})"
16 changes: 9 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,13 @@
from ..page_singleton import BackgroundPage


class BoxProxy:
# Currently only for use in the 'probe' pytest fixture.

"""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 +23,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)
60 changes: 6 additions & 54 deletions web-testbed/tests/tests_backend/proxies/button_proxy.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,12 @@
from ..page_singleton import BackgroundPage
from .base_proxy import BaseProxy


class ButtonProxy:
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()
class ButtonProxy(BaseProxy):
def __init__(self, text="Hello"):
code = (
"new_widget = toga.Button('Hello')\n"
f"new_widget = toga.Button({repr(text)})\n"
"self.my_widgets[new_widget.id] = new_widget\n"
"result = new_widget.id"
)
return page.eval_js("(code) => window.test_cmd(code)", code)
wid = self._page().eval_js("(code) => window.test_cmd(code)", code)
super().__init__(wid)
14 changes: 8 additions & 6 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."""
"""Minimal 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)
Loading