mirror of
https://github.com/apache/superset.git
synced 2026-05-06 16:34:32 +00:00
The configured SCREENSHOT_TILED_VIEWPORT_HEIGHT (default 2000px) was larger than Playwright's actual browser viewport (~768-1200px), causing scroll increments to skip content between tiles. Now we fetch the actual viewport height and use min(configured, actual) for scroll calculations to ensure complete content coverage. Added regression test to verify tiles properly cover all content when configured viewport exceeds actual viewport size. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
741 lines
28 KiB
Python
741 lines
28 KiB
Python
# 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.
|
||
|
||
import io
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import pytest
|
||
from PIL import Image
|
||
|
||
from superset.utils.screenshot_utils import (
|
||
combine_screenshot_tiles,
|
||
take_tiled_screenshot,
|
||
)
|
||
|
||
|
||
class TestCombineScreenshotTiles:
|
||
def _create_test_image(self, width: int, height: int, color: str = "red") -> bytes:
|
||
"""Helper to create test PNG image bytes."""
|
||
img = Image.new("RGB", (width, height), color)
|
||
output = io.BytesIO()
|
||
img.save(output, format="PNG")
|
||
return output.getvalue()
|
||
|
||
def test_empty_tiles_returns_empty_bytes(self):
|
||
"""Test that empty tiles list returns empty bytes."""
|
||
result = combine_screenshot_tiles([])
|
||
assert result == b""
|
||
|
||
def test_single_tile_returns_original(self):
|
||
"""Test that single tile returns the original image."""
|
||
test_image = self._create_test_image(100, 100)
|
||
result = combine_screenshot_tiles([test_image])
|
||
assert result == test_image
|
||
|
||
def test_combine_multiple_tiles_vertically(self):
|
||
"""Test combining multiple tiles into a single vertical image."""
|
||
# Create test images with different colors
|
||
tile1 = self._create_test_image(100, 50, "red")
|
||
tile2 = self._create_test_image(100, 75, "green")
|
||
tile3 = self._create_test_image(100, 25, "blue")
|
||
|
||
result = combine_screenshot_tiles([tile1, tile2, tile3])
|
||
|
||
# Verify result is not empty
|
||
assert result != b""
|
||
|
||
# Verify the combined image has correct dimensions
|
||
combined_img = Image.open(io.BytesIO(result))
|
||
assert combined_img.width == 100 # Max width of all tiles
|
||
assert combined_img.height == 150 # Sum of all heights (50 + 75 + 25)
|
||
|
||
# Verify the image format is PNG
|
||
assert combined_img.format == "PNG"
|
||
|
||
def test_combine_tiles_different_widths(self):
|
||
"""Test combining tiles with different widths uses max width."""
|
||
tile1 = self._create_test_image(50, 100, "red")
|
||
tile2 = self._create_test_image(150, 100, "green")
|
||
tile3 = self._create_test_image(100, 100, "blue")
|
||
|
||
result = combine_screenshot_tiles([tile1, tile2, tile3])
|
||
|
||
combined_img = Image.open(io.BytesIO(result))
|
||
assert combined_img.width == 150 # Max width
|
||
assert combined_img.height == 300 # Sum of heights
|
||
|
||
def test_combine_tiles_handles_pil_error(self):
|
||
"""Test that PIL errors are handled gracefully."""
|
||
# Create one valid image and one invalid
|
||
valid_tile = self._create_test_image(100, 100)
|
||
invalid_tile = b"invalid_image_data"
|
||
|
||
result = combine_screenshot_tiles([valid_tile, invalid_tile])
|
||
|
||
# Should return the first (valid) tile as fallback
|
||
assert result == valid_tile
|
||
|
||
def test_combine_tiles_logs_exception(self):
|
||
"""Test that exceptions are logged properly."""
|
||
with patch("superset.utils.screenshot_utils.logger") as mock_logger:
|
||
# Create invalid image data that will cause PIL to raise an exception
|
||
invalid_tile = b"definitely_not_an_image"
|
||
valid_tile = self._create_test_image(100, 100)
|
||
|
||
result = combine_screenshot_tiles([valid_tile, invalid_tile])
|
||
|
||
# Should have logged the exception
|
||
mock_logger.exception.assert_called_once()
|
||
# Should return first tile as fallback
|
||
assert result == valid_tile
|
||
|
||
|
||
class TestTakeTiledScreenshot:
|
||
@pytest.fixture
|
||
def mock_page(self):
|
||
"""Create a mock Playwright page object."""
|
||
page = MagicMock()
|
||
|
||
# Mock viewport size
|
||
page.viewport_size = {"width": 1024, "height": 768}
|
||
|
||
# Mock element locator
|
||
element = MagicMock()
|
||
page.locator.return_value = element
|
||
|
||
# Mock element info - simulating a 5000px tall dashboard
|
||
element_info = {"height": 5000, "top": 100, "left": 50, "width": 800}
|
||
viewport_info = {
|
||
"elementX": 50,
|
||
"elementY": 200,
|
||
"elementWidth": 800,
|
||
"elementHeight": 600,
|
||
"viewportWidth": 1024,
|
||
"viewportHeight": 768,
|
||
}
|
||
|
||
# For 7 tiles (5000px / 768px actual viewport = 6.5, rounded up to 7):
|
||
# 1 initial call + 7 scroll + 7 viewport info + 1 reset scroll = 16 calls
|
||
page.evaluate.side_effect = [
|
||
element_info, # Initial call for dashboard dimensions
|
||
None,
|
||
viewport_info, # Tile 1
|
||
None,
|
||
viewport_info, # Tile 2
|
||
None,
|
||
viewport_info, # Tile 3
|
||
None,
|
||
viewport_info, # Tile 4
|
||
None,
|
||
viewport_info, # Tile 5
|
||
None,
|
||
viewport_info, # Tile 6
|
||
None,
|
||
viewport_info, # Tile 7
|
||
None, # Final reset scroll call
|
||
]
|
||
|
||
# Mock screenshot method
|
||
fake_screenshot = b"fake_screenshot_data"
|
||
page.screenshot.return_value = fake_screenshot
|
||
|
||
return page
|
||
|
||
def test_successful_tiled_screenshot(self, mock_page):
|
||
"""Test successful tiled screenshot generation."""
|
||
with patch(
|
||
"superset.utils.screenshot_utils.combine_screenshot_tiles"
|
||
) as mock_combine:
|
||
mock_combine.return_value = b"combined_screenshot"
|
||
|
||
result = take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||
|
||
# Should return combined screenshot
|
||
assert result == b"combined_screenshot"
|
||
|
||
# Should have called screenshot method multiple times
|
||
# (7 tiles for 5000px height with 768px actual viewport)
|
||
assert mock_page.screenshot.call_count == 7
|
||
|
||
# Should have called combine function
|
||
mock_combine.assert_called_once()
|
||
|
||
def test_element_not_found_returns_none(self):
|
||
"""Test that missing element returns None."""
|
||
mock_page = MagicMock()
|
||
element = MagicMock()
|
||
element.wait_for.side_effect = Exception("Element not found")
|
||
mock_page.locator.return_value = element
|
||
|
||
result = take_tiled_screenshot(mock_page, "nonexistent", viewport_height=2000)
|
||
|
||
assert result is None
|
||
|
||
def test_tile_calculation_logic(self, mock_page):
|
||
"""Test that tiles are calculated correctly."""
|
||
# Mock dashboard height of 3500px with viewport of 2000px
|
||
element_info = {"height": 3500, "top": 100, "left": 50, "width": 800}
|
||
viewport_info = {
|
||
"elementX": 50,
|
||
"elementY": 200,
|
||
"elementWidth": 800,
|
||
"elementHeight": 600,
|
||
"viewportWidth": 1024,
|
||
"viewportHeight": 768,
|
||
}
|
||
|
||
# For 2 tiles (3500px / 2000px = 1.75, rounded up to 2):
|
||
# 1 initial call + 2 scroll + 2 viewport info + 1 reset scroll = 6 calls
|
||
mock_page.evaluate.side_effect = [
|
||
element_info,
|
||
None, # First scroll call
|
||
viewport_info, # First viewport info call
|
||
None, # Second scroll call
|
||
viewport_info, # Second viewport info call
|
||
None, # Reset scroll call
|
||
]
|
||
|
||
with patch(
|
||
"superset.utils.screenshot_utils.combine_screenshot_tiles"
|
||
) as mock_combine:
|
||
mock_combine.return_value = b"combined"
|
||
|
||
take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||
|
||
# Should take 2 screenshots (3500px / 2000px = 1.75, rounded up to 2)
|
||
assert mock_page.screenshot.call_count == 2
|
||
|
||
def test_scroll_positions_calculated_correctly(self, mock_page):
|
||
"""Test that scroll positions are calculated correctly."""
|
||
# Override the fixture's side_effect for this specific test
|
||
element_info = {"height": 5000, "top": 100, "left": 50, "width": 800}
|
||
viewport_info = {
|
||
"elementX": 50,
|
||
"elementY": 200,
|
||
"elementWidth": 800,
|
||
"elementHeight": 600,
|
||
"viewportWidth": 1024,
|
||
"viewportHeight": 768,
|
||
}
|
||
|
||
mock_page.evaluate.side_effect = [
|
||
element_info, # Initial call for dashboard dimensions
|
||
None,
|
||
viewport_info, # Tile 1
|
||
None,
|
||
viewport_info, # Tile 2
|
||
None,
|
||
viewport_info, # Tile 3
|
||
None,
|
||
viewport_info, # Tile 4
|
||
None,
|
||
viewport_info, # Tile 5
|
||
None,
|
||
viewport_info, # Tile 6
|
||
None,
|
||
viewport_info, # Tile 7
|
||
None, # Reset scroll call
|
||
]
|
||
|
||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||
take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||
|
||
# Check scroll positions (dashboard_top = 100, tile_height = 768)
|
||
scroll_calls = [
|
||
call
|
||
for call in mock_page.evaluate.call_args_list
|
||
if "scrollTo" in str(call)
|
||
]
|
||
|
||
# Should have scrolled to positions: 100, 868, 1636, 2404, 3172, 3940, 4708
|
||
expected_scrolls = [
|
||
"window.scrollTo(0, 100)",
|
||
"window.scrollTo(0, 868)",
|
||
"window.scrollTo(0, 1636)",
|
||
"window.scrollTo(0, 2404)",
|
||
"window.scrollTo(0, 3172)",
|
||
"window.scrollTo(0, 3940)",
|
||
"window.scrollTo(0, 4708)",
|
||
]
|
||
actual_scrolls = [call[0][0] for call in scroll_calls]
|
||
|
||
assert len(actual_scrolls) == 8 # 7 tile scrolls + 1 reset
|
||
for expected in expected_scrolls:
|
||
assert expected in actual_scrolls
|
||
|
||
def test_reset_scroll_position(self, mock_page):
|
||
"""Test that scroll position is reset after screenshot."""
|
||
# Override the fixture's side_effect for this specific test
|
||
element_info = {"height": 5000, "top": 100, "left": 50, "width": 800}
|
||
viewport_info = {
|
||
"elementX": 50,
|
||
"elementY": 200,
|
||
"elementWidth": 800,
|
||
"elementHeight": 600,
|
||
"viewportWidth": 1024,
|
||
"viewportHeight": 768,
|
||
}
|
||
|
||
mock_page.evaluate.side_effect = [
|
||
element_info, # Initial call for dashboard dimensions
|
||
None,
|
||
viewport_info, # Tile 1
|
||
None,
|
||
viewport_info, # Tile 2
|
||
None,
|
||
viewport_info, # Tile 3
|
||
None,
|
||
viewport_info, # Tile 4
|
||
None,
|
||
viewport_info, # Tile 5
|
||
None,
|
||
viewport_info, # Tile 6
|
||
None,
|
||
viewport_info, # Tile 7
|
||
None, # Reset scroll call
|
||
]
|
||
|
||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||
take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||
|
||
# Check that final call resets scroll to top
|
||
final_call = mock_page.evaluate.call_args_list[-1]
|
||
assert "window.scrollTo(0, 0)" in str(final_call)
|
||
|
||
def test_logs_dashboard_info(self, mock_page):
|
||
"""Test that dashboard info is logged."""
|
||
with patch("superset.utils.screenshot_utils.logger") as mock_logger:
|
||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||
take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||
|
||
# Should log dashboard dimensions with lazy logging format
|
||
mock_logger.info.assert_any_call(
|
||
"Dashboard: %sx%spx at (%s, %s)", 800, 5000, 50, 100
|
||
)
|
||
# Should log number of tiles with lazy logging format
|
||
mock_logger.info.assert_any_call("Taking %s screenshot tiles", 7)
|
||
|
||
def test_exception_handling_returns_none(self):
|
||
"""Test that exceptions are handled and None is returned."""
|
||
mock_page = MagicMock()
|
||
mock_page.locator.side_effect = Exception("Unexpected error")
|
||
|
||
with patch("superset.utils.screenshot_utils.logger") as mock_logger:
|
||
result = take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||
|
||
assert result is None
|
||
# The exception object is passed, not the string
|
||
call_args = mock_logger.exception.call_args
|
||
assert call_args[0][0] == "Tiled screenshot failed: %s"
|
||
assert str(call_args[0][1]) == "Unexpected error"
|
||
|
||
def test_wait_timeouts_between_tiles(self, mock_page):
|
||
"""Test that there are appropriate waits between tiles."""
|
||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||
take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||
|
||
# Should have called wait_for_timeout for each tile (7 tiles)
|
||
assert mock_page.wait_for_timeout.call_count == 7
|
||
|
||
# Each wait should be 2000ms (2 seconds)
|
||
for call in mock_page.wait_for_timeout.call_args_list:
|
||
assert call[0][0] == 2000
|
||
|
||
def test_screenshot_clip_parameters(self, mock_page):
|
||
"""Test that screenshot clipping parameters are correct."""
|
||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||
take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||
|
||
# Check screenshot calls have correct clip parameters
|
||
screenshot_calls = mock_page.screenshot.call_args_list
|
||
|
||
for call in screenshot_calls:
|
||
kwargs = call[1]
|
||
assert kwargs["type"] == "png"
|
||
assert "clip" in kwargs
|
||
|
||
clip = kwargs["clip"]
|
||
assert clip["x"] == 50
|
||
assert clip["y"] == 200
|
||
assert clip["width"] == 800
|
||
# Height should be min of viewport_height and remaining content
|
||
assert clip["height"] <= 600 # Element height from mock
|
||
|
||
def test_negative_element_position_clipped_to_zero(self):
|
||
"""Test that negative element positions are clipped to viewport bounds."""
|
||
mock_page = MagicMock()
|
||
mock_page.viewport_size = {"width": 1024, "height": 768}
|
||
|
||
# Mock element locator
|
||
element = MagicMock()
|
||
mock_page.locator.return_value = element
|
||
|
||
# Simulate element scrolled above viewport (negative Y position)
|
||
element_info = {"height": 3000, "top": 100, "left": 0, "width": 800}
|
||
viewport_info = {
|
||
"elementX": 0,
|
||
"elementY": -200, # Element is scrolled 200px above viewport
|
||
"elementWidth": 800,
|
||
"elementHeight": 600,
|
||
"viewportWidth": 1024,
|
||
"viewportHeight": 768,
|
||
}
|
||
|
||
# For 4 tiles (3000px / 768px = 3.9, rounded up to 4):
|
||
# 1 initial + 4 * (scroll + viewport info) + 1 reset = 10 calls
|
||
mock_page.evaluate.side_effect = [
|
||
element_info,
|
||
None,
|
||
viewport_info, # Tile 1
|
||
None,
|
||
viewport_info, # Tile 2
|
||
None,
|
||
viewport_info, # Tile 3
|
||
None,
|
||
viewport_info, # Tile 4
|
||
None, # Reset scroll
|
||
]
|
||
|
||
mock_page.screenshot.return_value = b"screenshot"
|
||
|
||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||
result = take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||
|
||
# Should complete successfully
|
||
assert result is not None
|
||
|
||
# Check that clip Y was adjusted to 0 (not negative)
|
||
screenshot_calls = mock_page.screenshot.call_args_list
|
||
for call in screenshot_calls:
|
||
clip = call[1]["clip"]
|
||
assert clip["y"] >= 0, "Clip Y should never be negative"
|
||
assert clip["x"] >= 0, "Clip X should never be negative"
|
||
|
||
def test_element_extends_beyond_viewport(self):
|
||
"""Test clipping when element extends beyond viewport boundaries."""
|
||
mock_page = MagicMock()
|
||
mock_page.viewport_size = {"width": 1024, "height": 768}
|
||
|
||
element = MagicMock()
|
||
mock_page.locator.return_value = element
|
||
|
||
element_info = {"height": 2000, "top": 0, "left": 0, "width": 1200}
|
||
|
||
# Element is wider than viewport
|
||
viewport_info = {
|
||
"elementX": 0,
|
||
"elementY": 100,
|
||
"elementWidth": 1200, # Wider than viewport
|
||
"elementHeight": 800,
|
||
"viewportWidth": 1024, # Viewport width
|
||
"viewportHeight": 768,
|
||
}
|
||
|
||
# For 3 tiles (2000px / 768px = 2.6, rounded up to 3):
|
||
# 1 initial + 3 * (scroll + viewport info) + 1 reset = 8 calls
|
||
mock_page.evaluate.side_effect = [
|
||
element_info,
|
||
None,
|
||
viewport_info, # Tile 1
|
||
None,
|
||
viewport_info, # Tile 2
|
||
None,
|
||
viewport_info, # Tile 3
|
||
None, # Reset scroll
|
||
]
|
||
|
||
mock_page.screenshot.return_value = b"screenshot"
|
||
|
||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||
result = take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||
|
||
assert result is not None
|
||
|
||
# Check that clip width was constrained to viewport
|
||
clip = mock_page.screenshot.call_args_list[0][1]["clip"]
|
||
assert clip["width"] <= 1024, "Clip width should not exceed viewport"
|
||
|
||
def test_invalid_clip_dimensions_skipped(self):
|
||
"""Test that tiles with invalid dimensions are skipped with a warning."""
|
||
mock_page = MagicMock()
|
||
mock_page.viewport_size = {"width": 1024, "height": 768}
|
||
|
||
element = MagicMock()
|
||
mock_page.locator.return_value = element
|
||
|
||
element_info = {"height": 4000, "top": 0, "left": 0, "width": 800}
|
||
|
||
# First tile: valid
|
||
valid_viewport_info = {
|
||
"elementX": 0,
|
||
"elementY": 100,
|
||
"elementWidth": 800,
|
||
"elementHeight": 600,
|
||
"viewportWidth": 1024,
|
||
"viewportHeight": 768,
|
||
}
|
||
|
||
# Second tile: invalid (negative height after calculation)
|
||
invalid_viewport_info = {
|
||
"elementX": 0,
|
||
"elementY": -1000, # Far above viewport
|
||
"elementWidth": 800,
|
||
"elementHeight": 100, # Not enough visible height
|
||
"viewportWidth": 1024,
|
||
"viewportHeight": 768,
|
||
}
|
||
|
||
# For 6 tiles (4000px / 768px = 5.2, rounded up to 6):
|
||
# 1 initial + 6 * (scroll + viewport info) + 1 reset = 14 calls
|
||
mock_page.evaluate.side_effect = [
|
||
element_info,
|
||
None,
|
||
valid_viewport_info, # Tile 1 - valid
|
||
None,
|
||
invalid_viewport_info, # Tile 2 - invalid, should be skipped
|
||
None,
|
||
valid_viewport_info, # Tile 3 - valid
|
||
None,
|
||
valid_viewport_info, # Tile 4 - valid
|
||
None,
|
||
valid_viewport_info, # Tile 5 - valid
|
||
None,
|
||
valid_viewport_info, # Tile 6 - valid
|
||
None, # Reset scroll
|
||
]
|
||
|
||
mock_page.screenshot.return_value = b"screenshot"
|
||
|
||
with patch("superset.utils.screenshot_utils.logger") as mock_logger:
|
||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||
result = take_tiled_screenshot(
|
||
mock_page, "dashboard", viewport_height=2000
|
||
)
|
||
|
||
# Should complete but with warning
|
||
assert result is not None
|
||
|
||
# Should have logged a warning about skipping tile
|
||
mock_logger.warning.assert_called_once()
|
||
warning_msg = mock_logger.warning.call_args[0][0]
|
||
assert "Skipping tile" in warning_msg
|
||
assert "invalid clip dimensions" in warning_msg
|
||
|
||
# Should have taken 5 screenshots (6 tiles - 1 invalid)
|
||
assert mock_page.screenshot.call_count == 5
|
||
|
||
def test_viewport_bounds_with_offset_element(self):
|
||
"""Test proper clipping for element with positive offset from viewport edge."""
|
||
mock_page = MagicMock()
|
||
mock_page.viewport_size = {"width": 1024, "height": 768}
|
||
|
||
element = MagicMock()
|
||
mock_page.locator.return_value = element
|
||
|
||
element_info = {"height": 2000, "top": 500, "left": 200, "width": 600}
|
||
|
||
# Element starts 200px from left edge
|
||
viewport_info = {
|
||
"elementX": 200, # Offset from left
|
||
"elementY": 150,
|
||
"elementWidth": 600,
|
||
"elementHeight": 500,
|
||
"viewportWidth": 1024,
|
||
"viewportHeight": 768,
|
||
}
|
||
|
||
# For 3 tiles (2000px / 768px = 2.6, rounded up to 3):
|
||
# 1 initial + 3 * (scroll + viewport info) + 1 reset = 8 calls
|
||
mock_page.evaluate.side_effect = [
|
||
element_info,
|
||
None,
|
||
viewport_info, # Tile 1
|
||
None,
|
||
viewport_info, # Tile 2
|
||
None,
|
||
viewport_info, # Tile 3
|
||
None, # Reset scroll
|
||
]
|
||
|
||
mock_page.screenshot.return_value = b"screenshot"
|
||
|
||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||
result = take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||
|
||
assert result is not None
|
||
|
||
# Check clip respects element position
|
||
clip = mock_page.screenshot.call_args_list[0][1]["clip"]
|
||
assert clip["x"] == 200, "Should preserve element X offset"
|
||
assert clip["y"] == 150, "Should preserve element Y offset"
|
||
assert clip["width"] == 600, "Should use element width"
|
||
|
||
def test_zero_width_element_skipped(self):
|
||
"""Test that elements with zero or negative width are skipped."""
|
||
mock_page = MagicMock()
|
||
mock_page.viewport_size = {"width": 1024, "height": 768}
|
||
|
||
element = MagicMock()
|
||
mock_page.locator.return_value = element
|
||
|
||
element_info = {"height": 2000, "top": 0, "left": 0, "width": 0}
|
||
|
||
viewport_info = {
|
||
"elementX": 0,
|
||
"elementY": 100,
|
||
"elementWidth": 0, # Zero width
|
||
"elementHeight": 600,
|
||
"viewportWidth": 1024,
|
||
"viewportHeight": 768,
|
||
}
|
||
|
||
# For 3 tiles (2000px / 768px = 2.6, rounded up to 3):
|
||
# 1 initial + 3 * (scroll + viewport info) + 1 reset = 8 calls
|
||
# All tiles will be skipped due to zero width
|
||
mock_page.evaluate.side_effect = [
|
||
element_info,
|
||
None,
|
||
viewport_info, # Tile 1 - skipped
|
||
None,
|
||
viewport_info, # Tile 2 - skipped
|
||
None,
|
||
viewport_info, # Tile 3 - skipped
|
||
None, # Reset scroll
|
||
]
|
||
|
||
with patch("superset.utils.screenshot_utils.logger") as mock_logger:
|
||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||
result = take_tiled_screenshot(
|
||
mock_page, "dashboard", viewport_height=2000
|
||
)
|
||
|
||
# Should handle gracefully
|
||
assert result is not None
|
||
|
||
# Should have logged warnings about invalid dimensions
|
||
# (3 times, once per tile)
|
||
assert mock_logger.warning.call_count == 3
|
||
for call in mock_logger.warning.call_args_list:
|
||
warning_msg = call[0][0]
|
||
assert "invalid clip dimensions" in warning_msg
|
||
|
||
# Should not have taken any screenshots
|
||
assert mock_page.screenshot.call_count == 0
|
||
|
||
def test_element_completely_above_viewport(self):
|
||
"""Test element that is completely scrolled above the viewport."""
|
||
mock_page = MagicMock()
|
||
mock_page.viewport_size = {"width": 1024, "height": 768}
|
||
|
||
element = MagicMock()
|
||
mock_page.locator.return_value = element
|
||
|
||
element_info = {"height": 2000, "top": 0, "left": 0, "width": 800}
|
||
|
||
# Element completely above viewport
|
||
viewport_info = {
|
||
"elementX": 0,
|
||
"elementY": -800, # Completely above viewport
|
||
"elementWidth": 800,
|
||
"elementHeight": 600,
|
||
"viewportWidth": 1024,
|
||
"viewportHeight": 768,
|
||
}
|
||
|
||
# For 3 tiles (2000px / 768px = 2.6, rounded up to 3):
|
||
# 1 initial + 3 * (scroll + viewport info) + 1 reset = 8 calls
|
||
# All tiles will be skipped because element is completely above viewport
|
||
mock_page.evaluate.side_effect = [
|
||
element_info,
|
||
None,
|
||
viewport_info, # Tile 1 - skipped
|
||
None,
|
||
viewport_info, # Tile 2 - skipped
|
||
None,
|
||
viewport_info, # Tile 3 - skipped
|
||
None, # Reset scroll
|
||
]
|
||
|
||
with patch("superset.utils.screenshot_utils.logger") as mock_logger:
|
||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||
result = take_tiled_screenshot(
|
||
mock_page, "dashboard", viewport_height=2000
|
||
)
|
||
|
||
# Should handle gracefully
|
||
assert result is not None
|
||
|
||
# Should have skipped all 3 tiles with warnings
|
||
assert mock_logger.warning.call_count == 3
|
||
|
||
# Should not have taken screenshots
|
||
assert mock_page.screenshot.call_count == 0
|
||
|
||
def test_scroll_increment_respects_actual_viewport_height(self):
|
||
"""When config viewport height > actual viewport, we still cover every tile."""
|
||
mock_page = MagicMock()
|
||
mock_page.viewport_size = {"width": 1600, "height": 1200}
|
||
|
||
element = MagicMock()
|
||
mock_page.locator.return_value = element
|
||
|
||
element_info = {"height": 3600, "top": 0, "left": 0, "width": 800}
|
||
viewport_info = {
|
||
"elementX": 0,
|
||
"elementY": 0,
|
||
"elementWidth": 800,
|
||
"elementHeight": 1200,
|
||
"viewportWidth": 1600,
|
||
"viewportHeight": 1200,
|
||
}
|
||
|
||
mock_page.evaluate.side_effect = [
|
||
element_info, # Initial call for dashboard dimensions
|
||
None, # First scroll
|
||
viewport_info, # First viewport info
|
||
None, # Second scroll
|
||
viewport_info, # Second viewport info
|
||
None, # Third scroll
|
||
viewport_info, # Third viewport info
|
||
None, # Reset scroll
|
||
]
|
||
|
||
mock_page.screenshot.return_value = b"screenshot"
|
||
|
||
with patch("superset.utils.screenshot_utils.combine_screenshot_tiles"):
|
||
take_tiled_screenshot(mock_page, "dashboard", viewport_height=2000)
|
||
|
||
# We expect three tiles (0–1200, 1200–2400, 2400–3600)
|
||
# even though config says 2000.
|
||
assert mock_page.screenshot.call_count == 3
|
||
|
||
scroll_calls = [
|
||
call
|
||
for call in mock_page.evaluate.call_args_list
|
||
if "scrollTo" in str(call)
|
||
]
|
||
actual_scrolls = [call[0][0] for call in scroll_calls]
|
||
|
||
# Should have scrolled to positions: 0, 1200, 2400, plus final reset to 0
|
||
assert len(actual_scrolls) == 4 # 3 tile scrolls + 1 reset
|
||
assert actual_scrolls == [
|
||
"window.scrollTo(0, 0)",
|
||
"window.scrollTo(0, 1200)",
|
||
"window.scrollTo(0, 2400)",
|
||
"window.scrollTo(0, 0)", # Reset scroll
|
||
]
|