Compare commits

...

2 Commits

Author SHA1 Message Date
Mehmet Salih Yavuz
f7318a6e7c fix(reports): apply animation wait only after spinners clear
Removes the early unconditional animation wait so the configured
SCREENSHOT_SELENIUM_ANIMATION_WAIT is applied once, after charts load,
rather than doubling it.
2026-06-30 11:26:06 +03:00
Mehmet Salih Yavuz
95f7589306 fix(reports): wait for ECharts animation after spinners clear in non-tiled screenshots 2026-06-25 19:35:38 +03:00
2 changed files with 86 additions and 4 deletions

View File

@@ -342,10 +342,10 @@ class WebDriverPlaywright(WebDriverProxy):
selenium_animation_wait = app.config[
"SCREENSHOT_SELENIUM_ANIMATION_WAIT"
]
logger.debug(
"Wait %i seconds for chart animation", selenium_animation_wait
)
page.wait_for_timeout(selenium_animation_wait * 1000)
# The animation wait is applied later, after the loading
# spinners clear: ECharts only starts its draw animation once
# data has arrived and the spinner is gone, so waiting here
# (before charts finish loading) would not cover it.
logger.debug(
"Taking a PNG screenshot of url %s as user %s",
url,
@@ -444,6 +444,13 @@ class WebDriverPlaywright(WebDriverProxy):
self._screenshot_load_wait,
)
raise
# Wait for chart animations (e.g. ECharts) to finish
# after the spinners clear. The draw animation only
# starts once data arrives and the spinner is gone, so
# without this wait a slow-loading chart is captured
# mid-render.
if selenium_animation_wait > 0:
page.wait_for_timeout(selenium_animation_wait * 1000)
img = WebDriverPlaywright._get_screenshot(
page, element, element_name
)
@@ -467,6 +474,12 @@ class WebDriverPlaywright(WebDriverProxy):
self._screenshot_load_wait,
)
raise
# Wait for chart animations (e.g. ECharts) to finish after
# the spinners clear. The draw animation only starts once
# data arrives and the spinner is gone, so without this wait
# a slow-loading chart is captured mid-render.
if selenium_animation_wait > 0:
page.wait_for_timeout(selenium_animation_wait * 1000)
img = WebDriverPlaywright._get_screenshot(
page, element, element_name
)

View File

@@ -974,3 +974,72 @@ class TestWebDriverPlaywrightErrorHandling:
"not falling back to avoid sending a blank PDF",
"http://example.com",
)
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", True)
@patch("superset.utils.webdriver._browser_manager")
@patch("superset.utils.webdriver.app")
def test_animation_wait_runs_after_spinners_clear(
self, mock_app, mock_browser_manager
):
"""
In the non-tiled path the animation wait must run *after* the spinner
check clears. ECharts only starts its draw animation once data arrives
and the spinner is gone, so waiting before that (the pre-existing wait)
leaves slow-loading charts captured mid-render. Regression test for the
mix-chart-not-rendering-fully-as-PNG report bug.
"""
animation_wait = 3
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": 0,
"SCREENSHOT_SELENIUM_ANIMATION_WAIT": animation_wait,
"SCREENSHOT_REPLACE_UNEXPECTED_ERRORS": False,
"SCREENSHOT_TILED_ENABLED": False,
"SCREENSHOT_LOCATE_WAIT": 10,
"SCREENSHOT_LOAD_WAIT": 60,
"SCREENSHOT_WAIT_FOR_ERROR_MODAL_VISIBLE": 10,
"SCREENSHOT_WAIT_FOR_ERROR_MODAL_INVISIBLE": 10,
}
mock_browser = MagicMock()
mock_context = MagicMock()
mock_page = MagicMock()
mock_element = MagicMock()
mock_browser_manager.get_browser.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"screenshot"
# Record the interleaved order of spinner check and timeouts.
manager = MagicMock()
manager.attach_mock(mock_page.wait_for_function, "wait_for_function")
manager.attach_mock(mock_page.wait_for_timeout, "wait_for_timeout")
manager.attach_mock(mock_element.screenshot, "screenshot")
with patch.object(WebDriverPlaywright, "auth", return_value=mock_context):
driver = WebDriverPlaywright("chrome")
driver.get_screenshot("http://example.com", "test-element", mock_user)
call_names = [c[0] for c in manager.mock_calls]
spinner_idx = call_names.index("wait_for_function")
screenshot_idx = call_names.index("screenshot")
# An animation wait of `animation_wait * 1000` must occur between the
# spinner clearing and the screenshot being taken.
post_spinner_anim_wait = [
i
for i, c in enumerate(manager.mock_calls)
if c[0] == "wait_for_timeout"
and c.args == (animation_wait * 1000,)
and spinner_idx < i < screenshot_idx
]
assert post_spinner_anim_wait, (
"Expected an animation wait_for_timeout between the spinner check "
"and the screenshot"
)