Skip to content

Conversation

@jmchilton
Copy link
Member

@jmchilton jmchilton commented Oct 17, 2025

Summary

This PR introduces comprehensive Playwright backend support to Galaxy's browser automation framework, enabling runtime selection between Selenium and Playwright while maintaining full backward compatibility. The work establishes a protocol-based architecture that abstracts ~60+ browser operations behind a unified interface (HasDriverProtocol and WebElementProtocol), implements a complete Playwright driver matching the existing Selenium API, and introduces a proxy pattern enabling NavigatesGalaxy and all Galaxy tests to work seamlessly with both backends.
The low-level browser interaction components now all have unit tests and the unit tests run against both implementations, the entire Galaxy Selenium test suite supports backend switching via GALAXY_TEST_DRIVER_BACKEND environment variable, and GitHub Actions workflows validate both backends in CI.


Establishing Abstractions in Existing Code

The first major challenge was identifying and eliminating direct Selenium API usage throughout the codebase. Through systematic analysis of navigates_galaxy.py (the 2800+ line Galaxy-specific automation layer), we discovered 132+ instances of direct self.driver access bypassing the HasDriver abstraction layer.

Gap Analysis & Abstraction Work

We categorized these gaps and systematically addressed them:

JavaScript Execution (8 instances → Abstracted)

  • Added execute_script(script, *args) with Selenium's arguments array pattern
  • Specialized methods: set_local_storage(), remove_local_storage(), scroll_into_view(), set_element_value(), execute_script_click()
  • All now work identically across both backends

Frame/Context Switching (4 instances → Abstracted)

  • Enhanced switch_to_frame() to accept string (name/id), int (index), or WebElement
  • Added switch_to_default_content() for returning to main page context
  • Both Selenium and Playwright implementations maintain consistent API despite different underlying mechanisms

Navigation & Cookies (Multiple instances → Abstracted)

  • Unified navigate_to(url) and re_get_with_query_params()
  • Added get_cookies() with consistent TypedDict return structure
  • Added page_source, current_url, page_title properties

ActionChains Patterns (5+ instances → Abstracted)

  • Added move_to_and_click(element) for hover-then-click pattern
  • Added hover(element) for mouse-over interactions
  • Added drag_and_drop(source, target) using JavaScript events for cross-backend compatibility
  • Created WaitMethodsMixin with 20+ wait methods shared between implementations

Alert Handling (Improved)

  • Refactored accept_alert() to return context manager
  • Selenium: Accepts alert on context exit (after it exists)
  • Playwright: Sets up handler on context entry (before alert triggers)
  • Usage: with driver.accept_alert(): driver.click_selector("#alert-button")

Remaining Direct Element Access

While ~60 instances of element interaction remain (.click(), .send_keys(), .get_attribute() on returned WebElements), these work correctly through the WebElementProtocol interface that both Selenium.WebElement and our PlaywrightElement wrapper implement. This allows elements returned from find methods to be used identically regardless of backend.


Playwright Implementation

Core Infrastructure (has_playwright_driver.py)

Created a complete HasPlaywrightDriver class (1134 lines) mirroring the entire HasDriver API:

Element Finding & Selection

  • Implemented locator translation: Selenium's By.ID, By.CLASS_NAME, By.XPATH → Playwright's #id, .class, xpath=...
  • Created _selenium_locator_to_playwright_selector() for seamless conversion
  • All find methods work with Galaxy's Target component system

Wait Strategy Implementation
Implemented 6 wait condition methods using Playwright's native APIs:

  • _wait_on_condition_present() - uses state="attached"
  • _wait_on_condition_visible() - uses state="visible"
  • _wait_on_condition_clickable() - checks state="visible" + is_enabled()
  • _wait_on_condition_invisible() - uses state="hidden"
  • _wait_on_condition_absent() - uses state="detached"
  • Timeout conversion helper (_timeout_in_ms()) for Playwright's millisecond-based API

Frame Handling Architecture

  • Tracked current frame context in _current_frame attribute
  • Ensures all operations (find, click, wait, assert) work correctly within iframes

Advanced Features

  • JavaScript Execution: Wraps scripts in arrow functions exposing arguments array to match Selenium
  • Accessibility Testing: Direct axe-core integration using page.evaluate() (no extra dependencies)
  • Keyboard/Mouse Actions: Implemented using page.keyboard.press(), element.hover(), element.click()
  • Drag & Drop: JavaScript-based using DataTransfer/DragEvent for consistency with Selenium

Element Wrapper Pattern (playwright_element.py)

Created PlaywrightElement class wrapping Playwright's ElementHandle to implement WebElementProtocol:

