From 25c039401d3a107f2fdf3e53c0e304e44e5af032 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 14 Nov 2024 15:27:09 +0100 Subject: [PATCH] fix(canvas snapshots): position mismatch in headless mode (#33575) --- .../trace/recorder/snapshotterInjected.ts | 13 ++++++++++ .../trace-viewer/src/sw/snapshotRenderer.ts | 24 ++++++++++++------- tests/library/trace-viewer.spec.ts | 2 +- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts index 47385c8323..0400c6deaf 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts @@ -47,6 +47,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript: const kTargetAttribute = '__playwright_target__'; const kCustomElementsAttribute = '__playwright_custom_elements__'; const kCurrentSrcAttribute = '__playwright_current_src__'; + const kBoundingRectAttribute = '__playwright_bounding_rect__'; // Symbols for our own info on Nodes/StyleSheets. const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_'); @@ -436,6 +437,18 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript: expectValue(value); attrs[kSelectedAttribute] = value; } + if (nodeName === 'CANVAS') { + const boundingRect = (element as HTMLCanvasElement).getBoundingClientRect(); + const value = JSON.stringify({ + left: boundingRect.left / window.innerWidth, + top: boundingRect.top / window.innerHeight, + right: boundingRect.right / window.innerWidth, + bottom: boundingRect.bottom / window.innerHeight + }); + expectValue(kBoundingRectAttribute); + expectValue(value); + attrs[kBoundingRectAttribute] = value; + } if (element.scrollTop) { expectValue(kScrollTopAttribute); expectValue(element.scrollTop); diff --git a/packages/trace-viewer/src/sw/snapshotRenderer.ts b/packages/trace-viewer/src/sw/snapshotRenderer.ts index a9f89ba273..f4e9d908fb 100644 --- a/packages/trace-viewer/src/sw/snapshotRenderer.ts +++ b/packages/trace-viewer/src/sw/snapshotRenderer.ts @@ -427,14 +427,20 @@ function snapshotScript(...targetIds: (string | undefined)[]) { for (const canvas of canvasElements) { const context = canvas.getContext('2d')!; - const boundingRect = canvas.getBoundingClientRect(); - const xStart = boundingRect.left / window.innerWidth; - const yStart = boundingRect.top / window.innerHeight; - const xEnd = boundingRect.right / window.innerWidth; - const yEnd = boundingRect.bottom / window.innerHeight; + const boundingRectAttribute = canvas.getAttribute('__playwright_bounding_rect__'); + canvas.removeAttribute('__playwright_bounding_rect__'); + if (!boundingRectAttribute) + continue; - const partiallyUncaptured = xEnd > 1 || yEnd > 1; - const fullyUncaptured = xStart > 1 || yStart > 1; + let boundingRect: { left: number, top: number, right: number, bottom: number }; + try { + boundingRect = JSON.parse(boundingRectAttribute); + } catch (e) { + continue; + } + + const partiallyUncaptured = boundingRect.right > 1 || boundingRect.bottom > 1; + const fullyUncaptured = boundingRect.left > 1 || boundingRect.top > 1; if (fullyUncaptured) { canvas.title = `Playwright couldn't capture canvas contents because it's located outside the viewport.`; continue; @@ -442,10 +448,10 @@ function snapshotScript(...targetIds: (string | undefined)[]) { drawCheckerboard(context, canvas); - context.drawImage(img, xStart * img.width, yStart * img.height, (xEnd - xStart) * img.width, (yEnd - yStart) * img.height, 0, 0, canvas.width, canvas.height); + context.drawImage(img, boundingRect.left * img.width, boundingRect.top * img.height, (boundingRect.right - boundingRect.left) * img.width, (boundingRect.bottom - boundingRect.top) * img.height, 0, 0, canvas.width, canvas.height); if (isUnderTest) // eslint-disable-next-line no-console - console.log(`canvas drawn:`, JSON.stringify([xStart, yStart, xEnd, yEnd].map(v => Math.floor(v * 100)))); + console.log(`canvas drawn:`, JSON.stringify([boundingRect.left, boundingRect.top, (boundingRect.right - boundingRect.left), (boundingRect.bottom - boundingRect.top)].map(v => Math.floor(v * 100)))); if (partiallyUncaptured) canvas.title = `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`; diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 0c8ad1aafd..0daf06298a 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -1510,7 +1510,7 @@ test('canvas clipping', async ({ runAndTrace, page, server }) => { }); const msg = await traceViewer.page.waitForEvent('console', { predicate: msg => msg.text().startsWith('canvas drawn:') }); - expect(msg.text()).toEqual('canvas drawn: [0,91,12,111]'); + expect(msg.text()).toEqual('canvas drawn: [0,91,11,20]'); const snapshot = await traceViewer.snapshotFrame('page.goto'); await expect(snapshot.locator('canvas')).toHaveAttribute('title', `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`);