diff --git a/packages/trace-viewer/src/sw/snapshotRenderer.ts b/packages/trace-viewer/src/sw/snapshotRenderer.ts index 0e508a50bf..8fbc6251db 100644 --- a/packages/trace-viewer/src/sw/snapshotRenderer.ts +++ b/packages/trace-viewer/src/sw/snapshotRenderer.ts @@ -301,6 +301,44 @@ function snapshotScript(screenshotURL: string | undefined, ...targetIds: (string const canvases = root.querySelectorAll('canvas'); if (canvases.length > 0 && screenshotURL) { + + function drawCanvasWarning(canvas: HTMLCanvasElement, partial: boolean) { + function createStripedPattern(lineWidth: number, spacing: number, slope: number, color: string) { + const can = document.createElement('canvas'); + const len = Math.hypot(1, slope); + + const w = can.width = 1 / len + spacing + 0.5 | 0; // round to nearest pixel + const h = can.height = slope / len + spacing * slope + 0.5 | 0; + + const ctx = can.getContext('2d')!; + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + ctx.beginPath(); + + // Line through top left and bottom right corners + ctx.moveTo(0, 0); + ctx.lineTo(w, h); + // Line through top right corner to add missing pixels + ctx.moveTo(0, -h); + ctx.lineTo(w * 2, h); + // Line through bottom left corner to add missing pixels + ctx.moveTo(-w, 0); + ctx.lineTo(w, h * 2); + + ctx.stroke(); + return ctx.createPattern(can, 'repeat')!; + } + + const ctx = canvas.getContext('2d')!; + ctx.fillStyle = 'red'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = createStripedPattern(4, 8, 1, 'orange'); + ctx.fillRect(0, 0, canvas.width, canvas.height); + + canvas.title = `Playwright couldn't capture ${partial ? 'full ' : ''}canvas contents because it's located ${partial ? 'partially ' : ''}outside the viewport.`; + } + const img = new Image(); img.onload = () => { for (const canvas of canvases) { @@ -312,9 +350,16 @@ function snapshotScript(screenshotURL: string | undefined, ...targetIds: (string const xEnd = boundingRect.right / window.innerWidth; const yEnd = boundingRect.bottom / window.innerHeight; + if (xStart > 1 || yStart > 1 || xEnd > 1 || yEnd > 1) + drawCanvasWarning(canvas, xStart < 1 && yStart < 1); + context.drawImage(img, xStart * img.width, yStart * img.height, (xEnd - xStart) * img.width, (yEnd - yStart) * img.height, 0, 0, canvas.width, canvas.height); } }; + img.onerror = () => { + for (const canvas of canvases) + drawCanvasWarning(canvas, false); + }; img.src = screenshotURL; } diff --git a/tests/assets/screenshots/canvas.html b/tests/assets/screenshots/canvas.html index 011148c5ff..a0ebc55829 100644 --- a/tests/assets/screenshots/canvas.html +++ b/tests/assets/screenshots/canvas.html @@ -7,4 +7,7 @@ ctx.fillRect(25, 25, 100, 100); ctx.clearRect(45, 45, 60, 60); ctx.strokeRect(50, 50, 50, 50); + + if (location.hash.includes('canvas-on-edge')) + canvas.style.marginTop = '90vh'; diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index d5df72d087..1a93953bba 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -1441,11 +1441,12 @@ test.skip('should allow showing screenshots instead of snapshots', async ({ runA test('canvas clipping', async ({ runAndTrace, page, server }) => { const traceViewer = await runAndTrace(async () => { - await page.goto(server.PREFIX + '/screenshots/canvas.html'); + await page.goto(server.PREFIX + '/screenshots/canvas.html#canvas-on-edge'); await page.waitForTimeout(1000); // ensure we could take a screenshot }); - await traceViewer.page.pause(); + 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.`); }); test.skip('should handle case where neither snapshots nor screenshots exist', async ({ runAndTrace, page, server }) => {