Skip to content

Commit

Permalink
preserve adopted styles from nodes being removed when virtual dom is …
Browse files Browse the repository at this point in the history
…in use
  • Loading branch information
megboehlert committed Jan 31, 2025
1 parent 83c66c4 commit a2feb20
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 7 deletions.
6 changes: 6 additions & 0 deletions .changeset/rare-snakes-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"rrweb": minor
"@rrweb/types": minor
---

Added a styleMap to store and retrieve adopted stylesheets not initially in the styleMirror when using the virtual DOM. When using the virtual DOM, adopted styles are applied after mutations, so any adopted styles from nodes - including styles from their child nodes - removed will not be applied to nodes that share the same styleId.
32 changes: 31 additions & 1 deletion packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import type {
styleDeclarationData,
adoptedStyleSheetData,
serializedElementNodeWithId,
styleParam
} from '@rrweb/types';
import {
polyfill,
Expand Down Expand Up @@ -151,6 +152,9 @@ export class Replayer {
// Used to track StyleSheetObjects adopted on multiple document hosts.
private styleMirror: StyleSheetMirror = new StyleSheetMirror();

// Used to store adopted styles that get skipped over when using virtual dom and skipping in the timeline.
private styleMap: Map<number, styleParam> = new Map();

// Used to track video & audio elements, and keep them in sync with general playback.
private mediaManager: MediaManager;

Expand Down Expand Up @@ -336,6 +340,7 @@ export class Replayer {
this.firstFullSnapshot = null;
this.mirror.reset();
this.styleMirror.reset();
this.styleMap = new Map();
this.mediaManager.reset();
});

Expand Down Expand Up @@ -556,6 +561,7 @@ export class Replayer {
this.mirror.reset();
this.styleMirror.reset();
this.mediaManager.reset();
this.styleMap = new Map();
this.config.root.removeChild(this.wrapper);
this.emitter.emit(ReplayerEvents.Destroy);
}
Expand Down Expand Up @@ -698,6 +704,7 @@ export class Replayer {
}
this.mediaManager.reset();
this.styleMirror.reset();
this.styleMap = new Map();
this.rebuildFullSnapshot(event, isSync);
this.iframe.contentWindow?.scrollTo(event.data.initialOffset);
};
Expand Down Expand Up @@ -2066,7 +2073,30 @@ export class Replayer {

private applyAdoptedStyleSheet(data: adoptedStyleSheetData) {
const targetHost = this.mirror.getNode(data.id);
if (!targetHost) return;
if (!targetHost) {
// if node was removed before styles were applied, we want to store the styles for future nodes
data.styles?.forEach((style) => {
const key = style.styleId;
if (this.styleMirror.getStyle(key) === null) this.styleMap.set(key, style);
});

return;
}
if (!data.styles || data.styles?.length < data.styleIds?.length) {
const styles: styleParam[] = [...data.styles || []];
data.styleIds?.forEach((styleId) => {
// styles either already exist in style mirror or in the data
if (this.styleMirror.getStyle(styleId) !== null) return;
if (styles.find((style) => style.styleId === styleId)) return;

// backup styles from removed nodes where original styles were not applied
const style: styleParam | undefined = this.styleMap.get(styleId);
if (style) styles.push(style);
this.styleMap.delete(styleId);
});

if (styles.length > 0) data.styles = [...styles];
}
// Create StyleSheet objects which will be adopted after.
data.styles?.forEach((style) => {
let newStyleSheet: CSSStyleSheet | null = null;
Expand Down
172 changes: 172 additions & 0 deletions packages/rrweb/test/events/adopted-style-sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,178 @@ const events: eventWithTime[] = [
},
timestamp: now + 550,
},
// Create shadow host #3
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
texts: [],
attributes: [],
removes: [],
adds: [
{
parentId: 7,
nextId: null,
node: {
type: 2,
tagName: 'div',
attributes: {},
childNodes: [],
id: 36,
},
},
{
parentId: 36,
nextId: null,
node: {
type: 2,
tagName: 'div',
id: 37,
childNodes: [],
isShadowHost: true,
attributes: {
id: 'shadow-host3'
}
},
}
],
},
timestamp: now + 600,
},
// Create a new shadow dom with button
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
texts: [],
attributes: [],
removes: [],
adds: [
{
parentId: 37,
nextId: null,
node: {
type: 2,
tagName: 'button',
attributes: {
class: 'blue-btn',
},
childNodes: [],
id: 38,
isShadow: true,
},
},
],
},
timestamp: now + 650,
},
// Adopt the stylesheet #5 on the shadow dom
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.AdoptedStyleSheet,
id: 37,
styleIds: [5],
styles: [
{
styleId: 5,
rules: [
{ rule: '.blue-btn { color: blue; }' }
]
}
]
},
timestamp: now + 700,
},
// Remove the parent of shadow host #3 and add a new shadow host #4
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
// remove parent of shadow host #3 which contained the element with styles for style id 5
removes: [
{
parentId: 7,
id: 36,
}
],
adds: [
{
parentId: 7,
nextId: null,
node: {
type: 2,
tagName: 'div',
attributes: {},
childNodes: [],
id: 40,
},
},
{
parentId: 40,
nextId: null,
node: {
type: 2,
tagName: 'div',
id: 41,
childNodes: [],
isShadowHost: true,
attributes: {
id: 'shadow-host4'
}
},
}
],
texts: [],
attributes: [],
},
timestamp: now + 750,
},
// Create a new shadow dom with button
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
texts: [],
attributes: [],
removes: [],
adds: [
{
parentId: 41,
nextId: null,
node: {
type: 2,
tagName: 'button',
attributes: {
class: 'blue-btn',
},
childNodes: [],
id: 42,
isShadow: true,
},
},
],
},
timestamp: now + 800,
},
// Adopt stlyesheet #5 and #6 on the shadow dom
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.AdoptedStyleSheet,
id: 41,
styleIds: [5, 6],
styles: [
{
styleId: 6,
rules: [
{ rule: '.blue-btn { border: 1px solid green }' }
]
}
]
},
timestamp: now + 850,
},
];

