Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/canvas-layout-dimensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"rrweb-snapshot": patch
"rrweb": patch
---

Fix problem where without `recordCanvas: true`, a canvas element is replayed without dimensions, potentially affecting layout
2 changes: 2 additions & 0 deletions packages/rrweb-snapshot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
},
"homepage": "https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-snapshot#readme",
"devDependencies": {
"@types/jest-image-snapshot": "^6.1.0",
"@rrweb/types": "^2.0.0-alpha.20",
"@rrweb/utils": "^2.0.0-alpha.20",
"@types/jsdom": "^20.0.0",
Expand All @@ -67,6 +68,7 @@
"typescript": "^5.4.5",
"vite": "^6.0.1",
"vite-plugin-dts": "^3.9.1",
"jest-image-snapshot": "^6.2.0",
"vitest": "^1.4.0"
},
"dependencies": {
Expand Down
3 changes: 3 additions & 0 deletions packages/rrweb-snapshot/src/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,9 @@ function buildNode(

if (name === 'rr_width') {
(node as HTMLElement).style.setProperty('width', value.toString());
if (tagName === 'canvas') {
(node as HTMLElement).style.setProperty('display', 'block');
}
} else if (name === 'rr_height') {
(node as HTMLElement).style.setProperty('height', value.toString());
} else if (
Expand Down
14 changes: 11 additions & 3 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,16 +655,16 @@ function serializeElementNode(
}

// canvas image data
if (tagName === 'canvas' && recordCanvas) {
if ((n as ICanvas).__context === '2d') {
if (tagName === 'canvas') {
if (recordCanvas && (n as ICanvas).__context === '2d') {
// only record this on 2d canvas
if (!is2DCanvasBlank(n as HTMLCanvasElement)) {
attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL(
dataURLOptions.type,
dataURLOptions.quality,
);
}
} else if (!('__context' in n)) {
} else if (recordCanvas && !('__context' in n)) {
// context is unknown, better not call getContext to trigger it
const canvasDataURL = (n as HTMLCanvasElement).toDataURL(
dataURLOptions.type,
Expand All @@ -684,6 +684,14 @@ function serializeElementNode(
if (canvasDataURL !== blankCanvasDataURL) {
attributes.rr_dataURL = canvasDataURL;
}
} else {
// explicitly record dimensions in case the unrecorded canvas affects layout;
// normally with UNSAFE_replayCanvas during replay the `canvas.getContext()`
// initialization will force browser to commit to the canvas's intrisic size
// as layout size
const { width, height } = n.getBoundingClientRect();
attributes.rr_width = `${width}px`;
attributes.rr_height = `${height}px`;
}
}
// save image offline
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,15 @@ exports[`integration tests > [html file]: block-element.html 1`] = `
</body></html>"
`;

exports[`integration tests > [html file]: canvas-layout.html 1`] = `
"<html xmlns=\\"http://www.w3.org/1999/xhtml\\"><head></head><body>
<div style=\\"background-color:grey;\\">
<img src=\\"http://localhost:3030/images/robot.png\\" alt=\\"This is a robot\\" style=\\"position:absolute;\\" />
<canvas width=\\"76\\" height=\\"96\\" style=\\"width: 76px; display: block; height: 96px;\\"></canvas>
</div>
</body></html>"
`;

exports[`integration tests > [html file]: compat-mode.html 1`] = `
"<!-- no doctype! --><html><head>
<title>Compat Mode; image resizing</title>
Expand Down
8 changes: 8 additions & 0 deletions packages/rrweb-snapshot/test/html/canvas-layout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<div style="background-color:grey;">
<img src="/images/robot.png" alt="This is a robot" style="position:absolute;" />
<canvas width="76" height="96"></canvas>
</div>
</body>
</html>
59 changes: 59 additions & 0 deletions packages/rrweb-snapshot/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
vi,
} from 'vitest';

import { toMatchImageSnapshot } from 'jest-image-snapshot';
import { getServerURL, waitForRAF } from './utils';

const htmlFolder = path.join(__dirname, 'html');
Expand Down Expand Up @@ -560,3 +561,61 @@ describe('shadow DOM integration tests', function (this: ISuite) {
await assertSnapshot(snapshotResult);
});
});

describe('snapshot/rebuild image tests', function (this: ISuite) {
expect.extend({ toMatchImageSnapshot });
vi.setConfig({ testTimeout: 30_000 });
let server: ISuite['server'];
let serverURL: ISuite['serverURL'];
let browser: ISuite['browser'];
let code: ISuite['code'];

beforeAll(async () => {
server = await startServer();
serverURL = getServerURL(server);
browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
code = fs.readFileSync(
path.resolve(__dirname, '../dist/rrweb-snapshot.umd.cjs'),
'utf-8',
);
});

afterAll(async () => {
await browser.close();
await server.close();
});

it('canvas dimensions are preserved after rebuild', async () => {
const page: puppeteer.Page = await browser.newPage();
page.on('console', (msg) => console.log(msg.text()));

// background-grey should extend to full rhs if <div> is dimensioned properly
await page.goto(`${serverURL}/html/canvas-layout.html`, {
waitUntil: 'load',
});
await waitForRAF(page);

await page.evaluate(`${code}
const snap = rrwebSnapshot.snapshot(document);
const { iframe, node } = rrwebSnapshot.rebuildIntoSandboxedIframe(
snap, {
root: document.body,
}
);
iframe.id = 'rebuild-iframe';
iframe.setAttribute('width', document.body.clientWidth);
iframe.setAttribute('height', document.body.clientHeight);
`);
await waitForRAF(page);

const iframeElement = await page.$('#rebuild-iframe');
const rebuildImage = await iframeElement!.screenshot();
expect(rebuildImage).toMatchImageSnapshot({
customSnapshotIdentifier: 'canvas-layout-rebuilt',
failureThreshold: 0.05,
failureThresholdType: 'percent',
});
});
});
35 changes: 28 additions & 7 deletions packages/rrweb/test/__snapshots__/integration.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,10 @@ exports[`record integration tests > can mask character data mutations 1`] = `
{
\\"type\\": 2,
\\"tagName\\": \\"canvas\\",
\\"attributes\\": {},
\\"attributes\\": {
\\"rr_width\\": \\"300px\\",
\\"rr_height\\": \\"150px\\"
},
\\"childNodes\\": [],
\\"id\\": 15
},
Expand Down Expand Up @@ -916,7 +919,10 @@ exports[`record integration tests > can mask character data mutations with regex
{
\\"type\\": 2,
\\"tagName\\": \\"canvas\\",
\\"attributes\\": {},
\\"attributes\\": {
\\"rr_width\\": \\"300px\\",
\\"rr_height\\": \\"150px\\"
},
\\"childNodes\\": [],
\\"id\\": 15
},
Expand Down Expand Up @@ -1803,7 +1809,10 @@ exports[`record integration tests > can record attribute mutation 1`] = `
{
\\"type\\": 2,
\\"tagName\\": \\"canvas\\",
\\"attributes\\": {},
\\"attributes\\": {
\\"rr_width\\": \\"300px\\",
\\"rr_height\\": \\"150px\\"
},
\\"childNodes\\": [],
\\"id\\": 15
},
Expand Down Expand Up @@ -1974,7 +1983,10 @@ exports[`record integration tests > can record character data muatations 1`] = `
{
\\"type\\": 2,
\\"tagName\\": \\"canvas\\",
\\"attributes\\": {},
\\"attributes\\": {
\\"rr_width\\": \\"300px\\",
\\"rr_height\\": \\"150px\\"
},
\\"childNodes\\": [],
\\"id\\": 15
},
Expand Down Expand Up @@ -2152,7 +2164,10 @@ exports[`record integration tests > can record childList mutations 1`] = `
{
\\"type\\": 2,
\\"tagName\\": \\"canvas\\",
\\"attributes\\": {},
\\"attributes\\": {
\\"rr_width\\": \\"300px\\",
\\"rr_height\\": \\"150px\\"
},
\\"childNodes\\": [],
\\"id\\": 15
},
Expand Down Expand Up @@ -5470,7 +5485,10 @@ exports[`record integration tests > handles null attribute values 1`] = `
{
\\"type\\": 2,
\\"tagName\\": \\"canvas\\",
\\"attributes\\": {},
\\"attributes\\": {
\\"rr_width\\": \\"300px\\",
\\"rr_height\\": \\"150px\\"
},
\\"childNodes\\": [],
\\"id\\": 15
},
Expand Down Expand Up @@ -17415,7 +17433,10 @@ exports[`record integration tests > will serialize node before record 1`] = `
{
\\"type\\": 2,
\\"tagName\\": \\"canvas\\",
\\"attributes\\": {},
\\"attributes\\": {
\\"rr_width\\": \\"300px\\",
\\"rr_height\\": \\"150px\\"
},
\\"childNodes\\": [],
\\"id\\": 15
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4873,7 +4873,10 @@ exports[`cross origin iframes > move-node.html > should record canvas elements 1
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"canvas\\",
\\"attributes\\": {},
\\"attributes\\": {
\\"rr_width\\": \\"300px\\",
\\"rr_height\\": \\"150px\\"
},
\\"childNodes\\": [],
\\"rootId\\": 11,
\\"id\\": 33
Expand Down
10 changes: 8 additions & 2 deletions packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,10 @@ exports[`record webgl > will record changes to a canvas element before the canva
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"canvas\\",
\\"attributes\\": {},
\\"attributes\\": {
\\"rr_width\\": \\"300px\\",
\\"rr_height\\": \\"150px\\"
},
\\"childNodes\\": [],
\\"id\\": 11
}
Expand Down Expand Up @@ -611,7 +614,10 @@ exports[`record webgl > will record changes to a canvas element before the canva
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"canvas\\",
\\"attributes\\": {},
\\"attributes\\": {
\\"rr_width\\": \\"300px\\",
\\"rr_height\\": \\"150px\\"
},
\\"childNodes\\": [],
\\"id\\": 11
}
Expand Down
Loading