migrate US-011: test_visualizations.py + shadow DOM support

Add PlaywrightShadowRoot class and shadow_root property to
PlaywrightElement, matching Selenium WebElement.shadow_root API.
Remove @selenium_only from 3 visualization tests. All tests skip
due to missing viz plugins on external server, not Playwright issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John Chilton
2026-02-12 14:55:55 -05:00
co-authored by Claude Opus 4.6
parent 7a4208f893
commit d705d47293
2 changed files with 44 additions and 61 deletions
+44 -57
View File
@@ -10,12 +10,34 @@ from typing import (
TYPE_CHECKING,
)
from playwright.sync_api import ElementHandle
from playwright.sync_api import (
ElementHandle,
JSHandle,
)
if TYPE_CHECKING:
from .has_playwright_driver import HasPlaywrightDriver
from .web_element_protocol import WebElementProtocol
class PlaywrightShadowRoot:
"""Wrapper around a shadow root JSHandle that provides find_element support."""
def __init__(self, shadow_root_handle: JSHandle, driver: "HasPlaywrightDriver"):
self._shadow_root = shadow_root_handle
self._driver = driver
def find_element(self, by: str = "id", value: Optional[str] = None) -> "WebElementProtocol":
if value is None:
raise ValueError("value parameter is required")
selector = self._driver._selenium_locator_to_playwright_selector(by, value)
result_handle = self._shadow_root.evaluate_handle(f"root => root.querySelector('{selector}')")
element_handle = result_handle.as_element()
if element_handle:
return PlaywrightElement(element_handle, self._driver)
raise Exception(f"No element found in shadow root with {by}='{value}'")
class PlaywrightElement:
"""
Wrapper around Playwright ElementHandle that implements WebElementProtocol.
@@ -24,14 +46,7 @@ class PlaywrightElement:
WebElement API, allowing the same code to work with both backends.
"""
def __init__(self, element_handle: ElementHandle, driver):
"""
Initialize PlaywrightElement wrapper.
Args:
element_handle: The Playwright ElementHandle to wrap
driver: The HasPlaywrightDriver instance (for find_element operations)
"""
def __init__(self, element_handle: ElementHandle, driver: "HasPlaywrightDriver"):
self._element = element_handle
self._driver = driver
@@ -119,57 +134,29 @@ class PlaywrightElement:
"""
self._element.evaluate("(el) => el.form ? el.form.submit() : el.submit()")
@property
def shadow_root(self) -> PlaywrightShadowRoot:
"""Access the shadow root of this element, matching Selenium's WebElement.shadow_root."""
handle = self._element.evaluate_handle("el => el.shadowRoot")
return PlaywrightShadowRoot(handle, self._driver)
def find_element(self, by: str = "id", value: Optional[str] = None) -> "WebElementProtocol":
"""
Find a child element within this element.
Delegates to the driver's element finding logic.
Args:
by: The locator strategy (e.g., "id", "css selector")
value: The locator value
Returns:
A PlaywrightElement wrapping the found ElementHandle
"""
# Convert the locator to Playwright selector
from .has_playwright_driver import HasPlaywrightDriver
if isinstance(self._driver, HasPlaywrightDriver):
if value is None:
raise ValueError("value parameter is required")
selector = self._driver._selenium_locator_to_playwright_selector(by, value)
# Use ElementHandle's query_selector to find within this element
found_element = self._element.query_selector(selector)
if found_element:
return PlaywrightElement(found_element, self._driver)
raise Exception(f"No element found with {by}='{value}'")
raise NotImplementedError("find_element within element not yet fully implemented")
"""Find a child element within this element."""
if value is None:
raise ValueError("value parameter is required")
selector = self._driver._selenium_locator_to_playwright_selector(by, value)
found_element = self._element.query_selector(selector)
if found_element:
return PlaywrightElement(found_element, self._driver)
raise Exception(f"No element found with {by}='{value}'")
def find_elements(self, by: str = "id", value: Optional[str] = None) -> list["WebElementProtocol"]:
"""
Find all child elements matching the locator within this element.
Delegates to the driver's element finding logic.
Args:
by: The locator strategy (e.g., "css selector", "xpath")
value: The locator value
Returns:
List of PlaywrightElement instances
"""
from .has_playwright_driver import HasPlaywrightDriver
if isinstance(self._driver, HasPlaywrightDriver):
if value is None:
raise ValueError("value parameter is required")
selector = self._driver._selenium_locator_to_playwright_selector(by, value)
found_elements = self._element.query_selector_all(selector)
return [PlaywrightElement(elem, self._driver) for elem in found_elements]
raise NotImplementedError("find_elements within element not yet fully implemented")
"""Find all child elements matching the locator within this element."""
if value is None:
raise ValueError("value parameter is required")
selector = self._driver._selenium_locator_to_playwright_selector(by, value)
found_elements = self._element.query_selector_all(selector)
return [PlaywrightElement(elem, self._driver) for elem in found_elements]
def content_frame(self):
"""
@@ -4,7 +4,6 @@ from galaxy_test.base.populators import (
)
from .framework import (
managed_history,
selenium_only,
selenium_test,
SeleniumTestCase,
)
@@ -32,7 +31,6 @@ class TestVisualizationsAnonymous(SeleniumTestCase):
self.wait_for_selector("#image-annotate")
self.screenshot("visualization_plugin_charts_image_annotate")
@selenium_only("Not yet migrated to support Playwright backend - need to implement shadow DOM absractions")
@selenium_test
@skip_without_visualization_plugin("tabulator")
def test_charts_tabulator(self):
@@ -47,7 +45,6 @@ class TestVisualizationsAnonymous(SeleniumTestCase):
self.wait_for_selector(".tabulator-table")
self.screenshot("visualization_plugin_tabulator_landing")
@selenium_only("Not yet migrated to support Playwright backend")
@selenium_test
@skip_without_visualization_plugin("h5web")
def test_charts_h5web(self):
@@ -65,7 +62,6 @@ class TestVisualizationsAnonymous(SeleniumTestCase):
class TestVisualizations(SeleniumTestCase):
ensure_registered = True
@selenium_only("Not yet migrated to support Playwright backend - need to implement shadow DOM absractions")
@selenium_test
@managed_history
@skip_without_visualization_plugin("igv")