.. image:: https://badge.fury.io/py/galaxy-selenium.svg
:target: https://pypi.org/project/galaxy-selenium/
Galaxy Browser Automation Framework
====================================
.. note::
The package is named ``galaxy-selenium`` for historical reasons, but a more accurate name
would be ``galaxy-browser-automation`` since it supports both Selenium and Playwright backends.
Overview
--------
This package provides a browser automation framework for Galaxy_ with:
* **Multi-backend support**: Selenium and Playwright backends with a unified API
* **Protocol-based architecture**: Clean separation between Galaxy logic and browser interaction
* **Comprehensive testing**: Full unit test coverage for both backends
* **CLI tooling**: Utilities for building command-line automation scripts
* **Type safety**: Full type hints with mypy validation
* Code: https://github.com/galaxyproject/galaxy
.. _Galaxy: http://galaxyproject.org/
Architecture
------------
Core Interfaces
~~~~~~~~~~~~~~~
The framework is built around two main protocol interfaces:
**HasDriverProtocol** (``has_driver_protocol.py``)
Defines ~60+ browser automation operations including:
- Element finding (by ID, selector, XPath, etc.)
- Wait methods (visible, clickable, absent, etc.)
- Interactions (click, hover, drag-and-drop, keyboard input)
- Navigation (URLs, frames, alerts)
- JavaScript execution
- Accessibility testing (axe-core integration)
**WebElementProtocol** (``web_element_protocol.py``)
Defines the common element API:
- ``text``, ``click()``, ``send_keys()``, ``clear()``
- ``get_attribute()``, ``is_displayed()``, ``is_enabled()``
- ``find_element()``, ``find_elements()``
- ``value_of_css_property()`` for CSS introspection
Architecture Diagram
~~~~~~~~~~~~~~~~~~~~
::
┌─────────────────────────────────────────────────────┐
│ Galaxy Application Layer │
│ │
│ NavigatesGalaxy (navigates_galaxy.py) │
│ - Galaxy-specific page objects │
│ - Workflow interactions │
│ - History management │
│ - Tool execution │
└────────────────┬────────────────────────────────────┘
│ extends
↓
┌─────────────────────────────────────────────────────┐
│ Browser Automation Abstraction Layer │
│ │
│ HasDriverProxy (has_driver_proxy.py) │
│ - Delegates to HasDriverProtocol implementation │
│ - Runtime backend selection via composition │
└────────────────┬────────────────────────────────────┘
│ uses
↓
┌──────────────────────────────────┬──────────────────┐
│ HasDriverProtocol │ │
│ (has_driver_protocol.py) │ │
│ - Abstract interface │ │
│ - ~60+ operations │ │
└──────────────┬───────────────────┴──────────────────┘
│ implemented by
┌─────────┴──────────┐
↓ ↓
┌─────────────┐ ┌──────────────────┐
│ HasDriver │ │ HasPlaywright │
│ │ │ Driver │
│ Selenium │ │ │
│ backend │ │ Playwright │
│ │ │ backend │
└──────┬──────┘ └────────┬─────────┘
│ │
↓ ↓
┌─────────────┐ ┌──────────────────┐
│ WebElement │ │ PlaywrightElement│
│ (Selenium) │ │ (wrapper) │
│ │ │ │
│ implements │ │ implements │
│ protocol │ │ protocol │
└─────────────┘ └──────────────────┘
│ │
└──────────┬─────────┘
↓
WebElementProtocol
(web_element_protocol.py)
Implementations
~~~~~~~~~~~~~~~
**Selenium Backend** (``has_driver.py``)
- Uses Selenium WebDriver
- Direct WebElement support (implements protocol natively)
- Mature, widely-used automation framework
- Supports remote execution (Selenium Grid)
**Playwright Backend** (``has_playwright_driver.py``)
- Uses Playwright sync API
- PlaywrightElement wrapper (adapts ElementHandle to protocol)
- Modern automation with auto-waiting
- Fast and reliable for modern web apps
- Local execution only (no remote support)
Both implementations:
- Share identical test suite (150+ parametrized tests)
- Provide consistent API via protocols
- Support headless and headed modes
- Include full type hints
Separation of Concerns
~~~~~~~~~~~~~~~~~~~~~~~
::
Application Logic │ Browser Automation
(Galaxy-specific) │ (Generic, reusable)
─────────────────────────────────────────────────
navigates_galaxy.py │ has_driver_protocol.py
- Galaxy UI interactions │ - Abstract interface
- Workflow automation │ - Element finding
- History operations │ - Wait strategies
- Tool wrappers │ - Interactions
│
smart_components.py │ has_driver.py
- Galaxy component │ - Selenium impl
wrappers │
- Page object patterns │ has_playwright_driver.py
│ - Playwright impl
│
│ web_element_protocol.py
│ - Element interface
**Key Principle**: Galaxy-specific logic lives in ``navigates_galaxy.py`` and extends the
generic browser automation protocols. This separation allows:
- Testing browser automation independently
- Reusing automation primitives across projects
- Switching backends without changing application code
- Clear boundaries between concerns
Testing
-------
The framework includes comprehensive unit tests in ``test/unit/selenium/test_has_driver.py``:
- **Parametrized tests**: Every test runs against Selenium, Playwright, and proxied backends
- **150+ test cases** covering all protocol methods
- **Test fixtures**: Reusable HTML pages served via local HTTP server
- **Scope-optimized**: Session-scoped server, function-scoped drivers
Run tests from the package directory::
# All tests
uv run pytest tests/seleniumtests/test_has_driver.py -v
# Specific test class
uv run pytest tests/seleniumtests/test_has_driver.py::TestElementFinding -v
# Type checking
make mypy
.. warning::
Always run pytest from the package directory (``packages/selenium/``), not from the
monorepo root. Running from root can cause fixture scope issues.
Building CLI Tools
------------------
The ``cli.py`` module provides infrastructure for building command-line automation tools.
Core Components
~~~~~~~~~~~~~~~
**add_selenium_arguments(parser)**
Adds standard CLI arguments:
- ``--selenium-browser``: Browser choice (chrome, firefox, auto)
- ``--selenium-headless``: Headless mode flag
- ``--backend``: Backend selection (selenium, playwright)
- ``--galaxy_url``: Target Galaxy instance URL
- ``--selenium-remote``: Remote execution (Selenium only)
**DriverWrapper**
Adapts argparse args to a NavigatesGalaxy instance:
- Handles backend selection
- Manages virtual display (for headless Selenium)
- Provides Galaxy navigation utilities
- Cleanup via ``finish()`` method
Example: dump_tour.py
~~~~~~~~~~~~~~~~~~~~~
The ``scripts/dump_tour.py`` script demonstrates CLI tool development:
.. code-block:: python
#!/usr/bin/env python
import argparse
from galaxy.selenium import cli
def main(argv=None):
args = _arg_parser().parse_args(argv)
driver_wrapper = cli.DriverWrapper(args)
# Use driver_wrapper for automation
callback = DumpTourCallback(driver_wrapper, args.output)
driver_wrapper.run_tour(args.tour, tour_callback=callback)
def _arg_parser():
parser = argparse.ArgumentParser(description="Walk a Galaxy tour")
parser.add_argument("tour", help="tour to walk")
parser.add_argument("-o", "--output", help="screenshot output dir")
parser = cli.add_selenium_arguments(parser) # Add standard args
return parser
class DumpTourCallback:
def handle_step(self, step, step_index: int):
self.driver_wrapper.save_screenshot(f"{output}/{step_index}.png")
Usage::
# With Selenium backend
python dump_tour.py my_tour.yaml --backend selenium --selenium-headless
# With Playwright backend
python dump_tour.py my_tour.yaml --backend playwright --galaxy_url http://localhost:8080
Building Your Own CLI Tool
~~~~~~~~~~~~~~~~~~~~~~~~~~~
1. **Import the CLI utilities**::
from galaxy.selenium import cli
2. **Create argument parser**::
parser = argparse.ArgumentParser(description="My automation tool")
parser.add_argument("--my-option", help="Custom option")
parser = cli.add_selenium_arguments(parser) # Add standard args
3. **Create DriverWrapper**::
args = parser.parse_args()
driver_wrapper = cli.DriverWrapper(args)
4. **Use NavigatesGalaxy API**::
# driver_wrapper has all NavigatesGalaxy methods
driver_wrapper.navigate_to(url)
driver_wrapper.click_selector("#my-button")
driver_wrapper.wait_for_selector_visible(".result")
5. **Clean up**::
driver_wrapper.finish() # Quits driver and stops virtual display
Development
-----------
Package Structure
~~~~~~~~~~~~~~~~~
::
packages/selenium/
├── galaxy/selenium/ # Symlink to lib/galaxy/selenium/
├── tests/seleniumtests/ # Symlink to test/unit/selenium/
├── README.rst # This file
└── pyproject.toml
lib/galaxy/selenium/ # Actual source code
├── has_driver_protocol.py # Protocol interface
├── has_driver.py # Selenium implementation
├── has_playwright_driver.py # Playwright implementation
├── web_element_protocol.py # Element interface
├── playwright_element.py # Element wrapper
├── navigates_galaxy.py # Galaxy-specific logic
├── smart_components.py # Component wrappers
├── cli.py # CLI utilities
└── scripts/
└── dump_tour.py # Example CLI tool
test/unit/selenium/ # Actual tests
├── conftest.py # Pytest fixtures
├── test_has_driver.py # Main test suite
└── fixtures/ # HTML test pages
Running Commands
~~~~~~~~~~~~~~~~
Always use ``uv run`` from the package directory::
# Run tests
uv run pytest tests/seleniumtests/test_has_driver.py -v
# Type checking
make mypy
# Linting
uv run ruff check .
Adding New Low-Level Browser Operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Low-level operations are generic browser automation primitives that work with both backends.
**Manual Process:**
1. **Add to protocol** (``has_driver_protocol.py``)::
@abstractmethod
def my_new_operation(self, param: str) -> bool:
"""Do something useful."""
...
2. **Implement in Selenium** (``has_driver.py``)::
def my_new_operation(self, param: str) -> bool:
# Selenium implementation
return self.driver.do_something(param)
3. **Implement in Playwright** (``has_playwright_driver.py``)::
def my_new_operation(self, param: str) -> bool:
# Playwright implementation
return self.page.do_something(param)
4. **Update proxy** (``has_driver_proxy.py``)::
def my_new_operation(self, param: str) -> bool:
"""Do something useful."""
return self._driver_impl.my_new_operation(param)
5. **Add tests** (``test_has_driver.py``)::
def test_my_new_operation(self, has_driver_instance, base_url):
"""Test new operation works on both backends."""
has_driver_instance.navigate_to(f"{base_url}/test.html")
result = has_driver_instance.my_new_operation("test")
assert result is True
**Automated Process:**
Use the ``/add-browser-operation`` slash command to automate these steps::
/add-browser-operation scroll element to center of viewport
This command will generate all the necessary code across all files and create tests.
Adding New Smart Component Operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Smart component operations are higher-level conveniences that may require adding low-level
operations first. These operations make Galaxy components more ergonomic to use in tests.
**When to Add Smart Operations:**
- When you find yourself repeating a pattern of low-level operations
- When Galaxy-specific UI patterns need convenient wrappers
- When you want to express test intent more clearly
**Process:**
1. **Determine if low-level support exists**:
Check if the required browser operation exists in ``HasDriverProtocol``. If not,
add it first using the process above (or ``/add-browser-operation``).
2. **Add to SmartTarget** (``smart_components.py``):
Add a method to the ``SmartTarget`` class::
def my_smart_operation(self, **kwds):
"""High-level operation description."""
# Delegate to has_driver protocol methods
element = self._has_driver.wait_for_visible(self._target, **kwds)
return self._has_driver.my_low_level_operation(element)
3. **Consider return value wrapping**:
If your operation returns a Component or Target, wrap it::
def get_child_component(self, name: str):
"""Get a child component and wrap it smartly."""
child = self._target.get_child(name)
return self._wrap(child) # Returns SmartTarget
4. **Add tests** (``test/unit/selenium/test_smart_components.py``):
Test with both backends using the ``has_driver_instance`` fixture::
def test_my_smart_operation(self, has_driver_instance, base_url):
has_driver_instance.navigate_to(f"{base_url}/smart_components.html")
# Use SmartComponent wrapping
component = SmartComponent(MyComponent(), has_driver_instance)
result = component.my_target.my_smart_operation()
assert result is not None
**Example: Adding a "wait_for_and_hover" operation**
This demonstrates when you need to add a low-level operation first:
1. **Check low-level support**: ``hover()`` already exists in ``HasDriverProtocol`` ✓
2. **Add to SmartTarget**::
def wait_for_and_hover(self, **kwds):
"""Wait for element to be visible and hover over it."""
element = self._has_driver.wait_for_visible(self._target, **kwds)
self._has_driver.hover(element)
return element
3. **Usage in tests**::
# Before: Multiple steps
element = driver.wait_for_visible(component.menu)
driver.hover(element)
# After: One expressive call
component.menu.wait_for_and_hover()
**Example: Adding operation requiring new low-level support**
When the operation needs a new browser primitive:
1. **Add low-level operation** (see "Adding New Low-Level Browser Operations"):
Add ``scroll_to_center(element)`` to protocols and implementations
2. **Add smart wrapper**::
def wait_for_and_scroll_to_center(self, **kwds):
"""Wait for element and scroll it to viewport center."""
element = self._has_driver.wait_for_visible(self._target, **kwds)
self._has_driver.scroll_to_center(element)
return element
3. **Test both levels**:
- Test low-level in ``test_has_driver.py``
- Test smart wrapper in ``test_smart_components.py``
See Also
--------
* `Selenium Documentation `_
* `Playwright Documentation `_
* `Galaxy Testing Documentation `_