Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
87fc298
Enable package-centric development.
jmchilton Oct 6, 2025
160e153
FIxes for pyproject.toml files.
jmchilton Oct 7, 2025
e90db61
Drop now redundant test-requirements.txt files.
jmchilton Oct 7, 2025
afe46a7
Type fix for ... Python 3.13?
jmchilton Oct 7, 2025
933ea89
Expand tooling docs for working with packages.
jmchilton Oct 7, 2025
760fff8
More documentation and QOL improvements for package-centric dev.
jmchilton Oct 7, 2025
a8fb820
Add script to auto-generate package pyproject.toml files from setup.cfg.
jmchilton Oct 7, 2025
72e3498
Fixes for uv packages structure.
jmchilton Oct 15, 2025
a410532
Delay import of optional dependency.
jmchilton Oct 15, 2025
fd10036
Drop sizzle selector support in galaxy-selenium.
jmchilton Oct 15, 2025
a47ca54
Bug fix in navigates_galaxy.
jmchilton Oct 16, 2025
94f2ffa
Unit tests for Selenium has_driver.
jmchilton Oct 12, 2025
a50a3fa
Unit tests for smart_components.py.
jmchilton Oct 14, 2025
7a5c654
Remove the seemingly unused wait_for_select from smart_components.
jmchilton Oct 14, 2025
2ee0779
Refactor smart_components to not depend on naviagtes_galaxy.
jmchilton Oct 12, 2025
5bbd5d1
Rework navigates_galaxy to use new abstraction for navigating to url.
jmchilton Oct 12, 2025
d74fe8d
Migrate direct JavaScript interaction in NavigatesGalaxy into abstrac…
jmchilton Oct 14, 2025
947d09c
Migrate direct frame interactions in naviagets galaxy into has driver...
jmchilton Oct 12, 2025
ad0b050
Migrate a bunch more selenium interactions from naviagtes_galaxy to h…
jmchilton Oct 19, 2025
cc9b778
Establish helpers to decouple test_has_driver from Selenium internals.
jmchilton Oct 13, 2025
1c85afe
Use has_driver abstractions to reduce test dependence on Selenium int…
jmchilton Oct 13, 2025
9e13167
Migrate screenshot abstractions into HasDriver.
jmchilton Oct 13, 2025
cae2334
Abstract out find_element.
jmchilton Oct 14, 2025
0d4b91e
Establish a Playwright implementation of our page interaction layer.
jmchilton Oct 13, 2025
a248596
Cookie and WebElement abstractions.
jmchilton Oct 13, 2025
dc2cb4e
Implement more has driver abstractions to decouple navigates_galaxy f…
jmchilton Oct 13, 2025
1986b80
Cleanup abstraction use in navigates_galaxy.py.
jmchilton Oct 17, 2025
086eac9
Update galaxy.selenium package to allow dispatching selenium or playw…
jmchilton Oct 17, 2025
468efd6
Allow reading most GALAXY_TEST_SELENIUM_ environment variables from a…
jmchilton Oct 15, 2025
21d8b5a
Add additional e2e testing abstractions used by selenium test case fr…
jmchilton Oct 15, 2025
0849f7a
Update Selenium Test Case framework to allow new playwright backend.
jmchilton Oct 15, 2025
cd5c958
Sharper selectors for signout components.
jmchilton Oct 16, 2025
75fce6a
Introduce select_by_value for uniform selection across browser automa…
jmchilton Oct 16, 2025
6ac14d4
Decorators to distinguish selenium only and playwright only tests.
jmchilton Oct 16, 2025
c1ad85a
Adjust tests for Playwright compatibility.
jmchilton Oct 16, 2025
8df6f68
Add component.wait_for_and_send_enter abstraction, fix test.
jmchilton Oct 16, 2025
8e9d350
run_tests.sh updates for playwright.
jmchilton Oct 16, 2025
8fcd393
Add GitHub Actions workflow for Playwright test execution
jmchilton Oct 16, 2025
8aa043b
More debugging in navigates_galaxy.py.
jmchilton Oct 16, 2025
aa44af4
Mark all the untested tests as Selenium only - we love a checklist.
jmchilton Oct 16, 2025
50a76ec
Implement css accessor for playwright element.
jmchilton Oct 16, 2025
4f99774
Working alert abstraction across playwright/selenium.
jmchilton Oct 16, 2025
a3adeab
Update tests for new abstractions...
jmchilton Oct 16, 2025
cb7b408
Full README for galaxy-selenium package.
jmchilton Oct 17, 2025
c1cd222
Refactor galaxy.tool_util.verify.wait -> galaxy.util.wait.
jmchilton Oct 22, 2025
5638ce8
Refactor browser availability caching to use lru_cache
jmchilton Oct 22, 2025
701ac9a
Fix ConfiguredDriver.__init__ docstring to reflect actual parameters
jmchilton Oct 22, 2025
4bd3a4c
Import and docstrings fixes from PR review.
jmchilton Oct 22, 2025
7392f29
Fix custom wait on conditions across browser abstractions.
jmchilton Oct 22, 2025
0202dc0
Implement distinction between quit/close in browser automation stuff.
jmchilton Oct 22, 2025
90cf6f1
Better documentation of type hacks in has_driver.
jmchilton Oct 22, 2025
277c414
Switch to main frame so we don't accessibility check the tool output.
jmchilton Oct 24, 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
96 changes: 96 additions & 0 deletions .github/workflows/playwright.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Playwright tests
on:
push:
paths-ignore:
- 'doc/**'
- 'packages/**'
pull_request:
paths-ignore:
- 'doc/**'
- 'packages/**'
schedule:
# Run at midnight UTC every Tuesday
- cron: '0 0 * * 2'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we really want to do that ?

