mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-01 15:37:32 +08:00
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:
+1
@@ -0,0 +1 @@
|
||||
../../../test/unit/selenium
|
||||
@@ -0,0 +1 @@
|
||||
"""Unit tests for galaxy.selenium package."""
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user