Warning
This document is for an old release of Galaxy. You can alternatively view this page in the latest release if it exists or view the top of the latest release's documentation.
Source code for galaxy.selenium.has_driver
"""A mixin to extend a class that has self.driver with higher-level constructs.
This should be mixed into classes with a self.driver and self.default_timeout
attribute.
"""
import abc
import threading
from contextlib import contextmanager
from typing import (
Any,
cast,
Generic,
Literal,
Optional,
Union,
)
from axe_selenium_python import Axe
from selenium.common.exceptions import (
NoSuchElementException,
TimeoutException as SeleniumTimeoutException,
)
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support.wait import WebDriverWait
from seletools.actions import drag_and_drop as seletools_drag_and_drop
from galaxy.navigation.components import Target
from galaxy.util import requests
from .axe_results import (
AxeResults,
NullAxeResults,
RealAxeResults,
)
from .has_driver_protocol import (
Cookie,
HasElementLocator,
TimeoutCallback,
WaitTypeT,
)
from .wait_methods_mixin import WaitMethodsMixin
from .web_element_protocol import WebElementProtocol
UNSPECIFIED_TIMEOUT = object()
HasFindElement = Union[WebDriver, WebElement]
DEFAULT_AXE_SCRIPT_URL = "https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.7.1/axe.min.js"
AXE_SCRIPT_HASH: dict[str, str] = {}
AXE_SCRIPT_HASH_LOCK = threading.Lock()
def get_axe_script(script_url: str) -> str:
with AXE_SCRIPT_HASH_LOCK:
if script_url not in AXE_SCRIPT_HASH:
content = requests.get(script_url).text
AXE_SCRIPT_HASH[script_url] = content
return AXE_SCRIPT_HASH[script_url]
def _webelement_to_protocol(element: WebElement) -> WebElementProtocol:
"""
Convert Selenium WebElement to WebElementProtocol type.
Selenium's WebElement satisfies WebElementProtocol at runtime, but mypy
doesn't recognize this because:
1. WebElement.find_element() has parameters typed as (by: Any, value: Any)
while our protocol requires (by: str, value: str | None)
2. WebElement.find_element() returns WebElement instead of WebElementProtocol
Since WebElement actually implements all required protocol methods correctly
at runtime, this cast is safe.
"""
return cast(WebElementProtocol, element)
def _webelements_to_protocol(elements: list[WebElement]) -> list[WebElementProtocol]:
"""
Convert list of Selenium WebElements to list of WebElementProtocol.
See _webelement_to_protocol for why this type conversion is necessary.
"""
return [cast(WebElementProtocol, element) for element in elements]
def _protocol_to_webelement(element: WebElementProtocol) -> WebElement:
"""See notes above for why these cannot be directly typed."""
return cast(WebElement, element)
def _cookies_to_typed(cookies: list[dict]) -> list[Cookie]:
"""
Convert Selenium's list[dict] cookies to list[Cookie].
Selenium's get_cookies() returns list[dict[Any, Any]], but the actual
dictionaries at runtime contain the correct Cookie keys. We use a TypedDict
to provide better type safety for cookie access. This cast is safe because
Selenium's cookie dictionaries contain all the keys defined in Cookie.
"""
return cookies # type: ignore[return-value]
[docs]
class TimeoutMessageMixin:
"""Mixin providing timeout message formatting for driver abstractions."""
def _timeout_message(self, on_str: str) -> str:
"""
Generate a timeout error message.
Args:
on_str: Description of what was being waited on
Returns:
Formatted timeout message string
"""
return f"Timeout waiting on {on_str}."
[docs]
class HasDriver(TimeoutMessageMixin, WaitMethodsMixin, Generic[WaitTypeT]):
by: type[By] = By
keys: type[Keys] = Keys
driver: WebDriver
axe_script_url: str = DEFAULT_AXE_SCRIPT_URL
axe_skip: bool = False
@property
def backend_type(self) -> Literal["selenium", "playwright"]:
"""Identify this as the Selenium backend implementation."""
return "selenium"
@property
def current_url(self) -> str:
"""
Get the current page URL.
Returns:
The current URL
"""
return self.driver.current_url
@property
def page_source(self) -> str:
"""
Get the HTML source of the current page.
Returns:
The page HTML as a string
"""
return self.driver.page_source
@property
def page_title(self) -> str:
"""
Get the title of the current page.
Returns:
The page title as a string
"""
return self.driver.title
[docs]
def re_get_with_query_params(self, params_str: str):
driver = self.driver
new_url = driver.current_url
if "?" not in new_url:
new_url += "?"
new_url += params_str
driver.get(new_url)
[docs]
def assert_selector(self, selector: str):
assert self.driver.find_element(By.CSS_SELECTOR, selector)
[docs]
def assert_disabled(self, selector_template: Target):
elements = self.find_elements(selector_template)
assert len(elements) > 0
for element in elements:
assert not element.is_enabled()
[docs]
def selector_is_displayed(self, selector: str):
element = self.driver.find_element(By.CSS_SELECTOR, selector)
return element.is_displayed()
[docs]
def is_displayed(self, selector_template: Target) -> bool:
element = self.driver.find_element(*selector_template.element_locator)
return element.is_displayed()
[docs]
def assert_selector_absent(self, selector: str):
assert len(self.driver.find_elements(By.CSS_SELECTOR, selector)) == 0
[docs]
def find_elements(self, selector_template: Target) -> list[WebElementProtocol]:
return _webelements_to_protocol(self.driver.find_elements(*selector_template.element_locator))
[docs]
def find_element(self, selector_template: HasElementLocator) -> WebElementProtocol:
"""
Find first element matching selector template (no waiting).
Args:
selector_template: Either a Target or a (locator_type, value) tuple
"""
# Dispatch on input type
if isinstance(selector_template, Target):
locator = selector_template.element_locator
else:
# It's a tuple (locator_type, value)
locator = selector_template
return _webelement_to_protocol(self.driver.find_element(*locator))
[docs]
def assert_absent(self, selector_template: Target) -> None:
elements = self.find_elements(selector_template)
if len(elements) != 0:
description = selector_template.description
any_displayed = False
for element in elements:
any_displayed = any_displayed or element.is_displayed()
msg = f"Expected DOM elements [{elements}] to be empty for selector target {description} - any actually displayed? [{any_displayed}]"
raise AssertionError(msg)
[docs]
def element_absent(self, selector_template: Target) -> bool:
return len(self.find_elements(selector_template)) == 0
[docs]
def switch_to_frame(self, frame_reference: Union[str, int, WebElement] = "frame"):
"""
Switch to an iframe or frame.
Args:
frame_reference: Can be:
- str: frame name or id (will wait for frame to be available)
- int: frame index
- WebElement: frame element
Returns:
The result of the switch operation
"""
if isinstance(frame_reference, str):
# Try to switch by name first, if that fails, try by ID
# We use NAME as the locator because frame_to_be_available_and_switch_to_it
# checks both name and id attributes
return self._wait_on_selenium_condition(ec.frame_to_be_available_and_switch_to_it(frame_reference))
elif isinstance(frame_reference, int):
# Switch by index
return self.driver.switch_to.frame(frame_reference)
else:
# Assume it's a WebElement
return self.driver.switch_to.frame(frame_reference)
[docs]
def switch_to_default_content(self):
"""
Switch back to the default content (main page context).
This exits any iframe/frame context and returns to the top-level page.
"""
self.driver.switch_to.default_content()
[docs]
def get_cookies(self) -> list[Cookie]:
"""
Get all cookies for the current domain.
Returns:
List of cookie dictionaries with keys like 'name', 'value', 'domain', 'path', etc.
"""
return _cookies_to_typed(self.driver.get_cookies())
# Implementation of WaitMethodsMixin abstract methods for Selenium
def _wait_on_condition_present(self, locator_tuple: tuple, message: str, **kwds) -> WebElement:
"""Wait for element to be present in DOM."""
return self._wait_on_selenium_condition(ec.presence_of_element_located(locator_tuple), message, **kwds)
def _wait_on_condition_visible(self, locator_tuple: tuple, message: str, **kwds) -> WebElement:
"""Wait for element to be visible."""
return self._wait_on_selenium_condition(ec.visibility_of_element_located(locator_tuple), message, **kwds)
def _wait_on_condition_clickable(self, locator_tuple: tuple, message: str, **kwds) -> WebElement:
"""Wait for element to be clickable."""
return self._wait_on_selenium_condition(ec.element_to_be_clickable(locator_tuple), message, **kwds)
def _wait_on_condition_invisible(self, locator_tuple: tuple, message: str, **kwds) -> WebElement:
"""Wait for element to be invisible or absent."""
return self._wait_on_selenium_condition(ec.invisibility_of_element_located(locator_tuple), message, **kwds)
def _wait_on_condition_absent(self, locator_tuple: tuple, message: str, **kwds) -> WebElement:
"""Wait for element to be completely absent from DOM."""
return self._wait_on_selenium_condition(
lambda driver: len(driver.find_elements(*locator_tuple)) == 0, message, **kwds
)
def _wait_on_condition_count(self, locator_tuple: tuple, n: int, message: str, **kwds) -> WebElement:
"""Wait for at least N elements."""
return self._wait_on_selenium_condition(
lambda driver: len(driver.find_elements(*locator_tuple)) >= n, message, **kwds
)
# TODO: typevar...
def _wait_on_custom(self, condition_func, message: str, **kwds) -> Any:
"""Wait on custom condition function."""
return self._wait_on_selenium_condition(condition_func, message, **kwds)
[docs]
def click(self, selector_template: Target):
element = self.driver.find_element(*selector_template.element_locator)
element.click()
def _wait_on_selenium_condition(self, condition, on_str: Optional[str] = None, **kwds):
if on_str is None:
on_str = str(condition)
wait = self.wait(**kwds)
return wait.until(condition, self._timeout_message(on_str))
[docs]
def drag_and_drop(self, source: WebElementProtocol, target: WebElementProtocol) -> None:
"""
Drag and drop from source element to target element.
Uses JavaScript-based drag and drop implementation for reliability.
Args:
source: The element to drag
target: The element to drop onto
"""
seletools_drag_and_drop(self.driver, source, target)
[docs]
def move_to_and_click(self, element: WebElementProtocol) -> None:
"""
Move to an element and click it using ActionChains.
This is useful when a simple click doesn't work due to element positioning.
Args:
element: The element to move to and click
"""
self.action_chains().move_to_element(element).click().perform()
[docs]
def hover(self, element: WebElement) -> None:
"""
Hover over an element (move mouse to element without clicking).
Args:
element: The element to hover over
"""
self.action_chains().move_to_element(element).perform()
[docs]
def send_enter(self, element: Optional[WebElement] = None):
self._send_key(Keys.ENTER, element)
[docs]
def send_escape(self, element: Optional[WebElement] = None):
self._send_key(Keys.ESCAPE, element)
[docs]
def send_backspace(self, element: Optional[WebElement] = None):
self._send_key(Keys.BACKSPACE, element)
[docs]
def aggressive_clear(self, element: WebElement) -> None:
# for when a simple .clear() doesn't work
self.driver.execute_script("arguments[0].value = '';", element)
for _ in range(25):
element.send_keys(Keys.BACKSPACE)
def _send_key(self, key: str, element: Optional[WebElement] = None):
if element is None:
self.action_chains().send_keys(key)
else:
element.send_keys(key)
@property
@abc.abstractmethod
def timeout_handler(self) -> TimeoutCallback:
"""Get timeout handler for application specific wait types."""
...
[docs]
def wait(self, timeout=UNSPECIFIED_TIMEOUT, wait_type: Optional[WaitTypeT] = None, **kwds):
if timeout is UNSPECIFIED_TIMEOUT:
timeout = self.timeout_handler(wait_type)
return WebDriverWait(self.driver, timeout)
[docs]
def click_xpath(self, xpath: str):
element = self.driver.find_element(By.XPATH, xpath)
element.click()
[docs]
def click_label(self, text: str):
element = self.driver.find_element(By.LINK_TEXT, text)
element.click()
[docs]
def click_selector(self, selector: str):
element = self.driver.find_element(By.CSS_SELECTOR, selector)
element.click()
[docs]
def fill(self, form: WebElement, info: dict):
for key, value in info.items():
try:
input_element = form.find_element(By.NAME, key)
except NoSuchElementException:
input_element = form.find_element(By.ID, key)
input_element.send_keys(value)
[docs]
def click_submit(self, form: WebElement):
submit_button = form.find_element(By.CSS_SELECTOR, "input[type='submit']")
submit_button.click()
[docs]
def prepend_timeout_message(
self, timeout_exception: SeleniumTimeoutException, message: str
) -> SeleniumTimeoutException:
msg = message
if timeout_msg := timeout_exception.msg:
msg += f" {timeout_msg}"
return SeleniumTimeoutException(
msg=msg,
screen=timeout_exception.screen,
stacktrace=timeout_exception.stacktrace,
)
[docs]
def accept_alert(self):
"""
Return a context manager for accepting alerts.
For Selenium, the alert must exist before it can be accepted,
so we wait until the context exits to accept it.
Usage:
with driver.accept_alert():
driver.click_selector("#button-that-shows-alert")
# Alert is automatically accepted here
"""
@contextmanager
def _accept_alert_context():
try:
yield
finally:
# Accept the alert after the context block completes
try:
alert = self.driver.switch_to.alert
alert.accept()
finally:
self.driver.switch_to.default_content()
return _accept_alert_context()
[docs]
def execute_script(self, script: str, *args):
"""
Execute JavaScript in the current browser context.
Args:
script: JavaScript code to execute
*args: Optional arguments to pass to the script (accessible as arguments[0], arguments[1], etc.)
Returns:
The return value of the script execution
"""
return self.driver.execute_script(script, *args)
[docs]
def set_local_storage(self, key: str, value: Union[str, float]) -> None:
"""
Set a value in the browser's localStorage.
Args:
key: The localStorage key
value: The value to store (will be JSON-stringified if not a string)
"""
self.execute_script(f"""window.localStorage.setItem("{key}", {value});""")
[docs]
def remove_local_storage(self, key: str) -> None:
"""
Remove a key from the browser's localStorage.
Args:
key: The localStorage key to remove
"""
self.execute_script(f"""window.localStorage.removeItem("{key}");""")
[docs]
def scroll_into_view(self, element: WebElement) -> None:
"""
Scroll an element into view using JavaScript.
Args:
element: The element to scroll into view
"""
self.execute_script("arguments[0].scrollIntoView(true);", element)
[docs]
def set_element_value(self, element: WebElement, value: str) -> None:
"""
Set an element's value property directly using JavaScript.
This is useful for contenteditable elements or when .clear() doesn't work.
Args:
element: The element to modify
value: The value to set
"""
self.execute_script(f"arguments[0].value = '{value}';", element)
[docs]
def execute_script_click(self, element: WebElement) -> None:
"""
Click an element using JavaScript instead of Selenium's native click.
This is useful when Selenium's click is intercepted or the element is not clickable.
Args:
element: The element to click
"""
self.execute_script("arguments[0].click();", element)
[docs]
def find_element_by_link_text(self, text: str, element: Optional[WebElement] = None) -> WebElementProtocol:
return _webelement_to_protocol(self._locator_aware(element).find_element(By.LINK_TEXT, text))
[docs]
def find_element_by_xpath(self, xpath: str, element: Optional[WebElement] = None) -> WebElementProtocol:
return _webelement_to_protocol(self._locator_aware(element).find_element(By.XPATH, xpath))
[docs]
def find_element_by_id(self, id: str, element: Optional[WebElement] = None) -> WebElementProtocol:
return _webelement_to_protocol(self._locator_aware(element).find_element(By.ID, id))
[docs]
def find_element_by_selector(self, selector: str, element: Optional[WebElement] = None) -> WebElementProtocol:
return _webelement_to_protocol(self._locator_aware(element).find_element(By.CSS_SELECTOR, selector))
[docs]
def find_elements_by_selector(
self, selector: str, element: Optional[WebElement] = None
) -> list[WebElementProtocol]:
"""
Find multiple elements by CSS selector.
Args:
selector: CSS selector string
element: Optional parent element to search within
Returns:
List of WebElementProtocol elements
"""
elements = self._locator_aware(element).find_elements(By.CSS_SELECTOR, selector)
return [_webelement_to_protocol(el) for el in elements]
[docs]
def get_input_value(self, element: WebElementProtocol) -> str:
"""
Get the value of an input element.
This provides a unified interface for getting input values across both
Selenium and Playwright backends. For Selenium, this uses get_attribute("value").
Args:
element: The input element to get the value from
Returns:
The current value of the input element, or empty string if no value
"""
value = element.get_attribute("value")
return value if value is not None else ""
[docs]
def select_by_value(self, selector_template: HasElementLocator, value: str) -> None:
"""
Select an option from a <select> element by its value attribute.
Args:
selector_template: Either a Target or a (locator_type, value) tuple for the select element
value: The value attribute of the option to select
"""
select_element = _protocol_to_webelement(self.find_element(selector_template))
select = Select(select_element)
select.select_by_value(value)
[docs]
def axe_eval(self, context: Optional[str] = None, write_to: Optional[str] = None) -> AxeResults:
if self.axe_skip:
return NullAxeResults()
content = get_axe_script(self.axe_script_url)
self.driver.execute_script(content)
axe = Axe(self.driver)
results = axe.run(context=context)
if write_to is not None:
axe.write_results(results, write_to)
return RealAxeResults(results)
[docs]
def save_screenshot(self, path: str) -> None:
"""
Save a screenshot to the specified path.
Args:
path: File path where the screenshot should be saved
"""
self.driver.save_screenshot(path)
[docs]
def get_screenshot_as_png(self) -> bytes:
"""
Capture a screenshot and return it as PNG bytes.
Returns:
PNG image data as bytes
"""
return self.driver.get_screenshot_as_png()
[docs]
def quit(self) -> None:
"""
Clean up and close the driver/browser.
This closes all windows/tabs and releases all system resources.
The driver cannot be used after calling this method.
"""
self.driver.quit()
def _locator_aware(self, element: Optional[WebElement] = None) -> HasFindElement:
if element is None:
return self.driver
else:
return element
[docs]
def double_click(self, element: WebElement) -> None:
"""
Double-click an element using ActionChains.
Args:
element: The element to double-click
"""
action_chains = self.action_chains()
action_chains.move_to_element(element).double_click().perform()
[docs]
def exception_indicates_click_intercepted(exception):
return "click intercepted" in str(exception)
[docs]
def exception_indicates_not_clickable(exception):
selenium_not_clickable = "not clickable" in str(exception)
playwright_not_enabled = "element is not enabled" in str(exception)
return selenium_not_clickable or playwright_not_enabled
__all__ = (
"exception_indicates_click_intercepted",
"exception_indicates_not_clickable",
"exception_indicates_stale_element",
"HasDriver",
"SeleniumTimeoutException",
"TimeoutMessageMixin",
)