Copy link
Copy Markdown
Member Author

@jmchilton jmchilton Oct 22, 2025

Choose a reason for hiding this comment

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

This same line is in the selenium version of this file. I am happy to drop it though if you'd like.

env:
GALAXY_CONFIG_GALAXY_URL_PREFIX: '/galaxypf'
GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8'
GALAXY_TEST_RAISE_EXCEPTION_ON_HISTORYLESS_HDA: '1'
GALAXY_TEST_SELENIUM_RETRIES: 1
GALAXY_TEST_SKIP_FLAKEY_TESTS_ON_ERROR: 1
GALAXY_TEST_SELENIUM_HEADLESS: 1
YARN_INSTALL_OPTS: --frozen-lockfile
GALAXY_CONFIG_SQLALCHEMY_WARN_20: '1'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-client:
uses: ./.github/workflows/build_client.yaml
test:
name: Test
needs: [build-client]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.9']
chunk: [0, 1, 2]
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
steps:
- if: github.event_name == 'schedule'
run: |
echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV
echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV
- uses: actions/checkout@v5
with:
path: 'galaxy root'
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: 'galaxy root/requirements.txt'
- name: Get full Python version
id: full-python-version
shell: bash
run: echo "version=$(python -c 'import sys; print("-".join(str(v) for v in sys.version_info))')" >> $GITHUB_OUTPUT
- name: Cache galaxy venv
uses: actions/cache@v4
with:
path: 'galaxy root/.venv'
key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-playwright
- name: Install Playwright
run: |
pip install playwright
playwright install chromium --with-deps
working-directory: 'galaxy root'
- name: Restore client cache
uses: actions/cache@v4
with:
key: galaxy-static-${{ needs.build-client.outputs.commit-id }}
path: 'galaxy root/static'
- name: Run tests
run: ./run_tests.sh --coverage -playwright lib/galaxy_test/selenium -- --num-shards=3 --shard-id=${{ matrix.chunk }}
working-directory: 'galaxy root'
- uses: codecov/codecov-action@v5
with:
flags: playwright
working-directory: 'galaxy root'
- uses: actions/upload-artifact@v4
if: failure()
with:
name: Playwright test results (${{ matrix.python-version }}, ${{ matrix.chunk }})
path: 'galaxy root/run_playwright_tests.html'
- uses: actions/upload-artifact@v4
if: failure()
with:
name: Playwright debug info (${{ matrix.python-version }}, ${{ matrix.chunk }})
path: 'galaxy root/database/test_errors'
3 changes: 3 additions & 0 deletions client/src/components/User/UserPreferences.vue
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ async function signOut() {
okVariant: "danger",
cancelTitle: "Cancel",
cancelVariant: "outline-primary",
// data-description cannot be set for this so falling back to
// setting a class for DOM inspection.
modalClass: "sign-out-modal",
centered: true,
});

