From cafacee2b9d5f68e9f1301e353fbcec7dd60be35 Mon Sep 17 00:00:00 2001 From: Mathew Sir Guest the best Date: Tue, 7 Jul 2026 23:23:17 -0600 Subject: [PATCH] test: verify screenshot seam alignment; release tile buffers --- .../plans/2026-07-07-wabot-modernization.md | 60 ++++++++++++++++--- src/wabot/screenshot.py | 14 +++-- tests/unit/test_screenshot.py | 42 +++++++++++++ 3 files changed, 102 insertions(+), 14 deletions(-) diff --git a/docs/superpowers/plans/2026-07-07-wabot-modernization.md b/docs/superpowers/plans/2026-07-07-wabot-modernization.md index ea1d10b..8a5a3cc 100644 --- a/docs/superpowers/plans/2026-07-07-wabot-modernization.md +++ b/docs/superpowers/plans/2026-07-07-wabot-modernization.md @@ -2298,6 +2298,48 @@ class TestChromiumPath: if c.args and str(c.args[0]).startswith("window.scrollTo") ] assert len(scroll_calls) >= 2 + + def test_tall_page_stitches_tiles_bottom_aligned(self, tmp_path): + driver = MagicMock(name="driver") + driver.capabilities = {"browserName": "chrome"} + driver.execute_script.side_effect = lambda script: { + "return document.body.parentNode.scrollWidth": 100, + "return document.body.parentNode.scrollHeight": 150, # 3 tiles of 60 + "return document.documentElement.clientWidth": 100, + "return window.innerHeight": 60, + }.get(script) # window.scrollTo(...) -> None + driver.get_screenshot_as_png.side_effect = [ + png_bytes(100, 60, (255, 0, 0)), # tile @ y=0 + png_bytes(100, 60, (0, 255, 0)), # tile @ y=60 + png_bytes(100, 60, (0, 0, 255)), # tile @ y=120, clamped to paste_y=90 + ] + out = tmp_path / "shot.png" + assert save_full_page(driver, str(out)) is True + img = Image.open(out) + assert img.size == (100, 150) + assert img.getpixel((50, 30)) == (255, 0, 0) # first tile + assert img.getpixel((50, 75)) == (0, 255, 0) # second tile (rows 60-89) + assert img.getpixel((50, 140)) == (0, 0, 255) # third tile, bottom-aligned + + def test_wide_page_clamps_last_column(self, tmp_path): + driver = MagicMock(name="driver") + driver.capabilities = {"browserName": "chrome"} + driver.execute_script.side_effect = lambda script: { + "return document.body.parentNode.scrollWidth": 150, # 2 columns of 100 + "return document.body.parentNode.scrollHeight": 60, + "return document.documentElement.clientWidth": 100, + "return window.innerHeight": 60, + }.get(script) + driver.get_screenshot_as_png.side_effect = [ + png_bytes(100, 60, (255, 0, 0)), # column @ x=0 + png_bytes(100, 60, (0, 0, 255)), # column @ x=100, clamped to paste_x=50 + ] + out = tmp_path / "shot.png" + assert save_full_page(driver, str(out)) is True + img = Image.open(out) + assert img.size == (150, 60) + assert img.getpixel((25, 30)) == (255, 0, 0) # left column (cols 0-49) + assert img.getpixel((140, 30)) == (0, 0, 255) # right column, clamped (cols 50-149) ``` - [ ] **Step 2: Run tests to verify they fail** @@ -2371,12 +2413,14 @@ def _stitch_chromium(driver, filename: str) -> bool: while x < total_width: driver.execute_script(f"window.scrollTo({x}, {y})") time.sleep(_SCROLL_SETTLE_SECONDS) - tile = Image.open(BytesIO(driver.get_screenshot_as_png())) - # the last row/column can't scroll a full viewport: paste aligned - # to the bottom/right edge instead of duplicating content - paste_x = min(x, total_width - viewport_width) - paste_y = min(y, total_height - viewport_height) - stitched.paste(tile, (max(paste_x, 0), max(paste_y, 0))) + # Tiles are device pixels; the canvas/scroll math is CSS pixels. Assumes + # devicePixelRatio == 1 (true for headless chromium in the test target). + with Image.open(BytesIO(driver.get_screenshot_as_png())) as tile: + # the last row/column can't scroll a full viewport: paste aligned + # to the bottom/right edge instead of duplicating content + paste_x = min(x, total_width - viewport_width) + paste_y = min(y, total_height - viewport_height) + stitched.paste(tile, (max(paste_x, 0), max(paste_y, 0))) x += viewport_width y += viewport_height stitched.save(filename) @@ -2387,7 +2431,7 @@ def _stitch_chromium(driver, filename: str) -> bool: - [ ] **Step 4: Run tests to verify they pass** Run: `uv run pytest tests/unit/test_screenshot.py -v` -Expected: 3 passed. Also run `uv run pytest` — all unit tests still pass. +Expected: 5 passed. Also run `uv run pytest` — all unit tests still pass. - [ ] **Step 5: Commit** @@ -2949,7 +2993,7 @@ Run: `uv run pytest tests/unit/test_api.py -v` Expected: 11 passed Run: `uv run pytest` -Expected: full unit suite passes (126 tests: pacing 7, sessions 14, hosts 19, reattach 6, fields 17, page 36, screenshot 3, browser 13, api 11) +Expected: full unit suite passes (128 tests: pacing 7, sessions 14, hosts 19, reattach 6, fields 17, page 36, screenshot 5, browser 13, api 11) - [ ] **Step 5: Commit** diff --git a/src/wabot/screenshot.py b/src/wabot/screenshot.py index 6dbdff7..2f79ced 100644 --- a/src/wabot/screenshot.py +++ b/src/wabot/screenshot.py @@ -59,12 +59,14 @@ def _stitch_chromium(driver, filename: str) -> bool: while x < total_width: driver.execute_script(f"window.scrollTo({x}, {y})") time.sleep(_SCROLL_SETTLE_SECONDS) - tile = Image.open(BytesIO(driver.get_screenshot_as_png())) - # the last row/column can't scroll a full viewport: paste aligned - # to the bottom/right edge instead of duplicating content - paste_x = min(x, total_width - viewport_width) - paste_y = min(y, total_height - viewport_height) - stitched.paste(tile, (max(paste_x, 0), max(paste_y, 0))) + # Tiles are device pixels; the canvas/scroll math is CSS pixels. Assumes + # devicePixelRatio == 1 (true for headless chromium in the test target). + with Image.open(BytesIO(driver.get_screenshot_as_png())) as tile: + # the last row/column can't scroll a full viewport: paste aligned + # to the bottom/right edge instead of duplicating content + paste_x = min(x, total_width - viewport_width) + paste_y = min(y, total_height - viewport_height) + stitched.paste(tile, (max(paste_x, 0), max(paste_y, 0))) x += viewport_width y += viewport_height stitched.save(filename) diff --git a/tests/unit/test_screenshot.py b/tests/unit/test_screenshot.py index 617f129..126430f 100644 --- a/tests/unit/test_screenshot.py +++ b/tests/unit/test_screenshot.py @@ -66,3 +66,45 @@ class TestChromiumPath: if c.args and str(c.args[0]).startswith("window.scrollTo") ] assert len(scroll_calls) >= 2 + + def test_tall_page_stitches_tiles_bottom_aligned(self, tmp_path): + driver = MagicMock(name="driver") + driver.capabilities = {"browserName": "chrome"} + driver.execute_script.side_effect = lambda script: { + "return document.body.parentNode.scrollWidth": 100, + "return document.body.parentNode.scrollHeight": 150, # 3 tiles of 60 + "return document.documentElement.clientWidth": 100, + "return window.innerHeight": 60, + }.get(script) # window.scrollTo(...) -> None + driver.get_screenshot_as_png.side_effect = [ + png_bytes(100, 60, (255, 0, 0)), # tile @ y=0 + png_bytes(100, 60, (0, 255, 0)), # tile @ y=60 + png_bytes(100, 60, (0, 0, 255)), # tile @ y=120, clamped to paste_y=90 + ] + out = tmp_path / "shot.png" + assert save_full_page(driver, str(out)) is True + img = Image.open(out) + assert img.size == (100, 150) + assert img.getpixel((50, 30)) == (255, 0, 0) # first tile + assert img.getpixel((50, 75)) == (0, 255, 0) # second tile (rows 60-89) + assert img.getpixel((50, 140)) == (0, 0, 255) # third tile, bottom-aligned + + def test_wide_page_clamps_last_column(self, tmp_path): + driver = MagicMock(name="driver") + driver.capabilities = {"browserName": "chrome"} + driver.execute_script.side_effect = lambda script: { + "return document.body.parentNode.scrollWidth": 150, # 2 columns of 100 + "return document.body.parentNode.scrollHeight": 60, + "return document.documentElement.clientWidth": 100, + "return window.innerHeight": 60, + }.get(script) + driver.get_screenshot_as_png.side_effect = [ + png_bytes(100, 60, (255, 0, 0)), # column @ x=0 + png_bytes(100, 60, (0, 0, 255)), # column @ x=100, clamped to paste_x=50 + ] + out = tmp_path / "shot.png" + assert save_full_page(driver, str(out)) is True + img = Image.open(out) + assert img.size == (150, 60) + assert img.getpixel((25, 30)) == (255, 0, 0) # left column (cols 0-49) + assert img.getpixel((140, 30)) == (0, 0, 255) # right column, clamped (cols 50-149)