mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
fix: Enable Playwright migration with graceful Selenium fallback (#35063)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,17 @@ from superset.utils.webdriver import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Import Playwright availability and install message
|
||||
try:
|
||||
from superset.utils.webdriver import (
|
||||
PLAYWRIGHT_AVAILABLE,
|
||||
PLAYWRIGHT_INSTALL_MESSAGE,
|
||||
)
|
||||
except ImportError:
|
||||
PLAYWRIGHT_AVAILABLE = False
|
||||
PLAYWRIGHT_INSTALL_MESSAGE = "Playwright module not found"
|
||||
|
||||
|
||||
DEFAULT_SCREENSHOT_WINDOW_SIZE = 800, 600
|
||||
DEFAULT_SCREENSHOT_THUMBNAIL_SIZE = 400, 300
|
||||
DEFAULT_CHART_WINDOW_SIZE = DEFAULT_CHART_THUMBNAIL_SIZE = 800, 600
|
||||
@@ -169,7 +180,19 @@ class BaseScreenshot:
|
||||
def driver(self, window_size: WindowSize | None = None) -> WebDriver:
|
||||
window_size = window_size or self.window_size
|
||||
if feature_flag_manager.is_feature_enabled("PLAYWRIGHT_REPORTS_AND_THUMBNAILS"):
|
||||
return WebDriverPlaywright(self.driver_type, window_size)
|
||||
# Try to use Playwright if available (supports WebGL/DeckGL, unlike Cypress)
|
||||
if PLAYWRIGHT_AVAILABLE:
|
||||
return WebDriverPlaywright(self.driver_type, window_size)
|
||||
|
||||
# Playwright not available, falling back to Selenium
|
||||
logger.info(
|
||||
"PLAYWRIGHT_REPORTS_AND_THUMBNAILS enabled but Playwright not "
|
||||
"installed. Falling back to Selenium (WebGL/Canvas charts may "
|
||||
"not render correctly). %s",
|
||||
PLAYWRIGHT_INSTALL_MESSAGE,
|
||||
)
|
||||
|
||||
# Use Selenium as default/fallback
|
||||
return WebDriverSelenium(self.driver_type, window_size)
|
||||
|
||||
def get_screenshot(
|
||||
|
||||
@@ -45,6 +45,13 @@ from superset.utils.screenshot_utils import take_tiled_screenshot
|
||||
WindowSize = tuple[int, int]
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Installation message for missing Playwright (Cypress doesn't work with DeckGL)
|
||||
PLAYWRIGHT_INSTALL_MESSAGE = (
|
||||
"To complete the migration from Cypress "
|
||||
"and enable WebGL/DeckGL screenshot support, install Playwright with: "
|
||||
"pip install playwright && playwright install chromium"
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
@@ -71,6 +78,67 @@ except ImportError:
|
||||
sync_playwright = None
|
||||
|
||||
|
||||
def check_playwright_availability() -> bool:
|
||||
"""
|
||||
Lightweight check for Playwright availability.
|
||||
|
||||
First checks if browser binary exists, falls back to launch test if needed.
|
||||
"""
|
||||
if sync_playwright is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
# First try lightweight check - just verify executable exists
|
||||
try:
|
||||
executable_path = p.chromium.executable_path
|
||||
if executable_path:
|
||||
return True
|
||||
except Exception:
|
||||
# Fall back to full launch test if executable_path fails
|
||||
logger.debug(
|
||||
"Executable path check failed, falling back to launch test"
|
||||
)
|
||||
|
||||
# Fallback: actually launch browser to ensure it works
|
||||
browser = p.chromium.launch(headless=True)
|
||||
browser.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Playwright module is installed but browser launch failed. "
|
||||
"Run 'playwright install chromium' to install browser binaries. "
|
||||
"Error: %s",
|
||||
str(e),
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
PLAYWRIGHT_AVAILABLE = check_playwright_availability()
|
||||
|
||||
|
||||
def validate_webdriver_config() -> dict[str, Any]:
|
||||
"""
|
||||
Validate webdriver configuration and dependencies.
|
||||
|
||||
Used to check migration status from Cypress to Playwright.
|
||||
Returns a dictionary with the status of available webdrivers
|
||||
and feature flags.
|
||||
"""
|
||||
from superset import feature_flag_manager
|
||||
|
||||
return {
|
||||
"selenium_available": True, # Always available as required dependency
|
||||
"playwright_available": PLAYWRIGHT_AVAILABLE,
|
||||
"playwright_feature_enabled": feature_flag_manager.is_feature_enabled(
|
||||
"PLAYWRIGHT_REPORTS_AND_THUMBNAILS"
|
||||
),
|
||||
"recommended_action": (
|
||||
PLAYWRIGHT_INSTALL_MESSAGE if not PLAYWRIGHT_AVAILABLE else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DashboardStandaloneMode(Enum):
|
||||
HIDE_NAV = 1
|
||||
HIDE_NAV_AND_TITLE = 2
|
||||
@@ -151,6 +219,15 @@ class WebDriverPlaywright(WebDriverProxy):
|
||||
def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # noqa: C901
|
||||
self, url: str, element_name: str, user: User
|
||||
) -> bytes | None:
|
||||
if not PLAYWRIGHT_AVAILABLE:
|
||||
logger.info(
|
||||
"Playwright not available - falling back to Selenium. "
|
||||
"Note: WebGL/Canvas charts may not render correctly with Selenium. "
|
||||
"%s",
|
||||
PLAYWRIGHT_INSTALL_MESSAGE,
|
||||
)
|
||||
return None
|
||||
|
||||
with sync_playwright() as playwright:
|
||||
browser_args = app.config["WEBDRIVER_OPTION_ARGS"]
|
||||
browser = playwright.chromium.launch(args=browser_args)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
# pylint: disable=import-outside-toplevel, unused-argument
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
@@ -25,6 +25,8 @@ from pytest_mock import MockerFixture
|
||||
from superset.utils.hashing import md5_sha_from_dict
|
||||
from superset.utils.screenshots import (
|
||||
BaseScreenshot,
|
||||
ChartScreenshot,
|
||||
DashboardScreenshot,
|
||||
ScreenshotCachePayload,
|
||||
ScreenshotCachePayloadType,
|
||||
)
|
||||
@@ -239,3 +241,150 @@ class TestScreenshotCachePayloadGetImage:
|
||||
|
||||
# Should be different BytesIO instances
|
||||
assert result1 is not result2
|
||||
|
||||
|
||||
class TestBaseScreenshotDriverFallback:
|
||||
"""Test BaseScreenshot.driver() fallback logic for Playwright migration."""
|
||||
|
||||
@patch("superset.utils.screenshots.PLAYWRIGHT_AVAILABLE", True)
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
def test_driver_returns_playwright_when_feature_enabled_and_available(
|
||||
self, mock_feature_flag, screenshot_obj
|
||||
):
|
||||
"""Test driver() returns WebDriverPlaywright when enabled and available."""
|
||||
mock_feature_flag.return_value = True
|
||||
|
||||
driver = screenshot_obj.driver()
|
||||
|
||||
assert driver.__class__.__name__ == "WebDriverPlaywright"
|
||||
mock_feature_flag.assert_called_once_with("PLAYWRIGHT_REPORTS_AND_THUMBNAILS")
|
||||
|
||||
@patch("superset.utils.screenshots.logger")
|
||||
@patch("superset.utils.screenshots.PLAYWRIGHT_AVAILABLE", False)
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
def test_driver_falls_back_to_selenium_when_playwright_unavailable(
|
||||
self, mock_feature_flag, mock_logger, screenshot_obj
|
||||
):
|
||||
"""Test driver() falls back to Selenium when Playwright unavailable."""
|
||||
mock_feature_flag.return_value = True
|
||||
|
||||
driver = screenshot_obj.driver()
|
||||
|
||||
assert driver.__class__.__name__ == "WebDriverSelenium"
|
||||
# Should log the fallback message
|
||||
mock_logger.info.assert_called_once()
|
||||
log_call = mock_logger.info.call_args[0][0]
|
||||
assert (
|
||||
"PLAYWRIGHT_REPORTS_AND_THUMBNAILS enabled but Playwright not installed"
|
||||
in log_call
|
||||
)
|
||||
assert "Falling back to Selenium" in log_call
|
||||
assert "WebGL/Canvas charts may not render correctly" in log_call
|
||||
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
def test_driver_uses_selenium_when_feature_flag_disabled(
|
||||
self, mock_feature_flag, screenshot_obj
|
||||
):
|
||||
"""Test driver() uses Selenium when feature flag disabled."""
|
||||
mock_feature_flag.return_value = False
|
||||
|
||||
driver = screenshot_obj.driver()
|
||||
|
||||
assert driver.__class__.__name__ == "WebDriverSelenium"
|
||||
mock_feature_flag.assert_called_once_with("PLAYWRIGHT_REPORTS_AND_THUMBNAILS")
|
||||
|
||||
@patch("superset.utils.screenshots.PLAYWRIGHT_AVAILABLE", True)
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
def test_driver_passes_window_size_to_playwright(
|
||||
self, mock_feature_flag, screenshot_obj
|
||||
):
|
||||
"""Test driver() passes window_size parameter to WebDriverPlaywright."""
|
||||
mock_feature_flag.return_value = True
|
||||
custom_window_size = (1200, 800)
|
||||
|
||||
driver = screenshot_obj.driver(window_size=custom_window_size)
|
||||
|
||||
assert driver._window == custom_window_size
|
||||
assert driver.__class__.__name__ == "WebDriverPlaywright"
|
||||
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
def test_driver_passes_window_size_to_selenium(
|
||||
self, mock_feature_flag, screenshot_obj
|
||||
):
|
||||
"""Test driver() passes window_size parameter to WebDriverSelenium."""
|
||||
mock_feature_flag.return_value = False
|
||||
custom_window_size = (1200, 800)
|
||||
|
||||
driver = screenshot_obj.driver(window_size=custom_window_size)
|
||||
|
||||
assert driver._window == custom_window_size
|
||||
assert driver.__class__.__name__ == "WebDriverSelenium"
|
||||
|
||||
@patch("superset.utils.screenshots.PLAYWRIGHT_AVAILABLE", True)
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
def test_driver_uses_default_window_size_when_none_provided(
|
||||
self, mock_feature_flag, screenshot_obj
|
||||
):
|
||||
"""Test driver() uses screenshot object's window_size when none provided."""
|
||||
mock_feature_flag.return_value = True
|
||||
|
||||
driver = screenshot_obj.driver()
|
||||
|
||||
assert driver._window == screenshot_obj.window_size
|
||||
assert driver.__class__.__name__ == "WebDriverPlaywright"
|
||||
|
||||
|
||||
class TestScreenshotSubclassesDriverBehavior:
|
||||
"""Test ChartScreenshot and DashboardScreenshot inherit driver behavior."""
|
||||
|
||||
@patch("superset.utils.screenshots.PLAYWRIGHT_AVAILABLE", True)
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
def test_chart_screenshot_uses_playwright_when_enabled(self, mock_feature_flag):
|
||||
"""Test ChartScreenshot uses Playwright when feature enabled."""
|
||||
mock_feature_flag.return_value = True
|
||||
|
||||
chart_screenshot = ChartScreenshot("http://example.com/chart", "digest")
|
||||
driver = chart_screenshot.driver()
|
||||
|
||||
assert driver.__class__.__name__ == "WebDriverPlaywright"
|
||||
assert driver._window == chart_screenshot.window_size
|
||||
|
||||
@patch("superset.utils.screenshots.logger")
|
||||
@patch("superset.utils.screenshots.PLAYWRIGHT_AVAILABLE", False)
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
def test_dashboard_screenshot_falls_back_to_selenium(
|
||||
self, mock_feature_flag, mock_logger
|
||||
):
|
||||
"""Test DashboardScreenshot falls back to Selenium if no Playwright."""
|
||||
mock_feature_flag.return_value = True
|
||||
|
||||
dashboard_screenshot = DashboardScreenshot(
|
||||
"http://example.com/dashboard", "digest"
|
||||
)
|
||||
driver = dashboard_screenshot.driver()
|
||||
|
||||
assert driver.__class__.__name__ == "WebDriverSelenium"
|
||||
assert driver._window == dashboard_screenshot.window_size
|
||||
|
||||
# Should log the fallback message
|
||||
mock_logger.info.assert_called_once()
|
||||
|
||||
@patch("superset.utils.screenshots.PLAYWRIGHT_AVAILABLE", True)
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
def test_custom_window_size_passed_to_driver(self, mock_feature_flag):
|
||||
"""Test custom window size is passed correctly to driver."""
|
||||
mock_feature_flag.return_value = True
|
||||
custom_window_size = (1920, 1080)
|
||||
custom_thumb_size = (960, 540)
|
||||
|
||||
chart_screenshot = ChartScreenshot(
|
||||
"http://example.com/chart",
|
||||
"digest",
|
||||
window_size=custom_window_size,
|
||||
thumb_size=custom_thumb_size,
|
||||
)
|
||||
|
||||
driver = chart_screenshot.driver()
|
||||
|
||||
assert driver._window == custom_window_size
|
||||
assert chart_screenshot.thumb_size == custom_thumb_size
|
||||
|
||||
100
tests/unit_tests/utils/test_playwright_migration_working.py
Normal file
100
tests/unit_tests/utils/test_playwright_migration_working.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Working tests for Playwright migration functionality.
|
||||
These tests demonstrate the core functionality works correctly.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from superset.utils.webdriver import (
|
||||
PLAYWRIGHT_AVAILABLE,
|
||||
validate_webdriver_config,
|
||||
)
|
||||
|
||||
|
||||
class TestPlaywrightMigrationCore:
|
||||
"""Core tests that demonstrate working Playwright migration functionality."""
|
||||
|
||||
def test_playwright_available_is_boolean(self):
|
||||
"""Test that PLAYWRIGHT_AVAILABLE is always a boolean."""
|
||||
assert isinstance(PLAYWRIGHT_AVAILABLE, bool)
|
||||
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
def test_validate_webdriver_config_structure(self, mock_feature_flag):
|
||||
"""Test that validate_webdriver_config returns correct structure."""
|
||||
mock_feature_flag.return_value = True
|
||||
|
||||
result = validate_webdriver_config()
|
||||
|
||||
# Check required keys exist
|
||||
required_keys = [
|
||||
"selenium_available",
|
||||
"playwright_available",
|
||||
"playwright_feature_enabled",
|
||||
"recommended_action",
|
||||
]
|
||||
for key in required_keys:
|
||||
assert key in result
|
||||
|
||||
# Check data types
|
||||
assert isinstance(result["selenium_available"], bool)
|
||||
assert isinstance(result["playwright_available"], bool)
|
||||
assert isinstance(result["playwright_feature_enabled"], bool)
|
||||
assert result["recommended_action"] is None or isinstance(
|
||||
result["recommended_action"], str
|
||||
)
|
||||
|
||||
# Selenium should always be available
|
||||
assert result["selenium_available"] is True
|
||||
|
||||
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", False)
|
||||
@patch("superset.utils.webdriver.logger")
|
||||
def test_webdriver_playwright_fallback_logging(self, mock_logger):
|
||||
"""Test that WebDriverPlaywright logs fallback correctly."""
|
||||
from superset.utils.webdriver import WebDriverPlaywright
|
||||
|
||||
mock_user = MagicMock()
|
||||
mock_user.username = "test_user"
|
||||
|
||||
driver = WebDriverPlaywright("chrome")
|
||||
result = driver.get_screenshot("http://example.com", "test-element", mock_user)
|
||||
|
||||
# Should return None when unavailable
|
||||
assert result is None
|
||||
|
||||
# Should log the fallback message
|
||||
mock_logger.info.assert_called_once()
|
||||
log_call = mock_logger.info.call_args[0][0]
|
||||
assert "Playwright not available" in log_call
|
||||
assert "falling back to Selenium" in log_call
|
||||
|
||||
def test_webdriver_classes_exist(self):
|
||||
"""Test that both WebDriver classes can be imported."""
|
||||
from superset.utils.webdriver import WebDriverPlaywright, WebDriverSelenium
|
||||
|
||||
# Should be able to create instances without errors
|
||||
playwright_driver = WebDriverPlaywright("chrome")
|
||||
selenium_driver = WebDriverSelenium("chrome")
|
||||
|
||||
assert playwright_driver is not None
|
||||
assert selenium_driver is not None
|
||||
|
||||
# Should have required attributes
|
||||
assert hasattr(playwright_driver, "_driver_type")
|
||||
assert hasattr(selenium_driver, "_driver_type")
|
||||
@@ -15,11 +15,18 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock, patch, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from superset.utils.webdriver import WebDriverSelenium
|
||||
from superset.utils.webdriver import (
|
||||
check_playwright_availability,
|
||||
PLAYWRIGHT_AVAILABLE,
|
||||
PLAYWRIGHT_INSTALL_MESSAGE,
|
||||
validate_webdriver_config,
|
||||
WebDriverPlaywright,
|
||||
WebDriverSelenium,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -265,3 +272,424 @@ class TestWebDriverSelenium:
|
||||
|
||||
# Should create driver without errors
|
||||
mock_driver_class.assert_called_once()
|
||||
|
||||
|
||||
class TestPlaywrightAvailabilityCheck:
|
||||
"""Test comprehensive Playwright availability checking."""
|
||||
|
||||
@patch("superset.utils.webdriver.sync_playwright", None)
|
||||
def test_check_playwright_availability_returns_false_when_module_not_available(
|
||||
self,
|
||||
):
|
||||
"""Test check_playwright_availability returns False when no module."""
|
||||
result = check_playwright_availability()
|
||||
assert result is False
|
||||
|
||||
@patch("superset.utils.webdriver.sync_playwright")
|
||||
@patch("superset.utils.webdriver.logger")
|
||||
def test_check_playwright_availability_uses_lightweight_check(
|
||||
self, mock_logger, mock_sync_playwright
|
||||
):
|
||||
"""Test check_playwright_availability uses executable_path first."""
|
||||
# Setup mocks for successful executable path check
|
||||
mock_playwright_instance = MagicMock()
|
||||
mock_sync_playwright.return_value.__enter__.return_value = (
|
||||
mock_playwright_instance
|
||||
)
|
||||
mock_playwright_instance.chromium.executable_path = "/path/to/chromium"
|
||||
|
||||
result = check_playwright_availability()
|
||||
|
||||
assert result is True
|
||||
# Should not launch browser if executable_path works
|
||||
mock_playwright_instance.chromium.launch.assert_not_called()
|
||||
|
||||
@patch("superset.utils.webdriver.sync_playwright")
|
||||
@patch("superset.utils.webdriver.logger")
|
||||
def test_check_playwright_availability_falls_back_to_launch(
|
||||
self, mock_logger, mock_sync_playwright
|
||||
):
|
||||
"""Test check_playwright_availability falls back to browser launch."""
|
||||
# Setup mocks where executable_path fails but launch succeeds
|
||||
mock_playwright_instance = MagicMock()
|
||||
mock_browser = MagicMock()
|
||||
|
||||
mock_sync_playwright.return_value.__enter__.return_value = (
|
||||
mock_playwright_instance
|
||||
)
|
||||
# Make executable_path raise exception
|
||||
type(mock_playwright_instance.chromium).executable_path = PropertyMock(
|
||||
side_effect=Exception("executable_path failed")
|
||||
)
|
||||
mock_playwright_instance.chromium.launch.return_value = mock_browser
|
||||
|
||||
result = check_playwright_availability()
|
||||
|
||||
assert result is True
|
||||
# Should fall back to browser launch
|
||||
mock_playwright_instance.chromium.launch.assert_called_once_with(headless=True)
|
||||
mock_browser.close.assert_called_once()
|
||||
|
||||
@patch("superset.utils.webdriver.sync_playwright")
|
||||
@patch("superset.utils.webdriver.logger")
|
||||
def test_check_playwright_availability_handles_browser_launch_failure(
|
||||
self, mock_logger, mock_sync_playwright
|
||||
):
|
||||
"""Test check_playwright_availability handles browser launch failures."""
|
||||
# Setup mocks to raise exception on browser launch
|
||||
mock_playwright_instance = MagicMock()
|
||||
mock_sync_playwright.return_value.__enter__.return_value = (
|
||||
mock_playwright_instance
|
||||
)
|
||||
# Mock executable_path to raise exception to force fallback to launch test
|
||||
type(mock_playwright_instance.chromium).executable_path = PropertyMock(
|
||||
side_effect=Exception("Executable path check failed")
|
||||
)
|
||||
mock_playwright_instance.chromium.launch.side_effect = Exception(
|
||||
"Browser binaries not installed"
|
||||
)
|
||||
|
||||
result = check_playwright_availability()
|
||||
|
||||
assert result is False
|
||||
mock_logger.warning.assert_called_once()
|
||||
warning_call = mock_logger.warning.call_args[0][0]
|
||||
assert (
|
||||
"Playwright module is installed but browser launch failed" in warning_call
|
||||
)
|
||||
assert "playwright install chromium" in warning_call
|
||||
|
||||
@patch("superset.utils.webdriver.sync_playwright")
|
||||
@patch("superset.utils.webdriver.logger")
|
||||
def test_check_playwright_availability_handles_context_manager_error(
|
||||
self, mock_logger, mock_sync_playwright
|
||||
):
|
||||
"""Test check_playwright_availability handles context manager errors."""
|
||||
# Setup mock to raise exception when entering context
|
||||
mock_sync_playwright.return_value.__enter__.side_effect = Exception(
|
||||
"Context error"
|
||||
)
|
||||
|
||||
result = check_playwright_availability()
|
||||
|
||||
assert result is False
|
||||
mock_logger.warning.assert_called_once()
|
||||
|
||||
|
||||
class TestPlaywrightMigrationSupport:
|
||||
"""Test Playwright migration and fallback functionality."""
|
||||
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
def test_validate_webdriver_config_all_available(self, mock_feature_flag):
|
||||
"""Test validate_webdriver_config when all dependencies available."""
|
||||
mock_feature_flag.return_value = True
|
||||
|
||||
result = validate_webdriver_config()
|
||||
|
||||
assert result["selenium_available"] is True
|
||||
assert isinstance(result["playwright_available"], bool)
|
||||
assert isinstance(result["playwright_feature_enabled"], bool)
|
||||
|
||||
if result["playwright_available"]:
|
||||
assert result["recommended_action"] is None
|
||||
else:
|
||||
assert result["recommended_action"] == PLAYWRIGHT_INSTALL_MESSAGE
|
||||
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
def test_validate_webdriver_config_feature_flag_disabled(self, mock_feature_flag):
|
||||
"""Test validate_webdriver_config when feature flag is disabled."""
|
||||
mock_feature_flag.return_value = False
|
||||
|
||||
result = validate_webdriver_config()
|
||||
|
||||
assert result["selenium_available"] is True
|
||||
assert result["playwright_feature_enabled"] is False
|
||||
|
||||
@patch("superset.extensions.feature_flag_manager.is_feature_enabled")
|
||||
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", False)
|
||||
def test_validate_webdriver_config_playwright_unavailable(self, mock_feature_flag):
|
||||
"""Test validate_webdriver_config when Playwright not available."""
|
||||
mock_feature_flag.return_value = True
|
||||
|
||||
result = validate_webdriver_config()
|
||||
|
||||
assert result["selenium_available"] is True
|
||||
assert result["playwright_available"] is False
|
||||
assert result["playwright_feature_enabled"] is True
|
||||
assert result["recommended_action"] == PLAYWRIGHT_INSTALL_MESSAGE
|
||||
|
||||
|
||||
class TestWebDriverPlaywrightFallback:
|
||||
"""Test WebDriverPlaywright fallback behavior when unavailable."""
|
||||
|
||||
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", False)
|
||||
@patch("superset.utils.webdriver.logger")
|
||||
def test_get_screenshot_returns_none_when_unavailable(self, mock_logger, mock_app):
|
||||
"""Test WebDriverPlaywright.get_screenshot returns None when unavailable."""
|
||||
mock_user = MagicMock()
|
||||
mock_user.username = "test_user"
|
||||
|
||||
driver = WebDriverPlaywright("chrome")
|
||||
result = driver.get_screenshot("http://example.com", "test-element", mock_user)
|
||||
|
||||
assert result is None
|
||||
|
||||
# Verify warning log was called with correct message
|
||||
mock_logger.info.assert_called_once()
|
||||
log_call = mock_logger.info.call_args[0][0]
|
||||
assert "Playwright not available" in log_call
|
||||
assert "falling back to Selenium" in log_call
|
||||
assert "WebGL/Canvas charts may not render correctly" in log_call
|
||||
# Check the substituted parameter
|
||||
assert mock_logger.info.call_args[0][1] == PLAYWRIGHT_INSTALL_MESSAGE
|
||||
|
||||
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", True)
|
||||
@patch("superset.utils.webdriver.sync_playwright")
|
||||
@patch("superset.utils.webdriver.app")
|
||||
def test_get_screenshot_works_when_available(self, mock_app, mock_sync_playwright):
|
||||
"""Test WebDriverPlaywright.get_screenshot works when Playwright available."""
|
||||
# Setup mocks
|
||||
mock_user = MagicMock()
|
||||
mock_user.username = "test_user"
|
||||
|
||||
mock_app.config = {
|
||||
"WEBDRIVER_OPTION_ARGS": [],
|
||||
"WEBDRIVER_WINDOW": {"pixel_density": 1},
|
||||
"SCREENSHOT_PLAYWRIGHT_DEFAULT_TIMEOUT": 30000,
|
||||
"SCREENSHOT_PLAYWRIGHT_WAIT_EVENT": "networkidle",
|
||||
"SCREENSHOT_SELENIUM_HEADSTART": 5,
|
||||
"SCREENSHOT_SELENIUM_ANIMATION_WAIT": 1,
|
||||
"SCREENSHOT_REPLACE_UNEXPECTED_ERRORS": False,
|
||||
"SCREENSHOT_TILED_ENABLED": False,
|
||||
"SCREENSHOT_LOCATE_WAIT": 10,
|
||||
"SCREENSHOT_LOAD_WAIT": 10,
|
||||
"SCREENSHOT_WAIT_FOR_ERROR_MODAL_VISIBLE": 10,
|
||||
"SCREENSHOT_WAIT_FOR_ERROR_MODAL_INVISIBLE": 10,
|
||||
}
|
||||
|
||||
# Setup playwright mocks
|
||||
mock_playwright_instance = MagicMock()
|
||||
mock_browser = MagicMock()
|
||||
mock_context = MagicMock()
|
||||
mock_page = MagicMock()
|
||||
mock_element = MagicMock()
|
||||
|
||||
mock_sync_playwright.return_value.__enter__.return_value = (
|
||||
mock_playwright_instance
|
||||
)
|
||||
mock_playwright_instance.chromium.launch.return_value = mock_browser
|
||||
mock_browser.new_context.return_value = mock_context
|
||||
mock_context.new_page.return_value = mock_page
|
||||
mock_page.locator.return_value = mock_element
|
||||
mock_element.screenshot.return_value = b"fake_screenshot"
|
||||
|
||||
# Mock the auth method
|
||||
with patch.object(WebDriverPlaywright, "auth") as mock_auth:
|
||||
mock_auth.return_value = mock_context
|
||||
|
||||
driver = WebDriverPlaywright("chrome")
|
||||
result = driver.get_screenshot(
|
||||
"http://example.com", "test-element", mock_user
|
||||
)
|
||||
|
||||
assert result == b"fake_screenshot"
|
||||
mock_page.goto.assert_called_once_with(
|
||||
"http://example.com", wait_until="networkidle"
|
||||
)
|
||||
|
||||
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", True)
|
||||
@patch("superset.utils.webdriver.sync_playwright")
|
||||
@patch("superset.utils.webdriver.logger")
|
||||
def test_get_screenshot_handles_playwright_timeout(
|
||||
self, mock_logger, mock_sync_playwright
|
||||
):
|
||||
"""Test WebDriverPlaywright handles PlaywrightTimeout gracefully."""
|
||||
from superset.utils.webdriver import PlaywrightTimeout
|
||||
|
||||
mock_user = MagicMock()
|
||||
mock_user.username = "test_user"
|
||||
|
||||
# Setup playwright mocks to raise timeout
|
||||
mock_playwright_instance = MagicMock()
|
||||
mock_browser = MagicMock()
|
||||
mock_context = MagicMock()
|
||||
mock_page = MagicMock()
|
||||
|
||||
mock_sync_playwright.return_value.__enter__.return_value = (
|
||||
mock_playwright_instance
|
||||
)
|
||||
mock_playwright_instance.chromium.launch.return_value = mock_browser
|
||||
mock_browser.new_context.return_value = mock_context
|
||||
mock_context.new_page.return_value = mock_page
|
||||
mock_page.goto.side_effect = PlaywrightTimeout()
|
||||
|
||||
with patch("superset.utils.webdriver.app") as mock_app:
|
||||
mock_app.config = {
|
||||
"WEBDRIVER_OPTION_ARGS": [],
|
||||
"WEBDRIVER_WINDOW": {"pixel_density": 1},
|
||||
"SCREENSHOT_PLAYWRIGHT_DEFAULT_TIMEOUT": 30000,
|
||||
"SCREENSHOT_PLAYWRIGHT_WAIT_EVENT": "networkidle",
|
||||
"SCREENSHOT_SELENIUM_HEADSTART": 5,
|
||||
"SCREENSHOT_LOCATE_WAIT": 10,
|
||||
"SCREENSHOT_LOAD_WAIT": 10,
|
||||
"SCREENSHOT_WAIT_FOR_ERROR_MODAL_VISIBLE": 10,
|
||||
"SCREENSHOT_WAIT_FOR_ERROR_MODAL_INVISIBLE": 10,
|
||||
"SCREENSHOT_REPLACE_UNEXPECTED_ERRORS": True,
|
||||
"SCREENSHOT_TILED_ENABLED": False,
|
||||
}
|
||||
|
||||
with patch.object(WebDriverPlaywright, "auth") as mock_auth:
|
||||
mock_auth.return_value = mock_context
|
||||
|
||||
driver = WebDriverPlaywright("chrome")
|
||||
result = driver.get_screenshot(
|
||||
"http://example.com", "test-element", mock_user
|
||||
)
|
||||
|
||||
# Should handle timeout gracefully and return None
|
||||
assert result is None
|
||||
mock_logger.exception.assert_called()
|
||||
exception_call = mock_logger.exception.call_args[0][0]
|
||||
assert "Web event %s not detected" in exception_call
|
||||
|
||||
|
||||
class TestWebDriverConstantsWithImportError:
|
||||
"""Test module-level constants behavior with import errors."""
|
||||
|
||||
def test_playwright_constants_defined_when_import_fails(self):
|
||||
"""Test constants are properly defined even when Playwright import fails."""
|
||||
# These should be available even when playwright is not installed
|
||||
assert PLAYWRIGHT_INSTALL_MESSAGE is not None
|
||||
assert isinstance(PLAYWRIGHT_INSTALL_MESSAGE, str)
|
||||
|
||||
# PLAYWRIGHT_AVAILABLE should be boolean regardless of installation
|
||||
assert isinstance(PLAYWRIGHT_AVAILABLE, bool)
|
||||
|
||||
@patch("superset.utils.webdriver.sync_playwright", None)
|
||||
def test_dummy_classes_when_playwright_unavailable(self):
|
||||
"""Test that dummy classes are defined when Playwright unavailable."""
|
||||
# Force reimport to test ImportError path
|
||||
from importlib import reload
|
||||
|
||||
import superset.utils.webdriver as webdriver_module
|
||||
|
||||
# Mock the import to fail
|
||||
with patch.dict("sys.modules", {"playwright.sync_api": None}):
|
||||
reload(webdriver_module)
|
||||
|
||||
# Should have dummy classes defined
|
||||
assert hasattr(webdriver_module, "BrowserContext")
|
||||
assert hasattr(webdriver_module, "PlaywrightError")
|
||||
assert hasattr(webdriver_module, "PlaywrightTimeout")
|
||||
|
||||
|
||||
class TestWebDriverPlaywrightErrorHandling:
|
||||
"""Test error handling in WebDriverPlaywright methods."""
|
||||
|
||||
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", True)
|
||||
@patch("superset.utils.webdriver.sync_playwright")
|
||||
@patch("superset.utils.webdriver.logger")
|
||||
def test_find_unexpected_errors_handles_playwright_error(
|
||||
self, mock_logger, mock_sync_playwright
|
||||
):
|
||||
"""Test find_unexpected_errors handles PlaywrightError gracefully."""
|
||||
from superset.utils.webdriver import PlaywrightError
|
||||
|
||||
mock_page = MagicMock()
|
||||
mock_page.get_by_role.side_effect = PlaywrightError("Test error")
|
||||
|
||||
result = WebDriverPlaywright.find_unexpected_errors(mock_page)
|
||||
|
||||
assert result == []
|
||||
mock_logger.exception.assert_called_once_with(
|
||||
"Failed to capture unexpected errors"
|
||||
)
|
||||
|
||||
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", True)
|
||||
@patch("superset.utils.webdriver.sync_playwright")
|
||||
@patch("superset.utils.webdriver.logger")
|
||||
def test_find_unexpected_errors_processes_alerts(
|
||||
self, mock_logger, mock_sync_playwright
|
||||
):
|
||||
"""Test find_unexpected_errors processes alert elements correctly."""
|
||||
mock_page = MagicMock()
|
||||
mock_alert_div = MagicMock()
|
||||
mock_button = MagicMock()
|
||||
mock_modal_content = MagicMock()
|
||||
mock_modal_body = MagicMock()
|
||||
mock_close_button = MagicMock()
|
||||
|
||||
# Setup the mock chain
|
||||
mock_page.get_by_role.return_value.all.return_value = [mock_alert_div]
|
||||
mock_alert_div.get_by_role.return_value = mock_button
|
||||
mock_page.locator.side_effect = [
|
||||
mock_modal_content,
|
||||
mock_modal_body,
|
||||
mock_close_button,
|
||||
mock_modal_content,
|
||||
]
|
||||
mock_modal_body.text_content.return_value = "Error message"
|
||||
mock_modal_body.inner_html.return_value = "Error message"
|
||||
|
||||
result = WebDriverPlaywright.find_unexpected_errors(mock_page)
|
||||
|
||||
assert result == ["Error message"]
|
||||
mock_button.click.assert_called_once()
|
||||
mock_close_button.click.assert_called_once()
|
||||
|
||||
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", True)
|
||||
@patch("superset.utils.webdriver.sync_playwright")
|
||||
@patch("superset.utils.webdriver.logger")
|
||||
def test_get_screenshot_logs_multiple_timeouts(
|
||||
self, mock_logger, mock_sync_playwright
|
||||
):
|
||||
"""Test that multiple timeout scenarios are logged appropriately."""
|
||||
from superset.utils.webdriver import PlaywrightTimeout
|
||||
|
||||
mock_user = MagicMock()
|
||||
mock_user.username = "test_user"
|
||||
|
||||
# Setup mocks
|
||||
mock_playwright_instance = MagicMock()
|
||||
mock_browser = MagicMock()
|
||||
mock_context = MagicMock()
|
||||
mock_page = MagicMock()
|
||||
mock_element = MagicMock()
|
||||
|
||||
mock_sync_playwright.return_value.__enter__.return_value = (
|
||||
mock_playwright_instance
|
||||
)
|
||||
mock_playwright_instance.chromium.launch.return_value = mock_browser
|
||||
mock_browser.new_context.return_value = mock_context
|
||||
mock_context.new_page.return_value = mock_page
|
||||
|
||||
# Mock locator to raise timeout on element wait
|
||||
mock_page.locator.return_value = mock_element
|
||||
mock_element.wait_for.side_effect = PlaywrightTimeout()
|
||||
|
||||
with patch("superset.utils.webdriver.app") as mock_app:
|
||||
mock_app.config = {
|
||||
"WEBDRIVER_OPTION_ARGS": [],
|
||||
"WEBDRIVER_WINDOW": {"pixel_density": 1},
|
||||
"SCREENSHOT_PLAYWRIGHT_DEFAULT_TIMEOUT": 30000,
|
||||
"SCREENSHOT_PLAYWRIGHT_WAIT_EVENT": "networkidle",
|
||||
"SCREENSHOT_SELENIUM_HEADSTART": 5,
|
||||
"SCREENSHOT_LOCATE_WAIT": 10,
|
||||
"SCREENSHOT_LOAD_WAIT": 10,
|
||||
"SCREENSHOT_WAIT_FOR_ERROR_MODAL_VISIBLE": 10,
|
||||
"SCREENSHOT_WAIT_FOR_ERROR_MODAL_INVISIBLE": 10,
|
||||
"SCREENSHOT_REPLACE_UNEXPECTED_ERRORS": True,
|
||||
"SCREENSHOT_TILED_ENABLED": False,
|
||||
}
|
||||
|
||||
with patch.object(WebDriverPlaywright, "auth") as mock_auth:
|
||||
mock_auth.return_value = mock_context
|
||||
|
||||
driver = WebDriverPlaywright("chrome")
|
||||
result = driver.get_screenshot(
|
||||
"http://example.com", "test-element", mock_user
|
||||
)
|
||||
|
||||
assert result is None
|
||||
# Should log timeout for element wait
|
||||
assert mock_logger.exception.call_count >= 1
|
||||
|
||||
Reference in New Issue
Block a user