Unit tests for Selenium has_driver.

If we can make this suite work with Playwright it would be a great step toward being able to run tests in both suites before ultimately migrating to Playwright.
This commit is contained in:
John Chilton
2025-10-12 16:31:11 -04:00
parent a47ca54acb
commit 94f2ffaf68
9 changed files with 886 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
../../../test/unit/selenium
+1
View File
@@ -0,0 +1 @@
"""Unit tests for galaxy.selenium package."""
+72
View File
@@ -0,0 +1,72 @@
"""Pytest configuration and fixtures for selenium unit tests."""
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.chrome.service import Service as ChromeService
from .test_server import TestHTTPServer
@pytest.fixture(scope="session")
def test_server():
"""
Create and start a test HTTP server for the entire test session.
Yields:
TestHTTPServer: Running HTTP server instance
"""
server = TestHTTPServer(port=0) # Use random available port
server.start()
yield server
server.stop()
@pytest.fixture(scope="function")
def chrome_options():
"""
Create Chrome options for headless testing.
Returns:
ChromeOptions: Configured Chrome options
"""
options = ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
options.add_argument("--window-size=1920,1080")
options.add_argument("--disable-extensions")
options.add_argument("--disable-popup-blocking")
return options
@pytest.fixture(scope="function")
def driver(chrome_options):
"""
Create a WebDriver instance for each test.
Args:
chrome_options: Chrome options fixture
Yields:
WebDriver: Chrome WebDriver instance
"""
_driver = webdriver.Chrome(options=chrome_options)
_driver.implicitly_wait(0) # Disable implicit waits, use explicit waits in tests
yield _driver
_driver.quit()
@pytest.fixture(scope="session")
def base_url(test_server):
"""
Get the base URL for the test server.
Args:
test_server: Test HTTP server fixture
Returns:
str: Base URL of the test server
"""
return test_server.get_url()
+1
View File
@@ -0,0 +1 @@
"""HTML fixtures for selenium unit tests."""
@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessibility Test Page</title>
</head>
<body>
<h1>Accessibility Test Page</h1>
<!-- Section with good accessibility -->
<section id="good-section" aria-label="Accessible form">
<h2>Accessible Form</h2>
<form>
<label for="good-input">Name:</label>
<input type="text" id="good-input" name="name" aria-required="true" />
<label for="good-email">Email:</label>
<input type="email" id="good-email" name="email" />
<button type="submit" aria-label="Submit form">Submit</button>
</form>
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect width='100' height='100' fill='blue'/%3E%3C/svg%3E" alt="Blue square" />
</section>
<!-- Section with accessibility violations -->
<section id="bad-section" aria-label="Section with violations">
<h2>Form with Violations</h2>
<!-- Missing label for input -->
<input type="text" id="no-label-input" name="nolabel" />
<!-- Image without alt text -->
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect width='100' height='100' fill='red'/%3E%3C/svg%3E" />
<!-- Button without accessible name -->
<button type="button"></button>
<!-- Low contrast text (white on white) -->
<div style="background-color: white; color: white;">Invisible text</div>
</section>
<!-- Section for testing context parameter -->
<section id="context-section" aria-label="Context test section">
<h2>Context Test Section</h2>
<label for="context-input">Context Input:</label>
<input type="text" id="context-input" name="context" />
<!-- This has a violation -->
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='50' height='50'%3E%3Ccircle cx='25' cy='25' r='20' fill='green'/%3E%3C/svg%3E" />
</section>
</body>
</html>
+93
View File
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Basic Test Page</title>
<style>
.hidden {
display: none;
}
.visible {
display: block;
}
.delayed-visible {
display: none;
}
#disabled-button {
pointer-events: none;
opacity: 0.5;
}
</style>
</head>
<body>
<h1 id="header">Test Page</h1>
<!-- Elements for testing finding methods -->
<div id="test-div" class="test-class">Test Div</div>
<p class="test-paragraph">Test Paragraph</p>
<span data-testid="test-span">Test Span</span>
<!-- Link for testing -->
<a href="#" id="test-link">Test Link</a>
<!-- Elements for visibility testing -->
<div id="visible-element" class="visible">I am visible</div>
<div id="hidden-element" class="hidden">I am hidden</div>
<div id="delayed-element" class="delayed-visible">I will become visible</div>
<!-- Form elements -->
<form id="test-form" action="/submit" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="username" />
<label for="password">Password:</label>
<input type="password" id="password" name="password" />
<label for="email">Email:</label>
<input type="email" name="email" id="email" />
<button type="button" id="clickable-button">Click Me</button>
<button type="button" id="disabled-button" disabled>Disabled Button</button>
<input type="submit" value="Submit" />
</form>
<!-- Elements for xpath testing -->
<div class="xpath-container">
<ul>
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
</ul>
</div>
<!-- Alert button -->
<button id="alert-button" onclick="alert('Test Alert')">Show Alert</button>
<!-- Frame for iframe testing -->
<iframe name="frame" id="test-frame" src="frame.html" style="width: 300px; height: 200px;"></iframe>
<script>
// Make element visible after delay
setTimeout(() => {
document.getElementById('delayed-element').classList.remove('delayed-visible');
document.getElementById('delayed-element').classList.add('visible');
}, 1000);
// Button click handler
document.getElementById('clickable-button').addEventListener('click', function() {
this.textContent = 'Clicked!';
this.classList.add('was-clicked');
});
// Form submission handler
document.getElementById('test-form').addEventListener('submit', function(e) {
e.preventDefault();
const result = document.createElement('div');
result.id = 'form-result';
result.textContent = 'Form submitted!';
document.body.appendChild(result);
});
</script>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Frame Content</title>
</head>
<body>
<h2 id="frame-header">Inside Frame</h2>
<p id="frame-content">This is content inside the frame.</p>
</body>
</html>
+575
View File
@@ -0,0 +1,575 @@
"""Unit tests for galaxy.selenium.has_driver module."""
import pytest
from selenium.common.exceptions import (
NoSuchElementException,
TimeoutException as SeleniumTimeoutException,
)
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from galaxy.navigation.components import Target
from galaxy.selenium.has_driver import (
HasDriver,
exception_indicates_click_intercepted,
exception_indicates_not_clickable,
exception_indicates_stale_element,
)
class SimpleTarget(Target):
"""Simple concrete implementation of Target for testing."""
def __init__(self, element_locator: tuple, description: str):
"""
Initialize target with locator and description.
Args:
element_locator: Tuple of (By, locator_string) for Selenium
description: Human-readable description
"""
self._element_locator = element_locator
self._description = description
@property
def description(self) -> str:
"""Return description."""
return self._description
@property
def element_locator(self):
"""Return Selenium locator tuple."""
return self._element_locator
@property
def component_locator(self):
"""Return component locator (not used in these tests)."""
raise NotImplementedError("component_locator not needed for these tests")
class TestHasDriverImpl(HasDriver):
"""
Concrete implementation of HasDriver for testing.
HasDriver is an abstract mixin that requires a driver and timeout implementation.
"""
def __init__(self, driver: WebDriver, default_timeout: float = 10.0):
"""
Initialize test implementation.
Args:
driver: Selenium WebDriver instance
default_timeout: Default timeout for waits
"""
self.driver = driver
self.default_timeout = default_timeout
def timeout_for(self, **kwds) -> float:
"""Return timeout value (required abstract method)."""
return kwds.get("timeout", self.default_timeout)
@pytest.fixture
def has_driver_instance(driver):
"""
Create a HasDriver instance for testing.
Args:
driver: WebDriver fixture
Returns:
TestHasDriverImpl: Concrete HasDriver implementation
"""
return TestHasDriverImpl(driver)
class TestElementFinding:
"""Tests for element finding methods."""
def test_assert_xpath(self, has_driver_instance: TestHasDriverImpl, base_url: str) -> None:
"""Test finding element by XPath assertion."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
has_driver_instance.assert_xpath("//h1[@id='header']")
def test_assert_xpath_fails_when_not_found(self, has_driver_instance: TestHasDriverImpl, base_url: str) -> None:
"""Test assert_xpath raises when element not found."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
with pytest.raises(NoSuchElementException):
has_driver_instance.assert_xpath("//div[@id='nonexistent']")
def test_assert_selector(self, has_driver_instance: TestHasDriverImpl, base_url: str) -> None:
"""Test finding element by CSS selector assertion."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
has_driver_instance.assert_selector("#test-div")
def test_assert_selector_fails_when_not_found(self, has_driver_instance: TestHasDriverImpl, base_url: str) -> None:
"""Test assert_selector raises when element not found."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
with pytest.raises(NoSuchElementException):
has_driver_instance.assert_selector("#nonexistent")
def test_find_element_by_id(self, has_driver_instance: TestHasDriverImpl, base_url: str) -> None:
"""Test finding element by ID."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.find_element_by_id("test-div")
assert element.text == "Test Div"
def test_find_element_by_xpath(self, has_driver_instance: TestHasDriverImpl, base_url: str) -> None:
"""Test finding element by XPath."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.find_element_by_xpath("//p[@class='test-paragraph']")
assert element.text == "Test Paragraph"
def test_find_element_by_selector(self, has_driver_instance: TestHasDriverImpl, base_url: str) -> None:
"""Test finding element by CSS selector."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.find_element_by_selector("[data-testid='test-span']")
assert element.text == "Test Span"
def test_find_element_by_link_text(self, has_driver_instance: TestHasDriverImpl, base_url: str) -> None:
"""Test finding element by link text."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.find_element_by_link_text("Test Link")
assert element.get_attribute("id") == "test-link"
def test_find_elements_with_target(self, has_driver_instance: TestHasDriverImpl, base_url: str) -> None:
"""Test finding multiple elements using Target."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.CLASS_NAME, "item"), description="list items")
elements = has_driver_instance.find_elements(target)
assert len(elements) == 3
class TestVisibilityAndPresence:
"""Tests for visibility and presence checking methods."""
def test_selector_is_displayed_visible_element(self, has_driver_instance, base_url):
"""Test checking if visible element is displayed."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
assert has_driver_instance.selector_is_displayed("#visible-element")
def test_selector_is_displayed_hidden_element(self, has_driver_instance, base_url):
"""Test checking if hidden element is not displayed."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
assert not has_driver_instance.selector_is_displayed("#hidden-element")
def test_is_displayed_with_target(self, has_driver_instance, base_url):
"""Test is_displayed with Target selector."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.ID, "visible-element"), description="visible element")
assert has_driver_instance.is_displayed(target)
def test_assert_selector_absent_or_hidden(self, has_driver_instance, base_url):
"""Test asserting element is absent or hidden."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
has_driver_instance.assert_selector_absent_or_hidden("#hidden-element")
def test_assert_absent_or_hidden_with_target(self, has_driver_instance, base_url):
"""Test assert_absent_or_hidden with Target."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.ID, "hidden-element"), description="hidden element")
has_driver_instance.assert_absent_or_hidden(target)
def test_assert_selector_absent(self, has_driver_instance, base_url):
"""Test asserting element is completely absent from DOM."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
has_driver_instance.assert_selector_absent("#nonexistent-element")
def test_assert_absent_with_target(self, has_driver_instance, base_url):
"""Test assert_absent with Target when element doesn't exist."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.ID, "nonexistent"), description="nonexistent element")
has_driver_instance.assert_absent(target)
def test_element_absent_returns_true(self, has_driver_instance, base_url):
"""Test element_absent returns True when element not in DOM."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.ID, "nonexistent"), description="nonexistent element")
assert has_driver_instance.element_absent(target)
def test_element_absent_returns_false(self, has_driver_instance, base_url):
"""Test element_absent returns False when element exists."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.ID, "test-div"), description="test div")
assert not has_driver_instance.element_absent(target)
def test_assert_disabled(self, has_driver_instance, base_url):
"""Test asserting element is disabled."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.ID, "disabled-button"), description="disabled button")
has_driver_instance.assert_disabled(target)
class TestWaitMethods:
"""Tests for wait methods."""
def test_wait_for_xpath(self, has_driver_instance, base_url):
"""Test waiting for element by XPath."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.wait_for_xpath("//h1[@id='header']")
assert element.text == "Test Page"
def test_wait_for_xpath_visible(self, has_driver_instance, base_url):
"""Test waiting for visible element by XPath."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.wait_for_xpath_visible("//div[@id='visible-element']")
assert element.is_displayed()
def test_wait_for_selector(self, has_driver_instance, base_url):
"""Test waiting for element by CSS selector."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.wait_for_selector("#test-div")
assert element.text == "Test Div"
def test_wait_for_present_with_target(self, has_driver_instance, base_url):
"""Test wait_for_present with Target."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.ID, "test-div"), description="test div")
element = has_driver_instance.wait_for_present(target)
assert element is not None
def test_wait_for_visible_with_target(self, has_driver_instance, base_url):
"""Test wait_for_visible with Target."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.ID, "visible-element"), description="visible element")
element = has_driver_instance.wait_for_visible(target)
assert element.is_displayed()
def test_wait_for_selector_visible(self, has_driver_instance, base_url):
"""Test waiting for visible element by CSS selector."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.wait_for_selector_visible("#visible-element")
assert element.is_displayed()
def test_wait_for_selector_clickable(self, has_driver_instance, base_url):
"""Test waiting for clickable element by CSS selector."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.wait_for_selector_clickable("#clickable-button")
assert element.is_enabled()
def test_wait_for_clickable_with_target(self, has_driver_instance, base_url):
"""Test wait_for_clickable with Target."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.ID, "clickable-button"), description="clickable button")
element = has_driver_instance.wait_for_clickable(target)
assert element.is_enabled()
def test_wait_for_selector_absent(self, has_driver_instance, base_url):
"""Test waiting for element to be absent."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
# Element doesn't exist, so wait should succeed immediately
has_driver_instance.wait_for_selector_absent("#nonexistent-element")
def test_wait_for_absent_with_target(self, has_driver_instance, base_url):
"""Test wait_for_absent with Target."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.ID, "nonexistent"), description="nonexistent element")
has_driver_instance.wait_for_absent(target)
def test_wait_for_selector_absent_or_hidden(self, has_driver_instance, base_url):
"""Test waiting for element to be absent or hidden."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
has_driver_instance.wait_for_selector_absent_or_hidden("#hidden-element")
def test_wait_for_absent_or_hidden_with_target(self, has_driver_instance, base_url):
"""Test wait_for_absent_or_hidden with Target."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.ID, "hidden-element"), description="hidden element")
has_driver_instance.wait_for_absent_or_hidden(target)
def test_wait_for_id(self, has_driver_instance, base_url):
"""Test waiting for element by ID."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.wait_for_id("test-div")
assert element.get_attribute("id") == "test-div"
def test_wait_for_element_count_of_at_least(self, has_driver_instance, base_url):
"""Test waiting for at least N elements."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.CLASS_NAME, "item"), description="list items")
has_driver_instance.wait_for_element_count_of_at_least(target, 3)
elements = has_driver_instance.find_elements(target)
assert len(elements) >= 3
def test_wait_timeout_with_custom_timeout(self, has_driver_instance, base_url):
"""Test that custom timeout is used."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
with pytest.raises(SeleniumTimeoutException):
has_driver_instance.wait_for_selector("#nonexistent", timeout=1)
def test_wait_for_delayed_element_becomes_visible(self, has_driver_instance, base_url):
"""Test waiting for element that becomes visible after delay."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
# Element becomes visible after 1 second
element = has_driver_instance.wait_for_selector_visible("#delayed-element", timeout=3)
assert element.is_displayed()
class TestClickAndInteraction:
"""Tests for click and interaction methods."""
def test_click_xpath(self, has_driver_instance, base_url):
"""Test clicking element by XPath."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
has_driver_instance.click_xpath("//button[@id='clickable-button']")
button = has_driver_instance.driver.find_element(By.ID, "clickable-button")
assert button.text == "Clicked!"
def test_click_selector(self, has_driver_instance, base_url):
"""Test clicking element by CSS selector."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
has_driver_instance.click_selector("#clickable-button")
button = has_driver_instance.driver.find_element(By.ID, "clickable-button")
assert button.text == "Clicked!"
def test_click_label(self, has_driver_instance, base_url):
"""Test clicking link by text."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
has_driver_instance.click_label("Test Link")
# Link was clicked (href="#" so stays on same page)
def test_click_with_target(self, has_driver_instance, base_url):
"""Test click method with Target."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
target = SimpleTarget(element_locator=(By.ID, "clickable-button"), description="clickable button")
has_driver_instance.click(target)
button = has_driver_instance.driver.find_element(By.ID, "clickable-button")
assert button.text == "Clicked!"
class TestFormInteraction:
"""Tests for form interaction methods."""
def test_fill_form(self, has_driver_instance, base_url):
"""Test filling form fields."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
form = has_driver_instance.driver.find_element(By.ID, "test-form")
form_data = {"username": "testuser", "password": "testpass", "email": "test@example.com"}
has_driver_instance.fill(form, form_data)
# Verify fields were filled
username = has_driver_instance.driver.find_element(By.ID, "username")
assert username.get_attribute("value") == "testuser"
password = has_driver_instance.driver.find_element(By.ID, "password")
assert password.get_attribute("value") == "testpass"
email = has_driver_instance.driver.find_element(By.ID, "email")
assert email.get_attribute("value") == "test@example.com"
def test_click_submit(self, has_driver_instance, base_url):
"""Test clicking submit button on form."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
form = has_driver_instance.driver.find_element(By.ID, "test-form")
has_driver_instance.click_submit(form)
# Verify form was submitted (result div appears)
result = has_driver_instance.wait_for_id("form-result", timeout=2)
assert result.text == "Form submitted!"
class TestActionChainsAndKeys:
"""Tests for action chains and key sending methods."""
def test_action_chains(self, has_driver_instance, base_url):
"""Test creating action chains."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
chains = has_driver_instance.action_chains()
assert chains is not None
def test_send_enter(self, has_driver_instance, base_url):
"""Test sending ENTER key."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.driver.find_element(By.ID, "username")
element.click()
has_driver_instance.send_enter(element)
# Key was sent (no exception)
def test_send_escape(self, has_driver_instance, base_url):
"""Test sending ESCAPE key."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.driver.find_element(By.ID, "username")
element.click()
has_driver_instance.send_escape(element)
# Key was sent (no exception)
def test_send_backspace(self, has_driver_instance, base_url):
"""Test sending BACKSPACE key."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
element = has_driver_instance.driver.find_element(By.ID, "username")
element.send_keys("test")
has_driver_instance.send_backspace(element)
# Verify one character was deleted
assert element.get_attribute("value") == "tes"
class TestFrameSwitching:
"""Tests for frame switching functionality."""
def test_switch_to_frame(self, has_driver_instance, base_url):
"""Test switching to iframe."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
has_driver_instance.switch_to_frame("frame")
# Verify we're in the frame by finding frame-specific element
frame_header = has_driver_instance.driver.find_element(By.ID, "frame-header")
assert frame_header.text == "Inside Frame"
# Switch back
has_driver_instance.driver.switch_to.default_content()
class TestAlertHandling:
"""Tests for alert handling."""
def test_accept_alert(self, has_driver_instance, base_url):
"""Test accepting browser alert."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
has_driver_instance.click_selector("#alert-button")
# Accept the alert
has_driver_instance.accept_alert()
# Verify we're back on the main page
header = has_driver_instance.driver.find_element(By.ID, "header")
assert header.text == "Test Page"
class TestUtilityMethods:
"""Tests for utility methods."""
def test_re_get_with_query_params_adds_question_mark(
self, has_driver_instance, base_url
):
"""Test adding query params to URL without existing params."""
has_driver_instance.driver.get(f"{base_url}/basic.html")
has_driver_instance.re_get_with_query_params("foo=bar")
assert "?foo=bar" in has_driver_instance.driver.current_url
def test_re_get_with_query_params_appends_to_existing(
self, has_driver_instance, base_url
):
"""Test adding query params to URL with existing params."""
has_driver_instance.driver.get(f"{base_url}/basic.html?existing=param")
has_driver_instance.re_get_with_query_params("foo=bar")
current_url = has_driver_instance.driver.current_url
assert "existing=param" in current_url
assert "foo=bar" in current_url
def test_prepend_timeout_message(self, has_driver_instance):
"""Test prepending message to timeout exception."""
original = SeleniumTimeoutException(msg="original message")
new_exception = has_driver_instance.prepend_timeout_message(
original, "New prefix:"
)
assert "New prefix:" in new_exception.msg
assert "original message" in new_exception.msg
class TestExceptionHelpers:
"""Tests for exception helper functions."""
def test_exception_indicates_click_intercepted(self):
"""Test detecting click intercepted exceptions."""
exc = Exception("Element click intercepted")
assert exception_indicates_click_intercepted(exc)
exc = Exception("Something else")
assert not exception_indicates_click_intercepted(exc)
def test_exception_indicates_not_clickable(self):
"""Test detecting not clickable exceptions."""
exc = Exception("Element is not clickable")
assert exception_indicates_not_clickable(exc)
exc = Exception("Something else")
assert not exception_indicates_not_clickable(exc)
def test_exception_indicates_stale_element(self):
"""Test detecting stale element exceptions."""
exc = Exception("stale element reference")
assert exception_indicates_stale_element(exc)
exc = Exception("Something else")
assert not exception_indicates_stale_element(exc)
class TestAccessibility:
"""Tests for axe_eval accessibility testing."""
def test_axe_eval_basic(self, has_driver_instance, base_url):
"""Test basic axe_eval functionality returns results."""
has_driver_instance.navigate_to(f"{base_url}/accessibility.html")
results = has_driver_instance.axe_eval()
# Should have some violations in our test page
violations = results.violations()
assert len(violations) > 0
def test_axe_eval_with_context(self, has_driver_instance, base_url):
"""Test axe_eval with context parameter to limit scope."""
has_driver_instance.navigate_to(f"{base_url}/accessibility.html")
# Run axe on just the good section
results = has_driver_instance.axe_eval(context="#good-section")
# Good section should have fewer/no violations
violations = results.violations()
# We expect fewer violations when scoped to good section
assert isinstance(violations, list)
def test_axe_eval_assert_passes(self, has_driver_instance, base_url):
"""Test axe_eval assert_passes for specific rules."""
has_driver_instance.navigate_to(f"{base_url}/accessibility.html")
results = has_driver_instance.axe_eval()
# Test that we can check if a specific rule passed
# document-title should pass on our test page (we have a <title> element)
results.assert_passes("document-title")
def test_axe_eval_assert_does_not_violate(self, has_driver_instance, base_url):
"""Test axe_eval assert_does_not_violate for specific rules."""
has_driver_instance.navigate_to(f"{base_url}/accessibility.html")
results = has_driver_instance.axe_eval(context="#good-section")
# Good section should not violate certain rules
# This is a sanity check - if this fails, our test fixture needs adjustment
try:
results.assert_does_not_violate("aria-roles")
except AssertionError:
pytest.fail("Good section should not have aria-roles violations")
def test_axe_eval_violations_with_impact(self, has_driver_instance, base_url):
"""Test filtering violations by impact level."""
has_driver_instance.navigate_to(f"{base_url}/accessibility.html")
results = has_driver_instance.axe_eval()
# Get violations with at least moderate impact
moderate_violations = results.violations_with_impact_of_at_least("moderate")
all_violations = results.violations()
# moderate+ violations should be subset of all violations
assert len(moderate_violations) <= len(all_violations)
assert isinstance(moderate_violations, list)
def test_axe_eval_with_axe_skip(self, driver, base_url):
"""Test that axe_eval returns NullAxeResults when axe_skip is True."""
# Create a test instance with axe_skip=True
class TestHasDriverSkipAxe(TestHasDriverImpl):
axe_skip = True
instance = TestHasDriverSkipAxe(driver)
instance.navigate_to(f"{base_url}/accessibility.html")
results = instance.axe_eval()
# Should return NullAxeResults which has no violations
violations = results.violations()
assert len(violations) == 0
# All assertions should pass silently with NullAxeResults
results.assert_passes("any-rule")
results.assert_does_not_violate("any-rule")
results.assert_no_violations_with_impact_of_at_least("critical")
+78
View File
@@ -0,0 +1,78 @@
"""Test server infrastructure for running selenium tests locally."""
import http.server
import socketserver
import threading
from pathlib import Path
from typing import Optional
class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
"""Custom request handler for serving test HTML files."""
def __init__(self, *args, directory: Optional[str] = None, **kwargs):
"""Initialize handler with custom directory."""
super().__init__(*args, directory=directory, **kwargs)
def log_message(self, format: str, *args):
"""Suppress log messages during tests."""
pass
class TestHTTPServer:
"""Simple HTTP server for serving test HTML pages."""
def __init__(self, port: int = 0, directory: Optional[Path] = None):
"""
Initialize test HTTP server.
Args:
port: Port to bind to (0 for random available port)
directory: Directory to serve files from
"""
self.port = port
self.directory = directory or Path(__file__).parent / "fixtures"
self.server: Optional[socketserver.TCPServer] = None
self.thread: Optional[threading.Thread] = None
def start(self):
"""Start the HTTP server in a background thread."""
def handler(*args, **kwargs):
return TestHTTPRequestHandler(*args, directory=str(self.directory), **kwargs)
self.server = socketserver.TCPServer(("localhost", self.port), handler)
self.port = self.server.server_address[1] # Get actual port if 0 was specified
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
self.thread.start()
def stop(self):
"""Stop the HTTP server."""
if self.server:
self.server.shutdown()
self.server.server_close()
if self.thread:
self.thread.join(timeout=5)
def get_url(self, path: str = "") -> str:
"""
Get full URL for a given path.
Args:
path: Path relative to served directory
Returns:
Full URL including protocol, host, port, and path
"""
path = path.lstrip("/")
return f"http://localhost:{self.port}/{path}"
def __enter__(self):
"""Context manager entry."""
self.start()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.stop()