class PlaywrightElement:
    def __init__(self, element_handle: ElementHandle, driver):
        self._element = element_handle
        self._driver = driver

    @property
    def text(self) -> str:
        content = self._element.text_content()
        return content.strip() if content is not None else ""

    def click(self) -> None:
        self._element.click()

    def get_attribute(self, name: str) -> Optional[str]:
        # Special case: "value" uses input_value() for inputs
        if name == "value":
            return self._element.input_value()
        return self._element.get_attribute(name)

Key Features:

  • Implements all WebElementProtocol methods (click, send_keys, clear, get_attribute, etc.)
  • Special handling for value attribute using input_value() to match Selenium behavior
  • Supports nested element finding (find_element, find_elements)
  • Added value_of_css_property() using window.getComputedStyle() for CSS introspection

Protocol-Based Architecture & Runtime Dispatching

The Protocol Layer

Defined two core protocol interfaces enabling polymorphic backend usage:

HasDriverProtocol (has_driver_protocol.py - 495 lines)

  • Defines ~61 abstract browser automation operations
  • Includes backend_type property for runtime introspection
  • Methods: element finding, waits, clicks, navigation, frames, JavaScript, accessibility, screenshots
  • Enables type-safe delegation without inheritance coupling

WebElementProtocol (web_element_protocol.py)

  • Defines common element API: text, click(), send_keys(), clear(), get_attribute(), is_displayed(), is_enabled(), value_of_css_property()
  • Implemented natively by Selenium's WebElement
  • Implemented via PlaywrightElement wrapper for Playwright

The Proxy Pattern

HasDriverProxy (has_driver_proxy.py - 495 lines)

  • Abstract base class delegating all operations to _driver_impl property
  • Provides ~61 delegation methods matching HasDriverProtocol
  • Enables composition over inheritance

NavigatesGalaxy Refactoring

  • Changed from inheriting HasDriver to inheriting HasDriverProxy
  • Eliminated all direct self.driver access (replaced with protocol methods)
  • Works identically with both Selenium and Playwright implementations
  • Zero code changes needed in Galaxy test suite

Runtime Backend Selection

ConfiguredDriver (driver_factory.py)

  • Added backend_type: Literal["selenium", "playwright"] parameter
  • Creates appropriate driver based on backend_type:
    if backend_type == "selenium":
        driver = webdriver.Chrome(options=options)
        return _SeleniumDriverImpl(driver, timeout_handler)
    else:  # playwright
        playwright = sync_playwright().start()
        browser = playwright.chromium.launch(headless=headless)
        page = browser.new_page()
        return _PlaywrightDriverImpl(
            PlaywrightResources(playwright, browser, page),
            timeout_handler
        )

Environment Variable Integration

  • GALAXY_TEST_DRIVER_BACKEND controls backend selection (defaults to "selenium")
  • Set via environment: GALAXY_TEST_DRIVER_BACKEND=playwright pytest ...
  • Or via config file: backend_type: playwright in YAML config
  • Framework code (lib/galaxy_test/selenium/framework.py) passes to ConfiguredDriver

Lifecycle Management

  • Created PlaywrightResources NamedTuple bundling (playwright, browser, page)
  • Proper cleanup in quit() method: closes page, browser, stops playwright
  • Prevents resource leaks in test runs

Smart Components Integration

Updated SmartComponent and SmartTarget to work with protocol-based drivers:

  • Changed type hints from HasDriver to HasDriverProtocol
  • All delegation methods work seamlessly with both backends
  • 53 unit tests validate SmartComponent functionality with both Selenium and Playwright

Test Migration Progress

Unit Test Infrastructure

Comprehensive Test Coverage (test_has_driver.py - 1246 lines)

  • 150+ parametrized tests covering all protocol methods
  • Every test runs 3 times: selenium, playwright, proxy-selenium
  • Test classes:
    • TestElementFinding (18 tests × 3 = 54 runs)
    • TestVisibilityAndPresence (20 tests × 3 = 60 runs)
    • TestWaitMethods (31 tests × 3 = 93 runs)
    • TestClickAndInteraction, TestActionChainsAndKeys, TestFrameSwitching
    • TestJavaScriptExecution, TestCookieManagement, TestAccessibility
    • TestFormInteraction, TestInputValueAbstraction, TestSelectByValue
    • TestCSSProperties, TestAlertHandling, TestScreenshots

Test Fixtures & Infrastructure

  • Created HTML test pages: basic.html, accessibility.html, smart_components.html, frame.html
  • TestHTTPServer serves pages locally during tests
  • Session-scoped test_server and base_url fixtures (fixed scope issues for monorepo execution)
  • Parametrized has_driver_instance fixture creates appropriate backend

