| # 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. |
| |
| from unittest.mock import MagicMock, patch, PropertyMock |
| |
| import pytest |
| |
| from superset.utils.webdriver import ( |
| check_playwright_availability, |
| PLAYWRIGHT_AVAILABLE, |
| PLAYWRIGHT_INSTALL_MESSAGE, |
| validate_webdriver_config, |
| WebDriverPlaywright, |
| WebDriverSelenium, |
| ) |
| |
| |
| @pytest.fixture |
| def mock_app(): |
| """Mock Flask app with webdriver configuration.""" |
| app = MagicMock() |
| app.config = { |
| "WEBDRIVER_TYPE": "chrome", |
| "WEBDRIVER_OPTION_ARGS": [], |
| "WEBDRIVER_CONFIGURATION": {}, |
| "SCREENSHOT_LOCATE_WAIT": 10, |
| "SCREENSHOT_LOAD_WAIT": 10, |
| } |
| return app |
| |
| |
| class TestWebDriverSelenium: |
| """Test WebDriverSelenium timeout handling for urllib3 2.x compatibility.""" |
| |
| @patch("superset.utils.webdriver.app") |
| @patch("superset.utils.webdriver.firefox") |
| @patch("superset.utils.webdriver.chrome") |
| def test_timeout_conversion_to_float( |
| self, mock_chrome, mock_firefox, mock_app_patch, mock_app |
| ): |
| """Test that timeout values are properly converted to float.""" |
| # Set up app mock to be used throughout |
| mock_app_patch.config = { |
| "WEBDRIVER_TYPE": "chrome", |
| "WEBDRIVER_OPTION_ARGS": [], |
| "SCREENSHOT_LOCATE_WAIT": 10, |
| "SCREENSHOT_LOAD_WAIT": 10, |
| "WEBDRIVER_WINDOW": {}, |
| "WEBDRIVER_CONFIGURATION": { |
| "timeout": "30", |
| "connect_timeout": "10.5", |
| "socket_timeout": 20, |
| "read_timeout": "15.0", |
| "command_executor_timeout": "25", |
| }, |
| } |
| |
| mock_driver_class = MagicMock() |
| mock_chrome.webdriver.WebDriver = mock_driver_class |
| mock_chrome.service.Service = MagicMock() |
| mock_options = MagicMock() |
| mock_options.add_argument = MagicMock() |
| mock_chrome.options.Options = MagicMock(return_value=mock_options) |
| |
| driver = WebDriverSelenium(driver_type="chrome") |
| driver.create() |
| |
| # Check that the driver was called with float timeout values |
| mock_driver_class.assert_called_once() |
| call_kwargs = mock_driver_class.call_args.kwargs |
| assert call_kwargs["timeout"] == 30.0 |
| assert call_kwargs["connect_timeout"] == 10.5 |
| assert call_kwargs["socket_timeout"] == 20.0 |
| assert call_kwargs["read_timeout"] == 15.0 |
| assert call_kwargs["command_executor_timeout"] == 25.0 |
| |
| @patch("superset.utils.webdriver.app") |
| @patch("superset.utils.webdriver.chrome") |
| def test_timeout_none_handling(self, mock_chrome, mock_app_patch, mock_app): |
| """Test that None, 'None', and 'null' timeout values are set to None.""" |
| mock_app_patch.config = { |
| "WEBDRIVER_TYPE": "chrome", |
| "WEBDRIVER_OPTION_ARGS": [], |
| "SCREENSHOT_LOCATE_WAIT": 10, |
| "SCREENSHOT_LOAD_WAIT": 10, |
| "WEBDRIVER_WINDOW": {}, |
| "WEBDRIVER_CONFIGURATION": { |
| "timeout": None, |
| "connect_timeout": "None", |
| "socket_timeout": "null", |
| }, |
| } |
| |
| mock_driver_class = MagicMock() |
| mock_chrome.webdriver.WebDriver = mock_driver_class |
| mock_chrome.service.Service = MagicMock() |
| mock_options = MagicMock() |
| mock_options.add_argument = MagicMock() |
| mock_chrome.options.Options = MagicMock(return_value=mock_options) |
| |
| driver = WebDriverSelenium(driver_type="chrome") |
| driver.create() |
| |
| # Check that None values are preserved |
| mock_driver_class.assert_called_once() |
| call_kwargs = mock_driver_class.call_args.kwargs |
| assert call_kwargs["timeout"] is None |
| assert call_kwargs["connect_timeout"] is None |
| assert call_kwargs["socket_timeout"] is None |
| |
| @patch("superset.utils.webdriver.app") |
| @patch("superset.utils.webdriver.chrome") |
| @patch("superset.utils.webdriver.logger") |
| def test_invalid_timeout_warning( |
| self, mock_logger, mock_chrome, mock_app_patch, mock_app |
| ): |
| """Test that invalid timeout values log warnings and are set to None.""" |
| mock_app_patch.config = { |
| "WEBDRIVER_TYPE": "chrome", |
| "WEBDRIVER_OPTION_ARGS": [], |
| "SCREENSHOT_LOCATE_WAIT": 10, |
| "SCREENSHOT_LOAD_WAIT": 10, |
| "WEBDRIVER_WINDOW": {}, |
| "WEBDRIVER_CONFIGURATION": { |
| "timeout": "invalid", |
| "connect_timeout": "not_a_number", |
| "Page_Load_Timeout": "abc123", # Test case-insensitive matching |
| }, |
| } |
| |
| mock_driver_class = MagicMock() |
| mock_chrome.webdriver.WebDriver = mock_driver_class |
| mock_chrome.service.Service = MagicMock() |
| mock_options = MagicMock() |
| mock_options.add_argument = MagicMock() |
| mock_chrome.options.Options = MagicMock(return_value=mock_options) |
| |
| driver = WebDriverSelenium(driver_type="chrome") |
| driver.create() |
| |
| # Check that invalid values are set to None |
| mock_driver_class.assert_called_once() |
| call_kwargs = mock_driver_class.call_args.kwargs |
| assert call_kwargs["timeout"] is None |
| assert call_kwargs["connect_timeout"] is None |
| assert call_kwargs["Page_Load_Timeout"] is None |
| |
| # Check that warnings were logged with lazy logging format |
| assert mock_logger.warning.call_count == 3 |
| mock_logger.warning.assert_any_call( |
| "Invalid timeout value for %s: %s, setting to None", "timeout", "invalid" |
| ) |
| mock_logger.warning.assert_any_call( |
| "Invalid timeout value for %s: %s, setting to None", |
| "connect_timeout", |
| "not_a_number", |
| ) |
| mock_logger.warning.assert_any_call( |
| "Invalid timeout value for %s: %s, setting to None", |
| "Page_Load_Timeout", |
| "abc123", |
| ) |
| |
| @patch("superset.utils.webdriver.app") |
| @patch("superset.utils.webdriver.chrome") |
| def test_non_timeout_config_preserved(self, mock_chrome, mock_app_patch, mock_app): |
| """Test that non-timeout configuration values are preserved.""" |
| mock_app_patch.config = { |
| "WEBDRIVER_TYPE": "chrome", |
| "WEBDRIVER_OPTION_ARGS": [], |
| "SCREENSHOT_LOCATE_WAIT": 10, |
| "SCREENSHOT_LOAD_WAIT": 10, |
| "WEBDRIVER_WINDOW": {}, |
| "WEBDRIVER_CONFIGURATION": { |
| "timeout": "30", |
| "some_other_option": "value", |
| "another_option": 123, |
| "boolean_option": True, |
| }, |
| } |
| |
| mock_driver_class = MagicMock() |
| mock_chrome.webdriver.WebDriver = mock_driver_class |
| mock_chrome.service.Service = MagicMock() |
| mock_options = MagicMock() |
| mock_options.add_argument = MagicMock() |
| mock_chrome.options.Options = MagicMock(return_value=mock_options) |
| |
| driver = WebDriverSelenium(driver_type="chrome") |
| driver.create() |
| |
| # Check that all config values are passed through |
| mock_driver_class.assert_called_once() |
| call_kwargs = mock_driver_class.call_args.kwargs |
| assert call_kwargs["timeout"] == 30.0 |
| assert call_kwargs["some_other_option"] == "value" |
| assert call_kwargs["another_option"] == 123 |
| assert call_kwargs["boolean_option"] is True |
| |
| @patch("superset.utils.webdriver.app") |
| @patch("superset.utils.webdriver.chrome") |
| def test_timeout_key_case_insensitive(self, mock_chrome, mock_app_patch, mock_app): |
| """Test that timeout detection is case-insensitive.""" |
| mock_app_patch.config = { |
| "WEBDRIVER_TYPE": "chrome", |
| "WEBDRIVER_OPTION_ARGS": [], |
| "SCREENSHOT_LOCATE_WAIT": 10, |
| "SCREENSHOT_LOAD_WAIT": 10, |
| "WEBDRIVER_WINDOW": {}, |
| "WEBDRIVER_CONFIGURATION": { |
| "TIMEOUT": "10", |
| "Connect_Timeout": "20", |
| "SOCKET_TIMEOUT": "30", |
| "connection_timeout_ms": "5000", # Contains 'connection_timeout' |
| }, |
| } |
| |
| mock_driver_class = MagicMock() |
| mock_chrome.webdriver.WebDriver = mock_driver_class |
| mock_chrome.service.Service = MagicMock() |
| mock_options = MagicMock() |
| mock_options.add_argument = MagicMock() |
| mock_chrome.options.Options = MagicMock(return_value=mock_options) |
| |
| driver = WebDriverSelenium(driver_type="chrome") |
| driver.create() |
| |
| # Check that all timeout values are converted to float |
| mock_driver_class.assert_called_once() |
| call_kwargs = mock_driver_class.call_args.kwargs |
| assert call_kwargs["TIMEOUT"] == 10.0 |
| assert call_kwargs["Connect_Timeout"] == 20.0 |
| assert call_kwargs["SOCKET_TIMEOUT"] == 30.0 |
| assert call_kwargs["connection_timeout_ms"] == 5000.0 |
| |
| @patch("superset.utils.webdriver.app") |
| @patch("superset.utils.webdriver.chrome") |
| def test_empty_webdriver_config(self, mock_chrome, mock_app_patch, mock_app): |
| """Test handling of empty webdriver configuration.""" |
| mock_app_patch.config = { |
| "WEBDRIVER_TYPE": "chrome", |
| "WEBDRIVER_OPTION_ARGS": [], |
| "SCREENSHOT_LOCATE_WAIT": 10, |
| "SCREENSHOT_LOAD_WAIT": 10, |
| "WEBDRIVER_WINDOW": {}, |
| "WEBDRIVER_CONFIGURATION": {}, |
| } |
| |
| mock_driver_class = MagicMock() |
| mock_chrome.webdriver.WebDriver = mock_driver_class |
| mock_chrome.service.Service = MagicMock() |
| mock_options = MagicMock() |
| mock_options.add_argument = MagicMock() |
| mock_chrome.options.Options = MagicMock(return_value=mock_options) |
| |
| driver = WebDriverSelenium(driver_type="chrome") |
| driver.create() |
| |
| # 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 |