Skip to content

Commit f1b23dd

Browse files
YunFeng0817Juice10
andauthored
fix: canvas data in iframe wasn't applied in the fast-forward mode (#944)
* fix: canvas data in iframe wasn't applied in the fastforward mode * add more comments * Update packages/rrdom/src/diff.ts Co-authored-by: Justin Halsall <[email protected]> * apply Juice10's suggestion Co-authored-by: Justin Halsall <[email protected]>
1 parent aecaefb commit f1b23dd

File tree

5 files changed

+232
-4
lines changed

5 files changed

+232
-4
lines changed

packages/rrdom/src/diff.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,27 @@ export function diff(
136136
break;
137137
}
138138
case 'CANVAS':
139-
(newTree as RRCanvasElement).canvasMutations.forEach(
140-
(canvasMutation) =>
139+
{
140+
const rrCanvasElement = newTree as RRCanvasElement;
141+
// This canvas element is created with initial data in an iframe element. https://github.com/rrweb-io/rrweb/pull/944
142+
if (rrCanvasElement.rr_dataURL !== null) {
143+
const image = document.createElement('img');
144+
image.onload = () => {
145+
const ctx = (oldElement as HTMLCanvasElement).getContext('2d');
146+
if (ctx) {
147+
ctx.drawImage(image, 0, 0, image.width, image.height);
148+
}
149+
};
150+
image.src = rrCanvasElement.rr_dataURL;
151+
}
152+
rrCanvasElement.canvasMutations.forEach((canvasMutation) =>
141153
replayer.applyCanvas(
142154
canvasMutation.event,
143155
canvasMutation.mutation,
144156
oldTree as HTMLCanvasElement,
145157
),
146-
);
158+
);
159+
}
147160
break;
148161
case 'STYLE':
149162
applyVirtualStyleRulesToNode(

packages/rrdom/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export class RRElement extends BaseRRElementImpl(RRNode) {
149149
export class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {}
150150

151151
export class RRCanvasElement extends RRElement implements IRRElement {
152+
public rr_dataURL: string | null = null;
152153
public canvasMutations: {
153154
event: canvasEventWithTime;
154155
mutation: canvasMutationData;

packages/rrweb-snapshot/src/rebuild.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,13 +228,20 @@ function buildNode(
228228
// handle internal attributes
229229
if (tagName === 'canvas' && name === 'rr_dataURL') {
230230
const image = document.createElement('img');
231-
image.src = value;
232231
image.onload = () => {
233232
const ctx = (node as HTMLCanvasElement).getContext('2d');
234233
if (ctx) {
235234
ctx.drawImage(image, 0, 0, image.width, image.height);
236235
}
237236
};
237+
image.src = value;
238+
type RRCanvasElement = {
239+
RRNodeType: NodeType;
240+
rr_dataURL: string;
241+
};
242+
// If the canvas element is created in RRDom runtime (seeking to a time point), the canvas context isn't supported. So the data has to be stored and not handled until diff process. https://github.com/rrweb-io/rrweb/pull/944
243+
if (((node as unknown) as RRCanvasElement).RRNodeType)
244+
((node as unknown) as RRCanvasElement).rr_dataURL = value;
238245
} else if (tagName === 'img' && name === 'rr_dataURL') {
239246
const image = node as HTMLImageElement;
240247
if (!image.currentSrc.startsWith('data:')) {
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { EventType, eventWithTime, IncrementalSource } from '../../src/types';
2+
3+
const now = Date.now();
4+
5+
const events: eventWithTime[] = [
6+
{
7+
type: EventType.DomContentLoaded,
8+
data: {},
9+
timestamp: now,
10+
},
11+
{
12+
type: EventType.Load,
13+
data: {},
14+
timestamp: now + 100,
15+
},
16+
{
17+
type: EventType.Meta,
18+
data: {
19+
href: 'http://localhost',
20+
width: 1200,
21+
height: 500,
22+
},
23+
timestamp: now + 100,
24+
},
25+
{
26+
type: EventType.FullSnapshot,
27+
data: {
28+
node: {
29+
type: 0,
30+
childNodes: [
31+
{
32+
type: 2,
33+
tagName: 'html',
34+
attributes: {},
35+
childNodes: [
36+
{
37+
type: 2,
38+
tagName: 'head',
39+
attributes: {},
40+
childNodes: [
41+
{ type: 3, textContent: '\n ', id: 4 },
42+
{
43+
type: 2,
44+
tagName: 'meta',
45+
attributes: { charset: 'utf-8' },
46+
childNodes: [],
47+
id: 5,
48+
},
49+
{ type: 3, textContent: ' \n ', id: 6 },
50+
],
51+
id: 3,
52+
},
53+
{ type: 3, textContent: '\n ', id: 7 },
54+
{
55+
type: 2,
56+
tagName: 'body',
57+
attributes: {},
58+
childNodes: [
59+
{ type: 3, textContent: '\n ', id: 9 },
60+
{
61+
type: 2,
62+
tagName: 'iframe',
63+
attributes: { id: 'target' },
64+
childNodes: [],
65+
id: 19,
66+
},
67+
{ type: 3, textContent: '\n\n', id: 27 },
68+
],
69+
id: 8,
70+
},
71+
],
72+
id: 2,
73+
},
74+
],
75+
compatMode: 'BackCompat',
76+
id: 1,
77+
},
78+
initialOffset: { left: 0, top: 0 },
79+
},
80+
timestamp: now + 200,
81+
},
82+
// add an iframe
83+
{
84+
type: EventType.IncrementalSnapshot,
85+
data: {
86+
source: IncrementalSource.Mutation,
87+
adds: [
88+
{
89+
parentId: 19,
90+
nextId: null,
91+
node: {
92+
type: 0,
93+
childNodes: [
94+
{
95+
type: 2,
96+
tagName: 'html',
97+
attributes: {},
98+
childNodes: [
99+
{
100+
type: 2,
101+
tagName: 'head',
102+
attributes: {},
103+
childNodes: [],
104+
rootId: 30,
105+
id: 32,
106+
},
107+
{
108+
type: 2,
109+
tagName: 'body',
110+
attributes: {},
111+
childNodes: [],
112+
rootId: 30,
113+
id: 33,
114+
},
115+
],
116+
rootId: 30,
117+
id: 31,
118+
},
119+
],
120+
compatMode: 'BackCompat',
121+
id: 30,
122+
},
123+
},
124+
],
125+
removes: [],
126+
texts: [],
127+
attributes: [],
128+
isAttachIframe: true,
129+
},
130+
timestamp: now + 500,
131+
},
132+
// add two canvas, one is blank ans the other is filled with data
133+
{
134+
type: EventType.IncrementalSnapshot,
135+
data: {
136+
source: 0,
137+
texts: [],
138+
attributes: [],
139+
removes: [],
140+
adds: [
141+
{
142+
parentId: 33,
143+
nextId: null,
144+
node: {
145+
type: 2,
146+
tagName: 'canvas',
147+
attributes: {
148+
width: '10',
149+
height: '10',
150+
id: 'blank_canvas',
151+
},
152+
childNodes: [],
153+
rootId: 30,
154+
id: 34,
155+
},
156+
},
157+
{
158+
parentId: 33,
159+
nextId: null,
160+
node: {
161+
type: 2,
162+
tagName: 'canvas',
163+
attributes: {
164+
width: '10',
165+
height: '10',
166+
rr_dataURL:
167+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAB5JREFUKFNjZCASMBKpjmEQKvzPwIDqrEHoRozgBQC/ZQELU4DiXAAAAABJRU5ErkJggg==',
168+
id: 'canvas_with_data',
169+
},
170+
childNodes: [],
171+
rootId: 30,
172+
id: 35,
173+
},
174+
},
175+
],
176+
},
177+
timestamp: now + 500,
178+
},
179+
];
180+
181+
export default events;

packages/rrweb/test/replayer.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import inputEvents from './events/input';
1515
import iframeEvents from './events/iframe';
1616
import shadowDomEvents from './events/shadow-dom';
1717
import StyleSheetTextMutation from './events/style-sheet-text-mutation';
18+
import canvasInIframe from './events/canvas-in-iframe';
1819

1920
interface ISuite {
2021
code: string;
@@ -613,6 +614,31 @@ describe('replayer', function () {
613614
).toEqual('shadow dom two');
614615
});
615616

617+
it('can fast-forward mutation events containing painted canvas in iframe', async () => {
618+
await page.evaluate(`
619+
events = ${JSON.stringify(canvasInIframe)};
620+
const { Replayer } = rrweb;
621+
var replayer = new Replayer(events,{showDebug:true});
622+
replayer.pause(550);
623+
`);
624+
const replayerIframe = await page.$('iframe');
625+
const contentDocument = await replayerIframe!.contentFrame()!;
626+
const iframe = await contentDocument!.$('iframe');
627+
expect(iframe).not.toBeNull();
628+
const docInIFrame = await iframe?.contentFrame();
629+
expect(docInIFrame).not.toBeNull();
630+
const canvasElements = await docInIFrame!.$$('canvas');
631+
// The first canvas is a blank one and the second is a painted one.
632+
expect(canvasElements.length).toEqual(2);
633+
634+
const dataUrls = await docInIFrame?.$$eval('canvas', (elements) =>
635+
elements.map((element) => (element as HTMLCanvasElement).toDataURL()),
636+
);
637+
expect(dataUrls?.length).toEqual(2);
638+
// The painted canvas's data should not be empty.
639+
expect(dataUrls![1]).not.toEqual(dataUrls![0]);
640+
});
641+
616642
it('can stream events in live mode', async () => {
617643
const status = await page.evaluate(`
618644
const { Replayer } = rrweb;

0 commit comments

Comments
 (0)