mirror of
https://github.com/apache/superset.git
synced 2026-07-05 22:35:34 +00:00
Compare commits
2 Commits
chore/ci/s
...
fix/report
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d40415650f | ||
|
|
1c9e0bcdaf |
@@ -89,6 +89,7 @@ def take_tiled_screenshot(
|
|||||||
element_name: str,
|
element_name: str,
|
||||||
tile_height: int,
|
tile_height: int,
|
||||||
load_wait: int = 60,
|
load_wait: int = 60,
|
||||||
|
animation_wait: int = 0,
|
||||||
) -> bytes | None:
|
) -> bytes | None:
|
||||||
"""
|
"""
|
||||||
Take a tiled screenshot of a large dashboard by scrolling and capturing sections.
|
Take a tiled screenshot of a large dashboard by scrolling and capturing sections.
|
||||||
@@ -98,6 +99,7 @@ def take_tiled_screenshot(
|
|||||||
element_name: CSS class name of the element to screenshot
|
element_name: CSS class name of the element to screenshot
|
||||||
tile_height: Height of each tile in pixels
|
tile_height: Height of each tile in pixels
|
||||||
load_wait: Seconds to wait for charts to load per tile (default 60)
|
load_wait: Seconds to wait for charts to load per tile (default 60)
|
||||||
|
animation_wait: Seconds to wait for chart animations per tile (default 0)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Combined screenshot bytes or None if failed
|
Combined screenshot bytes or None if failed
|
||||||
@@ -174,6 +176,12 @@ def take_tiled_screenshot(
|
|||||||
load_wait,
|
load_wait,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Wait for chart animations (e.g. ECharts) to finish after spinner clears.
|
||||||
|
# The global animation wait before tiling only covers the first tile;
|
||||||
|
# subsequent tiles need their own wait after data loads.
|
||||||
|
if animation_wait > 0:
|
||||||
|
page.wait_for_timeout(animation_wait * 1000)
|
||||||
|
|
||||||
# Calculate what portion of the element we want to capture for this tile
|
# Calculate what portion of the element we want to capture for this tile
|
||||||
tile_start_in_element = i * tile_height
|
tile_start_in_element = i * tile_height
|
||||||
remaining_content = dashboard_height - tile_start_in_element
|
remaining_content = dashboard_height - tile_start_in_element
|
||||||
|
|||||||
@@ -369,17 +369,15 @@ class WebDriverPlaywright(WebDriverProxy):
|
|||||||
element_name,
|
element_name,
|
||||||
tile_height,
|
tile_height,
|
||||||
load_wait=self._screenshot_load_wait,
|
load_wait=self._screenshot_load_wait,
|
||||||
|
animation_wait=selenium_animation_wait,
|
||||||
)
|
)
|
||||||
if img is None:
|
if img is None:
|
||||||
logger.warning(
|
logger.error(
|
||||||
(
|
"Tiled screenshot failed at url %s; "
|
||||||
"Tiled screenshot failed, "
|
"not falling back to avoid sending a blank PDF",
|
||||||
"falling back to standard screenshot"
|
url,
|
||||||
)
|
|
||||||
)
|
|
||||||
img = WebDriverPlaywright._get_screenshot(
|
|
||||||
page, element, element_name
|
|
||||||
)
|
)
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
# Standard screenshot captures the full element including
|
# Standard screenshot captures the full element including
|
||||||
# below-the-fold content, so wait for all spinners globally.
|
# below-the-fold content, so wait for all spinners globally.
|
||||||
|
|||||||
@@ -371,3 +371,29 @@ class TestTakeTiledScreenshot:
|
|||||||
|
|
||||||
sig = inspect.signature(take_tiled_screenshot)
|
sig = inspect.signature(take_tiled_screenshot)
|
||||||
assert sig.parameters["load_wait"].default == 60
|
assert sig.parameters["load_wait"].default == 60
|
||||||
|
|
||||||
|
def test_per_tile_animation_wait_called_per_tile(self, mock_page):
|
||||||
|
"""animation_wait adds an extra wait per tile after the spinner check."""
|
||||||
|
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||||||
|
take_tiled_screenshot(
|
||||||
|
mock_page, "dashboard", tile_height=2000, animation_wait=5
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3 tiles × (1 scroll settle + 1 animation wait) = 6 total calls
|
||||||
|
assert mock_page.wait_for_timeout.call_count == 6
|
||||||
|
|
||||||
|
animation_calls = [
|
||||||
|
call
|
||||||
|
for call in mock_page.wait_for_timeout.call_args_list
|
||||||
|
if call[0][0] == 5 * 1000
|
||||||
|
]
|
||||||
|
assert len(animation_calls) == 3
|
||||||
|
|
||||||
|
def test_animation_wait_default_is_zero(self):
|
||||||
|
"""animation_wait defaults to 0 so no extra per-tile wait by default."""
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from superset.utils.screenshot_utils import take_tiled_screenshot
|
||||||
|
|
||||||
|
sig = inspect.signature(take_tiled_screenshot)
|
||||||
|
assert sig.parameters["animation_wait"].default == 0
|
||||||
|
|||||||
@@ -898,3 +898,76 @@ class TestWebDriverPlaywrightErrorHandling:
|
|||||||
"dashboard",
|
"dashboard",
|
||||||
"http://example.com",
|
"http://example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", True)
|
||||||
|
@patch("superset.utils.webdriver.sync_playwright")
|
||||||
|
@patch("superset.utils.webdriver.logger")
|
||||||
|
@patch("superset.utils.webdriver.take_tiled_screenshot")
|
||||||
|
def test_tiled_screenshot_failure_returns_none_without_fallback(
|
||||||
|
self, mock_take_tiled, mock_logger, mock_sync_playwright
|
||||||
|
):
|
||||||
|
"""When take_tiled_screenshot fails, return None rather than fall back to a
|
||||||
|
potentially blank standard screenshot."""
|
||||||
|
mock_user = MagicMock()
|
||||||
|
mock_user.username = "test_user"
|
||||||
|
|
||||||
|
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.wait_for.return_value = None
|
||||||
|
mock_element.screenshot.return_value = b"should_not_be_called"
|
||||||
|
|
||||||
|
def evaluate_side_effect(script):
|
||||||
|
if "querySelectorAll" in script:
|
||||||
|
return 25 # chart_count >= threshold
|
||||||
|
if "const target" in script:
|
||||||
|
return 6000 # dashboard_height > height_threshold and > tile_height
|
||||||
|
return None
|
||||||
|
|
||||||
|
mock_page.evaluate.side_effect = evaluate_side_effect
|
||||||
|
mock_take_tiled.return_value = None # tiled screenshot fails
|
||||||
|
|
||||||
|
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": 0,
|
||||||
|
"SCREENSHOT_SELENIUM_ANIMATION_WAIT": 0,
|
||||||
|
"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": False,
|
||||||
|
"SCREENSHOT_TILED_ENABLED": True,
|
||||||
|
"SCREENSHOT_TILED_CHART_THRESHOLD": 20,
|
||||||
|
"SCREENSHOT_TILED_HEIGHT_THRESHOLD": 5000,
|
||||||
|
"SCREENSHOT_TILED_VIEWPORT_HEIGHT": 600,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(WebDriverPlaywright, "auth") as mock_auth:
|
||||||
|
mock_auth.return_value = mock_context
|
||||||
|
|
||||||
|
driver = WebDriverPlaywright("chrome")
|
||||||
|
result = driver.get_screenshot(
|
||||||
|
"http://example.com", "dashboard", mock_user
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
mock_element.screenshot.assert_not_called()
|
||||||
|
mock_logger.error.assert_any_call(
|
||||||
|
"Tiled screenshot failed at url %s; "
|
||||||
|
"not falling back to avoid sending a blank PDF",
|
||||||
|
"http://example.com",
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user