diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 1401347353..ae7f05f01c 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -298,11 +298,17 @@ export function needMaskingText( return true; } } else { - (node as HTMLElement).classList.forEach((className) => { + // tslint:disable-next-line: prefer-for-of + for ( + let eIndex = 0; + eIndex < (node as HTMLElement).classList.length; + eIndex++ + ) { + const className = (node as HTMLElement).classList[eIndex]; if (maskTextClass.test(className)) { return true; } - }); + } } if (maskTextSelector) { if ((node as HTMLElement).matches(maskTextSelector)) { @@ -379,7 +385,7 @@ function serializeNode( maskInputOptions: MaskInputOptions; maskTextFn: MaskTextFn | undefined; maskInputFn: MaskInputFn | undefined; - dataURLOptions?: DataURLOptions, + dataURLOptions?: DataURLOptions; inlineImages: boolean; recordCanvas: boolean; keepIframeSrcFn: KeepIframeSrcFn; @@ -516,17 +522,26 @@ function serializeNode( if ((n as ICanvas).__context === '2d') { // only record this on 2d canvas if (!is2DCanvasBlank(n as HTMLCanvasElement)) { - attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL(dataURLOptions.type, dataURLOptions.quality); + attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL( + dataURLOptions.type, + dataURLOptions.quality, + ); } } else if (!('__context' in n)) { // context is unknown, better not call getContext to trigger it - const canvasDataURL = (n as HTMLCanvasElement).toDataURL(dataURLOptions.type, dataURLOptions.quality); + const canvasDataURL = (n as HTMLCanvasElement).toDataURL( + dataURLOptions.type, + dataURLOptions.quality, + ); // create blank canvas of same dimensions const blankCanvas = document.createElement('canvas'); blankCanvas.width = (n as HTMLCanvasElement).width; blankCanvas.height = (n as HTMLCanvasElement).height; - const blankCanvasDataURL = blankCanvas.toDataURL(dataURLOptions.type, dataURLOptions.quality); + const blankCanvasDataURL = blankCanvas.toDataURL( + dataURLOptions.type, + dataURLOptions.quality, + ); // no need to save dataURL if it's the same as blank canvas if (canvasDataURL !== blankCanvasDataURL) { @@ -548,7 +563,10 @@ function serializeNode( canvasService!.width = image.naturalWidth; canvasService!.height = image.naturalHeight; canvasCtx!.drawImage(image, 0, 0); - attributes.rr_dataURL = canvasService!.toDataURL(dataURLOptions.type, dataURLOptions.quality); + attributes.rr_dataURL = canvasService!.toDataURL( + dataURLOptions.type, + dataURLOptions.quality, + ); } catch (err) { console.warn( `Cannot inline img src=${image.currentSrc}! Error: ${err}`, @@ -974,7 +992,7 @@ function snapshot( maskTextFn?: MaskTextFn; maskInputFn?: MaskTextFn; slimDOM?: boolean | SlimDOMOptions; - dataURLOptions?: DataURLOptions, + dataURLOptions?: DataURLOptions; inlineImages?: boolean; recordCanvas?: boolean; preserveWhiteSpace?: boolean; 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 2790f9f865..c8fd3fb493 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -227,6 +227,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 scroll events', async () => { await page.evaluate(` events = ${JSON.stringify(scrollEvents)}; @@ -246,7 +276,6 @@ describe('replayer', function () { ), ).toEqual(0); - const delay = 50; // restart the replayer await page.evaluate('replayer.play(0);'); await waitForRAF(page);