Expand Down
4 changes: 2 additions & 2 deletions client/src/utils/navigation/navigation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ change_user_address:

sign_out:
selectors:
cancel_button: '.modal-footer .btn-outline-primary'
sign_out_button: '.modal-footer .btn-danger'
cancel_button: '.sign-out-modal .modal-footer .btn-outline-primary'
sign_out_button: '.sign-out-modal .modal-footer .btn-danger'

dataset_details:
selectors:
Expand Down
3 changes: 2 additions & 1 deletion lib/galaxy/objectstore/templates/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from typing_extensions import (
Annotated,
Literal,
TypeAlias,
)

from galaxy.objectstore.badges import (
Expand All @@ -39,7 +40,7 @@
)

ObjectStoreTemplateVariableType = TemplateVariableType
ObjectStoreTemplateVariableValueType = TemplateVariableValueType
ObjectStoreTemplateVariableValueType: TypeAlias = TemplateVariableValueType
ObjectStoreTemplateType = Literal["aws_s3", "azure_blob", "boto3", "disk", "generic_s3", "onedata", "rucio", "irods"]


Expand Down
131 changes: 131 additions & 0 deletions lib/galaxy/selenium/availability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Browser availability checking utilities for Selenium and Playwright.

This module provides functions to check if browsers are available and properly
configured for automated testing. These functions are used for conditional test
skipping and diagnostic scripts.
"""

from functools import lru_cache

from playwright.sync_api import sync_playwright
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions

# Installation instruction constants
SELENIUM_INSTALL_INSTRUCTIONS = (
"Install Chrome browser and chromedriver. See: https://chromedriver.chromium.org/getting-started"
)

PLAYWRIGHT_INSTALL_INSTRUCTIONS = "Install with: playwright install chromium"

# Skip message constants for pytest
SELENIUM_BROWSER_NOT_AVAILABLE_MESSAGE = f"Selenium Chrome browser not available. {SELENIUM_INSTALL_INSTRUCTIONS}"

PLAYWRIGHT_BROWSER_NOT_AVAILABLE_MESSAGE = (
f"Playwright Chromium browser not available. {PLAYWRIGHT_INSTALL_INSTRUCTIONS}"
)


def is_selenium_browser_available() -> bool:
"""
Check if Selenium WebDriver can launch Chrome browser.

Returns:
bool: True if Chrome browser is available and can be launched, False otherwise
"""
try:
options = ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(options=options)
driver.quit()
return True
except Exception:
return False


def is_playwright_browser_available() -> bool:
"""
Check if Playwright Chromium browser is installed and available.

Returns:
bool: True if Playwright Chromium is installed, False otherwise
"""
try:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
browser.close()
return True
except Exception:
return False


def get_selenium_availability_message() -> str:
"""
Get a descriptive message about Selenium browser availability.

Returns:
str: Status message indicating if Selenium is available or how to enable it
"""
if is_selenium_browser_available():
return "Selenium Chrome browser is available"
else:
return SELENIUM_BROWSER_NOT_AVAILABLE_MESSAGE


def get_playwright_availability_message() -> str:
"""
Get a descriptive message about Playwright browser availability.

Returns:
str: Status message indicating if Playwright is available or how to enable it
"""
if is_playwright_browser_available():
return "Playwright Chromium browser is available"
else:
return PLAYWRIGHT_BROWSER_NOT_AVAILABLE_MESSAGE


@lru_cache(maxsize=1)
def check_selenium_cached() -> bool:
"""
Check Selenium browser availability with caching.

This avoids repeatedly launching browsers during test collection.
Uses lru_cache with maxsize=1 to cache the result of the check.

Returns:
bool: True if Selenium Chrome browser is available
"""
return is_selenium_browser_available()


@lru_cache(maxsize=1)
def check_playwright_cached() -> bool:
"""
Check Playwright browser availability with caching.

This avoids repeatedly launching browsers during test collection.
Uses lru_cache with maxsize=1 to cache the result of the check.

Returns:
bool: True if Playwright Chromium browser is available
"""
return is_playwright_browser_available()


__all__ = (
# Constants
"SELENIUM_INSTALL_INSTRUCTIONS",
"PLAYWRIGHT_INSTALL_INSTRUCTIONS",
"SELENIUM_BROWSER_NOT_AVAILABLE_MESSAGE",
"PLAYWRIGHT_BROWSER_NOT_AVAILABLE_MESSAGE",
# Functions
"is_selenium_browser_available",
"is_playwright_browser_available",
"get_selenium_availability_message",
"get_playwright_availability_message",
"check_selenium_cached",
"check_playwright_cached",
)
71 changes: 50 additions & 21 deletions lib/galaxy/selenium/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
REMOTE_PORT_DESCRIPTION = "Selenium hub remote port to use (if remote driver in use)."
GALAXY_URL_DESCRIPTION = "URL of Galaxy instance to target."
HEADLESS_DESCRIPTION = "Use local selenium headlessly (native in chrome, otherwise this requires pyvirtualdisplay)."
BACKEND_DESCRIPTION = "Browser automation backend to use (selenium or playwright)."

from typing import Literal
from urllib.parse import urljoin

from .driver_factory import (
get_local_driver,
get_remote_driver,
ConfiguredDriver,
virtual_display_if_enabled,
)
from .navigates_galaxy import NavigatesGalaxy
from .navigates_galaxy import (
galaxy_timeout_handler,
NavigatesGalaxy,
)


def add_selenium_arguments(parser):
Expand Down Expand Up @@ -50,29 +54,50 @@ def add_selenium_arguments(parser):
default="http://127.0.0.1:8080/",
help=GALAXY_URL_DESCRIPTION,
)
parser.add_argument(
"--backend",
default="selenium",
choices=["selenium", "playwright"],
help=BACKEND_DESCRIPTION,
)

return parser


class DriverWrapper(NavigatesGalaxy):
"""Adapt argparse command-line options to a concrete Selenium driver."""
"""Adapt argparse command-line options to a browser automation driver."""

def __init__(self, args):
browser = args.selenium_browser
self.display = virtual_display_if_enabled(args.selenium_headless)
if args.selenium_remote:
driver = get_remote_driver(
host=args.selenium_remote_host,
port=args.selenium_remote_port,
browser=browser,
)
else:
driver = get_local_driver(
browser=browser,
)
self.driver = driver
backend_type: Literal["selenium", "playwright"] = args.backend

# Validate remote option (Playwright doesn't support remote)
if backend_type == "playwright" and args.selenium_remote:
raise ValueError("Playwright backend does not support remote drivers")

# Set up virtual display for headless mode (Selenium only)
self.display = None
if backend_type == "selenium":
self.display = virtual_display_if_enabled(args.selenium_headless)

# Create configured driver with the specified backend
# TODO: parameterize timeout multiplier
self.configured_driver = ConfiguredDriver(
galaxy_timeout_handler(1.0),
browser=browser,
remote=args.selenium_remote,
remote_host=args.selenium_remote_host,
remote_port=args.selenium_remote_port,
headless=args.selenium_headless,
backend_type=backend_type,
)
self.target_url = args.galaxy_url

@property
def _driver_impl(self):
"""Provide driver implementation from configured_driver."""
return self.configured_driver.driver_impl

def build_url(self, url="", for_selenium: bool = True):
return urljoin(self.target_url, url)

Expand All @@ -87,17 +112,21 @@ def default_timeout(self):
return 15

def finish(self):
"""Clean up driver and display resources."""
exception = None

# Quit the driver (works for both backends via protocol)
try:
self.driver.close()
self.quit()
except Exception as e:
exception = e

try:
self.display.stop()
except Exception as e:
exception = e
# Stop virtual display if used (Selenium only)
if self.display is not None:
try:
self.display.stop()
except Exception as e:
exception = e

if exception is not None:
raise exception
Loading
Loading