Examples
********
The core idea in Balder is to define "features" and "devices" that represent parts of your system. For Textual testing,
BalderHub provides ready-made features like :class:`TextualControlFeature` (for controlling the app) and `TextualPage`
(that allows to write tests according the Page-Object-Model).
Application-Under-Test
======================
In this example section we want to test a Stopwatch application, similar to the
`Textuals-Tutorial section `__.
You can find the code of this application at
`the GitHub Repository `_.
Basic Setup
===========
Start by creating a setup file (e.g., ``setup_stopwatch.py``).
Here's how to configure the minimum required elements:
* Import the necessary modules from Balder and BalderHub.
* Define a custom ``AppFeature`` class that returns your Textual app instance.
* Create a Setup class with two devices: one for the app itself and one for the controller that interacts with it.
In code, this looks like shown below:
.. code-block:: python
# file `setup_stopwatch.py`
import balder
from balderhub.textual.lib.scenario_features import TextualControlFeature, AppFeature
from .dut.stopwatch import StopwatchApp
class MyAppFeature(AppFeature):
def get_app(self):
return StopwatchApp()
class SetupStopwatch(balder.Setup):
class App(balder.Device):
app = MyAppFeature()
@balder.connect(App, over_connection=balder.Connection)
class Controller(balder.Device):
textual = TextualControlFeature(App="App")
With this setup, Balder can launch your Textual app in a controlled environment for testing.
As this package does not provide any test scenarios by itself, you need to define one.
Adding Pages and Widgets
========================
To test specific parts of your app (like screens or widgets), BalderHub lets you define "pages" that map to UI elements.
A ``TextualPage`` represents a view in your app and provides properties for easy access to widgets.
Create a new file for pages (e.g., ``pages.py``).
Here's how to define a page for a stopwatch:
* Import additional utilities from BalderHub for pages, components, and selectors.
* Define a TextualPage subclass with properties for each widget you want to interact with.
* Use selectors to locate widgets by tag, ID, or other attributes.
.. code-block:: python
# file: pages.py
from balderhub.textual.lib.scenario_features import TextualPage
from balderhub.textual.lib.utils import components
from balderhub.textual.lib.utils.selector import Selector
class StopwatchPage(TextualPage):
"""
This page represents the stopwatch screen in your Textual app.
It defines properties for key widgets like the time display and buttons.
"""
@property
def numbers(self) -> components.widgets.Digits:
"""
Returns the time display widget.
Assumes it's a Digits widget with a tag 'TimeDisplay'.
"""
return components.widgets.Digits.by_selector(self.driver, Selector.by_tag('TimeDisplay'))
@property
def btn_start(self) -> components.widgets.Button:
"""
Returns the 'Start' button by its ID.
"""
return components.widgets.Button.by_selector(self.driver, Selector.by_id('start'))
@property
def btn_stop(self) -> components.widgets.Button:
"""
Returns the 'Stop' button by its ID.
"""
return components.widgets.Button.by_selector(self.driver, Selector.by_id('stop'))
@property
def btn_reset(self) -> components.widgets.Button:
"""
Returns the 'Reset' button by its ID.
"""
return components.widgets.Button.by_selector(self.driver, Selector.by_id('reset'))
Using the :class:`TextualPage` allows to define widgets according to the Page-Object-Model. Within this class there are
three main key concepts used here:
* ``TextualPage``: A base class from BalderHub that gives you a driver to interact with the app's UI.
* Properties: These are like getters for widgets. Use by_selector to find them dynamically.
* Selectors: Tools like Selector.by_id or Selector.by_tag help locate elements without hardcoding paths. This makes your tests more robust if the UI changes slightly.
Writing a Test Scenario
=======================
Now, let's use our page directly within a Balder scenario and add a new test to it.
.. code-block:: python
# file scenario_stopwatch.py
import datetime
import math
import time
import balder
from balderhub.textual.lib.scenario_features import TextualControlFeature
from .pages import StopwatchPage
class ScenarioStopWatch(balder.Scenario):
class App(balder.Device):
pass
@balder.connect(App, over_connection=balder.Connection)
class Controller(balder.Device):
textual = TextualControlFeature()
page = StopwatchPage()
def test_start_stop(self):
start_time = time.time()
self.Controller.page.btn_start.click()
time.sleep(1)
self.Controller.page.btn_stop.click()
expected_time = time.time() - start_time
displayed_time = datetime.time.fromisoformat(self.Controller.page.numbers.text)
displayed_sec = displayed_time.second + displayed_time.microsecond / 1_000_000
assert displayed_time.hour == 0
assert displayed_time.minute == 0
# we are using this high deviation because of performance issues in Textual Pilot
assert math.isclose(displayed_sec, expected_time, rel_tol=0.5), f"wrong time displayed: {displayed_sec} instead of {expected_time}"
That's it.
**Breaking It Down:**
* ``ScenarioStoppTime``: Inherits from balder.Scenario. Define devices here (like Controller with the page).
* ``test_start_stop``: A test method that simulates user actions:
* Access widgets via ``self.Controller.page.``.
* Use methods like ``.click()`` to interact.
* Read values with ``.text`` and assert them.
* Assertions: We use ``math.isclose`` for floating-point comparison since timings might not be exact.
* ``time.sleep``: This pauses the test to simulate time passing. In real tests, consider using more precise timing if needed.
Running Balder
==============
Before we can run Balder, we need to add the page to the setup too. Open the existing file ``setup_stopwatch.py`` and
add the page to the existing device ``Controller``:
.. code-block:: python
# file `setup_stopwatch.py`
...
from .dut.stopwatch import StopwatchApp
...
class SetupStopwatch(balder.Setup):
class App(balder.Device):
app = MyAppFeature()
@balder.connect(App, over_connection=balder.Connection)
class Controller(balder.Device):
textual = TextualControlFeature(App="App")
page = StopwatchPage()
To run this, ensure your setup file is imported or discoverable, then execute balder:
.. code-block:: shell
$ balder
The test will be executed. You should see something similar to the shown output below:
.. code-block:: shell
+----------------------------------------------------------------------------------------------------------------------+
| BALDER Testsystem |
| python version 3.12.12 (main, Dec 30 2025, 03:58:12) [GCC 14.2.0] | balder version 0.1.0 |
+----------------------------------------------------------------------------------------------------------------------+
Collect 1 Setups and 1 Scenarios
resolve them to 1 valid variations
================================================== START TESTSESSION ===================================================
SETUP SetupStopwatch
SCENARIO ScenarioStopWatch
VARIATION ScenarioStopWatch.App:SetupStopwatch.App | ScenarioStopWatch.Controller:SetupStopwatch.Controller
TEST ScenarioStopWatch.test_start_stop [.]
================================================== FINISH TESTSESSION ==================================================
TOTAL NOT_RUN: 0 | TOTAL FAILURE: 0 | TOTAL ERROR: 0 | TOTAL SUCCESS: 1 | TOTAL SKIP: 0 | TOTAL COVERED_BY: 0