Results: ✅ All 150+ tests passing across all 3 backend configurations

Integration Test Migration (Galaxy Test Suite)

Migration Statistics:

  • 57 test files analyzed (268 test methods, ~8600 lines)
  • All compatible tests migrated and passing with Playwright backend
  • 145+ tests marked with @selenium_only decorator for Selenium-specific features
  • CI validation: Both Selenium and Playwright workflows running successfully

Migration Tiers Completed:

Tier 1: MVP Targets (23 files, 31 tests)

  • Simple tests using pure abstractions
  • Examples: test_login.py, test_published_pages.py, test_anon_history.py
  • All passing with both backends

Tier 2: Medium Complexity (26 files, 136 tests)

  • Medium-sized files, moderate custom patterns
  • Examples: test_registration.py, test_workflow_sharing.py, test_pages.py
  • Most passing, some marked @selenium_only for Select class usage

Tier 3: Complex (8 files, 101 tests)

  • Large files (300-1600+ lines), heavy custom logic
  • Examples: test_uploads.py, test_tool_form.py, test_workflow_editor.py
  • Compatible portions migrated, Selenium-specific features decorated

Framework Integration:

  • Updated lib/galaxy_test/selenium/framework.py to pass backend_type to ConfiguredDriver
  • Added @selenium_only(reason) and @playwright_only(reason) decorators
  • Implemented config file loading via GALAXY_TEST_END_TO_END_CONFIG
  • All framework methods use abstractions (page_source, get_screenshot_as_png(), execute_script())

CI Workflows:

  • Created .github/workflows/playwright.yaml mirroring Selenium workflow structure
  • Updated run_tests.sh with a -playwright argument to mirror -selenium and used this from the new CI workflow.
  • Installs Playwright + Chromium in test job (no global install needed)
  • Parallel execution (3 chunks), PostgreSQL 17 service, codecov integration
  • Both Selenium and Playwright workflows passing in CI

Documentation

Comprehensive Guides Created:

  • README.rst: 480-line comprehensive package documentation
    • Architecture diagrams (layered architecture, separation of concerns)
    • Core interfaces documentation (HasDriverProtocol, WebElementProtocol)
    • Implementation details (Selenium vs Playwright)
    • CLI tooling guide (cli.py, DriverWrapper, dump_tour.py example)
    • Development guide (adding operations, testing, type checking)

Automation Support:

  • Created /add-browser-operation slash command for automated operation scaffolding
  • Generates protocol, implementations, proxy, and tests from description
  • Example: /add-browser-operation scroll element to center of viewport

Technical Achievements

Type Safety: ✅ All code passes mypy type checking with strict mode

  • Full type hints throughout protocol, implementations, and tests
  • Protocol pattern ensures type-safe polymorphism

Test Quality: ✅ 150+ unit tests, all parametrized for dual-backend validation

  • Every abstraction tested with both Selenium and Playwright
  • Comprehensive coverage of edge cases, error conditions, timeouts

Backward Compatibility: ✅ Zero breaking changes

  • Existing Selenium tests work without modification
  • Default backend remains Selenium
  • All existing APIs preserved

Performance: ✅ Tests run successfully in CI

  • Headless execution in CI (both backends)

Documentation: ✅ Extensive inline docs + external guides

  • 120+ line module docstring in has_playwright_driver.py
  • Usage examples, API compatibility notes, known limitations
  • Architecture diagrams, testing guides, migration plans

Future Work

Playwright-Specific Features (Deferred)

The architecture supports adding Playwright-exclusive capabilities:

  • Network request interception and mocking
  • Enhanced wait conditions (network idle, API responses)
  • Console error monitoring
  • Advanced browser context management
  • Geolocation and offline mode simulation

These can be added to a HasPlaywrightExtensions mixin when needed, with tests marked @playwright_only.

Remaining selenium_only Tests (145+)

Tests marked @selenium_only can be migrated by:

  • Implementing missing abstractions (e.g., Select class wrapper)
  • Adding element protocol methods (e.g., tag_name property)
  • Creating workarounds for Selenium-specific patterns

How to test the changes?

(Select all options that apply)

License

  • I agree to license these and all my past contributions to the core galaxy codebase under the MIT license.

@jmchilton jmchilton force-pushed the playwright_migrate_2 branch from 96bda85 to 3027f19 Compare October 19, 2025 00:27
@jmchilton jmchilton marked this pull request as ready for review October 20, 2025 15:22
@github-actions github-actions bot added this to the 26.0 milestone Oct 20, 2025
@mvdbeek
Copy link
Member

