Skip to content
Closed
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
6f5a0b9
Squashed 'testbed/toga_web_testing/' content from commit c653c6b77
Stringer90 Aug 28, 2025
f42e968
Merge commit '6f5a0b9f9ed950af4b549c8361f81e3dd771f84e' as 'testbed/t…
Stringer90 Aug 28, 2025
2593c40
Purge pyc files from repo.
freakboy3742 Aug 29, 2025
65c02f3
Reorganize code to make better use of Briefcase.
freakboy3742 Aug 29, 2025
6b94754
Fix web-testbed/pyproject.toml. Changes made by pre-commit
Stringer90 Sep 5, 2025
f57616d
Remove page singleton method, using class injection now. Experimented…
Stringer90 Sep 6, 2025
c365377
Merge pull request #1 from Stringer90/web-workspace/remove-page-singl…
Stringer90 Sep 6, 2025
c914cd7
Added base proxy to be more dynamic
vt37 Sep 7, 2025
7b87ea9
Modify conftest to reflect recent commits
vt37 Sep 7, 2025
e147f07
Deleted base.py
vt37 Sep 7, 2025
99a4658
Changed the wait times for page to 7 secs
vt37 Sep 7, 2025
91373bf
Make Button probe non-dynamic. Other minor changes.
Stringer90 Sep 8, 2025
94609a3
Rename 'page_singleton.py' to 'playwright_page.py'.
Stringer90 Sep 8, 2025
600498e
Merge pull request #2 from Stringer90/web-workspace/dynamic-proxy
Stringer90 Sep 8, 2025
44e7dfc
Merge pull request #3 from Stringer90/workspace
vt37 Sep 8, 2025
7678c9e
Works with button test_text. Trying new proxy architecture with non-w…
Stringer90 Sep 14, 2025
87ab682
Last 2 button tests work.
Stringer90 Sep 14, 2025
666a2d2
Minor changes from recent feedback. Needs the 'return repr(str(value)…
Stringer90 Sep 14, 2025
b403792
Proxify BoxProxy. Non-widget objects use 'my_objs' dict. Tried integr…
Stringer90 Sep 15, 2025
485fd60
Merge pull request #4 from Stringer90/web-workspace/button-press-test
Stringer90 Sep 15, 2025
7e89ee8
Added serialize to test_cmd
vt37 Sep 15, 2025
214bee1
Added unwrap and fix repr(str() issue to fit with the new format
vt37 Sep 15, 2025
ce7eab5
Merge pull request #5 from Stringer90/web-workspace/serialize-test-cm…
vt37 Sep 15, 2025
61e365c
Removed BoxProxy from wire_page as unneeded
vt37 Sep 15, 2025
14ab27f
Added back the '[dependency-groups]' header
vt37 Sep 16, 2025
96aaf04
Merge pull request #6 from Stringer90/workspace
vt37 Sep 16, 2025
b5fb51f
Restructure of proxy architecure both on app and test suite side. Mer…
Stringer90 Sep 19, 2025
5c4f4ec
Put all proxy definitions in 'object_proxies'. Probe structure change…
Stringer90 Sep 21, 2025
61b2522
Fix merge conflict.
Stringer90 Sep 21, 2025
c6bc67a
Modify app.py so test_cmd exposed only if env TOGA_WEB_TESTING is true
vt37 Sep 21, 2025
1b46a5f
Added TOGA_WEB_TESTING to True when running Playwright
vt37 Sep 21, 2025
f0a9065
Merge pull request #8 from Stringer90/workspace
vt37 Sep 23, 2025
dfbfcad
Added script, toga class shimming, web test harness for app.py, and o…
Stringer90 Sep 28, 2025
fe87b15
Remove 'page_singleton.py' no-op. Integrate 'AttributeProxy' with '__…
Stringer90 Sep 29, 2025
86b5f51
Merge pull request #9 from Stringer90/web-workspace/script-and-shim
Stringer90 Sep 29, 2025
3da83f0
Fix recursive method calls in '_deserialise_payload()'.
Stringer90 Sep 30, 2025
10dc309
Added local policies and object caching
vt37 Sep 30, 2025
bbc2b1e
Merge pull request #10 from Stringer90/workspace
vt37 Sep 30, 2025
2c058a2
Change proxy line protocol from sending code strings to the web app t…
Stringer90 Oct 2, 2025
c6119bd
Merge pull request #11 from Stringer90/workspace
Stringer90 Oct 2, 2025
5f83d4b
Added callable source deserialization to enable runner to host valida…
vt37 Oct 2, 2025
bd7522f
Added four new test suites and updated conftest with new fixtures
vt37 Oct 2, 2025
6fc9f1f
Add widget to SHIMS and update apply() to allow call imports.
vt37 Oct 3, 2025
5bd78a7
Added new widgets and update base_proxy to serialize callables
vt37 Oct 3, 2025
22fbe52
Added new widget probes
vt37 Oct 3, 2025
2ddbea6
Merge pull request #12 from Stringer90/web-workspace/python-callables
vt37 Oct 3, 2025
c905acc
Merge pull request #13 from Stringer90/web-workspace/python-callables
vt37 Oct 3, 2025
354f8ae
Fixed pre-commit issues
vt37 Oct 3, 2025
8145f08
Merge pull request #14 from Stringer90/workspace
vt37 Oct 3, 2025
20d97ce
Added date/time envelopes and tests
vt37 Oct 6, 2025
0beea39
Merge pull request #15 from Stringer90/web-workspace/date-time
vt37 Oct 6, 2025
3506200
Merge pull request #16 from Stringer90/workspace
vt37 Oct 6, 2025
e435dbb
Added back 'encoding.py' to fix errors. Quieter script output.
Stringer90 Oct 7, 2025
e6a2367
Fixed the name placeholder issue
vt37 Oct 13, 2025
9121213
Merge pull request #17 from Stringer90/workspace
vt37 Oct 13, 2025
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: 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
4 changes: 4 additions & 0 deletions travertino/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ classifiers = [
"Topic :: Software Development :: User Interfaces",
]

dependencies = [
"packaging",
]

[project.urls]
Homepage = "https://beeware.org/travertino"
Funding = "https://beeware.org/contributing/membership/"
Expand Down
15 changes: 15 additions & 0 deletions web-testbed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
This repository is dedicated to development, testing, and proof-of-concept work related to issue [3545](https://github.com/beeware/toga/issues/3545), which focuses on implementing testing for the web platform.

## How We Run this Test Suite
1. Open this directory.
2. Create a Python 3.12 virtual environment and install test requirements:
- `python3.12 -m venv venv`
- `source venv/bin/activate`
- `pip install -U pip`
- `pip install --group test`
- `playwright install chromium`
3. Run your Toga app as a web app.
- `briefcase run web`
4. In a separate terminal, run the test suite:
- `source venv/bin/activate`
- `pytest tests`
49 changes: 49 additions & 0 deletions web-testbed/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[project]
name = "testbed"
version = "0.0.1"

[dependency-groups]
test = [
"briefcase",
"playwright == 1.51.0",
# "pytest==8.4.1",
"pytest==8.3.5",
# "pytest-asyncio==1.1.0",
"pytest-asyncio==0.26.0",
"pytest-playwright==0.7.0",
]

[tool.briefcase]
project_name = "Toga Web Testbed"
bundle = "org.beeware.toga"
url = "https://beeware.org"
license = "BSD-3-Clause"
license-files = [
"LICENSE",
]
author = "Tiberius Yak"
author_email = "[email protected]"

[tool.briefcase.app.testbed]
formal_name = "Toga Testbed"
description = "A testbed for Toga visual tests"
icon = "icons/testbed"
sources = [
"src/testbed",
]
test_sources = [
"tests",
]
requires = [
"../travertino",
"../core",
]

[tool.briefcase.app.testbed.web]
requires = [
"../web"
]
style_framework = "Shoelace v2.3"

[tool.pytest.ini_options]
asyncio_mode = "auto"
Empty file.
4 changes: 4 additions & 0 deletions web-testbed/src/testbed/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from testbed.app import main

if __name__ == "__main__":
main().main_loop()
122 changes: 122 additions & 0 deletions web-testbed/src/testbed/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import os
import types
from unittest.mock import Mock

import toga
from toga.style import Pack
from toga.style.pack import COLUMN

try:
import js
except ModuleNotFoundError:
js = None
try:
from pyodide.ffi import create_proxy, to_js
except ModuleNotFoundError:
pyodide = None


def _truthy(v) -> bool:
return str(v).strip().lower() in {"1", "true", "yes", "on"}


def _web_testing_enabled() -> bool:
if _truthy(os.getenv("TOGA_WEB_TESTING")):
return True

if js is not None:
try:
if _truthy(getattr(js.window, "TOGA_WEB_TESTING", "")):
return True
qs = str(getattr(js.window, "location", None).search or "")
# enable if ?toga_web_testing=1 in url
if "toga_web_testing" in qs.lower():
return True
except Exception:
pass

return False


class HelloWorld(toga.App):
def startup(self):
main_box = toga.Box(style=Pack(direction=COLUMN))
self.label = toga.Label(id="myLabel", text="Test App - Toga Web Testing")

if _web_testing_enabled() and js is not None and create_proxy is not None:
self.my_objs = {}
js.window.test_cmd = create_proxy(self.cmd_test)

main_box.add(self.label)
self.main_window = toga.MainWindow(title=self.formal_name)
self.main_window.content = main_box
self.main_window.show()

def cmd_test(self, code):
env = {"self": self, "toga": toga, "my_objs": self.my_objs, "Mock": Mock}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what you're looking for here is vars(). That's a complete state of all variables that are visible in the current scope.

local = {}
try:
exec(code, env, local)
result = local.get("result", env.get("result"))
envelope = self._serialise_payload(result)
return to_js(envelope, dict_converter=js.Object.fromEntries)
except Exception as e:
return to_js(
{"type": "error", "value": str(e)}, dict_converter=js.Object.fromEntries
)

def _serialise_payload(self, x):
# primitives
if x is None:
return {"type": "none", "value": None}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value is likely redundant here.

if isinstance(x, bool):
return {"type": "bool", "value": x}
if isinstance(x, int):
return {"type": "int", "value": x}
if isinstance(x, float):
return {"type": "float", "value": x}
if isinstance(x, str):
return {"type": "str", "value": x}

# containers
if isinstance(x, list):
return {"type": "list", "items": [self._serialise_payload(i) for i in x]}
if isinstance(x, tuple):
return {"type": "tuple", "items": [self._serialise_payload(i) for i in x]}
if isinstance(x, dict):
items = []
for k, v in x.items():
if k is None:
key_env = {"type": "none", "value": None}
elif isinstance(k, bool):
key_env = {"type": "bool", "value": k}
elif isinstance(k, int):
key_env = {"type": "int", "value": k}
elif isinstance(k, float):
key_env = {"type": "float", "value": k}
elif isinstance(k, str):
key_env = {"type": "str", "value": k}
else:
key_env = {"type": "str", "value": str(k)}
items.append([key_env, self._serialise_payload(v)])
return {"type": "dict", "items": items}

# references by id
obj_id = self._key_for(x)
is_callable = callable(x) or isinstance(
x, (types.FunctionType, types.MethodType)
)
return {"type": "callable" if is_callable else "object", "id": obj_id}

def _key_for(self, x):
for k, v in self.my_objs.items():
if v is x:
return k
# If not registered, register it
k = str(id(x))
self.my_objs[k] = x
return k


def main():
return HelloWorld()
Empty file added web-testbed/tests/__init__.py
Empty file.
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)),
)
32 changes: 32 additions & 0 deletions web-testbed/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# from pytest import fixture, register_assert_rewrite, skip
# import toga

import pytest

from .tests_backend.playwright_page import BackgroundPage
from .tests_backend.proxies.app_proxy import AppProxy
from .tests_backend.proxies.base_proxy import BaseProxy
from .tests_backend.widgets.base import SimpleProbe


@pytest.fixture(scope="session")
def page():
p = BackgroundPage()
return p


# Inject Playwright page object into
@pytest.fixture(scope="session", autouse=True)
def _wire_page(page):
BaseProxy.page_provider = staticmethod(lambda: page)
SimpleProbe.page_provider = staticmethod(lambda: page)


@pytest.fixture(scope="session")
def app():
return AppProxy()


@pytest.fixture(scope="session")
def main_window(app):
return app.main_window
22 changes: 22 additions & 0 deletions web-testbed/tests/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# A test object that can be used as data
class MyObject:
def __str__(self):
return "My Test Object"


# The text examples must both increase and decrease in size between examples to
# ensure that reducing the size of a label doesn't prevent future labels from
# increasing in size.
TEXTS = [
"example",
"",
"a",
" ",
"ab",
"abc",
"hello world",
"hello\nworld",
"你好, wørłd!",
1234,
MyObject(),
]
Empty file.
Loading
Loading