mirror of
https://github.com/apache/superset.git
synced 2026-04-28 12:34:23 +00:00
Compare commits
3 Commits
emojis-one
...
playwright
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
482bef1507 | ||
|
|
20c04a4663 | ||
|
|
782f5eab16 |
@@ -120,15 +120,26 @@ def take_tiled_screenshot(
|
||||
dashboard_top,
|
||||
)
|
||||
|
||||
# Calculate number of tiles needed
|
||||
num_tiles = max(1, (dashboard_height + viewport_height - 1) // viewport_height)
|
||||
# Get actual viewport height to ensure we don't skip content
|
||||
actual_viewport_height = page.viewport_size["height"]
|
||||
tile_height = min(viewport_height, actual_viewport_height)
|
||||
|
||||
logger.info(
|
||||
"Viewport: configured=%s, actual=%s, using tile_height=%s",
|
||||
viewport_height,
|
||||
actual_viewport_height,
|
||||
tile_height,
|
||||
)
|
||||
|
||||
# Calculate number of tiles needed based on actual tile height
|
||||
num_tiles = max(1, (dashboard_height + tile_height - 1) // tile_height)
|
||||
logger.info("Taking %s screenshot tiles", num_tiles)
|
||||
|
||||
screenshot_tiles = []
|
||||
|
||||
for i in range(num_tiles):
|
||||
# Calculate scroll position to show this tile's content
|
||||
scroll_y = dashboard_top + (i * viewport_height)
|
||||
scroll_y = dashboard_top + (i * tile_height)
|
||||
|
||||
# Scroll the window to the desired position
|
||||
page.evaluate(f"window.scrollTo(0, {scroll_y})")
|
||||
@@ -139,29 +150,65 @@ def take_tiled_screenshot(
|
||||
# Wait for scroll to settle and content to load
|
||||
page.wait_for_timeout(2000) # 2 second wait per tile
|
||||
|
||||
# Get the current element position after scroll
|
||||
current_element_box = page.evaluate(f"""() => {{
|
||||
# Get the current element position after scroll and viewport size
|
||||
viewport_info = page.evaluate(f"""() => {{
|
||||
const el = document.querySelector(".{element_name}");
|
||||
const rect = el.getBoundingClientRect();
|
||||
return {{
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
elementX: rect.left,
|
||||
elementY: rect.top,
|
||||
elementWidth: rect.width,
|
||||
elementHeight: rect.height,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight
|
||||
}};
|
||||
}}""")
|
||||
|
||||
# Calculate what portion of the element we want to capture for this tile
|
||||
tile_start_in_element = i * viewport_height
|
||||
remaining_content = dashboard_height - tile_start_in_element
|
||||
tile_content_height = min(viewport_height, remaining_content)
|
||||
# Ensure clip coordinates are within viewport bounds
|
||||
# If element.top is negative, it's scrolled above viewport - start from y=0
|
||||
clip_y = max(0, viewport_info["elementY"])
|
||||
# If element.left is negative, start from x=0
|
||||
clip_x = max(0, viewport_info["elementX"])
|
||||
|
||||
# Calculate clip dimensions - capture what's visible of the element
|
||||
# Handle elements scrolled above viewport: if elementY is negative,
|
||||
# only the portion from (elementY + elementHeight) is visible
|
||||
if viewport_info["elementY"] < 0:
|
||||
# Element extends from above viewport - calculate visible portion
|
||||
visible_height = (
|
||||
viewport_info["elementY"] + viewport_info["elementHeight"]
|
||||
)
|
||||
clip_height = min(visible_height, viewport_info["viewportHeight"])
|
||||
else:
|
||||
# Element is within viewport
|
||||
clip_height = min(
|
||||
viewport_info["elementHeight"],
|
||||
viewport_info["viewportHeight"] - clip_y,
|
||||
)
|
||||
|
||||
clip_width = min(
|
||||
viewport_info["elementWidth"], viewport_info["viewportWidth"] - clip_x
|
||||
)
|
||||
|
||||
# Validate clip region before taking screenshot
|
||||
if clip_width <= 0 or clip_height <= 0:
|
||||
logger.warning(
|
||||
"Skipping tile %s/%s - invalid clip dimensions: %sx%s at (%s, %s)",
|
||||
i + 1,
|
||||
num_tiles,
|
||||
clip_width,
|
||||
clip_height,
|
||||
clip_x,
|
||||
clip_y,
|
||||
)
|
||||
continue
|
||||
|
||||
# Clip to capture only the current tile portion of the element
|
||||
clip = {
|
||||
"x": current_element_box["x"],
|
||||
"y": current_element_box["y"],
|
||||
"width": current_element_box["width"],
|
||||
"height": min(tile_content_height, current_element_box["height"]),
|
||||
"x": clip_x,
|
||||
"y": clip_y,
|
||||
"width": clip_width,
|
||||
"height": clip_height,
|
||||
}
|
||||
|
||||
# Take screenshot with clipping to capture only this tile's content
|
||||
|
||||
@@ -110,24 +110,42 @@ class TestTakeTiledScreenshot:
|
||||
"""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}
|
||||
element_box = {"x": 50, "y": 200, "width": 800, "height": 600}
|
||||
viewport_info = {
|
||||
"elementX": 50,
|
||||
"elementY": 200,
|
||||
"elementWidth": 800,
|
||||
"elementHeight": 600,
|
||||
"viewportWidth": 1024,
|
||||
"viewportHeight": 768,
|
||||
}
|
||||
|
||||
# For 3 tiles (5000px / 2000px = 2.5, rounded up to 3):
|
||||
# 1 initial call + 3 scroll + 3 element box + 1 reset scroll = 8 calls
|
||||
# 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, # First scroll call
|
||||
element_box, # First element box call
|
||||
None, # Second scroll call
|
||||
element_box, # Second element box call
|
||||
None, # Third scroll call
|
||||
element_box, # Third element box call
|
||||
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
|
||||
]
|
||||
|
||||
@@ -150,8 +168,8 @@ class TestTakeTiledScreenshot:
|
||||
assert result == b"combined_screenshot"
|
||||
|
||||
# Should have called screenshot method multiple times
|
||||
# (3 tiles for 5000px height)
|
||||
assert mock_page.screenshot.call_count == 3
|
||||
# (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()
|
||||
@@ -171,16 +189,23 @@ class TestTakeTiledScreenshot:
|
||||
"""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}
|
||||
element_box = {"x": 50, "y": 200, "width": 800, "height": 600}
|
||||
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 element box + 1 reset scroll = 6 calls
|
||||
# 1 initial call + 2 scroll + 2 viewport info + 1 reset scroll = 6 calls
|
||||
mock_page.evaluate.side_effect = [
|
||||
element_info,
|
||||
None, # First scroll call
|
||||
element_box, # First element box call
|
||||
viewport_info, # First viewport info call
|
||||
None, # Second scroll call
|
||||
element_box, # Second element box call
|
||||
viewport_info, # Second viewport info call
|
||||
None, # Reset scroll call
|
||||
]
|
||||
|
||||
@@ -198,38 +223,57 @@ class TestTakeTiledScreenshot:
|
||||
"""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}
|
||||
element_box = {"x": 50, "y": 200, "width": 800, "height": 600}
|
||||
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, # First scroll call
|
||||
element_box, # First element box call
|
||||
None, # Second scroll call
|
||||
element_box, # Second element box call
|
||||
None, # Third scroll call
|
||||
element_box, # Third element box call
|
||||
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)
|
||||
# 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, 2100, 4100
|
||||
# Should have scrolled to positions: 100, 868, 1636, 2404, 3172, 3940, 4708
|
||||
expected_scrolls = [
|
||||
"window.scrollTo(0, 100)",
|
||||
"window.scrollTo(0, 2100)",
|
||||
"window.scrollTo(0, 4100)",
|
||||
"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) == 4 # 3 tile scrolls + 1 reset
|
||||
assert len(actual_scrolls) == 8 # 7 tile scrolls + 1 reset
|
||||
for expected in expected_scrolls:
|
||||
assert expected in actual_scrolls
|
||||
|
||||
@@ -237,16 +281,31 @@ class TestTakeTiledScreenshot:
|
||||
"""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}
|
||||
element_box = {"x": 50, "y": 200, "width": 800, "height": 600}
|
||||
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, # First scroll call
|
||||
element_box, # First element box call
|
||||
None, # Second scroll call
|
||||
element_box, # Second element box call
|
||||
None, # Third scroll call
|
||||
element_box, # Third element box call
|
||||
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
|
||||
]
|
||||
|
||||
@@ -268,7 +327,7 @@ class TestTakeTiledScreenshot:
|
||||
"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", 3)
|
||||
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."""
|
||||
@@ -289,8 +348,8 @@ class TestTakeTiledScreenshot:
|
||||
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 (3 tiles)
|
||||
assert mock_page.wait_for_timeout.call_count == 3
|
||||
# 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:
|
||||
@@ -315,3 +374,367 @@ class TestTakeTiledScreenshot:
|
||||
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
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user