mvdbeek commented Oct 22, 2025

The integration selenium test seems related ?

- 'packages/**'
schedule:
# Run at midnight UTC every Tuesday
- cron: '0 0 * * 2'
Copy link
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
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.

Copy link
Member

@mvdbeek mvdbeek left a comment

Choose a reason for hiding this comment

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

This is both exciting and scary. I wouldn't want to figure out something like:

    def upload_queue_local_file(self, test_path, tab_id="regular"):
        if self.backend_type == "playwright":
            with self.page.expect_file_chooser() as fc_info:
                self.wait_for_and_click_selector(f"div#{tab_id} button#btn-local")
            file_chooser = fc_info.value
            file_chooser.set_files(test_path)
        else:
            self.wait_for_and_click_selector(f"div#{tab_id} button#btn-local")
            file_upload = self.wait_for_selector(f'div#{tab_id} input[type="file"]')
            file_upload.send_keys(test_path)

figuring this out for one automation frameworks tests my patience to its limit already ...

Is your assessment that we don't be needing to write test cases by hand anymore at all ?

I do like the added instruction files though, that's really cool.

@jmchilton
Copy link
Member Author

jmchilton commented Oct 22, 2025

figuring this out for one automation frameworks tests my patience to its limit already ...

Yeah - I imagine that at some point people will be writing Playwright only tests and the Selenium support in the framework layers and existing tests will be a passion project if anyone cares. I certainly don't want people to have to debug all tests in two frameworks - that would really suck. People should feel free to mark tests as selenium_only or playbook_only with decorators I think.

Is your assessment that we don't be needing to write test cases by hand anymore at all ?

No - not at all. I've not gotten anything automated to work in a way I'm happy with - I think updating the selectors, maintaining the components, doing the work - is important and useful.

Update: I mean these questions are disconnected but I do think the process of getting one test framework to run the test of the other at this point given the abstractions we have, examples already available, etc... would be pretty doable via Sonnet 4.5 - I have a markdown document I was going to throw at this and let Claude work through these but the PR is already very unwieldy.

@jmchilton jmchilton force-pushed the playwright_migrate_2 branch 6 times, most recently from 675753b to a0921f2 Compare October 23, 2025 20:23
jmchilton and others added 20 commits October 23, 2025 21:18
Creates playwright.yaml workflow mirroring the existing selenium.yaml structure
to run the test suite with the Playwright backend. The workflow maintains the
same configuration as Selenium tests (parallel execution across 3 chunks,
PostgreSQL service, client build dependencies) while setting
GALAXY_TEST_DRIVER_BACKEND=playwright to use the Playwright backend.

Key features:
- Sets GALAXY_TEST_DRIVER_BACKEND=playwright for backend selection
- Installs Playwright and Chromium browser with system dependencies in test job
- Uses headless mode (GALAXY_TEST_SELENIUM_HEADLESS=1) for CI execution
- Maintains same trigger paths, environment variables, and matrix strategy
- Uploads test results and debug artifacts on failure

Unlike Selenium which uses a separate setup_selenium.yaml reusable workflow for
global chromedriver installation, Playwright installation happens directly in
the test job since it requires the Python environment context (venv, working
directory) to be available.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Replace custom BrowserAvailability class and global cache with
two simple @lru_cache(maxsize=1) decorated functions. This provides
the same caching behavior with cleaner, more Pythonic code.

Benefits:
- Simpler code: Reduced from ~60 lines to ~30 lines
- Same functionality: Caches single result, avoids repeated browser launches
- More Pythonic: Uses standard library caching instead of custom implementation
- Cleaner API: No need to manage global cache object or expose the class

Thanks to @mvdbeek for the suggestion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
The docstring was documenting a non-existent 'default_timeout' parameter
and missing the actual 'timeout_handler' parameter. Updated to accurately
document all parameters in the correct order.

Changes:
- Added missing 'timeout_handler' parameter documentation
- Removed incorrect 'default_timeout' parameter
- Described timeout_handler as callback function that returns timeout value

Thanks to the PR review for catching this.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@jmchilton jmchilton force-pushed the playwright_migrate_2 branch from a0921f2 to 3c1bd35 Compare October 24, 2025 01:18
@jmchilton jmchilton force-pushed the playwright_migrate_2 branch from 3884130 to 277c414 Compare October 24, 2025 17:59
@jmchilton jmchilton merged commit 61a5a4c into galaxyproject:dev Oct 27, 2025
61 of 67 checks passed
@github-actions
Copy link

This PR was merged without a "kind/" label, please correct.

@nsoranzo nsoranzo deleted the playwright_migrate_2 branch October 27, 2025 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants