diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 794a61b34c..df567b3ae1 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -40,6 +40,7 @@ import { mouseMovePos, IWindow, canvasMutationCommand, + textMutation, } from '../types'; import { createMirror, @@ -171,11 +172,17 @@ export class Replayer { this.virtualStyleRulesMap = new Map(); this.emitter.on(ReplayerEvents.Flush, () => { - const { scrollMap, inputMap } = this.treeIndex.flush(); + const { scrollMap, inputMap, mutationData } = this.treeIndex.flush(); this.fragmentParentMap.forEach((parent, frag) => this.restoreRealParent(frag, parent), ); + // apply text needs to happen before virtual style rules gets applied + // as it can overwrite the contents of a stylesheet + for (const d of mutationData.texts) { + this.applyText(d, mutationData); + } + for (const node of this.virtualStyleRulesMap.keys()) { // restore css rules of style elements after they are mounted this.restoreNodeSheet(node); @@ -896,7 +903,16 @@ export class Replayer { case IncrementalSource.Mutation: { if (isSync) { d.adds.forEach((m) => this.treeIndex.add(m)); - d.texts.forEach((m) => this.treeIndex.text(m)); + d.texts.forEach((m) => { + const target = this.mirror.getNode(m.id); + const parent = (target?.parentNode as unknown) as INode | null; + // remove any style rules that pending + // for stylesheets where the contents get replaced + if (parent && this.virtualStyleRulesMap.has(parent)) + this.virtualStyleRulesMap.delete(parent); + + this.treeIndex.text(m); + }); d.attributes.forEach((m) => this.treeIndex.attribute(m)); d.removes.forEach((m) => this.treeIndex.remove(m, this.mirror)); } @@ -1677,6 +1693,18 @@ export class Replayer { } } + private applyText(d: textMutation, mutation: mutationData) { + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(mutation, d.id); + } + try { + ((target as Node) as HTMLElement).textContent = d.value; + } catch (error) { + // for safe + } + } + private legacy_resolveMissingNode( map: missingNodeMap, parent: Node, diff --git a/packages/rrweb/test/__snapshots__/replayer.test.ts.snap b/packages/rrweb/test/__snapshots__/replayer.test.ts.snap index 677562cfae..c471f9d958 100644 --- a/packages/rrweb/test/__snapshots__/replayer.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/replayer.test.ts.snap @@ -56,27 +56,27 @@ html.rrweb-paused *, html.rrweb-paused ::before, html.rrweb-paused ::after { ani file-cid-1 @charset \\"utf-8\\"; -.c011xx { padding: 1.3125rem; flex: 0 0 auto; width: 100%; } +.css-added-at-500 { padding: 1.3125rem; flex: 0 0 auto; width: 100%; } file-cid-2 @charset \\"utf-8\\"; -.c01x { opacity: 1; transform: translateX(0px); } +.css-added-at-200-overwritten-at-3000 { opacity: 1; transform: translateX(0px); } -.css-added-at-400 { border: 1px solid blue; } +.css-added-at-400-overwritten-at-3000 { border: 1px solid blue; } file-cid-3 @charset \\"utf-8\\"; -.css-1uxxxx3 { position: fixed; top: 0px; right: 0px; left: 4rem; z-index: 15; flex-shrink: 0; height: 0.25rem; overflow: hidden; background-color: rgb(17, 171, 209); } +.css-added-at-200 { position: fixed; top: 0px; right: 0px; left: 4rem; z-index: 15; flex-shrink: 0; height: 0.25rem; overflow: hidden; background-color: rgb(17, 171, 209); } -.css-1c9xxxx { height: 0.25rem; background-color: rgb(190, 232, 242); opacity: 0; transition: opacity 0.5s ease 0s; } +.css-added-at-200.alt { height: 0.25rem; background-color: rgb(190, 232, 242); opacity: 0; transition: opacity 0.5s ease 0s; } .css-added-at-1000-deleted-at-2500 { display: flex; flex-direction: column; min-width: 60rem; min-height: 100vh; color: blue; } -.css-lsxxx { padding-left: 4rem; } +.css-added-at-200.alt2 { padding-left: 4rem; } " `; diff --git a/packages/rrweb/test/events/style-sheet-rule-events.ts b/packages/rrweb/test/events/style-sheet-rule-events.ts index 0e26673916..0536ea56e3 100644 --- a/packages/rrweb/test/events/style-sheet-rule-events.ts +++ b/packages/rrweb/test/events/style-sheet-rule-events.ts @@ -54,7 +54,7 @@ const events: eventWithTime[] = [ type: 3, isStyle: true, textContent: - '\n.c01x {\n opacity: 1;\n transform: translateX(0);\n}\n', + '\n.css-added-at-200-overwritten-at-3000 {\n opacity: 1;\n transform: translateX(0);\n}\n', }, ], }, @@ -64,7 +64,7 @@ const events: eventWithTime[] = [ tagName: 'style', attributes: { _cssText: - '.css-1uxxxx3 { position: fixed; top: 0px; right: 0px; left: 4rem; z-index: 15; flex-shrink: 0; height: 0.25rem; overflow: hidden; background-color: rgb(17, 171, 209); }.css-1c9xxxx { height: 0.25rem; background-color: rgb(190, 232, 242); opacity: 0; transition: opacity 0.5s ease 0s; }.css-lsxxx { padding-left: 4rem; }', + '.css-added-at-200 { position: fixed; top: 0px; right: 0px; left: 4rem; z-index: 15; flex-shrink: 0; height: 0.25rem; overflow: hidden; background-color: rgb(17, 171, 209); }.css-added-at-200.alt { height: 0.25rem; background-color: rgb(190, 232, 242); opacity: 0; transition: opacity 0.5s ease 0s; }.css-added-at-200.alt2 { padding-left: 4rem; }', 'data-emotion': 'css', }, childNodes: [ @@ -111,7 +111,8 @@ const events: eventWithTime[] = [ id: 101, adds: [ { - rule: '.css-added-at-400{border: 1px solid blue;}', + rule: + '.css-added-at-400-overwritten-at-3000 {border: 1px solid blue;}', index: 1, }, ], @@ -141,7 +142,7 @@ const events: eventWithTime[] = [ type: 3, isStyle: true, textContent: - '\n.c011xx {\n padding: 1.3125rem;\n flex: none;\n width: 100%;\n}\n', + '\n.css-added-at-500 {\n padding: 1.3125rem;\n flex: none;\n width: 100%;\n}\n', }, nextId: null, parentId: 255, @@ -184,6 +185,37 @@ const events: eventWithTime[] = [ type: EventType.IncrementalSnapshot, timestamp: now + 2500, }, + // overwrite all contents of stylesheet + { + data: { + texts: [ + { + id: 102, + value: '.all-css-overwritten-at-3000 { color: indigo; }', + }, + ], + attributes: [], + removes: [], + adds: [], + source: IncrementalSource.Mutation, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 3000, + }, + { + data: { + id: 101, + adds: [ + { + rule: '.css-added-at-3100{color:blue;}', + index: 1, + }, + ], + source: IncrementalSource.StyleSheetRule, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 3100, + }, ]; export default events; diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index cd35fb2a51..b563732f81 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -223,6 +223,36 @@ describe('replayer', function () { expect(result).toEqual(false); }); + it("should overwrite all StyleSheetRules by replacing style element's textContent while fast-forwarding", async () => { + await page.evaluate(`events = ${JSON.stringify(styleSheetRuleEvents)}`); + const result = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events); + replayer.pause(3500); + const rules = [...replayer.iframe.contentDocument.styleSheets].map( + (sheet) => [...sheet.rules], + ).flat(); + rules.some((x) => x.selectorText === '.css-added-at-200-overwritten-at-3000'); + `); + + expect(result).toEqual(false); + }); + + it('should apply fast-forwarded StyleSheetRules that came after stylesheet textContent overwrite', async () => { + await page.evaluate(`events = ${JSON.stringify(styleSheetRuleEvents)}`); + const result = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events); + replayer.pause(3500); + const rules = [...replayer.iframe.contentDocument.styleSheets].map( + (sheet) => [...sheet.rules], + ).flat(); + rules.some((x) => x.selectorText === '.css-added-at-3100'); + `); + + expect(result).toEqual(true); + }); + it('can fast-forward mutation events containing nested iframe elements', async () => { await page.evaluate(` events = ${JSON.stringify(iframeEvents)};