Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

preserve adopted styles from nodes being removed when virtual dom is in use #36

Merged
merged 2 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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/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 {
styleSheetRuleData,
styleDeclarationData,
adoptedStyleSheetData,
styleParam
} from '@rrweb/types';
import {
polyfill,
Expand Down Expand Up @@ -152,6 +153,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 adoptedStyleMap: 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 @@ -337,6 +341,7 @@ export class Replayer {
this.firstFullSnapshot = null;
this.mirror.reset();
this.styleMirror.reset();
this.adoptedStyleMap = new Map();
this.mediaManager.reset();
});

Expand Down Expand Up @@ -557,6 +562,7 @@ export class Replayer {
this.mirror.reset();
this.styleMirror.reset();
this.mediaManager.reset();
this.adoptedStyleMap = new Map();
this.config.root.removeChild(this.wrapper);
this.emitter.emit(ReplayerEvents.Destroy);
}
Expand Down Expand Up @@ -699,6 +705,7 @@ export class Replayer {
}
this.mediaManager.reset();
this.styleMirror.reset();
this.adoptedStyleMap = new Map();
this.rebuildFullSnapshot(event, isSync);
this.iframe.contentWindow?.scrollTo(event.data.initialOffset);
};
Expand Down Expand Up @@ -2070,7 +2077,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.adoptedStyleMap.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.adoptedStyleMap.get(styleId);
if (style) styles.push(style);
this.adoptedStyleMap.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 @@ -827,7 +827,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 @@ -881,13 +881,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 @@ -459,14 +459,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
Loading