Skip to content

Commit 848c5fb

Browse files
jana-selvackunki
andauthored
#287 working examples for functional ui (#288)
* working examples for functional ui * removed unwanted comment * added new line * project:fix * Update doc/developer_guide/developer-guide.rst Co-authored-by: Christoph Kuhnke <[email protected]> * Update doc/developer_guide/developer-guide.rst Co-authored-by: Christoph Kuhnke <[email protected]> * Update doc/developer_guide/developer-guide.rst Co-authored-by: Christoph Kuhnke <[email protected]> * Update test/unit/ui/test_solara.py Co-authored-by: Christoph Kuhnke <[email protected]> * Update test/unit/ui/test_solara.py Co-authored-by: Christoph Kuhnke <[email protected]> * Update test/unit/ui/test_solara.py Co-authored-by: Christoph Kuhnke <[email protected]> * Update test/unit/ui/test_solara.py Co-authored-by: Christoph Kuhnke <[email protected]> * removed unwanted comments * review comments fix * project:fix * added few docstring * added change log * poetry lock after poetry update * fix for ai lab issue exasol/ai-lab#428 * Update test/unit/ui/app.py Co-authored-by: Christoph Kuhnke <[email protected]> --------- Co-authored-by: Christoph Kuhnke <[email protected]>
1 parent 2caaa8f commit 848c5fb

File tree

9 files changed

+3338
-1638
lines changed

9 files changed

+3338
-1638
lines changed

doc/changes/unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
# Unreleased
2+
* #287: working examples for functional UI

doc/developer_guide/developer-guide.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,35 @@ particular flavor to be present.
8888

8989
In consequence, updating the SLCR version potentially may require updating the
9090
NC tests as well, e.g. the name of the flavor used in the tests.
91+
92+
93+
UI Testing Setup
94+
================
95+
96+
To run UI tests using [Playwright](https://playwright.dev/) and Pytest, follow these steps to ensure all dependencies are installed and snapshots are updated correctly.
97+
98+
1. **Install Playwright browser binaries**
99+
100+
This command installs the required browser (Chromium) for Playwright:
101+
102+
.. code-block:: bash
103+
104+
playwright install chromium
105+
106+
2. **Install system dependencies for Playwright**
107+
108+
This is necessary especially on Linux systems to ensure all required libraries are available:
109+
110+
.. code-block:: bash
111+
112+
playwright install-deps
113+
114+
3. **Run UI tests and update Solara snapshots**
115+
116+
Use the following command to run UI tests and update the reference [solara](https://solara.dev/) snapshots used for visual comparison:
117+
118+
.. code-block:: bash
119+
120+
pytest test/ui/*.py --solara-update-snapshots
121+
122+
This will overwrite existing snapshots with new ones generated during the test.

poetry.lock

Lines changed: 3136 additions & 1638 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies = [
1919
"sqlcipher3-binary (>= 0.5.0); sys_platform==\"linux\"",
2020
"sqlcipher3 (>= 0.5.0); sys_platform==\"darwin\" or sys_platform==\"win32\"",
2121
"exasol-saas-api (>=0.9.0, <3)", # needed to get database id for database name
22+
"pydantic (==2.11.5)", # See https://github.com/exasol/ai-lab/issues/428
2223
]
2324

2425
[project.optional-dependencies]
@@ -77,6 +78,7 @@ pytest-mock = "^3.7.0"
7778
pytest_dependency = ">=0.6.0"
7879
exasol-toolbox = "^1.9.0"
7980
pytest-exasol-backend = ">=1.1.0,<2"
81+
pytest-ipywidgets = ">=1.52.0,<2.0.0"
8082

8183

8284
[tool.pytest.ini_options]

test/ui/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from pathlib import Path
2+
from typing import Any
3+
4+
import pytest
5+
6+
7+
@pytest.fixture(scope="session")
8+
def solara_snapshots_directory(request: Any) -> Path:
9+
path = Path(request.config.rootpath) / "test" / "ui" / "snapshots"
10+
if not path.exists():
11+
path.mkdir(exist_ok=True, parents=True)
12+
return path

test/ui/test_gui.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import ipywidgets as widgets
2+
import playwright.sync_api
3+
import solara
4+
from IPython.display import display
5+
6+
7+
def test_widget_button_solara(
8+
solara_test, page_session: playwright.sync_api.Page, assert_solara_snapshot
9+
):
10+
"""
11+
Test that clicking an ipywidgets.Button updates its description.
12+
13+
Simulates a button click in a browser using Playwright, checks that the button text
14+
changes as expected, and verifies the result with a UI snapshot.
15+
"""
16+
button = widgets.Button(description="Click Me!")
17+
18+
def change_description(obj):
19+
button.description = "Tested event"
20+
21+
button.on_click(change_description)
22+
display(button)
23+
button_sel = page_session.locator("text=Click Me!")
24+
button_sel.wait_for()
25+
button_sel.click()
26+
page_session.locator("text=Tested event").wait_for()
27+
assert_solara_snapshot(
28+
page_session.locator("text=Tested event").screenshot(),
29+
postfix="-ipyvuetify3" if solara.util.ipyvuetify_major_version == 3 else "",
30+
)

test/unit/ui/app.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""
2+
A simple ipywidgets application demonstrating interactive widget behavior.
3+
This file is being called from test/unit/ui/test_solara.py
4+
5+
This module defines a basic user interface with a text box and a button using ipywidgets.
6+
The text box is initially set to "init". When the button is clicked, the text box's value
7+
is updated to "click" via an event handler. The widgets are combined in a vertical box (VBox)
8+
layout and exposed as the variable `app` for integration with other systems or testing.
9+
10+
Widgets:
11+
- Text: Displays a string, initially "init".
12+
- Button: When clicked, sets the text widget's value to "click".
13+
"""
14+
15+
import ipywidgets
16+
17+
text = ipywidgets.Text(value="init")
18+
button = ipywidgets.Button(description="Click me")
19+
20+
21+
def on_button_clicked(b):
22+
text.value = "click"
23+
24+
25+
button.on_click(on_button_clicked)
26+
app = ipywidgets.VBox([text, button])

test/unit/ui/conftest.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pytest
2+
import solara.server.app
3+
import solara.server.kernel_context
4+
from solara.server import kernel
5+
from solara.server.kernel_context import VirtualKernelContext
6+
7+
8+
@pytest.fixture(autouse=True)
9+
def kernel_context():
10+
kernel_shared = kernel.Kernel()
11+
context = VirtualKernelContext(id="1", kernel=kernel_shared, session_id="session-1")
12+
try:
13+
with context:
14+
yield context
15+
finally:
16+
with context:
17+
context.close()
18+
19+
20+
@pytest.fixture()
21+
def no_kernel_context(kernel_context):
22+
context = solara.server.kernel_context.get_current_context()
23+
solara.server.kernel_context.set_current_context(None)
24+
try:
25+
yield
26+
finally:
27+
solara.server.kernel_context.set_current_context(context)

test/unit/ui/test_solara.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import importlib.resources
2+
import logging
3+
from contextlib import contextmanager
4+
from pathlib import Path
5+
6+
import ipywidgets
7+
import solara
8+
from solara.server import reload
9+
from solara.server.app import AppScript
10+
11+
logger = logging.getLogger("solara.server.app_test")
12+
13+
APP_SRC = importlib.resources.files("test.unit.ui") / "app.py"
14+
reload.reloader.start()
15+
16+
17+
@contextmanager
18+
def app_box_and_rc(app_name, kernel_context):
19+
app = AppScript(str(app_name))
20+
app.init()
21+
try:
22+
with kernel_context:
23+
# get root widget
24+
el = app.run()
25+
root = solara.RoutingProvider(
26+
children=[el], routes=app.routes, pathname="/"
27+
)
28+
# rc = render context
29+
box, rc = solara.render(root, handle_error=False)
30+
yield box, rc
31+
finally:
32+
app.close()
33+
34+
35+
def test_notebook_widget(kernel_context, no_kernel_context):
36+
"""
37+
The fixture no_kernel_context is not used directly in this test but is required, though, to
38+
make the test pass.
39+
"""
40+
with app_box_and_rc(APP_SRC, kernel_context) as (box, rc):
41+
button = rc.find(ipywidgets.Button).widget
42+
text = rc.find(ipywidgets.Text).widget
43+
assert isinstance(button, ipywidgets.Button)
44+
assert isinstance(text, ipywidgets.Text)
45+
assert text.value == "init"
46+
button.click()
47+
assert text.value == "click"
48+
49+
50+
def test_ipywidgets_update_global_state():
51+
"""
52+
Test that clicking an ipywidgets.Button correctly updates a global state dictionary
53+
with the current value of an ipywidgets.Text widget.
54+
55+
This test simulates user input by setting the value of the Text widget,
56+
confirms that the global state does not update before the button is clicked,
57+
and then checks that clicking the button updates the global state as expected.
58+
"""
59+
import ipywidgets as widgets
60+
61+
global_state = {"username": ""}
62+
textbox = widgets.Text()
63+
button = widgets.Button(description="Submit")
64+
65+
def on_click(b):
66+
global_state["username"] = textbox.value
67+
68+
button.on_click(on_click)
69+
textbox.value = "alice" # Simulate user input
70+
assert global_state["username"] == "" # assert global state is unchanged
71+
button.click() # Simulate button click
72+
assert global_state["username"] == "alice"

0 commit comments

Comments
 (0)