export default events;
25 changes: 23 additions & 2 deletions packages/rrweb/test/replayer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,7 @@ describe('replayer', function () {
var replayer = new Replayer(events,{showDebug:true});
replayer.play();
`);
await page.waitForTimeout(600);
await page.waitForTimeout(900);
const iframe = await page.$('iframe');
const contentDocument = await iframe!.contentFrame()!;
const colorRGBMap = {
Expand Down Expand Up @@ -882,13 +882,34 @@ describe('replayer', function () {
).color,
),
).toEqual(colorRGBMap.green);

// check the adopted stylesheet #5 is applied on the shadow dom #4's root
expect(await contentDocument!.evaluate(() =>
window.getComputedStyle(
document
.querySelector('#shadow-host4')!
.shadowRoot!.querySelector('button')!,
).color,
),
).toEqual(colorRGBMap.blue);

// check the adopted stylesheet #6 is applied on the shadow dom #4's root
expect(await contentDocument!.evaluate(() =>
window.getComputedStyle(
document
.querySelector('#shadow-host4')!
.shadowRoot!.querySelector('button')!,
).border,
),
).toEqual(`1px solid ${colorRGBMap.green}`);

};
await checkCorrectness();

// To test the correctness of replaying adopted stylesheet events in the fast-forward mode.
await page.evaluate('replayer.play(0);');
await waitForRAF(page);
await page.evaluate('replayer.pause(600);');
await page.evaluate('replayer.pause(900);');
await checkCorrectness();
});

Expand Down
10 changes: 6 additions & 4 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,14 +451,16 @@ export type styleSheetRuleParam = {

export type styleSheetRuleCallback = (s: styleSheetRuleParam) => void;

export type styleParam = {
styleId: number;
rules: styleSheetAddRule[];
};

export type adoptedStyleSheetParam = {
// id indicates the node id of document or shadow DOMs' host element.
id: number;
// New CSSStyleSheets which have never appeared before.
styles?: {
styleId: number;
rules: styleSheetAddRule[];
}[];
styles?: styleParam[];
// StyleSheet ids to be adopted.
styleIds: number[];
};
Expand Down

0 comments on commit a2feb20

Please sign in to comment.