From dbb6fa77dce160bf3427938a2eb5dc1df0f52c7d Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 10 Dec 2024 13:49:04 -0500 Subject: [PATCH] fix: remote CSS does not get rebuilt properly (#226) This fixes an issue where inlined CSS from a remotely loaded `` does not get applied properly due to object reference mutation. --- packages/rrweb/src/replay/index.ts | 25 +++-- .../test/__snapshots__/replayer.test.ts.snap | 67 ++++++++++++++ packages/rrweb/test/replayer.test.ts | 18 ++++ packages/rrweb/test/utils.ts | 92 +++++++++++++++++++ 4 files changed, 192 insertions(+), 10 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 5dacdc104d..a80e1de592 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1792,17 +1792,22 @@ export class Replayer { const newSn = mirror.getMeta( target as Node & RRNode, ) as serializedElementNodeWithId; - Object.assign( - newSn.attributes, - mutation.attributes as attributes, + const newNode = buildNodeWithSN( + { + ...newSn, + attributes: { + ...newSn.attributes, + ...(mutation.attributes as attributes), + }, + }, + { + doc: target.ownerDocument as Document, // can be Document or RRDocument + mirror: mirror as Mirror, + skipChild: true, + hackCss: true, + cache: this.cache, + }, ); - const newNode = buildNodeWithSN(newSn, { - doc: target.ownerDocument as Document, // can be Document or RRDocument - mirror: mirror as Mirror, - skipChild: true, - hackCss: true, - cache: this.cache, - }); const siblingNode = target.nextSibling; const parentNode = target.parentNode; if (newNode && parentNode) { diff --git a/packages/rrweb/test/__snapshots__/replayer.test.ts.snap b/packages/rrweb/test/__snapshots__/replayer.test.ts.snap index c4edd6d540..1682d09ed5 100644 --- a/packages/rrweb/test/__snapshots__/replayer.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/replayer.test.ts.snap @@ -158,6 +158,73 @@ file-cid-3 " `; +exports[`replayer > can handle remote stylesheets 1`] = ` +"file-frame-2 + + + + + +
+
+ +
+ + + + +file-frame-3 + + + + + + + + + + +file-cid-0 +@charset \\"utf-8\\"; + +.rr-block { background: currentcolor; } + +noscript { display: none !important; } + +html.rrweb-paused *, html.rrweb-paused ::before, html.rrweb-paused ::after { animation-play-state: paused !important; } + + +file-cid-1 +@charset \\"utf-8\\"; + +.OverlayDrawer-modal-187 { } + +.OverlayDrawer-paper-188 { width: 100%; } + +@media (min-width: 48em) { + .OverlayDrawer-paper-188 { width: 38rem; } +} + +@media (min-width: 48em) { +} + +@media (min-width: 48em) { +} +" +`; + exports[`replayer > can handle removing style elements 1`] = ` "file-frame-1 diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index 96e2012b15..c38ec356da 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -8,6 +8,7 @@ import { launchPuppeteer, sampleEvents as events, sampleStyleSheetRemoveEvents as stylesheetRemoveEvents, + sampleRemoteStyleSheetEvents as remoteStyleSheetEvents, waitForRAF, } from './utils'; import styleSheetRuleEvents from './events/style-sheet-rule-events'; @@ -209,6 +210,23 @@ describe('replayer', function () { await assertDomSnapshot(page); }); + it('can handle remote stylesheets', async () => { + await page.evaluate(`events = ${JSON.stringify(remoteStyleSheetEvents)}`); + const actionLength = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events); + replayer.play(2500); + replayer['timer']['actions'].length; + `); + expect(actionLength).toEqual( + remoteStyleSheetEvents.filter( + (e) => e.timestamp - remoteStyleSheetEvents[0].timestamp >= 2500, + ).length, + ); + + await assertDomSnapshot(page); + }); + it('can fast forward selection events', async () => { await page.evaluate(`events = ${JSON.stringify(selectionEvents)}`); diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 50b39ccb99..9699543a4a 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -561,6 +561,98 @@ export const sampleStyleSheetRemoveEvents: eventWithTime[] = [ }, ]; +export const sampleRemoteStyleSheetEvents: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 1000, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1000, + height: 800, + }, + timestamp: now + 1000, + }, + { + type: EventType.FullSnapshot, + data: { + node: { + type: 0, + childNodes: [ + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'link', + attributes: { + rel: 'stylesheet', + href: '', + }, + childNodes: [], + id: 4, + }, + ], + id: 3, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + id: 6, + }, + ], + id: 2, + }, + ], + id: 1, + }, + initialOffset: { + top: 0, + left: 0, + }, + }, + timestamp: now + 1000, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [ + { + id: 4, + attributes: { + href: null, + rel: null, + _cssText: + '.OverlayDrawer-modal-187 { }.OverlayDrawer-paper-188 { width: 100%; }@media (min-width: 48em) {\n .OverlayDrawer-paper-188 { width: 38rem; }\n}@media (min-width: 48em) {\n}@media (min-width: 48em) {\n}', + }, + }, + ], + removes: [], + adds: [], + }, + timestamp: now + 2000, + }, +]; + export const polyfillWebGLGlobals = () => { // polyfill as jsdom does not have support for these classes // consider replacing with https://www.npmjs.com/package/canvas