From 9c7e0580f1dd578b95c507da7bde58e16f05388a Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sun, 14 Aug 2022 19:26:13 +1000 Subject: [PATCH 01/43] test(recording side): add test case for adopted stylesheets in shadow doms and iframe --- packages/rrweb/test/record.test.ts | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index f9b46aea02..ea2e39ee1c 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -570,6 +570,68 @@ describe('record', function (this: ISuite) { assertSnapshot(ctx.events); }); + + it('captures adopted stylesheets in shadow doms and iframe', async () => { + ctx.events = []; + await ctx.page.evaluate(() => { + document.body.innerHTML = ` +
div in outermost document
+
+
+ + `; + + const sheet = new CSSStyleSheet(); + // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet + sheet.replaceSync('div { color: yellow; }'); + // Add stylesheet to a document. + // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet + document.adoptedStyleSheets = [sheet]; + + // Add stylesheet to a shadow host. + const host = document.querySelector('#shadow-host1'); + const shadow = host!.attachShadow({ mode: 'open' }); + shadow.innerHTML = + '
div in shadow dom 1
span in shadow dom 1'; + const sheet2 = new CSSStyleSheet(); + // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet + sheet2.replaceSync('span { color: red; }'); + // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet + shadow.adoptedStyleSheets = [sheet, sheet2]; + + // Add stylesheet to an IFrame document. + const iframe = document.querySelector('iframe'); + // @ts-ignore - TS doesn't support `CSSStyleSheet constructor` yet + const sheet3 = new iframe!.contentWindow!.CSSStyleSheet(); + sheet3.replaceSync('h1 { color: blue; }'); + // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet + iframe.contentDocument.adoptedStyleSheets = [sheet3]; + // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet + const ele = iframe.contentDocument.createElement('h1'); + ele.innerText = 'h1 in iframe'; + iframe!.contentDocument!.body.appendChild(ele); + + ((window as unknown) as IWindow).rrweb.record({ + emit: ((window.top as unknown) as IWindow).emit, + }); + + // Make incremental changes to shadow dom. + setTimeout(() => { + const host = document.querySelector('#shadow-host2'); + const shadow = host!.attachShadow({ mode: 'open' }); + shadow.innerHTML = + '
div in shadow dom 2
span in shadow dom 2'; + const sheet3 = new CSSStyleSheet(); + // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet + sheet3.replaceSync('span { color: green; }'); + // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet + shadow.adoptedStyleSheets = [sheet, sheet3]; + }, 10); + }); + await waitForRAF(ctx.page); // wait till events get sent + + assertSnapshot(ctx.events); + }); }); describe('record iframes', function (this: ISuite) { From 4c2ea18ae6f6913f7af3b9a2d5a24061b7d76e39 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 18 Aug 2022 12:11:56 +1000 Subject: [PATCH 02/43] add type definition for adopted StyleSheets --- packages/rrweb/src/types.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 7a1779147c..7b137babf8 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -92,6 +92,7 @@ export enum IncrementalSource { Drag, StyleDeclaration, Selection, + AdoptStyleSheet, } export type mutationData = { @@ -147,6 +148,10 @@ export type selectionData = { source: IncrementalSource.Selection; } & selectionParam; +export type adoptedStyleSheetData = { + source: IncrementalSource.AdoptStyleSheet; +} & adoptedStyleSheetParam; + export type incrementalData = | mutationData | mousemoveData @@ -159,7 +164,8 @@ export type incrementalData = | canvasMutationData | fontData | selectionData - | styleDeclarationData; + | styleDeclarationData + | adoptedStyleSheetData; export type event = | domContentLoadedEvent @@ -505,9 +511,18 @@ export type styleSheetDeleteRule = { }; export type styleSheetRuleParam = { - id: number; + id?: number; + styleId?: number; removes?: styleSheetDeleteRule[]; adds?: styleSheetAddRule[]; + replace?: string; + replaceSync?: string; +}; + +export type adoptedStyleSheetParam = { + // 0 indicates the outermost document, other numbers indicate id of IFrame elements or shadow DOMs' hosts. + id: number; + styleId: number; }; export type styleSheetRuleCallback = (s: styleSheetRuleParam) => void; From 755d9f7a7f451f7904e9ad8c91f0389cdc919491 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 19 Aug 2022 10:57:35 +1000 Subject: [PATCH 03/43] create a StyleSheet Mirror --- packages/rrweb/src/utils.ts | 24 ++++++++++++++ packages/rrweb/test/util.test.ts | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 packages/rrweb/test/util.test.ts diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 2d0838b6f3..59a2b51518 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -448,3 +448,27 @@ export function uniqueTextMutations(mutations: textMutation[]): textMutation[] { return uniqueMutations; } + +export class StyleSheetMirror { + private id = 1; + private styleIDMap = new WeakMap(); + + getId(stylesheet: CSSStyleSheet): number { + return this.styleIDMap.get(stylesheet) ?? -1; + } + + has(stylesheet: CSSStyleSheet): boolean { + return this.styleIDMap.has(stylesheet); + } + + add(stylesheet: CSSStyleSheet): boolean { + if (this.has(stylesheet)) return false; + this.styleIDMap.set(stylesheet, this.id++); + return true; + } + + reset(): void { + this.styleIDMap = new WeakMap(); + this.id = 1; + } +} diff --git a/packages/rrweb/test/util.test.ts b/packages/rrweb/test/util.test.ts new file mode 100644 index 0000000000..9f6cce1781 --- /dev/null +++ b/packages/rrweb/test/util.test.ts @@ -0,0 +1,56 @@ +/** + * @jest-environment jsdom + */ +import { StyleSheetMirror } from '../src/utils'; + +describe('Utilities for other modules', () => { + describe('StyleSheetMirror', () => { + it('should create a StyleSheetMirror', () => { + const mirror = new StyleSheetMirror(); + expect(mirror).toBeDefined(); + expect(mirror.add).toBeDefined(); + expect(mirror.has).toBeDefined(); + expect(mirror.reset).toBeDefined(); + expect(mirror.getId).toBeDefined(); + }); + + it('can add CSSStyleSheet into the mirror', () => { + const mirror = new StyleSheetMirror(); + const styleSheet = new CSSStyleSheet(); + expect(mirror.has(styleSheet)).toBeFalsy(); + expect(mirror.add(styleSheet)).toBeTruthy(); + expect(mirror.has(styleSheet)).toBeTruthy(); + expect(mirror.add(styleSheet)).toBeFalsy(); + + for (let i = 0; i < 10; i++) { + const styleSheet = new CSSStyleSheet(); + expect(mirror.has(styleSheet)).toBeFalsy(); + expect(mirror.add(styleSheet)).toBeTruthy(); + expect(mirror.has(styleSheet)).toBeTruthy(); + } + }); + + it('can get the id from the mirror', () => { + const mirror = new StyleSheetMirror(); + for (let i = 0; i < 10; i++) { + const styleSheet = new CSSStyleSheet(); + mirror.add(styleSheet); + expect(mirror.getId(styleSheet)).toBe(i + 1); + } + expect(mirror.getId(new CSSStyleSheet())).toBe(-1); + }); + + it('can reset the mirror', () => { + const mirror = new StyleSheetMirror(); + const styleList: CSSStyleSheet[] = []; + for (let i = 0; i < 10; i++) { + const styleSheet = new CSSStyleSheet(); + mirror.add(styleSheet); + expect(mirror.getId(styleSheet)).toBe(i + 1); + styleList.push(styleSheet); + } + expect(mirror.reset()).toBeUndefined(); + for (let s of styleList) expect(mirror.has(s)).toBeFalsy(); + }); + }); +}); From 5ef9a77b9ad8f834768a4533c44ca4900ddb4f87 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 19 Aug 2022 12:26:19 +1000 Subject: [PATCH 04/43] enable to record the outermost document's adoptedStyleSheet --- packages/rrweb-snapshot/src/snapshot.ts | 26 +----------- packages/rrweb-snapshot/src/utils.ts | 25 +++++++++++ .../src/record/constructableStyleSheets.d.ts | 10 +++++ packages/rrweb/src/record/index.ts | 42 ++++++++++++++----- packages/rrweb/src/utils.ts | 9 ++-- packages/rrweb/test/util.test.ts | 7 ++-- packages/rrweb/tsconfig.json | 3 +- 7 files changed, 80 insertions(+), 42 deletions(-) create mode 100644 packages/rrweb/src/record/constructableStyleSheets.d.ts diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 38df3b6954..303bd1b7ba 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -19,6 +19,7 @@ import { isShadowRoot, maskInputValue, isNativeShadowDom, + getCssRulesString, } from './utils'; let _id = 1; @@ -47,31 +48,6 @@ function getValidTagName(element: HTMLElement): string { return processedTagName; } -function getCssRulesString(s: CSSStyleSheet): string | null { - try { - const rules = s.rules || s.cssRules; - return rules ? Array.from(rules).map(getCssRuleString).join('') : null; - } catch (error) { - return null; - } -} - -function getCssRuleString(rule: CSSRule): string { - let cssStringified = rule.cssText; - if (isCSSImportRule(rule)) { - try { - cssStringified = getCssRulesString(rule.styleSheet) || cssStringified; - } catch { - // ignore - } - } - return cssStringified; -} - -function isCSSImportRule(rule: CSSRule): rule is CSSImportRule { - return 'styleSheet' in rule; -} - function stringifyStyleSheet(sheet: CSSStyleSheet): string { return sheet.cssRules ? Array.from(sheet.cssRules) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 1c151a1e25..afc008eef8 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -24,6 +24,31 @@ export function isNativeShadowDom(shadowRoot: ShadowRoot) { return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]'; } +export function getCssRulesString(s: CSSStyleSheet): string | null { + try { + const rules = s.rules || s.cssRules; + return rules ? Array.from(rules).map(getCssRuleString).join('') : null; + } catch (error) { + return null; + } +} + +export function getCssRuleString(rule: CSSRule): string { + let cssStringified = rule.cssText; + if (isCSSImportRule(rule)) { + try { + cssStringified = getCssRulesString(rule.styleSheet) || cssStringified; + } catch { + // ignore + } + } + return cssStringified; +} + +export function isCSSImportRule(rule: CSSRule): rule is CSSImportRule { + return 'styleSheet' in rule; +} + export class Mirror implements IMirror { private idNodeMap: idNodeMap = new Map(); private nodeMetaMap: nodeMetaMap = new WeakMap(); diff --git a/packages/rrweb/src/record/constructableStyleSheets.d.ts b/packages/rrweb/src/record/constructableStyleSheets.d.ts new file mode 100644 index 0000000000..42d2360798 --- /dev/null +++ b/packages/rrweb/src/record/constructableStyleSheets.d.ts @@ -0,0 +1,10 @@ +// This informs the TS compiler about constructed stylesheets. +// It can be removed when this is fixed: https://github.com/Microsoft/TypeScript/issues/30022 +declare interface DocumentOrShadowRoot { + adoptedStyleSheets: CSSStyleSheet[]; +} + +declare interface CSSStyleSheet { + replace(text: string): void; + replaceSync(text: string): void; +} diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 756c21c56b..24b51c1273 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -3,6 +3,7 @@ import { MaskInputOptions, SlimDOMOptions, createMirror, + getCssRuleString, } from 'rrweb-snapshot'; import { initObservers, mutationBuffers } from './observer'; import { @@ -13,6 +14,7 @@ import { hasShadowRoot, isSerializedIframe, isSerializedStylesheet, + StyleSheetMirror, } from '../utils'; import { EventType, @@ -24,6 +26,7 @@ import { mutationCallbackParam, scrollCallback, canvasMutationParam, + styleSheetRuleParam, } from '../types'; import { IframeManager } from './iframe-manager'; import { ShadowDomManager } from './shadow-dom-manager'; @@ -42,6 +45,7 @@ let wrappedEmit!: (e: eventWithTime, isCheckout?: boolean) => void; let takeFullSnapshot!: (isCheckout?: boolean) => void; const mirror = createMirror(); +const styleMirror = new StyleSheetMirror(); function record( options: recordOptions = {}, ): listenerHandler | undefined { @@ -213,6 +217,17 @@ function record( }), ); + const wrappedStyleSheetRulesEmit = (r: styleSheetRuleParam) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + ...r, + }, + }), + ); + const iframeManager = new IframeManager({ mutationCb: wrappedMutationEmit, }); @@ -301,6 +316,22 @@ function record( keepIframeSrcFn, }); + // Some old browsers don't support adoptedStyleSheets. + if (document.adoptedStyleSheets) { + for (const sheet of document.adoptedStyleSheets) { + const styleId = styleMirror.add(sheet); + const rules = Array.from(sheet.rules || CSSRule); + wrappedStyleSheetRulesEmit({ + adds: rules.map((r) => { + return { + rule: getCssRuleString(r), + }; + }), + styleId, + }); + } + } + if (!node) { return console.warn('Failed to snapshot the document'); } @@ -400,16 +431,7 @@ function record( }, }), ), - styleSheetRuleCb: (r) => - wrappedEmit( - wrapEvent({ - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.StyleSheetRule, - ...r, - }, - }), - ), + styleSheetRuleCb: wrappedStyleSheetRulesEmit, styleDeclarationCb: (r) => wrappedEmit( wrapEvent({ diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 59a2b51518..6778f7717b 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -461,10 +461,13 @@ export class StyleSheetMirror { return this.styleIDMap.has(stylesheet); } - add(stylesheet: CSSStyleSheet): boolean { - if (this.has(stylesheet)) return false; + /** + * @returns If the stylesheet is in the mirror, returns the id of the stylesheet. If not, return the new assigned id. + */ + add(stylesheet: CSSStyleSheet): number { + if (this.has(stylesheet)) return this.getId(stylesheet); this.styleIDMap.set(stylesheet, this.id++); - return true; + return this.id - 1; } reset(): void { diff --git a/packages/rrweb/test/util.test.ts b/packages/rrweb/test/util.test.ts index 9f6cce1781..bb34efcebb 100644 --- a/packages/rrweb/test/util.test.ts +++ b/packages/rrweb/test/util.test.ts @@ -18,14 +18,15 @@ describe('Utilities for other modules', () => { const mirror = new StyleSheetMirror(); const styleSheet = new CSSStyleSheet(); expect(mirror.has(styleSheet)).toBeFalsy(); - expect(mirror.add(styleSheet)).toBeTruthy(); + expect(mirror.add(styleSheet)).toEqual(1); expect(mirror.has(styleSheet)).toBeTruthy(); - expect(mirror.add(styleSheet)).toBeFalsy(); + // This stylesheet has been added before so just return its assigned id. + expect(mirror.add(styleSheet)).toEqual(1); for (let i = 0; i < 10; i++) { const styleSheet = new CSSStyleSheet(); expect(mirror.has(styleSheet)).toBeFalsy(); - expect(mirror.add(styleSheet)).toBeTruthy(); + expect(mirror.add(styleSheet)).toEqual(i + 2); expect(mirror.has(styleSheet)).toBeTruthy(); } }); diff --git a/packages/rrweb/tsconfig.json b/packages/rrweb/tsconfig.json index c95913f748..f1e5bba3e5 100644 --- a/packages/rrweb/tsconfig.json +++ b/packages/rrweb/tsconfig.json @@ -19,6 +19,7 @@ "src", "scripts", "node_modules/@types/css-font-loading-module/index.d.ts", - "node_modules/@types/jest-image-snapshot/index.d.ts" + "node_modules/@types/jest-image-snapshot/index.d.ts", + "constructableStyleSheets.d.ts" ] } From 2dd6ba01a3e43d22eb9d18b3f120cb5b657ee7bb Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 19 Aug 2022 14:07:45 +1000 Subject: [PATCH 05/43] enable to serialize all stylesheets in documents (iframe) and shadow roots --- packages/rrweb/src/record/iframe-manager.ts | 13 +++- packages/rrweb/src/record/index.ts | 42 +++++------ packages/rrweb/src/record/mutation.ts | 6 +- .../rrweb/src/record/shadow-dom-manager.ts | 4 ++ .../rrweb/src/record/stylesheet-manager.ts | 70 ++++++++++++++----- packages/rrweb/src/types.ts | 10 +-- 6 files changed, 98 insertions(+), 47 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index db2d85d44d..56b61a089a 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -1,13 +1,19 @@ import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot'; import type { mutationCallBack } from '../types'; +import type { StylesheetManager } from './stylesheet-manager'; export class IframeManager { private iframes: WeakMap = new WeakMap(); private mutationCb: mutationCallBack; private loadListener?: (iframeEl: HTMLIFrameElement) => unknown; + private stylesheetManager: StylesheetManager; - constructor(options: { mutationCb: mutationCallBack }) { + constructor(options: { + mutationCb: mutationCallBack; + stylesheetManager: StylesheetManager; + }) { this.mutationCb = options.mutationCb; + this.stylesheetManager = options.stylesheetManager; } public addIframe(iframeEl: HTMLIFrameElement) { @@ -37,5 +43,10 @@ export class IframeManager { isAttachIframe: true, }); this.loadListener?.(iframeEl); + + if (iframeEl.contentDocument && iframeEl.contentDocument.adoptedStyleSheets) + this.stylesheetManager.adoptStyleSheets( + iframeEl.contentDocument.adoptedStyleSheets, + ); } } diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 24b51c1273..48adf444ff 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -3,7 +3,6 @@ import { MaskInputOptions, SlimDOMOptions, createMirror, - getCssRuleString, } from 'rrweb-snapshot'; import { initObservers, mutationBuffers } from './observer'; import { @@ -14,7 +13,6 @@ import { hasShadowRoot, isSerializedIframe, isSerializedStylesheet, - StyleSheetMirror, } from '../utils'; import { EventType, @@ -27,6 +25,7 @@ import { scrollCallback, canvasMutationParam, styleSheetRuleParam, + adoptedStyleSheetParam, } from '../types'; import { IframeManager } from './iframe-manager'; import { ShadowDomManager } from './shadow-dom-manager'; @@ -45,7 +44,6 @@ let wrappedEmit!: (e: eventWithTime, isCheckout?: boolean) => void; let takeFullSnapshot!: (isCheckout?: boolean) => void; const mirror = createMirror(); -const styleMirror = new StyleSheetMirror(); function record( options: recordOptions = {}, ): listenerHandler | undefined { @@ -228,12 +226,26 @@ function record( }), ); - const iframeManager = new IframeManager({ + const wrappedAdoptedStyleSheetEmit = (a: adoptedStyleSheetParam) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.AdoptedStyleSheet, + ...a, + }, + }), + ); + + const stylesheetManager = new StylesheetManager({ mutationCb: wrappedMutationEmit, + styleRulesCb: wrappedStyleSheetRulesEmit, + adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit, }); - const stylesheetManager = new StylesheetManager({ + const iframeManager = new IframeManager({ mutationCb: wrappedMutationEmit, + stylesheetManager: stylesheetManager, }); const canvasManager = new CanvasManager({ @@ -300,7 +312,7 @@ function record( iframeManager.addIframe(n as HTMLIFrameElement); } if (isSerializedStylesheet(n, mirror)) { - stylesheetManager.addStylesheet(n as HTMLLinkElement); + stylesheetManager.trackLinkElement(n as HTMLLinkElement); } if (hasShadowRoot(n)) { shadowDomManager.addShadowRoot(n.shadowRoot, document); @@ -311,26 +323,14 @@ function record( shadowDomManager.observeAttachShadow(iframe); }, onStylesheetLoad: (linkEl, childSn) => { - stylesheetManager.attachStylesheet(linkEl, childSn, mirror); + stylesheetManager.attachLinkElement(linkEl, childSn, mirror); }, keepIframeSrcFn, }); // Some old browsers don't support adoptedStyleSheets. - if (document.adoptedStyleSheets) { - for (const sheet of document.adoptedStyleSheets) { - const styleId = styleMirror.add(sheet); - const rules = Array.from(sheet.rules || CSSRule); - wrappedStyleSheetRulesEmit({ - adds: rules.map((r) => { - return { - rule: getCssRuleString(r), - }; - }), - styleId, - }); - } - } + document.adoptedStyleSheets && + stylesheetManager.adoptStyleSheets(document.adoptedStyleSheets); if (!node) { return console.warn('Failed to snapshot the document'); diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index df3b3017c2..0345b0935e 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -308,7 +308,9 @@ export default class MutationBuffer { this.iframeManager.addIframe(currentN as HTMLIFrameElement); } if (isSerializedStylesheet(currentN, this.mirror)) { - this.stylesheetManager.addStylesheet(currentN as HTMLLinkElement); + this.stylesheetManager.trackLinkElement( + currentN as HTMLLinkElement, + ); } if (hasShadowRoot(n)) { this.shadowDomManager.addShadowRoot(n.shadowRoot, document); @@ -319,7 +321,7 @@ export default class MutationBuffer { this.shadowDomManager.observeAttachShadow(iframe); }, onStylesheetLoad: (link, childSn) => { - this.stylesheetManager.attachStylesheet(link, childSn, this.mirror); + this.stylesheetManager.attachLinkElement(link, childSn, this.mirror); }, }); if (sn) { diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 90a1d321d6..f5d216c30b 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -73,6 +73,10 @@ export class ShadowDomManager { doc: (shadowRoot as unknown) as Document, mirror: this.mirror, }); + if (shadowRoot.adoptedStyleSheets) + this.bypassOptions.stylesheetManager.adoptStyleSheets( + shadowRoot.adoptedStyleSheets, + ); } /** diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts index 01a69744d7..acfc724329 100644 --- a/packages/rrweb/src/record/stylesheet-manager.ts +++ b/packages/rrweb/src/record/stylesheet-manager.ts @@ -1,29 +1,30 @@ import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot'; -import type { mutationCallBack } from '../types'; +import { getCssRuleString } from 'rrweb-snapshot'; +import type { + adoptedStyleSheetCallback, + mutationCallBack, + styleSheetRuleCallback, +} from '../types'; +import { StyleSheetMirror } from '../utils'; export class StylesheetManager { - private trackedStylesheets: WeakSet = new WeakSet(); + private trackedLinkElements: WeakSet = new WeakSet(); private mutationCb: mutationCallBack; + private styleRulesCb: styleSheetRuleCallback; + private adoptedStyleSheetCb: adoptedStyleSheetCallback; + public styleMirror = new StyleSheetMirror(); - constructor(options: { mutationCb: mutationCallBack }) { + constructor(options: { + mutationCb: mutationCallBack; + styleRulesCb: styleSheetRuleCallback; + adoptedStyleSheetCb: adoptedStyleSheetCallback; + }) { this.mutationCb = options.mutationCb; + this.styleRulesCb = options.styleRulesCb; + this.adoptedStyleSheetCb = options.adoptedStyleSheetCb; } - public addStylesheet(linkEl: HTMLLinkElement) { - if (this.trackedStylesheets.has(linkEl)) return; - - this.trackedStylesheets.add(linkEl); - this.trackStylesheet(linkEl); - } - - // TODO: take snapshot on stylesheet reload by applying event listener - private trackStylesheet(linkEl: HTMLLinkElement) { - // linkEl.addEventListener('load', () => { - // // re-loaded, maybe take another snapshot? - // }); - } - - public attachStylesheet( + public attachLinkElement( linkEl: HTMLLinkElement, childSn: serializedNodeWithId, mirror: Mirror, @@ -40,6 +41,37 @@ export class StylesheetManager { texts: [], attributes: [], }); - this.addStylesheet(linkEl); + + this.trackLinkElement(linkEl); + } + + public trackLinkElement(linkEl: HTMLLinkElement) { + if (this.trackedLinkElements.has(linkEl)) return; + + this.trackedLinkElements.add(linkEl); + this.trackStylesheetInLinkElement(linkEl); + } + + public adoptStyleSheets(sheets: CSSStyleSheet[]) { + for (const sheet of sheets) { + if (this.styleMirror.has(sheet)) continue; + const styleId = this.styleMirror.add(sheet); + const rules = Array.from(sheet.rules || CSSRule); + this.styleRulesCb({ + adds: rules.map((r) => { + return { + rule: getCssRuleString(r), + }; + }), + styleId, + }); + } + } + + // TODO: take snapshot on stylesheet reload by applying event listener + private trackStylesheetInLinkElement(linkEl: HTMLLinkElement) { + // linkEl.addEventListener('load', () => { + // // re-loaded, maybe take another snapshot? + // }); } } diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 7b137babf8..3a2fa20888 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -92,7 +92,7 @@ export enum IncrementalSource { Drag, StyleDeclaration, Selection, - AdoptStyleSheet, + AdoptedStyleSheet, } export type mutationData = { @@ -149,7 +149,7 @@ export type selectionData = { } & selectionParam; export type adoptedStyleSheetData = { - source: IncrementalSource.AdoptStyleSheet; + source: IncrementalSource.AdoptedStyleSheet; } & adoptedStyleSheetParam; export type incrementalData = @@ -519,13 +519,15 @@ export type styleSheetRuleParam = { replaceSync?: string; }; +export type styleSheetRuleCallback = (s: styleSheetRuleParam) => void; + export type adoptedStyleSheetParam = { // 0 indicates the outermost document, other numbers indicate id of IFrame elements or shadow DOMs' hosts. id: number; - styleId: number; + styleIds: number[]; }; -export type styleSheetRuleCallback = (s: styleSheetRuleParam) => void; +export type adoptedStyleSheetCallback = (a: adoptedStyleSheetParam) => void; export type styleDeclarationParam = { id: number; From 20279ae1e2f64624263d852600b44148904c9d88 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 19 Aug 2022 15:29:55 +1000 Subject: [PATCH 06/43] enable to record adopted stylesheets while building full snapshot --- packages/rrweb/src/record/iframe-manager.ts | 1 + packages/rrweb/src/record/index.ts | 5 ++- .../rrweb/src/record/shadow-dom-manager.ts | 1 + .../rrweb/src/record/stylesheet-manager.ts | 33 ++++++++++++------- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 56b61a089a..5e19999410 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -47,6 +47,7 @@ export class IframeManager { if (iframeEl.contentDocument && iframeEl.contentDocument.adoptedStyleSheets) this.stylesheetManager.adoptStyleSheets( iframeEl.contentDocument.adoptedStyleSheets, + mirror.getId(iframeEl), ); } } diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 48adf444ff..b56b33880d 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -330,7 +330,10 @@ function record( // Some old browsers don't support adoptedStyleSheets. document.adoptedStyleSheets && - stylesheetManager.adoptStyleSheets(document.adoptedStyleSheets); + stylesheetManager.adoptStyleSheets( + document.adoptedStyleSheets, + mirror.getId(document), + ); if (!node) { return console.warn('Failed to snapshot the document'); diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index f5d216c30b..819b35bb2c 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -76,6 +76,7 @@ export class ShadowDomManager { if (shadowRoot.adoptedStyleSheets) this.bypassOptions.stylesheetManager.adoptStyleSheets( shadowRoot.adoptedStyleSheets, + this.mirror.getId(shadowRoot.host), ); } diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts index acfc724329..15f7140b5d 100644 --- a/packages/rrweb/src/record/stylesheet-manager.ts +++ b/packages/rrweb/src/record/stylesheet-manager.ts @@ -52,20 +52,29 @@ export class StylesheetManager { this.trackStylesheetInLinkElement(linkEl); } - public adoptStyleSheets(sheets: CSSStyleSheet[]) { + public adoptStyleSheets(sheets: CSSStyleSheet[], hostId: number) { + if (sheets.length === 0) return; + const adoptedStyleSheetData = { + id: hostId, + styleIds: [] as number[], + }; for (const sheet of sheets) { - if (this.styleMirror.has(sheet)) continue; - const styleId = this.styleMirror.add(sheet); - const rules = Array.from(sheet.rules || CSSRule); - this.styleRulesCb({ - adds: rules.map((r) => { - return { - rule: getCssRuleString(r), - }; - }), - styleId, - }); + let styleId; + if (!this.styleMirror.has(sheet)) { + styleId = this.styleMirror.add(sheet); + const rules = Array.from(sheet.rules || CSSRule); + this.styleRulesCb({ + adds: rules.map((r) => { + return { + rule: getCssRuleString(r), + }; + }), + styleId, + }); + } else styleId = this.styleMirror.getId(sheet); + adoptedStyleSheetData.styleIds.push(styleId); } + this.adoptedStyleSheetCb(adoptedStyleSheetData); } // TODO: take snapshot on stylesheet reload by applying event listener From 2fbe2e253e40964aa484bc2e3b5327255ab4904f Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 20 Aug 2022 14:45:57 +1000 Subject: [PATCH 07/43] test: add test case for mutations on adoptedStyleSheets --- packages/rrweb/test/record.test.ts | 57 +++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index ea2e39ee1c..d72451bf15 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -453,6 +453,62 @@ describe('record', function (this: ISuite) { assertSnapshot(ctx.events); }); + it('captures mutations on adopted stylesheets', async () => { + await ctx.page.evaluate(() => { + document.body.innerHTML = ` +
div in outermost document
+ + `; + + const sheet = new CSSStyleSheet(); + // Add stylesheet to a document. + // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet + document.adoptedStyleSheets = [sheet]; + + const iframe = document.querySelector('iframe'); + // @ts-ignore - TS doesn't support `CSSStyleSheet constructor` yet + const sheet2 = new iframe!.contentWindow!.CSSStyleSheet(); + // Add stylesheet to an IFrame document. + // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet + iframe.contentDocument.adoptedStyleSheets = [sheet2]; + iframe!.contentDocument!.body.innerHTML = '

h1 in iframe

'; + + const { record } = ((window as unknown) as IWindow).rrweb; + record({ + emit: ((window as unknown) as IWindow).emit, + }); + + setTimeout(() => { + // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet + sheet.replace('div { color: yellow; }'); + sheet2.replace('h1 { color: blue; }'); + }, 0); + + setTimeout(() => { + // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet + sheet.replaceSync('div { display: inline ; }'); + sheet2.replaceSync('h1 { font-size: large; }'); + }, 5); + + setTimeout(() => { + (sheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green'); + (sheet.cssRules[0] as CSSStyleRule).style.removeProperty('display'); + (sheet2.cssRules[0] as CSSStyleRule).style.setProperty( + 'font-size', + 'medium', + ); + sheet2.insertRule('h2 { color: red; }'); + }, 10); + + setTimeout(() => { + sheet.insertRule('body { border: 2px solid blue; }', 1); + sheet2.deleteRule(0); + }, 15); + }); + await waitForRAF(ctx.page); + assertSnapshot(ctx.events); + }); + it('captures stylesheets in iframes with `blob:` url', async () => { await ctx.page.evaluate(() => { const iframe = document.createElement('iframe'); @@ -572,7 +628,6 @@ describe('record', function (this: ISuite) { }); it('captures adopted stylesheets in shadow doms and iframe', async () => { - ctx.events = []; await ctx.page.evaluate(() => { document.body.innerHTML = `
div in outermost document
From 655e15872ca69063e848c59025669dbb8130fbc4 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 20 Aug 2022 15:29:11 +1000 Subject: [PATCH 08/43] defer to record adoptedStyleSheets to avoid create events before full snapshot --- packages/rrweb/src/record/iframe-manager.ts | 5 ++++- packages/rrweb/src/record/index.ts | 14 +++++++------- packages/rrweb/src/record/shadow-dom-manager.ts | 13 ++++++++----- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 5e19999410..93934608e2 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -44,7 +44,10 @@ export class IframeManager { }); this.loadListener?.(iframeEl); - if (iframeEl.contentDocument && iframeEl.contentDocument.adoptedStyleSheets) + if ( + iframeEl.contentDocument && + iframeEl.contentDocument.adoptedStyleSheets?.length > 0 + ) this.stylesheetManager.adoptStyleSheets( iframeEl.contentDocument.adoptedStyleSheets, mirror.getId(iframeEl), diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index b56b33880d..369504a132 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -328,13 +328,6 @@ function record( keepIframeSrcFn, }); - // Some old browsers don't support adoptedStyleSheets. - document.adoptedStyleSheets && - stylesheetManager.adoptStyleSheets( - document.adoptedStyleSheets, - mirror.getId(document), - ); - if (!node) { return console.warn('Failed to snapshot the document'); } @@ -364,6 +357,13 @@ function record( }), ); mutationBuffers.forEach((buf) => buf.unlock()); // generate & emit any mutations that happened during snapshotting, as can now apply against the newly built mirror + + // Some old browsers don't support adoptedStyleSheets. + document.adoptedStyleSheets?.length > 0 && + stylesheetManager.adoptStyleSheets( + document.adoptedStyleSheets, + mirror.getId(document), + ); }; try { diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 819b35bb2c..dfa8c7b093 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -73,11 +73,14 @@ export class ShadowDomManager { doc: (shadowRoot as unknown) as Document, mirror: this.mirror, }); - if (shadowRoot.adoptedStyleSheets) - this.bypassOptions.stylesheetManager.adoptStyleSheets( - shadowRoot.adoptedStyleSheets, - this.mirror.getId(shadowRoot.host), - ); + if (shadowRoot.adoptedStyleSheets?.length > 0) + // Defer this to avoid adoptedStyleSheet events being created before the full snapshot is created. + Promise.resolve().then(() => { + this.bypassOptions.stylesheetManager.adoptStyleSheets( + shadowRoot.adoptedStyleSheets, + this.mirror.getId(shadowRoot.host), + ); + }); } /** From cdf9a375cc3fe447a093680f4e45dbc7ea409a39 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 20 Aug 2022 18:16:13 +1000 Subject: [PATCH 09/43] feat: enable to track the mutation of AdoptedStyleSheets --- packages/rrweb/src/record/observer.ts | 152 +++++++-- .../rrweb/src/record/shadow-dom-manager.ts | 2 +- packages/rrweb/src/types.ts | 3 +- .../test/__snapshots__/record.test.ts.snap | 304 ++++++++++++++++++ packages/rrweb/test/record.test.ts | 1 + 5 files changed, 439 insertions(+), 23 deletions(-) diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 9350906cec..2f88ccdabb 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -1,4 +1,4 @@ -import { MaskInputOptions, maskInputValue } from 'rrweb-snapshot'; +import { MaskInputOptions, maskInputValue, Mirror } from 'rrweb-snapshot'; import type { FontFaceSet } from 'css-font-loading-module'; import { throttle, @@ -9,6 +9,7 @@ import { isBlocked, isTouchEvent, patch, + StyleSheetMirror, } from '../utils'; import { mutationCallBack, @@ -484,8 +485,8 @@ function getNestedCSSRulePositions(rule: CSSRule): number[] { ); const index = rules.indexOf(childRule); pos.unshift(index); - } else { - const rules = Array.from(childRule.parentStyleSheet!.cssRules); + } else if (childRule.parentStyleSheet) { + const rules = Array.from(childRule.parentStyleSheet.cssRules); const index = rules.indexOf(childRule); pos.unshift(index); } @@ -494,8 +495,30 @@ function getNestedCSSRulePositions(rule: CSSRule): number[] { return recurse(rule, positions); } +/** + * For StyleSheets in Element, this function retrieves id of its host element. + * For adopted StyleSheets, this function retrieves its styleId from a styleMirror. + */ +function getIdOrStyleId( + sheet: CSSStyleSheet | undefined | null, + mirror: Mirror, + styleMirror: StyleSheetMirror, +): { + styleId?: number; + id?: number; +} { + let id, styleId; + if (!sheet) return {}; + if (sheet.ownerNode) id = mirror.getId(sheet.ownerNode as Node); + else styleId = styleMirror.getId(sheet); + return { + styleId, + id, + }; +} + function initStyleSheetObserver( - { styleSheetRuleCb, mirror }: observerParam, + { styleSheetRuleCb, mirror, stylesheetManager }: observerParam, { win }: { win: IWindow }, ): listenerHandler { // eslint-disable-next-line @typescript-eslint/unbound-method @@ -505,10 +528,16 @@ function initStyleSheetObserver( rule: string, index?: number, ) { - const id = mirror.getId(this.ownerNode as Node); - if (id !== -1) { + const { id, styleId } = getIdOrStyleId( + this, + mirror, + stylesheetManager.styleMirror, + ); + + if (id !== -1 || styleId !== -1) { styleSheetRuleCb({ id, + styleId, adds: [{ rule, index }], }); } @@ -521,16 +550,66 @@ function initStyleSheetObserver( this: CSSStyleSheet, index: number, ) { - const id = mirror.getId(this.ownerNode as Node); - if (id !== -1) { + const { id, styleId } = getIdOrStyleId( + this, + mirror, + stylesheetManager.styleMirror, + ); + + if (id !== -1 || styleId !== -1) { styleSheetRuleCb({ id, + styleId, removes: [{ index }], }); } return deleteRule.apply(this, [index]); }; + // eslint-disable-next-line @typescript-eslint/unbound-method + const replace = win.CSSStyleSheet.prototype.replace; + win.CSSStyleSheet.prototype.replace = function ( + this: CSSStyleSheet, + text: string, + ) { + const { id, styleId } = getIdOrStyleId( + this, + mirror, + stylesheetManager.styleMirror, + ); + + if (id !== -1 || styleId !== -1) { + styleSheetRuleCb({ + id, + styleId, + replace: text, + }); + } + return replace.apply(this, [text]); + }; + + // eslint-disable-next-line @typescript-eslint/unbound-method + const replaceSync = win.CSSStyleSheet.prototype.replaceSync; + win.CSSStyleSheet.prototype.replaceSync = function ( + this: CSSStyleSheet, + text: string, + ) { + const { id, styleId } = getIdOrStyleId( + this, + mirror, + stylesheetManager.styleMirror, + ); + + if (id !== -1 || styleId !== -1) { + styleSheetRuleCb({ + id, + styleId, + replaceSync: text, + }); + } + return replaceSync.apply(this, [text]); + }; + const supportedNestedCSSRuleTypes: { [key: string]: GroupingCSSRuleTypes; } = {}; @@ -567,12 +646,21 @@ function initStyleSheetObserver( deleteRule: type.prototype.deleteRule, }; - type.prototype.insertRule = function (rule: string, index?: number) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const id = mirror.getId(this.parentStyleSheet.ownerNode as Node); - if (id !== -1) { + type.prototype.insertRule = function ( + this: CSSGroupingRule, + rule: string, + index?: number, + ) { + const { id, styleId } = getIdOrStyleId( + this.parentStyleSheet, + mirror, + stylesheetManager.styleMirror, + ); + + if (id !== -1 || styleId !== -1) { styleSheetRuleCb({ id, + styleId, adds: [ { rule, @@ -587,12 +675,20 @@ function initStyleSheetObserver( return unmodifiedFunctions[typeKey].insertRule.apply(this, [rule, index]); }; - type.prototype.deleteRule = function (index: number) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const id = mirror.getId(this.parentStyleSheet.ownerNode as Node); - if (id !== -1) { + type.prototype.deleteRule = function ( + this: CSSGroupingRule, + index: number, + ) { + const { id, styleId } = getIdOrStyleId( + this.parentStyleSheet, + mirror, + stylesheetManager.styleMirror, + ); + + if (id !== -1 || styleId !== -1) { styleSheetRuleCb({ id, + styleId, removes: [ { index: [...getNestedCSSRulePositions(this as CSSRule), index] }, ], @@ -605,6 +701,8 @@ function initStyleSheetObserver( return () => { win.CSSStyleSheet.prototype.insertRule = insertRule; win.CSSStyleSheet.prototype.deleteRule = deleteRule; + win.CSSStyleSheet.prototype.replace = replace; + win.CSSStyleSheet.prototype.replaceSync = replaceSync; Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule; type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule; @@ -613,7 +711,7 @@ function initStyleSheetObserver( } function initStyleDeclarationObserver( - { styleDeclarationCb, mirror }: observerParam, + { styleDeclarationCb, mirror, stylesheetManager }: observerParam, { win }: { win: IWindow }, ): listenerHandler { // eslint-disable-next-line @typescript-eslint/unbound-method @@ -624,15 +722,21 @@ function initStyleDeclarationObserver( value: string, priority: string, ) { - const id = mirror.getId(this.parentRule?.parentStyleSheet?.ownerNode); - if (id !== -1) { + const { id, styleId } = getIdOrStyleId( + this.parentRule?.parentStyleSheet, + mirror, + stylesheetManager.styleMirror, + ); + if (id !== -1 || styleId !== -1) { styleDeclarationCb({ id, + styleId, set: { property, value, priority, }, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion index: getNestedCSSRulePositions(this.parentRule!), }); } @@ -645,13 +749,19 @@ function initStyleDeclarationObserver( this: CSSStyleDeclaration, property: string, ) { - const id = mirror.getId(this.parentRule?.parentStyleSheet?.ownerNode); - if (id !== -1) { + const { id, styleId } = getIdOrStyleId( + this.parentRule?.parentStyleSheet, + mirror, + stylesheetManager.styleMirror, + ); + if (id !== -1 || styleId !== -1) { styleDeclarationCb({ id, + styleId, remove: { property, }, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion index: getNestedCSSRulePositions(this.parentRule!), }); } diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index dfa8c7b093..0e6a3f4f66 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -75,7 +75,7 @@ export class ShadowDomManager { }); if (shadowRoot.adoptedStyleSheets?.length > 0) // Defer this to avoid adoptedStyleSheet events being created before the full snapshot is created. - Promise.resolve().then(() => { + void Promise.resolve().then(() => { this.bypassOptions.stylesheetManager.adoptStyleSheets( shadowRoot.adoptedStyleSheets, this.mirror.getId(shadowRoot.host), diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 3a2fa20888..016d60fcb3 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -530,7 +530,8 @@ export type adoptedStyleSheetParam = { export type adoptedStyleSheetCallback = (a: adoptedStyleSheetParam) => void; export type styleDeclarationParam = { - id: number; + id?: number; + styleId?: number; index: number[]; set?: { property: string; diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index 2c2b7c8d0d..70ce41209c 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -327,6 +327,310 @@ exports[`record captures inserted style text nodes correctly 1`] = ` ]" `; +exports[`record captures mutations on adopted stylesheets 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"div in outermost document\\", + \\"id\\": 8 + } + ], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\" \\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"adds\\": [], + \\"styleId\\": 1 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 1, + \\"styleIds\\": [ + 1 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 10, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 12, + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"h1\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"h1 in iframe\\", + \\"rootId\\": 12, + \\"id\\": 17 + } + ], + \\"rootId\\": 12, + \\"id\\": 16 + } + ], + \\"rootId\\": 12, + \\"id\\": 15 + } + ], + \\"rootId\\": 12, + \\"id\\": 13 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 12 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"adds\\": [], + \\"styleId\\": 2 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 10, + \\"styleIds\\": [ + 2 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 1, + \\"replace\\": \\"div { color: yellow; }\\" + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 2, + \\"replace\\": \\"h1 { color: blue; }\\" + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 1, + \\"replaceSync\\": \\"div { display: inline ; }\\" + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 2, + \\"replaceSync\\": \\"h1 { font-size: large; }\\" + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"styleId\\": 1, + \\"set\\": { + \\"property\\": \\"color\\", + \\"value\\": \\"green\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"styleId\\": 1, + \\"remove\\": { + \\"property\\": \\"display\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"styleId\\": 2, + \\"set\\": { + \\"property\\": \\"font-size\\", + \\"value\\": \\"medium\\", + \\"priority\\": \\"important\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 2, + \\"adds\\": [ + { + \\"rule\\": \\"h2 { color: red; }\\" + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 1, + \\"adds\\": [ + { + \\"rule\\": \\"body { border: 2px solid blue; }\\", + \\"index\\": 1 + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 2, + \\"removes\\": [ + { + \\"index\\": 0 + } + ] + } + } +]" +`; + exports[`record captures nested stylesheet rules 1`] = ` "[ { diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index d72451bf15..084554c250 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -496,6 +496,7 @@ describe('record', function (this: ISuite) { (sheet2.cssRules[0] as CSSStyleRule).style.setProperty( 'font-size', 'medium', + 'important', ); sheet2.insertRule('h2 { color: red; }'); }, 10); From 8002fb4b9f89da121bfb772c57ab14d92dcec317 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 24 Aug 2022 11:45:26 +1000 Subject: [PATCH 10/43] Merge branch 'fix-shadowdom-record' into construct-style --- packages/rrweb/src/record/mutation.ts | 20 +++++++++-- .../__snapshots__/integration.test.ts.snap | 34 +++++++++++++------ packages/rrweb/test/integration.test.ts | 3 +- packages/rrweb/test/utils.ts | 9 ----- 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 0345b0935e..17609ce3cb 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -372,15 +372,31 @@ export default class MutationBuffer { } if (!node) { for (let index = addList.length - 1; index >= 0; index--) { - const _node = addList.get(index)!; + const _node = addList.get(index); // ensure _node is defined before attempting to find value if (_node) { const parentId = this.mirror.getId(_node.value.parentNode); const nextId = getNextId(_node.value); - if (parentId !== -1 && nextId !== -1) { + + if (nextId === -1) continue; + // nextId !== -1 && parentId !== -1 + else if (parentId !== -1) { node = _node; break; } + // nextId !== -1 && parentId === -1 This branch can happen if the node is the child of shadow root + else { + const nodeInShadowDom = _node.value; + // Get the host of the shadow dom and treat it as parent node. + const shadowHost: Element | null = nodeInShadowDom.getRootNode + ? (nodeInShadowDom.getRootNode() as ShadowRoot)?.host + : null; + const parentId = this.mirror.getId(shadowHost); + if (parentId !== -1) { + node = _node; + break; + } + } } } } diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index b25764af26..61516c91ee 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -12053,6 +12053,18 @@ exports[`record integration tests should record shadow DOM 1`] = ` \\"id\\": 43, \\"isShadow\\": true } + }, + { + \\"parentId\\": 22, + \\"nextId\\": 43, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 44, + \\"isShadow\\": true + } } ] } @@ -12073,7 +12085,7 @@ exports[`record integration tests should record shadow DOM 1`] = ` \\"tagName\\": \\"p\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 44 + \\"id\\": 45 } } ] @@ -12104,12 +12116,12 @@ exports[`record integration tests should record shadow DOM 1`] = ` \\"removes\\": [], \\"adds\\": [ { - \\"parentId\\": 44, + \\"parentId\\": 45, \\"nextId\\": null, \\"node\\": { \\"type\\": 3, \\"textContent\\": \\"hi\\", - \\"id\\": 45 + \\"id\\": 46 } } ] @@ -12123,18 +12135,18 @@ exports[`record integration tests should record shadow DOM 1`] = ` \\"attributes\\": [], \\"removes\\": [ { - \\"parentId\\": 44, - \\"id\\": 45 + \\"parentId\\": 45, + \\"id\\": 46 } ], \\"adds\\": [ { - \\"parentId\\": 44, + \\"parentId\\": 45, \\"nextId\\": null, \\"node\\": { \\"type\\": 3, \\"textContent\\": \\"123\\", - \\"id\\": 46 + \\"id\\": 47 } } ] @@ -12149,24 +12161,24 @@ exports[`record integration tests should record shadow DOM 1`] = ` \\"removes\\": [], \\"adds\\": [ { - \\"parentId\\": 44, + \\"parentId\\": 45, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"span\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 47, + \\"id\\": 48, \\"isShadow\\": true } }, { - \\"parentId\\": 47, + \\"parentId\\": 48, \\"nextId\\": null, \\"node\\": { \\"type\\": 3, \\"textContent\\": \\"nested shadow dom\\", - \\"id\\": 48 + \\"id\\": 49 } } ] diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 0da1ebdb12..733e03180b 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -561,6 +561,7 @@ describe('record integration tests', function (this: ISuite) { const el = document.querySelector('.my-element') as HTMLDivElement; const shadowRoot = el.shadowRoot as ShadowRoot; + shadowRoot.appendChild(document.createElement('span')); shadowRoot.appendChild(document.createElement('p')); sleep(1) .then(() => { @@ -667,7 +668,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = await page.evaluate('window.snapshots'); assertSnapshot(snapshots); }); - + // https://github.com/webcomponents/polyfills/tree/master/packages/shadydom it('should record shadow doms polyfilled by shadydom', async () => { const page: puppeteer.Page = await browser.newPage(); diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 5fd249b52a..f132a11c51 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -81,7 +81,6 @@ export const startServer = (defaultPort: number = 3030) => resolve(s); }) .on('error', (e) => { - console.log('port in use, trying next one'); s.listen().on('listening', () => { resolve(s); }); @@ -152,14 +151,6 @@ function stringifySnapshots(snapshots: eventWithTime[]): string { coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript } } - - // strip blob:urls as they are different every time - console.log( - a.attributes.src, - 'src' in a.attributes && - a.attributes.src && - typeof a.attributes.src === 'string', - ); }); s.data.adds.forEach((add) => { if (add.node.type === NodeType.Element) { From 25a033ffdcdfb5f446e7feb5c2b00f1e40bcc8b3 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 24 Aug 2022 11:52:45 +1000 Subject: [PATCH 11/43] fix: incorrect id conditional judgement --- packages/rrweb/src/record/observer.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 2f88ccdabb..cc2b751ca3 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -534,7 +534,7 @@ function initStyleSheetObserver( stylesheetManager.styleMirror, ); - if (id !== -1 || styleId !== -1) { + if ((id && id !== -1) || (styleId && styleId !== -1)) { styleSheetRuleCb({ id, styleId, @@ -556,7 +556,7 @@ function initStyleSheetObserver( stylesheetManager.styleMirror, ); - if (id !== -1 || styleId !== -1) { + if ((id && id !== -1) || (styleId && styleId !== -1)) { styleSheetRuleCb({ id, styleId, @@ -578,7 +578,7 @@ function initStyleSheetObserver( stylesheetManager.styleMirror, ); - if (id !== -1 || styleId !== -1) { + if ((id && id !== -1) || (styleId && styleId !== -1)) { styleSheetRuleCb({ id, styleId, @@ -600,7 +600,8 @@ function initStyleSheetObserver( stylesheetManager.styleMirror, ); - if (id !== -1 || styleId !== -1) { + if ((id && id !== -1) || (styleId && styleId !== -1)) { + console.log(id, styleId); styleSheetRuleCb({ id, styleId, @@ -657,7 +658,7 @@ function initStyleSheetObserver( stylesheetManager.styleMirror, ); - if (id !== -1 || styleId !== -1) { + if ((id && id !== -1) || (styleId && styleId !== -1)) { styleSheetRuleCb({ id, styleId, @@ -685,7 +686,7 @@ function initStyleSheetObserver( stylesheetManager.styleMirror, ); - if (id !== -1 || styleId !== -1) { + if ((id && id !== -1) || (styleId && styleId !== -1)) { styleSheetRuleCb({ id, styleId, @@ -727,7 +728,7 @@ function initStyleDeclarationObserver( mirror, stylesheetManager.styleMirror, ); - if (id !== -1 || styleId !== -1) { + if ((id && id !== -1) || (styleId && styleId !== -1)) { styleDeclarationCb({ id, styleId, @@ -754,7 +755,7 @@ function initStyleDeclarationObserver( mirror, stylesheetManager.styleMirror, ); - if (id !== -1 || styleId !== -1) { + if ((id && id !== -1) || (styleId && styleId !== -1)) { styleDeclarationCb({ id, styleId, From 05072dcf87d6f16f859b36492134f516da9e8fac Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 24 Aug 2022 14:46:09 +1000 Subject: [PATCH 12/43] test: add a test case for replaying side --- .../rrweb/test/events/adopted-style-sheet.ts | 368 ++++++++++++++++++ packages/rrweb/test/replayer.test.ts | 49 +++ 2 files changed, 417 insertions(+) create mode 100644 packages/rrweb/test/events/adopted-style-sheet.ts diff --git a/packages/rrweb/test/events/adopted-style-sheet.ts b/packages/rrweb/test/events/adopted-style-sheet.ts new file mode 100644 index 0000000000..8a0c62fc29 --- /dev/null +++ b/packages/rrweb/test/events/adopted-style-sheet.ts @@ -0,0 +1,368 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); + +const events: eventWithTime[] = [ + { type: EventType.DomContentLoaded, data: {}, timestamp: now }, + { + type: EventType.Meta, + data: { + href: 'about:blank', + width: 1920, + height: 1080, + }, + timestamp: now + 100, + }, + { + type: EventType.FullSnapshot, + data: { + node: { + type: 0, + childNodes: [ + { + type: 1, + name: 'html', + publicId: '', + systemId: '', + id: 2, + }, + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + id: 4, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'n ', + id: 6, + }, + { + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'div in outermost document', + id: 8, + }, + ], + id: 7, + }, + { + type: 3, + textContent: 'n ', + id: 9, + }, + { + type: 2, + tagName: 'div', + attributes: { + id: 'shadow-host1', + }, + childNodes: [ + { + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'div in shadow dom 1', + id: 12, + }, + ], + id: 11, + isShadow: true, + }, + { + type: 2, + tagName: 'span', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'span in shadow dom 1', + id: 14, + }, + ], + id: 13, + isShadow: true, + }, + ], + id: 10, + isShadowHost: true, + }, + { + type: 3, + textContent: 'n ', + id: 15, + }, + { + type: 2, + tagName: 'div', + attributes: { + id: 'shadow-host2', + }, + childNodes: [], + id: 16, + }, + { + type: 3, + textContent: 'n ', + id: 17, + }, + { + type: 2, + tagName: 'iframe', + attributes: {}, + childNodes: [], + id: 18, + }, + { + type: 3, + textContent: 'n ', + id: 19, + }, + ], + id: 5, + }, + ], + id: 3, + }, + ], + id: 1, + }, + initialOffset: { + left: 0, + top: 0, + }, + }, + timestamp: now + 100, + }, + // Create a unattached stylesheet #1 object at 200ms + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + adds: [ + { + rule: 'div { color: yellow; }', + }, + ], + styleId: 1, + }, + timestamp: now + 200, + }, + // Adopt the stylesheet #1 on document + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.AdoptedStyleSheet, + id: 1, + styleIds: [1], + }, + timestamp: now + 210, + }, + // Add an IFrame element + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 18, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + rootId: 20, + id: 22, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'h1', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'h1 in iframe', + rootId: 20, + id: 25, + }, + ], + rootId: 20, + id: 24, + }, + ], + rootId: 20, + id: 23, + }, + ], + rootId: 20, + id: 21, + }, + ], + compatMode: 'BackCompat', + id: 20, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 250, + }, + // Create a unattached stylesheet #2 object + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + adds: [ + { + rule: 'span { color: red; }', + }, + ], + styleId: 2, + }, + timestamp: now + 300, + }, + // Adopt the stylesheet #2 on a shadow root + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.AdoptedStyleSheet, + id: 10, + styleIds: [1, 2], + }, + timestamp: now + 300, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + adds: [ + { + rule: 'h1 { color: blue; }', + }, + ], + styleId: 3, + }, + timestamp: now + 300, + }, + // Adopt the stylesheet #3 on document of the IFrame element + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.AdoptedStyleSheet, + id: 18, + styleIds: [3], + }, + timestamp: now + 300, + }, + // create a new shadow dom + { + type: EventType.IncrementalSnapshot, + data: { + source: 0, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 16, + nextId: null, + node: { + type: 2, + tagName: 'span', + attributes: {}, + childNodes: [], + id: 26, + isShadow: true, + }, + }, + { + parentId: 26, + nextId: null, + node: { + type: 3, + textContent: 'span in shadow dom 2', + id: 27, + }, + }, + { + parentId: 16, + nextId: 26, + node: { + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [], + id: 28, + isShadow: true, + }, + }, + { + parentId: 28, + nextId: null, + node: { + type: 3, + textContent: 'div in shadow dom 2', + id: 29, + }, + }, + ], + }, + timestamp: now + 500, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + styleId: 4, + adds: [{ rule: 'span { color: green; }' }], + }, + timestamp: now + 550, + }, + // Adopt the stylesheet #4 on the shadow dom + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.AdoptedStyleSheet, + id: 16, + styleIds: [4], + }, + timestamp: now + 550, + }, +]; + +export default events; diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index a2453eb68e..6a06c6fc3a 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -17,6 +17,7 @@ import selectionEvents from './events/selection'; import shadowDomEvents from './events/shadow-dom'; import StyleSheetTextMutation from './events/style-sheet-text-mutation'; import canvasInIframe from './events/canvas-in-iframe'; +import adoptedStyleSheet from './events/adopted-style-sheet'; interface ISuite { code: string; @@ -718,4 +719,52 @@ describe('replayer', function () { wrapper = await page.$(`.${replayerWrapperClassName}`); expect(wrapper).toBeNull(); }); + + it('can fast-forward adopted stylesheet events', async () => { + await page.evaluate(` + events = ${JSON.stringify(adoptedStyleSheet)}; + const { Replayer } = rrweb; + var replayer = new Replayer(events,{showDebug:true}); + replayer.pause(550); + `); + const iframe = await page.$('iframe'); + const contentDocument = await iframe!.contentFrame()!; + // check the adopted stylesheet is applied on the outermost document + expect( + await contentDocument!.$eval( + 'div', + (element) => (element as HTMLElement).style.color, + ), + ).toEqual('yellow'); + + // check the adopted stylesheet is applied on the shadow dom #1's root + expect( + await contentDocument!.evaluate( + () => + document + .querySelector('#shadow-host1') + ?.shadowRoot?.querySelector('span')?.style.color, + ), + ).toEqual('red'); + + // check the adopted stylesheet is applied on document of the IFrame element + expect( + await contentDocument!.$eval( + 'iframe', + (element) => + (element as HTMLIFrameElement).contentDocument?.querySelector('h1') + ?.style.color, + ), + ).toEqual('blue'); + + // check the adopted stylesheet is applied on the shadow dom #2's root + expect( + await contentDocument!.evaluate( + () => + document + .querySelector('#shadow-host2') + ?.shadowRoot?.querySelector('span')?.style.color, + ), + ).toEqual('green'); + }); }); From 0c4a9158652cba55fda82475b82e3613feaa518b Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 25 Aug 2022 19:01:13 +1000 Subject: [PATCH 13/43] tweak the style mirror for replayer --- packages/rrweb/src/utils.ts | 16 +++++++++++++--- packages/rrweb/test/util.test.ts | 21 ++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 6778f7717b..145a52bb3d 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -452,6 +452,7 @@ export function uniqueTextMutations(mutations: textMutation[]): textMutation[] { export class StyleSheetMirror { private id = 1; private styleIDMap = new WeakMap(); + private idStyleMap = new Map(); getId(stylesheet: CSSStyleSheet): number { return this.styleIDMap.get(stylesheet) ?? -1; @@ -464,10 +465,19 @@ export class StyleSheetMirror { /** * @returns If the stylesheet is in the mirror, returns the id of the stylesheet. If not, return the new assigned id. */ - add(stylesheet: CSSStyleSheet): number { + add(stylesheet: CSSStyleSheet, id?: number): number { if (this.has(stylesheet)) return this.getId(stylesheet); - this.styleIDMap.set(stylesheet, this.id++); - return this.id - 1; + let newId: number; + if (id === undefined) { + newId = this.id++; + } else newId = id; + this.styleIDMap.set(stylesheet, newId); + this.idStyleMap.set(newId, stylesheet); + return newId; + } + + getStyle(id: number): CSSStyleSheet | null { + return this.idStyleMap.get(id) || null; } reset(): void { diff --git a/packages/rrweb/test/util.test.ts b/packages/rrweb/test/util.test.ts index bb34efcebb..a9190e721e 100644 --- a/packages/rrweb/test/util.test.ts +++ b/packages/rrweb/test/util.test.ts @@ -14,7 +14,7 @@ describe('Utilities for other modules', () => { expect(mirror.getId).toBeDefined(); }); - it('can add CSSStyleSheet into the mirror', () => { + it('can add CSSStyleSheet into the mirror without ID parameter', () => { const mirror = new StyleSheetMirror(); const styleSheet = new CSSStyleSheet(); expect(mirror.has(styleSheet)).toBeFalsy(); @@ -31,6 +31,16 @@ describe('Utilities for other modules', () => { } }); + it('can add CSSStyleSheet into the mirror with ID parameter', () => { + const mirror = new StyleSheetMirror(); + for (let i = 0; i < 10; i++) { + const styleSheet = new CSSStyleSheet(); + expect(mirror.has(styleSheet)).toBeFalsy(); + expect(mirror.add(styleSheet, i)).toEqual(i); + expect(mirror.has(styleSheet)).toBeTruthy(); + } + }); + it('can get the id from the mirror', () => { const mirror = new StyleSheetMirror(); for (let i = 0; i < 10; i++) { @@ -41,6 +51,15 @@ describe('Utilities for other modules', () => { expect(mirror.getId(new CSSStyleSheet())).toBe(-1); }); + it('can get CSSStyleSheet objects with id', () => { + const mirror = new StyleSheetMirror(); + for (let i = 0; i < 10; i++) { + const styleSheet = new CSSStyleSheet(); + mirror.add(styleSheet); + expect(mirror.getStyle(i + 1)).toBe(styleSheet); + } + }); + it('can reset the mirror', () => { const mirror = new StyleSheetMirror(); const styleList: CSSStyleSheet[] = []; From d3227fbdb2ac696c1b1df44a1cb635ef43f8e300 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 26 Aug 2022 11:26:28 +1000 Subject: [PATCH 14/43] feat: enable to replay adoptedStyleSheet events --- .../rrweb/src/record/stylesheet-manager.ts | 1 + packages/rrweb/src/replay/index.ts | 103 ++++++++++++++---- .../test/__snapshots__/record.test.ts.snap | 6 +- .../rrweb/test/events/adopted-style-sheet.ts | 14 ++- packages/rrweb/test/replayer.test.ts | 101 ++++++++++------- 5 files changed, 159 insertions(+), 66 deletions(-) diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts index 15f7140b5d..e53023d020 100644 --- a/packages/rrweb/src/record/stylesheet-manager.ts +++ b/packages/rrweb/src/record/stylesheet-manager.ts @@ -70,6 +70,7 @@ export class StylesheetManager { }; }), styleId, + id: hostId, }); } else styleId = this.styleMirror.getId(sheet); adoptedStyleSheetData.styleIds.push(styleId); diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index f368bdafc9..c26ba5c22d 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -72,6 +72,7 @@ import { getNestedRule, getPositionsAndIndex, uniqueTextMutations, + StyleSheetMirror, } from '../utils'; import getInjectStyleRules from './styles/inject-style'; import './styles/style.css'; @@ -136,6 +137,9 @@ export class Replayer { private mirror: Mirror = createMirror(); + // Used to track StyleSheetObjects adopted on multiple document hosts. + private styleMirror: StyleSheetMirror = new StyleSheetMirror(); + private firstFullSnapshot: eventWithTime | true | null = null; private newDocumentQueue: addedNodeMutation[] = []; @@ -1178,9 +1182,11 @@ export class Replayer { } case IncrementalSource.StyleSheetRule: { if (this.usingVirtualDom) { - const target = this.virtualDom.mirror.getNode(d.id) as RRStyleElement; + const target = this.virtualDom.mirror.getNode( + d.id!, + ) as RRStyleElement; if (!target) { - return this.debugNodeNotFound(d, d.id); + return this.debugNodeNotFound(d, d.id!); } const rules: VirtualStyleRules = target.rules; d.adds?.forEach(({ rule, index: nestedIndex }) => @@ -1194,17 +1200,49 @@ export class Replayer { rules?.push({ index: nestedIndex, type: StyleRuleType.Remove }), ); } else { - const target = this.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(d, d.id); - } - const styleSheet = (target as HTMLStyleElement).sheet!; + let styleSheet: CSSStyleSheet | null = null; + if (d.styleId) { + styleSheet = this.styleMirror.getStyle(d.styleId); + if (!styleSheet && d.id) { + /** + * Constructed StyleSheet can't share across multiple documents. + * The replayer has to get the correct host window to recreate a StyleSheetObject. + */ + const host = this.mirror.getNode(d.id); + if (host) { + let newStyleSheet: CSSStyleSheet | null = null; + + if (host.nodeName === 'IFRAME') { + const iframeWin = (host as HTMLIFrameElement) + .contentWindow as IWindow; + if (iframeWin) newStyleSheet = new iframeWin.CSSStyleSheet(); + } else if (hasShadowRoot(host)) { + const ownerWindow = host.ownerDocument?.defaultView; + if (ownerWindow) + newStyleSheet = new ownerWindow.CSSStyleSheet(); + } else if (host.nodeName === '#document') { + const ownerWindow = (host as Document).defaultView; + if (ownerWindow) + newStyleSheet = new ownerWindow.CSSStyleSheet(); + } + if (newStyleSheet) { + this.styleMirror.add(newStyleSheet, d.styleId); + styleSheet = newStyleSheet; + } + } + } + } else if (d.id) + styleSheet = + (this.mirror.getNode(d.id) as HTMLStyleElement)?.sheet || null; + + if (!styleSheet) return; d.adds?.forEach(({ rule, index: nestedIndex }) => { try { if (Array.isArray(nestedIndex)) { const { positions, index } = getPositionsAndIndex(nestedIndex); const nestedRule = getNestedRule( - styleSheet.cssRules, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + styleSheet!.cssRules, positions, ); nestedRule.insertRule(rule, index); @@ -1212,8 +1250,10 @@ export class Replayer { const index = nestedIndex === undefined ? undefined - : Math.min(nestedIndex, styleSheet.cssRules.length); - styleSheet.insertRule(rule, index); + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Math.min(nestedIndex, styleSheet!.cssRules.length); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + styleSheet!.insertRule(rule, index); } } catch (e) { /** @@ -1232,7 +1272,8 @@ export class Replayer { if (Array.isArray(nestedIndex)) { const { positions, index } = getPositionsAndIndex(nestedIndex); const nestedRule = getNestedRule( - styleSheet.cssRules, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + styleSheet!.cssRules, positions, ); nestedRule.deleteRule(index || 0); @@ -1250,9 +1291,11 @@ export class Replayer { } case IncrementalSource.StyleDeclaration: { if (this.usingVirtualDom) { - const target = this.virtualDom.mirror.getNode(d.id) as RRStyleElement; + const target = this.virtualDom.mirror.getNode( + d.id!, + ) as RRStyleElement; if (!target) { - return this.debugNodeNotFound(d, d.id); + return this.debugNodeNotFound(d, d.id!); } const rules: VirtualStyleRules = target.rules; d.set && @@ -1268,13 +1311,13 @@ export class Replayer { ...d.remove, }); } else { - const target = (this.mirror.getNode( - d.id, - ) as Node) as HTMLStyleElement; - if (!target) { - return this.debugNodeNotFound(d, d.id); - } - const styleSheet = target.sheet!; + let styleSheet: CSSStyleSheet | null = null; + if (d.id) + styleSheet = + (this.mirror.getNode(d.id) as HTMLStyleElement)?.sheet || null; + else if (d.styleId) styleSheet = this.styleMirror.getStyle(d.styleId); + + if (!styleSheet) return; if (d.set) { const rule = (getNestedRule( styleSheet.rules, @@ -1349,6 +1392,26 @@ export class Replayer { this.applySelection(d); break; } + case IncrementalSource.AdoptedStyleSheet: { + if (isSync) { + // TODO support sync mode + break; + } + const targetHost = this.mirror.getNode(d.id); + if (!targetHost) return; + const stylesToAdopt = d.styleIds + .map((styleId) => this.styleMirror.getStyle(styleId)) + .filter((style) => style !== null) as CSSStyleSheet[]; + if (targetHost.nodeName === 'IFRAME') { + const iframeDoc = (targetHost as HTMLIFrameElement).contentDocument; + if (iframeDoc) iframeDoc.adoptedStyleSheets = stylesToAdopt; + } else if (hasShadowRoot(targetHost)) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (targetHost as HTMLElement).shadowRoot!.adoptedStyleSheets = stylesToAdopt; + else if (targetHost.nodeName === '#document') + (targetHost as Document).adoptedStyleSheets = stylesToAdopt; + break; + } default: } } diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index 70ce41209c..8a76cf1c31 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -422,7 +422,8 @@ exports[`record captures mutations on adopted stylesheets 1`] = ` \\"data\\": { \\"source\\": 8, \\"adds\\": [], - \\"styleId\\": 1 + \\"styleId\\": 1, + \\"id\\": 1 } }, { @@ -504,7 +505,8 @@ exports[`record captures mutations on adopted stylesheets 1`] = ` \\"data\\": { \\"source\\": 8, \\"adds\\": [], - \\"styleId\\": 2 + \\"styleId\\": 2, + \\"id\\": 10 } }, { diff --git a/packages/rrweb/test/events/adopted-style-sheet.ts b/packages/rrweb/test/events/adopted-style-sheet.ts index 8a0c62fc29..4a8c3b6089 100644 --- a/packages/rrweb/test/events/adopted-style-sheet.ts +++ b/packages/rrweb/test/events/adopted-style-sheet.ts @@ -45,7 +45,7 @@ const events: eventWithTime[] = [ childNodes: [ { type: 3, - textContent: 'n ', + textContent: '\n ', id: 6, }, { @@ -63,7 +63,7 @@ const events: eventWithTime[] = [ }, { type: 3, - textContent: 'n ', + textContent: '\n ', id: 9, }, { @@ -107,7 +107,7 @@ const events: eventWithTime[] = [ }, { type: 3, - textContent: 'n ', + textContent: '\n ', id: 15, }, { @@ -121,7 +121,7 @@ const events: eventWithTime[] = [ }, { type: 3, - textContent: 'n ', + textContent: '\n ', id: 17, }, { @@ -133,7 +133,7 @@ const events: eventWithTime[] = [ }, { type: 3, - textContent: 'n ', + textContent: '\n ', id: 19, }, ], @@ -163,6 +163,7 @@ const events: eventWithTime[] = [ }, ], styleId: 1, + id: 1, }, timestamp: now + 200, }, @@ -253,6 +254,7 @@ const events: eventWithTime[] = [ }, ], styleId: 2, + id: 10, }, timestamp: now + 300, }, @@ -276,6 +278,7 @@ const events: eventWithTime[] = [ }, ], styleId: 3, + id: 18, }, timestamp: now + 300, }, @@ -349,6 +352,7 @@ const events: eventWithTime[] = [ data: { source: IncrementalSource.StyleSheetRule, styleId: 4, + id: 16, adds: [{ rule: 'span { color: green; }' }], }, timestamp: now + 550, diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index 6a06c6fc3a..f5f2746614 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -720,51 +720,74 @@ describe('replayer', function () { expect(wrapper).toBeNull(); }); - it('can fast-forward adopted stylesheet events', async () => { + it('can replay adopted stylesheet events', async () => { await page.evaluate(` events = ${JSON.stringify(adoptedStyleSheet)}; const { Replayer } = rrweb; var replayer = new Replayer(events,{showDebug:true}); - replayer.pause(550); + replayer.play(); `); + await page.waitForTimeout(600); const iframe = await page.$('iframe'); const contentDocument = await iframe!.contentFrame()!; - // check the adopted stylesheet is applied on the outermost document - expect( - await contentDocument!.$eval( - 'div', - (element) => (element as HTMLElement).style.color, - ), - ).toEqual('yellow'); - - // check the adopted stylesheet is applied on the shadow dom #1's root - expect( - await contentDocument!.evaluate( - () => - document - .querySelector('#shadow-host1') - ?.shadowRoot?.querySelector('span')?.style.color, - ), - ).toEqual('red'); - - // check the adopted stylesheet is applied on document of the IFrame element - expect( - await contentDocument!.$eval( - 'iframe', - (element) => - (element as HTMLIFrameElement).contentDocument?.querySelector('h1') - ?.style.color, - ), - ).toEqual('blue'); - - // check the adopted stylesheet is applied on the shadow dom #2's root - expect( - await contentDocument!.evaluate( - () => - document - .querySelector('#shadow-host2') - ?.shadowRoot?.querySelector('span')?.style.color, - ), - ).toEqual('green'); + const colorRGBMap = { + yellow: 'rgb(255, 255, 0)', + red: 'rgb(255, 0, 0)', + blue: 'rgb(0, 0, 255)', + green: 'rgb(0, 128, 0)', + }; + const checkCorrectness = async () => { + // check the adopted stylesheet is applied on the outermost document + expect( + await contentDocument!.$eval( + 'div', + (element) => window.getComputedStyle(element).color, + ), + ).toEqual(colorRGBMap.yellow); + + // check the adopted stylesheet is applied on the shadow dom #1's root + expect( + await contentDocument!.evaluate( + () => + window.getComputedStyle( + document + .querySelector('#shadow-host1')! + .shadowRoot!.querySelector('span')!, + ).color, + ), + ).toEqual(colorRGBMap.red); + + // check the adopted stylesheet is applied on document of the IFrame element + expect( + await contentDocument!.$eval( + 'iframe', + (element) => + window.getComputedStyle( + (element as HTMLIFrameElement).contentDocument!.querySelector( + 'h1', + )!, + ).color, + ), + ).toEqual(colorRGBMap.blue); + + // check the adopted stylesheet is applied on the shadow dom #2's root + expect( + await contentDocument!.evaluate( + () => + window.getComputedStyle( + document + .querySelector('#shadow-host2')! + .shadowRoot!.querySelector('span')!, + ).color, + ), + ).toEqual(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 checkCorrectness(); }); }); From 22127e2355e9c00bc7d6bbcc21999af64e647e39 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 26 Aug 2022 12:21:42 +1000 Subject: [PATCH 15/43] fix: rule index wasn't recorded when serializing the adoptedStyleSheets --- packages/rrweb/src/record/stylesheet-manager.ts | 3 ++- packages/rrweb/test/record.test.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts index e53023d020..d654e3570a 100644 --- a/packages/rrweb/src/record/stylesheet-manager.ts +++ b/packages/rrweb/src/record/stylesheet-manager.ts @@ -64,9 +64,10 @@ export class StylesheetManager { styleId = this.styleMirror.add(sheet); const rules = Array.from(sheet.rules || CSSRule); this.styleRulesCb({ - adds: rules.map((r) => { + adds: rules.map((r, index) => { return { rule: getCssRuleString(r), + index, }; }), styleId, diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 084554c250..011c194edd 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -639,7 +639,9 @@ describe('record', function (this: ISuite) { const sheet = new CSSStyleSheet(); // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet - sheet.replaceSync('div { color: yellow; }'); + sheet.replaceSync( + 'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}', + ); // Add stylesheet to a document. // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet document.adoptedStyleSheets = [sheet]; From 2a36b45bea6a19b9536b0552adeb885549eee2bd Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sun, 28 Aug 2022 13:34:01 +1000 Subject: [PATCH 16/43] add test case for mutation of stylesheet objects and add support for replace & replaceSync --- packages/rrweb/package.json | 1 + packages/rrweb/src/replay/index.ts | 17 +- .../adopted-style-sheet-modification.ts | 322 ++++++++++++++++++ packages/rrweb/test/record.test.ts | 21 +- packages/rrweb/test/replayer.test.ts | 147 ++++++++ packages/rrweb/tsconfig.json | 3 +- yarn.lock | 5 + 7 files changed, 502 insertions(+), 14 deletions(-) create mode 100644 packages/rrweb/test/events/adopted-style-sheet-modification.ts diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 2c19de3e10..61b8d61420 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -52,6 +52,7 @@ "@types/offscreencanvas": "^2019.6.4", "@types/prettier": "^2.3.2", "@types/puppeteer": "^5.4.4", + "construct-style-sheets-polyfill": "^3.1.0", "cross-env": "^5.2.0", "esbuild": "^0.14.38", "fast-mhtml": "^1.1.9", diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index c26ba5c22d..b76998d262 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1252,8 +1252,7 @@ export class Replayer { ? undefined : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion Math.min(nestedIndex, styleSheet!.cssRules.length); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - styleSheet!.insertRule(rule, index); + styleSheet?.insertRule(rule, index); } } catch (e) { /** @@ -1286,6 +1285,20 @@ export class Replayer { */ } }); + + if (d.replace) + try { + styleSheet.replace(d.replace); + } catch (e) { + // for safety + } + + if (d.replaceSync) + try { + styleSheet.replaceSync(d.replaceSync); + } catch (e) { + // for safety + } } break; } diff --git a/packages/rrweb/test/events/adopted-style-sheet-modification.ts b/packages/rrweb/test/events/adopted-style-sheet-modification.ts new file mode 100644 index 0000000000..4dc5e442e0 --- /dev/null +++ b/packages/rrweb/test/events/adopted-style-sheet-modification.ts @@ -0,0 +1,322 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); +const events: eventWithTime[] = [ + { + type: EventType.Meta, + data: { + href: 'about:blank', + width: 1920, + height: 1080, + }, + timestamp: now, + }, + { + type: EventType.FullSnapshot, + data: { + node: { + type: 0, + childNodes: [ + { + type: 1, + name: 'html', + publicId: '', + systemId: '', + id: 2, + }, + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + id: 4, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: '\n ', + id: 6, + }, + { + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'div in outermost document', + id: 8, + }, + ], + id: 7, + }, + { + type: 3, + textContent: ' \n ', + id: 9, + }, + { + type: 2, + tagName: 'iframe', + attributes: {}, + childNodes: [], + id: 10, + }, + { + type: 3, + textContent: '\n ', + id: 11, + }, + ], + id: 5, + }, + ], + id: 3, + }, + ], + id: 1, + }, + initialOffset: { + left: 0, + top: 0, + }, + }, + timestamp: now + 100, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + adds: [], + styleId: 1, + id: 1, + }, + timestamp: now + 150, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.AdoptedStyleSheet, + id: 1, + styleIds: [1], + }, + timestamp: now + 150, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: 0, + adds: [ + { + parentId: 10, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + rootId: 12, + id: 14, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'h1', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'h1 in iframe', + rootId: 12, + id: 17, + }, + ], + rootId: 12, + id: 16, + }, + ], + rootId: 12, + id: 15, + }, + ], + rootId: 12, + id: 13, + }, + ], + compatMode: 'BackCompat', + id: 12, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 200, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + adds: [], + styleId: 2, + id: 10, + }, + timestamp: now + 250, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.AdoptedStyleSheet, + id: 10, + styleIds: [2], + }, + timestamp: now + 250, + }, + // use CSSStyleSheet.replace api + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + styleId: 1, + replace: 'div { color: yellow; }', + }, + timestamp: now + 300, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + styleId: 2, + replace: 'h1 { color: blue; }', + }, + timestamp: now + 300, + }, + // use CSSStyleSheet.replaceSync api + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + styleId: 1, + replaceSync: 'div { display: inline ; }', + }, + timestamp: now + 400, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + styleId: 2, + replaceSync: 'h1 { font-size: large; }', + }, + timestamp: now + 400, + }, + // use StyleDeclaration.setProperty api + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleDeclaration, + styleId: 1, + set: { + property: 'color', + value: 'green', + priority: undefined, + }, + index: [0], + }, + timestamp: now + 500, + }, + // use StyleDeclaration.removeProperty api + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleDeclaration, + styleId: 1, + remove: { + property: 'display', + }, + index: [0], + }, + timestamp: now + 500, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleDeclaration, + styleId: 2, + set: { + property: 'font-size', + value: 'medium', + priority: 'important', + }, + index: [0], + }, + timestamp: now + 500, + }, + // use CSSStyleSheet.insertRule api + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + styleId: 2, + adds: [ + { + rule: 'h2 { color: red; }', + }, + ], + }, + timestamp: now + 500, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + styleId: 1, + adds: [ + { + rule: 'body { border: 2px solid blue; }', + index: 1, + }, + ], + }, + timestamp: now + 600, + }, + // use CSSStyleSheet.deleteRule api + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + styleId: 2, + removes: [ + { + index: 0, + }, + ], + }, + timestamp: now + 600, + }, +]; + +export default events; diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 011c194edd..60057e2825 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import type * as puppeteer from 'puppeteer'; +import 'construct-style-sheets-polyfill'; import { recordOptions, listenerHandler, @@ -462,15 +463,15 @@ describe('record', function (this: ISuite) { const sheet = new CSSStyleSheet(); // Add stylesheet to a document. - // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet + document.adoptedStyleSheets = [sheet]; const iframe = document.querySelector('iframe'); // @ts-ignore - TS doesn't support `CSSStyleSheet constructor` yet const sheet2 = new iframe!.contentWindow!.CSSStyleSheet(); // Add stylesheet to an IFrame document. - // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet - iframe.contentDocument.adoptedStyleSheets = [sheet2]; + + iframe!.contentDocument!.adoptedStyleSheets = [sheet2]; iframe!.contentDocument!.body.innerHTML = '

h1 in iframe

'; const { record } = ((window as unknown) as IWindow).rrweb; @@ -643,7 +644,7 @@ describe('record', function (this: ISuite) { 'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}', ); // Add stylesheet to a document. - // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet + document.adoptedStyleSheets = [sheet]; // Add stylesheet to a shadow host. @@ -652,9 +653,9 @@ describe('record', function (this: ISuite) { shadow.innerHTML = '
div in shadow dom 1
span in shadow dom 1'; const sheet2 = new CSSStyleSheet(); - // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet + sheet2.replaceSync('span { color: red; }'); - // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet + shadow.adoptedStyleSheets = [sheet, sheet2]; // Add stylesheet to an IFrame document. @@ -662,10 +663,10 @@ describe('record', function (this: ISuite) { // @ts-ignore - TS doesn't support `CSSStyleSheet constructor` yet const sheet3 = new iframe!.contentWindow!.CSSStyleSheet(); sheet3.replaceSync('h1 { color: blue; }'); - // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet - iframe.contentDocument.adoptedStyleSheets = [sheet3]; - // @ts-ignore - TS doesn't support `CSSStyleSheet.adoptedStyleSheets` yet - const ele = iframe.contentDocument.createElement('h1'); + + iframe!.contentDocument!.adoptedStyleSheets = [sheet3]; + + const ele = iframe!.contentDocument!.createElement('h1'); ele.innerText = 'h1 in iframe'; iframe!.contentDocument!.body.appendChild(ele); diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index f5f2746614..ec6b1cb9c0 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import type * as puppeteer from 'puppeteer'; +import 'construct-style-sheets-polyfill'; import { assertDomSnapshot, launchPuppeteer, @@ -18,6 +19,7 @@ import shadowDomEvents from './events/shadow-dom'; import StyleSheetTextMutation from './events/style-sheet-text-mutation'; import canvasInIframe from './events/canvas-in-iframe'; import adoptedStyleSheet from './events/adopted-style-sheet'; +import adoptedStyleSheetModification from './events/adopted-style-sheet-modification'; interface ISuite { code: string; @@ -790,4 +792,149 @@ describe('replayer', function () { await page.evaluate('replayer.pause(600);'); await checkCorrectness(); }); + + it('can replay modification events for adoptedStyleSheet', async () => { + await page.evaluate(` + events = ${JSON.stringify(adoptedStyleSheetModification)}; + const { Replayer } = rrweb; + var replayer = new Replayer(events,{showDebug:true}); + replayer.play(); + `); + const iframe = await page.$('iframe'); + const contentDocument = await iframe!.contentFrame()!; + + // At 250ms, the adopted stylesheet is still empty. + const check250ms = async () => { + await page.waitForTimeout(250); + expect( + await contentDocument!.evaluate( + () => + document.adoptedStyleSheets.length === 1 && + document.adoptedStyleSheets[0].cssRules.length === 0, + ), + ).toBeTruthy(); + expect( + await contentDocument!.evaluate( + () => + document.querySelector('iframe')!.contentDocument! + .adoptedStyleSheets.length === 1 && + document.querySelector('iframe')!.contentDocument! + .adoptedStyleSheets[0].cssRules.length === 0, + ), + ).toBeTruthy(); + }; + + // At 300ms, the adopted stylesheet is replaced with new content. + const check300ms = async () => { + await page.waitForTimeout(50); + expect( + await contentDocument!.evaluate( + () => + document.adoptedStyleSheets[0].cssRules.length === 1 && + document.adoptedStyleSheets[0].cssRules[0].cssText === + 'div { color: yellow; }', + ), + ).toBeTruthy(); + expect( + await contentDocument!.evaluate( + () => + document.querySelector('iframe')!.contentDocument! + .adoptedStyleSheets[0].cssRules.length === 1 && + document.querySelector('iframe')!.contentDocument! + .adoptedStyleSheets[0].cssRules[0].cssText === + 'h1 { color: blue; }', + ), + ).toBeTruthy(); + }; + + // At 400ms, check replaceSync API. + const check400ms = async () => { + await page.waitForTimeout(100); + expect( + await contentDocument!.evaluate( + () => + document.adoptedStyleSheets[0].cssRules.length === 1 && + document.adoptedStyleSheets[0].cssRules[0].cssText === + 'div { display: inline; }', + ), + ).toBeTruthy(); + expect( + await contentDocument!.evaluate( + () => + document.querySelector('iframe')!.contentDocument! + .adoptedStyleSheets[0].cssRules.length === 1 && + document.querySelector('iframe')!.contentDocument! + .adoptedStyleSheets[0].cssRules[0].cssText === + 'h1 { font-size: large; }', + ), + ).toBeTruthy(); + }; + + // At 500ms, check CSSStyleDeclaration API. + const check500ms = async () => { + await page.waitForTimeout(100); + expect( + await contentDocument!.evaluate( + () => + document.adoptedStyleSheets[0].cssRules.length === 1 && + document.adoptedStyleSheets[0].cssRules[0].cssText === + 'div { color: green; }', + ), + ).toBeTruthy(); + expect( + await contentDocument!.evaluate( + () => + document.querySelector('iframe')!.contentDocument! + .adoptedStyleSheets[0].cssRules.length === 2 && + document.querySelector('iframe')!.contentDocument! + .adoptedStyleSheets[0].cssRules[0].cssText === + 'h2 { color: red; }' && + document.querySelector('iframe')!.contentDocument! + .adoptedStyleSheets[0].cssRules[1].cssText === + 'h1 { font-size: medium !important; }', + ), + ).toBeTruthy(); + }; + + // At 600ms, check insertRule and deleteRule API. + const check600ms = async () => { + await page.waitForTimeout(100); + expect( + await contentDocument!.evaluate( + () => + document.adoptedStyleSheets[0].cssRules.length === 2 && + document.adoptedStyleSheets[0].cssRules[0].cssText === + 'div { color: green; }' && + document.adoptedStyleSheets[0].cssRules[1].cssText === + 'body { border: 2px solid blue; }', + ), + ).toBeTruthy(); + expect( + await contentDocument!.evaluate( + () => + document.querySelector('iframe')!.contentDocument! + .adoptedStyleSheets[0].cssRules.length === 1 && + document.querySelector('iframe')!.contentDocument! + .adoptedStyleSheets[0].cssRules[0].cssText === + 'h1 { font-size: medium !important; }', + ), + ).toBeTruthy(); + }; + + await check250ms(); + await check300ms(); + await check400ms(); + await check500ms(); + await check600ms(); + + // To test the correctness of replaying adopted stylesheet mutation events in the fast-forward mode. + await page.evaluate('replayer.play(0);'); + await waitForRAF(page); + await page.evaluate('replayer.pause(600);'); + await check250ms(); + await check300ms(); + await check400ms(); + await check500ms(); + await check600ms(); + }); }); diff --git a/packages/rrweb/tsconfig.json b/packages/rrweb/tsconfig.json index f1e5bba3e5..c95913f748 100644 --- a/packages/rrweb/tsconfig.json +++ b/packages/rrweb/tsconfig.json @@ -19,7 +19,6 @@ "src", "scripts", "node_modules/@types/css-font-loading-module/index.d.ts", - "node_modules/@types/jest-image-snapshot/index.d.ts", - "constructableStyleSheets.d.ts" + "node_modules/@types/jest-image-snapshot/index.d.ts" ] } diff --git a/yarn.lock b/yarn.lock index 64e609bdf7..ef5ad984c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3629,6 +3629,11 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= +construct-style-sheets-polyfill@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-3.1.0.tgz#c490abd79efdb359fafa62ec14ea55232be0eecf" + integrity sha512-HBLKP0chz8BAY6rBdzda11c3wAZeCZ+kIG4weVC2NM3AXzxx09nhe8t0SQNdloAvg5GLuHwq/0SPOOSPvtCcKw== + content-disposition@0.5.3: version "0.5.3" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz" From e020877d5907788ff55a141b80e56acaaa0283eb Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sun, 28 Aug 2022 14:35:32 +1000 Subject: [PATCH 17/43] refactor: improve the code quality --- packages/rrweb/src/replay/index.ts | 22 ++++++++----------- packages/rrweb/test/replayer.test.ts | 32 +++++++--------------------- 2 files changed, 17 insertions(+), 37 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index b76998d262..b55f65a802 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1211,20 +1211,16 @@ export class Replayer { const host = this.mirror.getNode(d.id); if (host) { let newStyleSheet: CSSStyleSheet | null = null; - - if (host.nodeName === 'IFRAME') { - const iframeWin = (host as HTMLIFrameElement) + let hostWindow: IWindow | null = null; + if (host.nodeName === 'IFRAME') + hostWindow = (host as HTMLIFrameElement) .contentWindow as IWindow; - if (iframeWin) newStyleSheet = new iframeWin.CSSStyleSheet(); - } else if (hasShadowRoot(host)) { - const ownerWindow = host.ownerDocument?.defaultView; - if (ownerWindow) - newStyleSheet = new ownerWindow.CSSStyleSheet(); - } else if (host.nodeName === '#document') { - const ownerWindow = (host as Document).defaultView; - if (ownerWindow) - newStyleSheet = new ownerWindow.CSSStyleSheet(); - } + else if (hasShadowRoot(host)) + hostWindow = host.ownerDocument?.defaultView || null; + else if (host.nodeName === '#document') + hostWindow = (host as Document).defaultView; + + hostWindow && (newStyleSheet = new hostWindow.CSSStyleSheet()); if (newStyleSheet) { this.styleMirror.add(newStyleSheet, d.styleId); styleSheet = newStyleSheet; diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index ec6b1cb9c0..a8f39530db 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -803,8 +803,8 @@ describe('replayer', function () { const iframe = await page.$('iframe'); const contentDocument = await iframe!.contentFrame()!; - // At 250ms, the adopted stylesheet is still empty. - const check250ms = async () => { + const checkCorrectness = async () => { + // At 250ms, the adopted stylesheet is still empty. await page.waitForTimeout(250); expect( await contentDocument!.evaluate( @@ -822,10 +822,8 @@ describe('replayer', function () { .adoptedStyleSheets[0].cssRules.length === 0, ), ).toBeTruthy(); - }; - // At 300ms, the adopted stylesheet is replaced with new content. - const check300ms = async () => { + // At 300ms, the adopted stylesheet is replaced with new content. await page.waitForTimeout(50); expect( await contentDocument!.evaluate( @@ -845,10 +843,8 @@ describe('replayer', function () { 'h1 { color: blue; }', ), ).toBeTruthy(); - }; - // At 400ms, check replaceSync API. - const check400ms = async () => { + // At 400ms, check replaceSync API. await page.waitForTimeout(100); expect( await contentDocument!.evaluate( @@ -868,10 +864,8 @@ describe('replayer', function () { 'h1 { font-size: large; }', ), ).toBeTruthy(); - }; - // At 500ms, check CSSStyleDeclaration API. - const check500ms = async () => { + // At 500ms, check CSSStyleDeclaration API. await page.waitForTimeout(100); expect( await contentDocument!.evaluate( @@ -894,10 +888,8 @@ describe('replayer', function () { 'h1 { font-size: medium !important; }', ), ).toBeTruthy(); - }; - // At 600ms, check insertRule and deleteRule API. - const check600ms = async () => { + // At 600ms, check insertRule and deleteRule API. await page.waitForTimeout(100); expect( await contentDocument!.evaluate( @@ -921,20 +913,12 @@ describe('replayer', function () { ).toBeTruthy(); }; - await check250ms(); - await check300ms(); - await check400ms(); - await check500ms(); - await check600ms(); + await checkCorrectness(); // To test the correctness of replaying adopted stylesheet mutation events in the fast-forward mode. await page.evaluate('replayer.play(0);'); await waitForRAF(page); await page.evaluate('replayer.pause(600);'); - await check250ms(); - await check300ms(); - await check400ms(); - await check500ms(); - await check600ms(); + await checkCorrectness(); }); }); From edd094a5afd02183ee660c3d0caed5551ece86a7 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sun, 28 Aug 2022 21:45:44 +1000 Subject: [PATCH 18/43] feat: monkey patch adoptedStyleSheet API to track its modification --- packages/rrweb/src/record/iframe-manager.ts | 2 +- packages/rrweb/src/record/observer.ts | 66 ++- .../rrweb/src/record/shadow-dom-manager.ts | 21 +- packages/rrweb/src/replay/index.ts | 10 +- .../test/__snapshots__/record.test.ts.snap | 415 +++++++++++++++++- .../adopted-style-sheet-modification.ts | 4 +- .../rrweb/test/events/adopted-style-sheet.ts | 4 +- packages/rrweb/test/record.test.ts | 13 +- 8 files changed, 511 insertions(+), 24 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 93934608e2..d1e431a01a 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -50,7 +50,7 @@ export class IframeManager { ) this.stylesheetManager.adoptStyleSheets( iframeEl.contentDocument.adoptedStyleSheets, - mirror.getId(iframeEl), + mirror.getId(iframeEl.contentDocument), ); } } diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index cc2b751ca3..70d929826e 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -601,7 +601,6 @@ function initStyleSheetObserver( ); if ((id && id !== -1) || (styleId && styleId !== -1)) { - console.log(id, styleId); styleSheetRuleCb({ id, styleId, @@ -711,6 +710,69 @@ function initStyleSheetObserver( }; } +export function initAdoptedStyleSheetObserver( + { + mirror, + stylesheetManager, + }: Pick, + host: Document | ShadowRoot, +): listenerHandler { + let hostId: number | null = null; + // host of adoptedStyleSheets is outermost document or IFrame's document + if (host.nodeName === '#document') hostId = mirror.getId(host); + // The host is a ShadowRoot. + else hostId = mirror.getId((host as ShadowRoot).host); + + const patchTarget = + host.nodeName === '#document' + ? (host as Document).defaultView?.Document + : host.ownerDocument?.defaultView?.ShadowRoot; + const originalPropertyDescriptor = Object.getOwnPropertyDescriptor( + patchTarget?.prototype, + 'adoptedStyleSheets', + ); + if ( + hostId === null || + hostId === -1 || + !patchTarget || + !originalPropertyDescriptor + ) + return () => { + // + }; + + // Patch adoptedStyleSheets by overriding the original one. + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get(): CSSStyleSheet[] { + return originalPropertyDescriptor.get?.call(this) as CSSStyleSheet[]; + }, + set(sheets: CSSStyleSheet[]) { + const result = originalPropertyDescriptor.set?.call(this, sheets); + if (hostId !== null && hostId !== -1) { + try { + stylesheetManager.adoptStyleSheets(sheets, hostId); + } catch (e) { + // for safety + } + } + return result; + }, + }); + + return () => { + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + // eslint-disable-next-line @typescript-eslint/unbound-method + get: originalPropertyDescriptor.get, + // eslint-disable-next-line @typescript-eslint/unbound-method + set: originalPropertyDescriptor.set, + }); + }; +} + function initStyleDeclarationObserver( { styleDeclarationCb, mirror, stylesheetManager }: observerParam, { win }: { win: IWindow }, @@ -1016,6 +1078,7 @@ export function initObservers( const mediaInteractionHandler = initMediaInteractionObserver(o); const styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow }); + const adoptedStyleSheetObserver = initAdoptedStyleSheetObserver(o, o.doc); const styleDeclarationObserver = initStyleDeclarationObserver(o, { win: currentWindow, }); @@ -1044,6 +1107,7 @@ export function initObservers( inputHandler(); mediaInteractionHandler(); styleSheetObserver(); + adoptedStyleSheetObserver(); styleDeclarationObserver(); fontObserver(); selectionObserver(); diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 0e6a3f4f66..582cf9c302 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -4,7 +4,11 @@ import type { MutationBufferParam, SamplingStrategy, } from '../types'; -import { initMutationObserver, initScrollObserver } from './observer'; +import { + initMutationObserver, + initScrollObserver, + initAdoptedStyleSheetObserver, +} from './observer'; import { patch } from '../utils'; import type { Mirror } from 'rrweb-snapshot'; import { isNativeShadowDom } from 'rrweb-snapshot'; @@ -73,14 +77,21 @@ export class ShadowDomManager { doc: (shadowRoot as unknown) as Document, mirror: this.mirror, }); - if (shadowRoot.adoptedStyleSheets?.length > 0) - // Defer this to avoid adoptedStyleSheet events being created before the full snapshot is created. - void Promise.resolve().then(() => { + // Defer this to avoid adoptedStyleSheet events being created before the full snapshot is created or attachShadow action is recorded. + setTimeout(() => { + if (shadowRoot.adoptedStyleSheets?.length > 0) this.bypassOptions.stylesheetManager.adoptStyleSheets( shadowRoot.adoptedStyleSheets, this.mirror.getId(shadowRoot.host), ); - }); + initAdoptedStyleSheetObserver( + { + mirror: this.mirror, + stylesheetManager: this.bypassOptions.stylesheetManager, + }, + shadowRoot, + ); + }, 0); } /** diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index b55f65a802..8a2b9737bd 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1212,10 +1212,7 @@ export class Replayer { if (host) { let newStyleSheet: CSSStyleSheet | null = null; let hostWindow: IWindow | null = null; - if (host.nodeName === 'IFRAME') - hostWindow = (host as HTMLIFrameElement) - .contentWindow as IWindow; - else if (hasShadowRoot(host)) + if (hasShadowRoot(host)) hostWindow = host.ownerDocument?.defaultView || null; else if (host.nodeName === '#document') hostWindow = (host as Document).defaultView; @@ -1411,10 +1408,7 @@ export class Replayer { const stylesToAdopt = d.styleIds .map((styleId) => this.styleMirror.getStyle(styleId)) .filter((style) => style !== null) as CSSStyleSheet[]; - if (targetHost.nodeName === 'IFRAME') { - const iframeDoc = (targetHost as HTMLIFrameElement).contentDocument; - if (iframeDoc) iframeDoc.adoptedStyleSheets = stylesToAdopt; - } else if (hasShadowRoot(targetHost)) + if (hasShadowRoot(targetHost)) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion (targetHost as HTMLElement).shadowRoot!.adoptedStyleSheets = stylesToAdopt; else if (targetHost.nodeName === '#document') diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index 8a76cf1c31..f1cdfe0990 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -197,6 +197,417 @@ exports[`record captures CORS stylesheets that are still loading 1`] = ` ]" `; +exports[`record captures adopted stylesheets in shadow doms and iframe 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"div in outermost document\\", + \\"id\\": 8 + } + ], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"shadow-host1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"div in shadow dom 1\\", + \\"id\\": 12 + } + ], + \\"id\\": 11, + \\"isShadow\\": true + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"span in shadow dom 1\\", + \\"id\\": 14 + } + ], + \\"id\\": 13, + \\"isShadow\\": true + } + ], + \\"id\\": 10, + \\"isShadowHost\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"shadow-host2\\" + }, + \\"childNodes\\": [], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"adds\\": [ + { + \\"rule\\": \\"div { color: yellow; }\\", + \\"index\\": 0 + }, + { + \\"rule\\": \\"h2 { color: orange; }\\", + \\"index\\": 1 + }, + { + \\"rule\\": \\"h3 { font-size: larger; }\\", + \\"index\\": 2 + } + ], + \\"styleId\\": 1, + \\"id\\": 1 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 1, + \\"styleIds\\": [ + 1 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"adds\\": [ + { + \\"rule\\": \\"span { color: red; }\\", + \\"index\\": 0 + } + ], + \\"styleId\\": 2, + \\"id\\": 10 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 10, + \\"styleIds\\": [ + 1, + 2 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 18, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 20, + \\"id\\": 22 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"h1\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"h1 in iframe\\", + \\"rootId\\": 20, + \\"id\\": 25 + } + ], + \\"rootId\\": 20, + \\"id\\": 24 + } + ], + \\"rootId\\": 20, + \\"id\\": 23 + } + ], + \\"rootId\\": 20, + \\"id\\": 21 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 20 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"adds\\": [ + { + \\"rule\\": \\"h1 { color: blue; }\\", + \\"index\\": 0 + } + ], + \\"styleId\\": 3, + \\"id\\": 20 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 20, + \\"styleIds\\": [ + 3 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"adds\\": [ + { + \\"rule\\": \\"span { color: green; }\\", + \\"index\\": 0 + } + ], + \\"styleId\\": 4, + \\"id\\": 1 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 1, + \\"styleIds\\": [ + 4, + 1, + 2 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"adds\\": [ + { + \\"rule\\": \\"h2 { color: purple; }\\", + \\"index\\": 0 + } + ], + \\"styleId\\": 5, + \\"id\\": 20 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 20, + \\"styleIds\\": [ + 5, + 3 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 26, + \\"isShadow\\": true + } + }, + { + \\"parentId\\": 26, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"span in shadow dom 2\\", + \\"id\\": 27 + } + }, + { + \\"parentId\\": 16, + \\"nextId\\": 26, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 28, + \\"isShadow\\": true + } + }, + { + \\"parentId\\": 28, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"div in shadow dom 2\\", + \\"id\\": 29 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 16, + \\"styleIds\\": [ + 1, + 4 + ] + } + } +]" +`; + exports[`record captures inserted style text nodes correctly 1`] = ` "[ { @@ -506,14 +917,14 @@ exports[`record captures mutations on adopted stylesheets 1`] = ` \\"source\\": 8, \\"adds\\": [], \\"styleId\\": 2, - \\"id\\": 10 + \\"id\\": 12 } }, { \\"type\\": 3, \\"data\\": { \\"source\\": 15, - \\"id\\": 10, + \\"id\\": 12, \\"styleIds\\": [ 2 ] diff --git a/packages/rrweb/test/events/adopted-style-sheet-modification.ts b/packages/rrweb/test/events/adopted-style-sheet-modification.ts index 4dc5e442e0..570bb6851c 100644 --- a/packages/rrweb/test/events/adopted-style-sheet-modification.ts +++ b/packages/rrweb/test/events/adopted-style-sheet-modification.ts @@ -182,7 +182,7 @@ const events: eventWithTime[] = [ source: IncrementalSource.StyleSheetRule, adds: [], styleId: 2, - id: 10, + id: 12, }, timestamp: now + 250, }, @@ -190,7 +190,7 @@ const events: eventWithTime[] = [ type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.AdoptedStyleSheet, - id: 10, + id: 12, styleIds: [2], }, timestamp: now + 250, diff --git a/packages/rrweb/test/events/adopted-style-sheet.ts b/packages/rrweb/test/events/adopted-style-sheet.ts index 4a8c3b6089..1098c96cee 100644 --- a/packages/rrweb/test/events/adopted-style-sheet.ts +++ b/packages/rrweb/test/events/adopted-style-sheet.ts @@ -278,7 +278,7 @@ const events: eventWithTime[] = [ }, ], styleId: 3, - id: 18, + id: 20, }, timestamp: now + 300, }, @@ -287,7 +287,7 @@ const events: eventWithTime[] = [ type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.AdoptedStyleSheet, - id: 18, + id: 20, styleIds: [3], }, timestamp: now + 300, diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 60057e2825..46d246ef6b 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -680,11 +680,18 @@ describe('record', function (this: ISuite) { const shadow = host!.attachShadow({ mode: 'open' }); shadow.innerHTML = '
div in shadow dom 2
span in shadow dom 2'; - const sheet3 = new CSSStyleSheet(); + const sheet4 = new CSSStyleSheet(); // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet - sheet3.replaceSync('span { color: green; }'); + sheet4.replaceSync('span { color: green; }'); // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet - shadow.adoptedStyleSheets = [sheet, sheet3]; + shadow.adoptedStyleSheets = [sheet, sheet4]; + + document.adoptedStyleSheets = [sheet4, sheet, sheet2]; + + // @ts-ignore - TS doesn't support `CSSStyleSheet constructor` yet + const sheet5 = new iframe!.contentWindow!.CSSStyleSheet(); + sheet5.replaceSync('h2 { color: purple; }'); + iframe!.contentDocument!.adoptedStyleSheets = [sheet5, sheet3]; }, 10); }); await waitForRAF(ctx.page); // wait till events get sent From 040a987aa6ec86465c766a8e196b9db4ccd3f3b5 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Mon, 29 Aug 2022 11:17:25 +1000 Subject: [PATCH 19/43] feat: add support for checkouting fullsnapshot --- packages/rrweb-snapshot/src/rebuild.ts | 8 +++----- packages/rrweb-snapshot/src/utils.ts | 7 ++++++- packages/rrweb/src/record/index.ts | 3 +++ packages/rrweb/src/record/stylesheet-manager.ts | 5 +++++ packages/rrweb/src/replay/index.ts | 2 ++ packages/rrweb/src/utils.ts | 1 + packages/rrweb/test/util.test.ts | 2 ++ 7 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 60e3669b11..4eef992871 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -336,11 +336,9 @@ export function buildNodeWithSN( if (!node) { return null; } - if (n.rootId) { - console.assert( - (mirror.getNode(n.rootId) as Document) === doc, - 'Target document should have the same root id.', - ); + // If the snapshot is created by checkout, the rootId doesn't change but the iframe's document can be changed automatically when a new iframe element is created. + if (n.rootId && (mirror.getNode(n.rootId) as Document) !== doc) { + mirror.replace(n.rootId, doc); } // use target document as root document if (n.type === NodeType.Document) { diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index afc008eef8..77019df12a 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -101,7 +101,12 @@ export class Mirror implements IMirror { } replace(id: number, n: Node) { - this.idNodeMap.set(id, n); + const oldNode = this.getNode(id); + if (oldNode) { + const meta = this.nodeMetaMap.get(oldNode); + if (meta) this.nodeMetaMap.set(n, meta); + this.idNodeMap.set(id, n); + } } reset() { diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 369504a132..b3cd16670e 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -294,6 +294,9 @@ function record( isCheckout, ); + // When we take a full snapshot, old tracked StyleSheets need to be removed. + stylesheetManager.reset(); + mutationBuffers.forEach((buf) => buf.lock()); // don't allow any mirror modifications during snapshotting const node = snapshot(document, { mirror, diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts index d654e3570a..f6248bac19 100644 --- a/packages/rrweb/src/record/stylesheet-manager.ts +++ b/packages/rrweb/src/record/stylesheet-manager.ts @@ -79,6 +79,11 @@ export class StylesheetManager { this.adoptedStyleSheetCb(adoptedStyleSheetData); } + public reset() { + this.styleMirror.reset(); + this.trackedLinkElements = new WeakSet(); + } + // TODO: take snapshot on stylesheet reload by applying event listener private trackStylesheetInLinkElement(linkEl: HTMLLinkElement) { // linkEl.addEventListener('load', () => { diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 8a2b9737bd..78705e973e 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -257,6 +257,7 @@ export class Replayer { this.emitter.on(ReplayerEvents.PlayBack, () => { this.firstFullSnapshot = null; this.mirror.reset(); + this.styleMirror.reset(); }); const timer = new Timer([], config?.speed || defaultConfig.speed); @@ -605,6 +606,7 @@ export class Replayer { } this.rebuildFullSnapshot(event, isSync); this.iframe.contentWindow?.scrollTo(event.data.initialOffset); + this.styleMirror.reset(); }; break; case EventType.IncrementalSnapshot: diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 145a52bb3d..4b9e75d463 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -482,6 +482,7 @@ export class StyleSheetMirror { reset(): void { this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); this.id = 1; } } diff --git a/packages/rrweb/test/util.test.ts b/packages/rrweb/test/util.test.ts index a9190e721e..964005c8c6 100644 --- a/packages/rrweb/test/util.test.ts +++ b/packages/rrweb/test/util.test.ts @@ -71,6 +71,8 @@ describe('Utilities for other modules', () => { } expect(mirror.reset()).toBeUndefined(); for (let s of styleList) expect(mirror.has(s)).toBeFalsy(); + for (let i = 0; i < 10; i++) expect(mirror.getStyle(i + 1)).toBeNull(); + expect(mirror.add(new CSSStyleSheet())).toBe(1); }); }); }); From 1df970aed6122f7e5592ae0cd7bb747ba7ec7192 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Mon, 29 Aug 2022 13:02:10 +1000 Subject: [PATCH 20/43] CI: fix failed type checks --- packages/rrdom-nodejs/tsconfig.json | 7 ++++++- packages/rrdom/tsconfig.json | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/rrdom-nodejs/tsconfig.json b/packages/rrdom-nodejs/tsconfig.json index 4a4f18a080..998e41aa80 100644 --- a/packages/rrdom-nodejs/tsconfig.json +++ b/packages/rrdom-nodejs/tsconfig.json @@ -16,5 +16,10 @@ }, "compileOnSave": true, "exclude": ["test"], - "include": ["src", "test.d.ts", "../rrweb/src/record/workers/workers.d.ts"] + "include": [ + "src", + "test.d.ts", + "../rrweb/src/record/workers/workers.d.ts", + "../rrweb/src/record/constructableStyleSheets.d.ts" + ] } diff --git a/packages/rrdom/tsconfig.json b/packages/rrdom/tsconfig.json index c5d366adf8..bcde0b2f16 100644 --- a/packages/rrdom/tsconfig.json +++ b/packages/rrdom/tsconfig.json @@ -16,5 +16,9 @@ }, "compileOnSave": true, "exclude": ["test"], - "include": ["src", "../rrweb/src/record/workers/workers.d.ts"] + "include": [ + "src", + "../rrweb/src/record/workers/workers.d.ts", + "../rrweb/src/record/constructableStyleSheets.d.ts" + ] } From ba0bcf8009ddf21cfbe6a34f9547d71935f99fef Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Mon, 5 Sep 2022 17:16:02 +1000 Subject: [PATCH 21/43] test: add test case for nested shadow doms and iframe elements --- .../test/__snapshots__/record.test.ts.snap | 477 +++++++++++++++++- packages/rrweb/test/record.test.ts | 73 ++- 2 files changed, 536 insertions(+), 14 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index f1cdfe0990..df801ee854 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -197,6 +197,481 @@ exports[`record captures CORS stylesheets that are still loading 1`] = ` ]" `; +exports[`record captures adopted stylesheets in nested shadow doms and iframes 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"shadow-host-1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"entry\\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"iframe-1\\" + }, + \\"childNodes\\": [], + \\"id\\": 9, + \\"isShadow\\": true + } + ], + \\"id\\": 7, + \\"isShadowHost\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 11, + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"shadow-host-2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"iframe-2\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 11, + \\"id\\": 16, + \\"isShadow\\": true + } + ], + \\"rootId\\": 11, + \\"id\\": 15, + \\"isShadowHost\\": true + } + ], + \\"rootId\\": 11, + \\"id\\": 14 + } + ], + \\"rootId\\": 11, + \\"id\\": 12 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 17, + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"shadow-host-3\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"iframe-3\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 17, + \\"id\\": 22, + \\"isShadow\\": true + } + ], + \\"rootId\\": 17, + \\"id\\": 21, + \\"isShadowHost\\": true + } + ], + \\"rootId\\": 17, + \\"id\\": 20 + } + ], + \\"rootId\\": 17, + \\"id\\": 18 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 17 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"shadow-host-4\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"iframe-4\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 28, + \\"isShadow\\": true + } + ], + \\"rootId\\": 23, + \\"id\\": 27, + \\"isShadowHost\\": true + } + ], + \\"rootId\\": 23, + \\"id\\": 26 + } + ], + \\"rootId\\": 23, + \\"id\\": 24 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 23 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 28, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 29, + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"shadow-host-5\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 29, + \\"id\\": 33, + \\"isShadowHost\\": true + } + ], + \\"rootId\\": 29, + \\"id\\": 32 + } + ], + \\"rootId\\": 29, + \\"id\\": 30 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 29 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"adds\\": [ + { + \\"rule\\": \\"h1 { color: blue; }\\", + \\"index\\": 0 + } + ], + \\"styleId\\": 1, + \\"id\\": 29 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 29, + \\"styleIds\\": [ + 1 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"adds\\": [ + { + \\"rule\\": \\"div { font-size: large; }\\", + \\"index\\": 0 + } + ], + \\"styleId\\": 2, + \\"id\\": 33 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 33, + \\"styleIds\\": [ + 2 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 1, + \\"adds\\": [ + { + \\"rule\\": \\"div { display: inline ; }\\", + \\"index\\": 1 + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 2, + \\"replaceSync\\": \\"h1 { font-size: large; }\\" + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"adds\\": [ + { + \\"rule\\": \\"span { background-color: red; }\\", + \\"index\\": 0 + } + ], + \\"styleId\\": 3, + \\"id\\": 29 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 29, + \\"styleIds\\": [ + 3, + 2 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 33, + \\"styleIds\\": [ + 1, + 3 + ] + } + } +]" +`; + exports[`record captures adopted stylesheets in shadow doms and iframe 1`] = ` "[ { @@ -798,7 +1273,7 @@ exports[`record captures mutations on adopted stylesheets 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\" \\\\n \\", + \\"textContent\\": \\"\\\\n \\", \\"id\\": 9 }, { diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 46d246ef6b..49d95b9935 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -457,7 +457,7 @@ describe('record', function (this: ISuite) { it('captures mutations on adopted stylesheets', async () => { await ctx.page.evaluate(() => { document.body.innerHTML = ` -
div in outermost document
+
div in outermost document
`; @@ -467,10 +467,10 @@ describe('record', function (this: ISuite) { document.adoptedStyleSheets = [sheet]; const iframe = document.querySelector('iframe'); - // @ts-ignore - TS doesn't support `CSSStyleSheet constructor` yet - const sheet2 = new iframe!.contentWindow!.CSSStyleSheet(); - // Add stylesheet to an IFrame document. + const sheet2 = new (iframe!.contentWindow! as Window & + typeof globalThis).CSSStyleSheet(); + // Add stylesheet to an IFrame document. iframe!.contentDocument!.adoptedStyleSheets = [sheet2]; iframe!.contentDocument!.body.innerHTML = '

h1 in iframe

'; @@ -480,13 +480,11 @@ describe('record', function (this: ISuite) { }); setTimeout(() => { - // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet sheet.replace('div { color: yellow; }'); sheet2.replace('h1 { color: blue; }'); }, 0); setTimeout(() => { - // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet sheet.replaceSync('div { display: inline ; }'); sheet2.replaceSync('h1 { font-size: large; }'); }, 5); @@ -511,6 +509,58 @@ describe('record', function (this: ISuite) { assertSnapshot(ctx.events); }); + it('captures adopted stylesheets in nested shadow doms and iframes', async () => { + await ctx.page.evaluate(() => { + document.body.innerHTML = ` +
entry
+ `; + + let shadowHost = document.querySelector('div')!; + shadowHost!.attachShadow({ mode: 'open' }); + let iframeDocument: Document; + const NestedDepth = 4; + // construct nested shadow doms and iframe elements + for (let i = 1; i <= NestedDepth; i++) { + const shadowRoot = shadowHost.shadowRoot!; + const iframeElement = document.createElement('iframe'); + shadowRoot.appendChild(iframeElement); + iframeElement.id = `iframe-${i}`; + iframeDocument = iframeElement.contentDocument!; + shadowHost = iframeDocument.createElement('div'); + shadowHost.id = `shadow-host-${i + 1}`; + iframeDocument.body.append(shadowHost); + shadowHost!.attachShadow({ mode: 'open' }); + } + + const iframeWin = iframeDocument!.defaultView!; + const sheet1 = new iframeWin.CSSStyleSheet(); + sheet1.replaceSync('h1 {color: blue;}'); + iframeDocument!.adoptedStyleSheets = [sheet1]; + const sheet2 = new iframeWin.CSSStyleSheet(); + sheet2.replaceSync('div {font-size: large;}'); + shadowHost.shadowRoot!.adoptedStyleSheets = [sheet2]; + + const { record } = ((window as unknown) as IWindow).rrweb; + record({ + emit: ((window as unknown) as IWindow).emit, + }); + + setTimeout(() => { + sheet1.insertRule('div { display: inline ; }', 1); + sheet2.replaceSync('h1 { font-size: large; }'); + }, 20); + + setTimeout(() => { + const sheet3 = new iframeWin.CSSStyleSheet(); + sheet3.replaceSync('span {background-color: red;}'); + iframeDocument!.adoptedStyleSheets = [sheet3, sheet2]; + shadowHost.shadowRoot!.adoptedStyleSheets = [sheet1, sheet3]; + }, 25); + }); + await waitForRAF(ctx.page); + assertSnapshot(ctx.events); + }); + it('captures stylesheets in iframes with `blob:` url', async () => { await ctx.page.evaluate(() => { const iframe = document.createElement('iframe'); @@ -639,7 +689,6 @@ describe('record', function (this: ISuite) { `; const sheet = new CSSStyleSheet(); - // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet sheet.replaceSync( 'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}', ); @@ -660,8 +709,8 @@ describe('record', function (this: ISuite) { // Add stylesheet to an IFrame document. const iframe = document.querySelector('iframe'); - // @ts-ignore - TS doesn't support `CSSStyleSheet constructor` yet - const sheet3 = new iframe!.contentWindow!.CSSStyleSheet(); + const sheet3 = new (iframe!.contentWindow! as IWindow & + typeof globalThis).CSSStyleSheet(); sheet3.replaceSync('h1 { color: blue; }'); iframe!.contentDocument!.adoptedStyleSheets = [sheet3]; @@ -681,15 +730,13 @@ describe('record', function (this: ISuite) { shadow.innerHTML = '
div in shadow dom 2
span in shadow dom 2'; const sheet4 = new CSSStyleSheet(); - // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet sheet4.replaceSync('span { color: green; }'); - // @ts-ignore - TS doesn't support `CSSStyleSheet.replaceSync` yet shadow.adoptedStyleSheets = [sheet, sheet4]; document.adoptedStyleSheets = [sheet4, sheet, sheet2]; - // @ts-ignore - TS doesn't support `CSSStyleSheet constructor` yet - const sheet5 = new iframe!.contentWindow!.CSSStyleSheet(); + const sheet5 = new (iframe!.contentWindow! as IWindow & + typeof globalThis).CSSStyleSheet(); sheet5.replaceSync('h2 { color: purple; }'); iframe!.contentDocument!.adoptedStyleSheets = [sheet5, sheet3]; }, 10); From ae4a0ec2b1b0a7543f86323d810d3b487505ecac Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Mon, 5 Sep 2022 22:05:58 +1000 Subject: [PATCH 22/43] feat: add support for adoptedStyleSheets in VirtualDom mode --- package.json | 3 - packages/rrdom/src/diff.ts | 127 ++-------- packages/rrdom/src/index.ts | 17 +- packages/rrdom/test/diff.test.ts | 141 ++--------- packages/rrweb/src/replay/index.ts | 364 ++++++++++++++------------- packages/rrweb/test/replayer.test.ts | 56 +++-- yarn.lock | 7 +- 7 files changed, 284 insertions(+), 431 deletions(-) diff --git a/package.json b/package.json index 452d5f18ff..2c83c8b386 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,5 @@ "repl": "cd packages/rrweb && npm run repl", "lint": "yarn run concurrently --success=all -r -m=1 'yarn run markdownlint docs' 'yarn eslint packages/*/src --ext .ts,.tsx,.js,.jsx,.svelte'", "lint:report": "yarn eslint --output-file eslint_report.json --format json packages/*/src --ext .ts,.tsx,.js,.jsx" - }, - "resolutions": { - "**/jsdom/cssom": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz" } } diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 8456dd3398..7cf0f20465 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -4,6 +4,8 @@ import type { canvasEventWithTime, inputData, scrollData, + styleDeclarationData, + styleSheetRuleData, } from 'rrweb/src/types'; import type { IRRCDATASection, @@ -79,6 +81,10 @@ export type ReplayerHandler = { ) => void; applyInput: (data: inputData) => void; applyScroll: (data: scrollData, isSync: boolean) => void; + applyStyleSheetMutation: ( + data: styleDeclarationData | styleSheetRuleData, + styleSheet: CSSStyleSheet, + ) => void; }; export function diff( @@ -159,20 +165,25 @@ export function diff( } break; case 'STYLE': - applyVirtualStyleRulesToNode( - oldElement as HTMLStyleElement, - (newTree as RRStyleElement).rules, - ); + { + const styleSheet = (oldElement as HTMLStyleElement).sheet; + styleSheet && + (newTree as RRStyleElement).rules.forEach((data) => + replayer.applyStyleSheetMutation(data, styleSheet), + ); + } break; } if (newRRElement.shadowRoot) { if (!oldElement.shadowRoot) oldElement.attachShadow({ mode: 'open' }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const oldChildren = oldElement.shadowRoot!.childNodes; const newChildren = newRRElement.shadowRoot.childNodes; if (oldChildren.length > 0 || newChildren.length > 0) diffChildren( Array.from(oldChildren), newChildren, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion oldElement.shadowRoot!, replayer, rrnodeMirror, @@ -333,6 +344,7 @@ function diffChildren( } indexInOld = oldIdToIndex[rrnodeMirror.getId(newStartNode)]; if (indexInOld) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const nodeToMove = oldChildren[indexInOld]!; parentNode.insertBefore(nodeToMove, oldStartNode); diff(nodeToMove, newStartNode, replayer, rrnodeMirror); @@ -437,110 +449,3 @@ export function createOrGetNode( if (sn) domMirror.add(node, { ...sn }); return node; } - -export function getNestedRule( - rules: CSSRuleList, - position: number[], -): CSSGroupingRule { - const rule = rules[position[0]] as CSSGroupingRule; - if (position.length === 1) { - return rule; - } else { - return getNestedRule( - (rule.cssRules[position[1]] as CSSGroupingRule).cssRules, - position.slice(2), - ); - } -} - -export enum StyleRuleType { - Insert, - Remove, - Snapshot, - SetProperty, - RemoveProperty, -} -type InsertRule = { - cssText: string; - type: StyleRuleType.Insert; - index?: number | number[]; -}; -type RemoveRule = { - type: StyleRuleType.Remove; - index: number | number[]; -}; -type SetPropertyRule = { - type: StyleRuleType.SetProperty; - index: number[]; - property: string; - value: string | null; - priority: string | undefined; -}; -type RemovePropertyRule = { - type: StyleRuleType.RemoveProperty; - index: number[]; - property: string; -}; - -export type VirtualStyleRules = Array< - InsertRule | RemoveRule | SetPropertyRule | RemovePropertyRule ->; - -export function getPositionsAndIndex(nestedIndex: number[]) { - const positions = [...nestedIndex]; - const index = positions.pop(); - return { positions, index }; -} - -export function applyVirtualStyleRulesToNode( - styleNode: HTMLStyleElement, - virtualStyleRules: VirtualStyleRules, -) { - const sheet = styleNode.sheet!; - - virtualStyleRules.forEach((rule) => { - if (rule.type === StyleRuleType.Insert) { - try { - if (Array.isArray(rule.index)) { - const { positions, index } = getPositionsAndIndex(rule.index); - const nestedRule = getNestedRule(sheet.cssRules, positions); - nestedRule.insertRule(rule.cssText, index); - } else { - sheet.insertRule(rule.cssText, rule.index); - } - } catch (e) { - /** - * sometimes we may capture rules with browser prefix - * insert rule with prefixs in other browsers may cause Error - */ - } - } else if (rule.type === StyleRuleType.Remove) { - try { - if (Array.isArray(rule.index)) { - const { positions, index } = getPositionsAndIndex(rule.index); - const nestedRule = getNestedRule(sheet.cssRules, positions); - nestedRule.deleteRule(index || 0); - } else { - sheet.deleteRule(rule.index); - } - } catch (e) { - /** - * accessing styleSheet rules may cause SecurityError - * for specific access control settings - */ - } - } else if (rule.type === StyleRuleType.SetProperty) { - const nativeRule = (getNestedRule( - sheet.cssRules, - rule.index, - ) as unknown) as CSSStyleRule; - nativeRule.style.setProperty(rule.property, rule.value, rule.priority); - } else if (rule.type === StyleRuleType.RemoveProperty) { - const nativeRule = (getNestedRule( - sheet.cssRules, - rule.index, - ) as unknown) as CSSStyleRule; - nativeRule.style.removeProperty(rule.property); - } - }); -} diff --git a/packages/rrdom/src/index.ts b/packages/rrdom/src/index.ts index 0caefa8d92..776427a11a 100644 --- a/packages/rrdom/src/index.ts +++ b/packages/rrdom/src/index.ts @@ -12,8 +12,9 @@ import type { canvasEventWithTime, inputData, scrollData, + styleSheetRuleData, + styleDeclarationData, } from 'rrweb/src/types'; -import type { VirtualStyleRules } from './diff'; import { BaseRRNode as RRNode, BaseRRCDATASectionImpl, @@ -164,7 +165,7 @@ export class RRCanvasElement extends RRElement implements IRRElement { } export class RRStyleElement extends RRElement { - public rules: VirtualStyleRules = []; + public rules: (styleSheetRuleData | styleDeclarationData)[] = []; } export class RRIFrameElement extends RRElement { @@ -312,7 +313,8 @@ export function buildFromDom( } if (node.nodeName === 'IFRAME') { - walk((node as HTMLIFrameElement).contentDocument!, rrNode); + const iframeDoc = (node as HTMLIFrameElement).contentDocument; + iframeDoc && walk(iframeDoc, rrNode); } else if ( node.nodeType === NodeType.DOCUMENT_NODE || node.nodeType === NodeType.ELEMENT_NODE || @@ -323,6 +325,7 @@ export function buildFromDom( node.nodeType === NodeType.ELEMENT_NODE && (node as HTMLElement).shadowRoot ) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion walk((node as HTMLElement).shadowRoot!, rrNode); node.childNodes.forEach((childNode) => walk(childNode, rrNode)); } @@ -475,11 +478,5 @@ function walk(node: IRRNode, mirror: IMirror, blankSpace: string) { export { RRNode }; -export { - diff, - createOrGetNode, - StyleRuleType, - ReplayerHandler, - VirtualStyleRules, -} from './diff'; +export { diff, createOrGetNode, ReplayerHandler } from './diff'; export * from './document'; diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 989c9ae083..554ffc55e4 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -2,14 +2,7 @@ * @jest-environment jsdom */ import { getDefaultSN, RRDocument, RRMediaElement } from '../src'; -import { - applyVirtualStyleRulesToNode, - createOrGetNode, - diff, - ReplayerHandler, - StyleRuleType, - VirtualStyleRules, -} from '../src/diff'; +import { createOrGetNode, diff, ReplayerHandler } from '../src/diff'; import { NodeType as RRNodeType, serializedNodeWithId, @@ -17,11 +10,8 @@ import { Mirror, } from 'rrweb-snapshot'; import type { IRRNode } from '../src/document'; -import { - canvasMutationData, - EventType, - IncrementalSource, -} from 'rrweb/src/types'; +import type { canvasMutationData, styleSheetRuleData } from 'rrweb/src/types'; +import { EventType, IncrementalSource } from 'rrweb/src/types'; const elementSn = { type: RRNodeType.Element, @@ -101,6 +91,7 @@ describe('diff algorithm for rrdom', () => { applyCanvas: () => {}, applyInput: () => {}, applyScroll: () => {}, + applyStyleSheetMutation: () => {}, }; }); @@ -162,12 +153,23 @@ describe('diff algorithm for rrdom', () => { document.documentElement.appendChild(element); const rrDocument = new RRDocument(); const rrStyle = rrDocument.createElement('style'); - rrStyle.rules = [ - { cssText: 'div{color: black;}', type: StyleRuleType.Insert, index: 0 }, - ]; + const styleData: styleSheetRuleData = { + source: IncrementalSource.StyleSheetRule, + adds: [ + { + rule: 'div{color: black;}', + index: 0, + }, + ], + }; + rrStyle.rules = [styleData]; + replayer.applyStyleSheetMutation = jest.fn(); diff(element, rrStyle, replayer); - expect(element.sheet!.cssRules.length).toEqual(1); - expect(element.sheet!.cssRules[0].cssText).toEqual('div {color: black;}'); + expect(replayer.applyStyleSheetMutation).toHaveBeenCalledTimes(1); + expect(replayer.applyStyleSheetMutation).toHaveBeenCalledWith( + styleData, + element.sheet, + ); }); it('should diff a canvas element', () => { @@ -1262,107 +1264,4 @@ describe('diff algorithm for rrdom', () => { expect(mirror.getMeta(result)).toEqual(mirror.getMeta(text)); }); }); - - describe('apply virtual style rules to node', () => { - it('should insert rule at index 0 in empty sheet', () => { - document.write(''); - const styleEl = document.getElementsByTagName('style')[0]; - - const cssText = '.added-rule {border: 1px solid yellow;}'; - - const virtualStyleRules: VirtualStyleRules = [ - { cssText, index: 0, type: StyleRuleType.Insert }, - ]; - applyVirtualStyleRulesToNode(styleEl, virtualStyleRules); - - expect(styleEl.sheet?.cssRules?.length).toEqual(1); - expect(styleEl.sheet?.cssRules[0].cssText).toEqual(cssText); - }); - - it('should insert rule at index 0 and keep exsisting rules', () => { - document.write(` - - `); - const styleEl = document.getElementsByTagName('style')[0]; - - const cssText = '.added-rule {border: 1px solid yellow;}'; - const virtualStyleRules: VirtualStyleRules = [ - { cssText, index: 0, type: StyleRuleType.Insert }, - ]; - applyVirtualStyleRulesToNode(styleEl, virtualStyleRules); - - expect(styleEl.sheet?.cssRules?.length).toEqual(3); - expect(styleEl.sheet?.cssRules[0].cssText).toEqual(cssText); - }); - - it('should delete rule at index 0', () => { - document.write(` - - `); - const styleEl = document.getElementsByTagName('style')[0]; - - const virtualStyleRules: VirtualStyleRules = [ - { index: 0, type: StyleRuleType.Remove }, - ]; - applyVirtualStyleRulesToNode(styleEl, virtualStyleRules); - - expect(styleEl.sheet?.cssRules?.length).toEqual(1); - expect(styleEl.sheet?.cssRules[0].cssText).toEqual('div {color: black;}'); - }); - - it('should insert rule at index [0,0] and keep existing rules', () => { - document.write(` - - `); - const styleEl = document.getElementsByTagName('style')[0]; - - const cssText = '.added-rule {border: 1px solid yellow;}'; - const virtualStyleRules: VirtualStyleRules = [ - { cssText, index: [0, 0], type: StyleRuleType.Insert }, - ]; - applyVirtualStyleRulesToNode(styleEl, virtualStyleRules); - - expect( - (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules?.length, - ).toEqual(3); - expect( - (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules[0].cssText, - ).toEqual(cssText); - }); - - it('should delete rule at index [0,1]', () => { - document.write(` - - `); - const styleEl = document.getElementsByTagName('style')[0]; - - const virtualStyleRules: VirtualStyleRules = [ - { index: [0, 1], type: StyleRuleType.Remove }, - ]; - applyVirtualStyleRulesToNode(styleEl, virtualStyleRules); - - expect( - (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules?.length, - ).toEqual(1); - expect( - (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules[0].cssText, - ).toEqual('a {color: blue;}'); - }); - }); }); diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 78705e973e..fb685c6d6c 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -9,7 +9,6 @@ import { } from 'rrweb-snapshot'; import { RRDocument, - StyleRuleType, createOrGetNode, buildFromNode, buildFromDom, @@ -25,7 +24,6 @@ import type { RRCanvasElement, ReplayerHandler, Mirror as RRDOMMirror, - VirtualStyleRules, } from 'rrdom'; import * as mittProxy from 'mitt'; import { polyfill as smoothscrollPolyfill } from './smoothscroll'; @@ -60,6 +58,9 @@ import { canvasMutationParam, canvasEventWithTime, selectionData, + styleSheetRuleData, + styleDeclarationData, + adoptedStyleSheetData, } from '../types'; import { polyfill, @@ -150,6 +151,15 @@ export class Replayer { // In the fast-forward mode, only the last selection data needs to be applied. private lastSelectionData: selectionData | null = null; + // In the fast-forward mode using VirtualDom optimization, all stylesheetRule, and styleDeclaration events on constructed StyleSheets will be delayed to get applied until the flush stage. + private constructedStyleMutations: ( + | styleSheetRuleData + | styleDeclarationData + )[] = []; + + // Similar to the reason for constructedStyleMutations. + private adoptedStyleSheets: adoptedStyleSheetData[] = []; + constructor( events: Array, config?: Partial, @@ -203,13 +213,23 @@ export class Replayer { }, applyInput: this.applyInput.bind(this), applyScroll: this.applyScroll.bind(this), + applyStyleSheetMutation: ( + data: styleDeclarationData | styleSheetRuleData, + styleSheet: CSSStyleSheet, + ) => { + if (data.source === IncrementalSource.StyleSheetRule) + this.applyStyleSheetRule(data, styleSheet); + else if (data.source === IncrementalSource.StyleDeclaration) + this.applyStyleDeclaration(data, styleSheet); + }, }; - diff( - this.iframe.contentDocument!, - this.virtualDom, - replayerHandler, - this.virtualDom.mirror, - ); + this.iframe.contentDocument && + diff( + this.iframe.contentDocument, + this.virtualDom, + replayerHandler, + this.virtualDom.mirror, + ); this.virtualDom.destroyTree(); this.usingVirtualDom = false; @@ -237,6 +257,26 @@ export class Replayer { } } } + + this.constructedStyleMutations.forEach((data) => { + let styleSheet: CSSStyleSheet | null = null; + if (data.styleId) { + styleSheet = this.styleMirror.getStyle(data.styleId); + if (!styleSheet && data.id) + styleSheet = this.constructCSSStyleSheet(data.id, data.styleId); + } + if (!styleSheet) return; + if (data.source === IncrementalSource.StyleSheetRule) + this.applyStyleSheetRule(data, styleSheet); + else if (data.source === IncrementalSource.StyleDeclaration) + this.applyStyleDeclaration(data, styleSheet); + }); + this.constructedStyleMutations = []; + + this.adoptedStyleSheets.forEach((data) => { + this.applyAdoptedStyleSheet(data); + }); + this.adoptedStyleSheets = []; } if (this.mousePos) { @@ -761,14 +801,13 @@ export class Replayer { getDefaultSN(styleEl, this.virtualDom.unserializedId), ); (documentElement as RRElement).insertBefore(styleEl, head as RRElement); - for (let idx = 0; idx < injectStylesRules.length; idx++) { - // push virtual styles - styleEl.rules.push({ - cssText: injectStylesRules[idx], - type: StyleRuleType.Insert, - index: idx, - }); - } + styleEl.rules.push({ + source: IncrementalSource.StyleSheetRule, + adds: injectStylesRules.map((cssText, index) => ({ + rule: cssText, + index, + })), + }); } else { const styleEl = document.createElement('style'); (documentElement as HTMLElement).insertBefore( @@ -1184,163 +1223,41 @@ export class Replayer { } case IncrementalSource.StyleSheetRule: { if (this.usingVirtualDom) { - const target = this.virtualDom.mirror.getNode( - d.id!, - ) as RRStyleElement; - if (!target) { - return this.debugNodeNotFound(d, d.id!); - } - const rules: VirtualStyleRules = target.rules; - d.adds?.forEach(({ rule, index: nestedIndex }) => - rules?.push({ - cssText: rule, - index: nestedIndex, - type: StyleRuleType.Insert, - }), - ); - d.removes?.forEach(({ index: nestedIndex }) => - rules?.push({ index: nestedIndex, type: StyleRuleType.Remove }), - ); + if (d.styleId) this.constructedStyleMutations.push(d); + else if (d.id) + (this.virtualDom.mirror.getNode( + d.id, + ) as RRStyleElement | null)?.rules.push(d); } else { let styleSheet: CSSStyleSheet | null = null; if (d.styleId) { styleSheet = this.styleMirror.getStyle(d.styleId); - if (!styleSheet && d.id) { - /** - * Constructed StyleSheet can't share across multiple documents. - * The replayer has to get the correct host window to recreate a StyleSheetObject. - */ - const host = this.mirror.getNode(d.id); - if (host) { - let newStyleSheet: CSSStyleSheet | null = null; - let hostWindow: IWindow | null = null; - if (hasShadowRoot(host)) - hostWindow = host.ownerDocument?.defaultView || null; - else if (host.nodeName === '#document') - hostWindow = (host as Document).defaultView; - - hostWindow && (newStyleSheet = new hostWindow.CSSStyleSheet()); - if (newStyleSheet) { - this.styleMirror.add(newStyleSheet, d.styleId); - styleSheet = newStyleSheet; - } - } - } + if (!styleSheet && d.id) + styleSheet = this.constructCSSStyleSheet(d.id, d.styleId); } else if (d.id) styleSheet = (this.mirror.getNode(d.id) as HTMLStyleElement)?.sheet || null; - if (!styleSheet) return; - d.adds?.forEach(({ rule, index: nestedIndex }) => { - try { - if (Array.isArray(nestedIndex)) { - const { positions, index } = getPositionsAndIndex(nestedIndex); - const nestedRule = getNestedRule( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - styleSheet!.cssRules, - positions, - ); - nestedRule.insertRule(rule, index); - } else { - const index = - nestedIndex === undefined - ? undefined - : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Math.min(nestedIndex, styleSheet!.cssRules.length); - styleSheet?.insertRule(rule, index); - } - } catch (e) { - /** - * sometimes we may capture rules with browser prefix - * insert rule with prefixs in other browsers may cause Error - */ - /** - * accessing styleSheet rules may cause SecurityError - * for specific access control settings - */ - } - }); - - d.removes?.forEach(({ index: nestedIndex }) => { - try { - if (Array.isArray(nestedIndex)) { - const { positions, index } = getPositionsAndIndex(nestedIndex); - const nestedRule = getNestedRule( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - styleSheet!.cssRules, - positions, - ); - nestedRule.deleteRule(index || 0); - } else { - styleSheet?.deleteRule(nestedIndex); - } - } catch (e) { - /** - * same as insertRule - */ - } - }); - - if (d.replace) - try { - styleSheet.replace(d.replace); - } catch (e) { - // for safety - } - - if (d.replaceSync) - try { - styleSheet.replaceSync(d.replaceSync); - } catch (e) { - // for safety - } + this.applyStyleSheetRule(d, styleSheet); } break; } case IncrementalSource.StyleDeclaration: { if (this.usingVirtualDom) { - const target = this.virtualDom.mirror.getNode( - d.id!, - ) as RRStyleElement; - if (!target) { - return this.debugNodeNotFound(d, d.id!); - } - const rules: VirtualStyleRules = target.rules; - d.set && - rules.push({ - type: StyleRuleType.SetProperty, - index: d.index, - ...d.set, - }); - d.remove && - rules.push({ - type: StyleRuleType.RemoveProperty, - index: d.index, - ...d.remove, - }); + if (d.styleId) { + this.constructedStyleMutations.push(d); + } else if (d.id) + (this.virtualDom.mirror.getNode( + d.id, + ) as RRStyleElement | null)?.rules.push(d); } else { let styleSheet: CSSStyleSheet | null = null; - if (d.id) + if (d.styleId) styleSheet = this.styleMirror.getStyle(d.styleId); + else if (d.id) styleSheet = (this.mirror.getNode(d.id) as HTMLStyleElement)?.sheet || null; - else if (d.styleId) styleSheet = this.styleMirror.getStyle(d.styleId); - if (!styleSheet) return; - if (d.set) { - const rule = (getNestedRule( - styleSheet.rules, - d.index, - ) as unknown) as CSSStyleRule; - rule.style.setProperty(d.set.property, d.set.value, d.set.priority); - } - - if (d.remove) { - const rule = (getNestedRule( - styleSheet.rules, - d.index, - ) as unknown) as CSSStyleRule; - rule.style.removeProperty(d.remove.property); - } + this.applyStyleDeclaration(d, styleSheet); } break; } @@ -1401,20 +1318,8 @@ export class Replayer { break; } case IncrementalSource.AdoptedStyleSheet: { - if (isSync) { - // TODO support sync mode - break; - } - const targetHost = this.mirror.getNode(d.id); - if (!targetHost) return; - const stylesToAdopt = d.styleIds - .map((styleId) => this.styleMirror.getStyle(styleId)) - .filter((style) => style !== null) as CSSStyleSheet[]; - if (hasShadowRoot(targetHost)) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (targetHost as HTMLElement).shadowRoot!.adoptedStyleSheets = stylesToAdopt; - else if (targetHost.nodeName === '#document') - (targetHost as Document).adoptedStyleSheets = stylesToAdopt; + if (isSync) this.adoptedStyleSheets.push(d); + else this.applyAdoptedStyleSheet(d); break; } default: @@ -1858,6 +1763,127 @@ export class Replayer { } } + private applyStyleSheetRule( + data: styleSheetRuleData, + styleSheet: CSSStyleSheet, + ) { + data.adds?.forEach(({ rule, index: nestedIndex }) => { + try { + if (Array.isArray(nestedIndex)) { + const { positions, index } = getPositionsAndIndex(nestedIndex); + const nestedRule = getNestedRule(styleSheet.cssRules, positions); + nestedRule.insertRule(rule, index); + } else { + const index = + nestedIndex === undefined + ? undefined + : Math.min(nestedIndex, styleSheet.cssRules.length); + styleSheet?.insertRule(rule, index); + } + } catch (e) { + /** + * sometimes we may capture rules with browser prefix + * insert rule with prefixs in other browsers may cause Error + */ + /** + * accessing styleSheet rules may cause SecurityError + * for specific access control settings + */ + } + }); + + data.removes?.forEach(({ index: nestedIndex }) => { + try { + if (Array.isArray(nestedIndex)) { + const { positions, index } = getPositionsAndIndex(nestedIndex); + const nestedRule = getNestedRule(styleSheet.cssRules, positions); + nestedRule.deleteRule(index || 0); + } else { + styleSheet?.deleteRule(nestedIndex); + } + } catch (e) { + /** + * same as insertRule + */ + } + }); + + if (data.replace) + try { + styleSheet.replace(data.replace); + } catch (e) { + // for safety + } + + if (data.replaceSync) + try { + styleSheet.replaceSync(data.replaceSync); + } catch (e) { + // for safety + } + } + + private applyStyleDeclaration( + data: styleDeclarationData, + styleSheet: CSSStyleSheet, + ) { + if (data.set) { + const rule = (getNestedRule( + styleSheet.rules, + data.index, + ) as unknown) as CSSStyleRule; + rule.style.setProperty( + data.set.property, + data.set.value, + data.set.priority, + ); + } + + if (data.remove) { + const rule = (getNestedRule( + styleSheet.rules, + data.index, + ) as unknown) as CSSStyleRule; + rule.style.removeProperty(data.remove.property); + } + } + + private applyAdoptedStyleSheet(data: adoptedStyleSheetData) { + const targetHost = this.mirror.getNode(data.id); + if (!targetHost) return; + const stylesToAdopt = data.styleIds + .map((styleId) => this.styleMirror.getStyle(styleId)) + .filter((style) => style !== null) as CSSStyleSheet[]; + if (hasShadowRoot(targetHost)) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (targetHost as HTMLElement).shadowRoot!.adoptedStyleSheets = stylesToAdopt; + else if (targetHost.nodeName === '#document') + (targetHost as Document).adoptedStyleSheets = stylesToAdopt; + } + + /** + * Constructed StyleSheet can't share across multiple documents. + * The replayer has to get the correct host window to recreate a StyleSheetObject. + */ + private constructCSSStyleSheet( + hostId: number, + styleId: number, + ): CSSStyleSheet | null { + const host = this.mirror.getNode(hostId); + if (!host) return null; + let newStyleSheet: CSSStyleSheet | null = null; + let hostWindow: IWindow | null = null; + if (hasShadowRoot(host)) + hostWindow = host.ownerDocument?.defaultView || null; + else if (host.nodeName === '#document') + hostWindow = (host as Document).defaultView; + + if (!hostWindow) return null; + newStyleSheet = new hostWindow.CSSStyleSheet(); + this.styleMirror.add(newStyleSheet, styleId); + return newStyleSheet; + } + private legacy_resolveMissingNode( map: missingNodeMap, parent: Node | RRNode, diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index a8f39530db..161cf340f9 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -803,9 +803,8 @@ describe('replayer', function () { const iframe = await page.$('iframe'); const contentDocument = await iframe!.contentFrame()!; - const checkCorrectness = async () => { - // At 250ms, the adopted stylesheet is still empty. - await page.waitForTimeout(250); + // At 250ms, the adopted stylesheet is still empty. + const check250ms = async () => { expect( await contentDocument!.evaluate( () => @@ -822,9 +821,10 @@ describe('replayer', function () { .adoptedStyleSheets[0].cssRules.length === 0, ), ).toBeTruthy(); + }; - // At 300ms, the adopted stylesheet is replaced with new content. - await page.waitForTimeout(50); + // At 300ms, the adopted stylesheet is replaced with new content. + const check300ms = async () => { expect( await contentDocument!.evaluate( () => @@ -843,9 +843,10 @@ describe('replayer', function () { 'h1 { color: blue; }', ), ).toBeTruthy(); + }; - // At 400ms, check replaceSync API. - await page.waitForTimeout(100); + // At 400ms, check replaceSync API. + const check400ms = async () => { expect( await contentDocument!.evaluate( () => @@ -864,9 +865,10 @@ describe('replayer', function () { 'h1 { font-size: large; }', ), ).toBeTruthy(); + }; - // At 500ms, check CSSStyleDeclaration API. - await page.waitForTimeout(100); + // At 500ms, check CSSStyleDeclaration API. + const check500ms = async () => { expect( await contentDocument!.evaluate( () => @@ -888,9 +890,10 @@ describe('replayer', function () { 'h1 { font-size: medium !important; }', ), ).toBeTruthy(); + }; - // At 600ms, check insertRule and deleteRule API. - await page.waitForTimeout(100); + // At 600ms, check insertRule and deleteRule API. + const check600ms = async () => { expect( await contentDocument!.evaluate( () => @@ -913,12 +916,37 @@ describe('replayer', function () { ).toBeTruthy(); }; - await checkCorrectness(); + await page.waitForTimeout(250); + await check250ms(); + + await page.waitForTimeout(50); + await check300ms(); + + await page.waitForTimeout(100); + await check400ms(); + + await page.waitForTimeout(100); + await check500ms(); + + await page.waitForTimeout(100); + await check600ms(); // To test the correctness of replaying adopted stylesheet mutation events in the fast-forward mode. await page.evaluate('replayer.play(0);'); await waitForRAF(page); - await page.evaluate('replayer.pause(600);'); - await checkCorrectness(); + await page.evaluate('replayer.pause(260);'); + await check250ms(); + + await page.evaluate('replayer.pause(310);'); + await check300ms(); + + await page.evaluate('replayer.pause(410);'); + await check400ms(); + + await page.evaluate('replayer.pause(510);'); + await check500ms(); + + await page.evaluate('replayer.pause(610);'); + await check600ms(); }); }); diff --git a/yarn.lock b/yarn.lock index ef5ad984c8..d5a7e69bbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3974,9 +3974,10 @@ csso@^4.0.2: dependencies: css-tree "^1.1.2" -cssom@^0.4.4, "cssom@https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz": - version "0.6.0" - resolved "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" +cssom@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" + integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== cssom@^0.5.0: version "0.5.0" From d029ef63b43c2cac330ab93244c0157e3fe8440b Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Tue, 6 Sep 2022 01:06:28 +1000 Subject: [PATCH 23/43] style: format files --- guide.md | 2 +- .../src/record/constructableStyleSheets.d.ts | 2 +- packages/rrweb/src/record/mutation.ts | 3 ++- .../record/observers/canvas/canvas-manager.ts | 26 ++++++++++++++----- packages/rrweb/src/types.ts | 4 +-- packages/rrweb/src/utils.ts | 2 +- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/guide.md b/guide.md index 6d2173df37..fa990655b3 100644 --- a/guide.md +++ b/guide.md @@ -143,7 +143,7 @@ The parameter of `rrweb.record` accepts the following options. | blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter | | blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter | | ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter | -| ignoreCSSAttributes | null | array of CSS attributes that should be ignored | +| ignoreCSSAttributes | null | array of CSS attributes that should be ignored | | maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter | | maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter | | maskAllInputs | false | mask all input content as \* | diff --git a/packages/rrweb/src/record/constructableStyleSheets.d.ts b/packages/rrweb/src/record/constructableStyleSheets.d.ts index 42d2360798..df00485fb2 100644 --- a/packages/rrweb/src/record/constructableStyleSheets.d.ts +++ b/packages/rrweb/src/record/constructableStyleSheets.d.ts @@ -5,6 +5,6 @@ declare interface DocumentOrShadowRoot { } declare interface CSSStyleSheet { - replace(text: string): void; + replace(text: string): Promise; replaceSync(text: string): void; } diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index e2e2429214..b3135bb2b8 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -572,7 +572,8 @@ export default class MutationBuffer { /** * Parent is blocked, ignore all child mutations */ - if (isBlocked(m.target, this.blockClass, this.blockSelector, true)) return; + if (isBlocked(m.target, this.blockClass, this.blockSelector, true)) + return; m.addedNodes.forEach((n) => this.genAdds(n, m.target)); m.removedNodes.forEach((n) => { diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index 5b2a874d01..557826f077 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -60,11 +60,17 @@ export class CanvasManager { mutationCb: canvasMutationCallback; win: IWindow; blockClass: blockClass; - blockSelector: string | null, + blockSelector: string | null; mirror: Mirror; sampling?: 'all' | number; }) { - const { sampling = 'all', win, blockClass, blockSelector, recordCanvas } = options; + const { + sampling = 'all', + win, + blockClass, + blockSelector, + recordCanvas, + } = options; this.mutationCb = options.mutationCb; this.mirror = options.mirror; @@ -97,7 +103,11 @@ export class CanvasManager { blockClass: blockClass, blockSelector: string | null, ) { - const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector); + const canvasContextReset = initCanvasContextObserver( + win, + blockClass, + blockSelector, + ); const snapshotInProgressMap: Map = new Map(); const worker = new ImageBitmapDataURLWorker() as ImageBitmapDataURLRequestWorker; worker.onmessage = (e) => { @@ -142,11 +152,11 @@ export class CanvasManager { const getCanvas = (): HTMLCanvasElement[] => { const matchedCanvas: HTMLCanvasElement[] = []; - win.document.querySelectorAll('canvas').forEach(canvas => { + win.document.querySelectorAll('canvas').forEach((canvas) => { if (!isBlocked(canvas, blockClass, blockSelector, true)) { matchedCanvas.push(canvas); } - }) + }); return matchedCanvas; }; @@ -215,7 +225,11 @@ export class CanvasManager { this.startRAFTimestamping(); this.startPendingCanvasMutationFlusher(); - const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector); + const canvasContextReset = initCanvasContextObserver( + win, + blockClass, + blockSelector, + ); const canvas2DReset = initCanvas2DMutationObserver( this.processMutation.bind(this), win, diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index ac1e4595c1..9a125869f8 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -251,7 +251,7 @@ export type recordOptions = { maskInputFn?: MaskInputFn; maskTextFn?: MaskTextFn; slimDOMOptions?: SlimDOMOptions | 'all' | true; - ignoreCSSAttributes?:Set; + ignoreCSSAttributes?: Set; inlineStylesheet?: boolean; hooks?: hooksParam; packFn?: PackFn; @@ -301,7 +301,7 @@ export type observerParam = { stylesheetManager: StylesheetManager; shadowDomManager: ShadowDomManager; canvasManager: CanvasManager; - ignoreCSSAttributes:Set; + ignoreCSSAttributes: Set; plugins: Array<{ observer: ( cb: (...arg: Array) => void, diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 249a89105f..b782bd52e2 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -205,7 +205,7 @@ export function isBlocked( ? (node as HTMLElement) : node.parentElement; if (!el) return false; - + if (typeof blockClass === 'string') { if (el.classList.contains(blockClass)) return true; if (checkAncestors && el.closest('.' + blockClass) !== null) return true; From 09797c30672b65dc0080ba9383df4e17b13714a2 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Tue, 6 Sep 2022 01:13:31 +1000 Subject: [PATCH 24/43] test: improve the robustness of the test case --- packages/rrweb/test/record.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 392d7e1be0..cad6bda640 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -557,16 +557,16 @@ describe('record', function (this: ISuite) { setTimeout(() => { sheet1.insertRule('div { display: inline ; }', 1); sheet2.replaceSync('h1 { font-size: large; }'); - }, 20); + }, 50); setTimeout(() => { const sheet3 = new iframeWin.CSSStyleSheet(); sheet3.replaceSync('span {background-color: red;}'); iframeDocument!.adoptedStyleSheets = [sheet3, sheet2]; shadowHost.shadowRoot!.adoptedStyleSheets = [sheet1, sheet3]; - }, 25); + }, 60); }); - await waitForRAF(ctx.page); + await ctx.page.waitForTimeout(60); assertSnapshot(ctx.events); }); From 0c353119ad9658605d59603da529fcf9f0f784a4 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Tue, 6 Sep 2022 11:58:06 +1000 Subject: [PATCH 25/43] CI: fix an eslint error --- packages/rrweb/src/replay/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index fb685c6d6c..cc8c7f3257 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1810,7 +1810,7 @@ export class Replayer { if (data.replace) try { - styleSheet.replace(data.replace); + void styleSheet.replace(data.replace); } catch (e) { // for safety } From 48eed10f1681db2f758c00819416d3fc75228f4a Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Tue, 6 Sep 2022 13:12:41 +1000 Subject: [PATCH 26/43] test: improve the robustness of the test case --- packages/rrweb/test/record.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index cad6bda640..f64258d6a1 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -566,7 +566,7 @@ describe('record', function (this: ISuite) { shadowHost.shadowRoot!.adoptedStyleSheets = [sheet1, sheet3]; }, 60); }); - await ctx.page.waitForTimeout(60); + await ctx.page.waitForTimeout(100); assertSnapshot(ctx.events); }); From abf51350ba749377f0d58db394bbd3f98c139880 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 7 Sep 2022 11:09:57 +1000 Subject: [PATCH 27/43] fix: adoptedStyleSheets not applied in fast-forward mode (virtual dom optimization not used) --- packages/rrweb/src/replay/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index cc8c7f3257..75ee2d483d 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1318,7 +1318,7 @@ export class Replayer { break; } case IncrementalSource.AdoptedStyleSheet: { - if (isSync) this.adoptedStyleSheets.push(d); + if (this.usingVirtualDom) this.adoptedStyleSheets.push(d); else this.applyAdoptedStyleSheet(d); break; } From 89c9f91f9e3ddc620cb767bc1d9a7230ad4eee28 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 7 Sep 2022 15:58:59 +1000 Subject: [PATCH 28/43] refactor the data structure of adoptedStyleSheet event to make it more efficient and robust --- packages/rrweb/src/record/index.ts | 24 +- .../rrweb/src/record/stylesheet-manager.ts | 15 +- packages/rrweb/src/replay/index.ts | 110 ++++---- packages/rrweb/src/types.ts | 8 +- .../test/__snapshots__/record.test.ts.snap | 238 ++++++++---------- .../adopted-style-sheet-modification.ts | 32 +-- .../rrweb/test/events/adopted-style-sheet.ts | 88 +++---- 7 files changed, 219 insertions(+), 296 deletions(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 0a13532817..368d7587b5 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -24,7 +24,6 @@ import { mutationCallbackParam, scrollCallback, canvasMutationParam, - styleSheetRuleParam, adoptedStyleSheetParam, } from '../types'; import { IframeManager } from './iframe-manager'; @@ -217,17 +216,6 @@ function record( }), ); - const wrappedStyleSheetRulesEmit = (r: styleSheetRuleParam) => - wrappedEmit( - wrapEvent({ - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.StyleSheetRule, - ...r, - }, - }), - ); - const wrappedAdoptedStyleSheetEmit = (a: adoptedStyleSheetParam) => wrappedEmit( wrapEvent({ @@ -241,7 +229,6 @@ function record( const stylesheetManager = new StylesheetManager({ mutationCb: wrappedMutationEmit, - styleRulesCb: wrappedStyleSheetRulesEmit, adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit, }); @@ -440,7 +427,16 @@ function record( }, }), ), - styleSheetRuleCb: wrappedStyleSheetRulesEmit, + styleSheetRuleCb: (r) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + ...r, + }, + }), + ), styleDeclarationCb: (r) => wrappedEmit( wrapEvent({ diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts index f6248bac19..251d115498 100644 --- a/packages/rrweb/src/record/stylesheet-manager.ts +++ b/packages/rrweb/src/record/stylesheet-manager.ts @@ -2,6 +2,7 @@ import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot'; import { getCssRuleString } from 'rrweb-snapshot'; import type { adoptedStyleSheetCallback, + adoptedStyleSheetParam, mutationCallBack, styleSheetRuleCallback, } from '../types'; @@ -10,17 +11,14 @@ import { StyleSheetMirror } from '../utils'; export class StylesheetManager { private trackedLinkElements: WeakSet = new WeakSet(); private mutationCb: mutationCallBack; - private styleRulesCb: styleSheetRuleCallback; private adoptedStyleSheetCb: adoptedStyleSheetCallback; public styleMirror = new StyleSheetMirror(); constructor(options: { mutationCb: mutationCallBack; - styleRulesCb: styleSheetRuleCallback; adoptedStyleSheetCb: adoptedStyleSheetCallback; }) { this.mutationCb = options.mutationCb; - this.styleRulesCb = options.styleRulesCb; this.adoptedStyleSheetCb = options.adoptedStyleSheetCb; } @@ -54,28 +52,29 @@ export class StylesheetManager { public adoptStyleSheets(sheets: CSSStyleSheet[], hostId: number) { if (sheets.length === 0) return; - const adoptedStyleSheetData = { + const adoptedStyleSheetData: adoptedStyleSheetParam = { id: hostId, styleIds: [] as number[], }; + const styles: NonNullable = []; for (const sheet of sheets) { let styleId; if (!this.styleMirror.has(sheet)) { styleId = this.styleMirror.add(sheet); const rules = Array.from(sheet.rules || CSSRule); - this.styleRulesCb({ - adds: rules.map((r, index) => { + styles.push({ + styleId, + rules: rules.map((r, index) => { return { rule: getCssRuleString(r), index, }; }), - styleId, - id: hostId, }); } else styleId = this.styleMirror.getId(sheet); adoptedStyleSheetData.styleIds.push(styleId); } + if (styles.length > 0) adoptedStyleSheetData.styles = styles; this.adoptedStyleSheetCb(adoptedStyleSheetData); } diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 75ee2d483d..68ae34a86a 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -259,17 +259,7 @@ export class Replayer { } this.constructedStyleMutations.forEach((data) => { - let styleSheet: CSSStyleSheet | null = null; - if (data.styleId) { - styleSheet = this.styleMirror.getStyle(data.styleId); - if (!styleSheet && data.id) - styleSheet = this.constructCSSStyleSheet(data.id, data.styleId); - } - if (!styleSheet) return; - if (data.source === IncrementalSource.StyleSheetRule) - this.applyStyleSheetRule(data, styleSheet); - else if (data.source === IncrementalSource.StyleDeclaration) - this.applyStyleDeclaration(data, styleSheet); + this.applyStyleSheetMutation(data); }); this.constructedStyleMutations = []; @@ -1221,44 +1211,15 @@ export class Replayer { } break; } - case IncrementalSource.StyleSheetRule: { + case IncrementalSource.StyleSheetRule: + case IncrementalSource.StyleDeclaration: { if (this.usingVirtualDom) { if (d.styleId) this.constructedStyleMutations.push(d); else if (d.id) (this.virtualDom.mirror.getNode( d.id, ) as RRStyleElement | null)?.rules.push(d); - } else { - let styleSheet: CSSStyleSheet | null = null; - if (d.styleId) { - styleSheet = this.styleMirror.getStyle(d.styleId); - if (!styleSheet && d.id) - styleSheet = this.constructCSSStyleSheet(d.id, d.styleId); - } else if (d.id) - styleSheet = - (this.mirror.getNode(d.id) as HTMLStyleElement)?.sheet || null; - if (!styleSheet) return; - this.applyStyleSheetRule(d, styleSheet); - } - break; - } - case IncrementalSource.StyleDeclaration: { - if (this.usingVirtualDom) { - if (d.styleId) { - this.constructedStyleMutations.push(d); - } else if (d.id) - (this.virtualDom.mirror.getNode( - d.id, - ) as RRStyleElement | null)?.rules.push(d); - } else { - let styleSheet: CSSStyleSheet | null = null; - if (d.styleId) styleSheet = this.styleMirror.getStyle(d.styleId); - else if (d.id) - styleSheet = - (this.mirror.getNode(d.id) as HTMLStyleElement)?.sheet || null; - if (!styleSheet) return; - this.applyStyleDeclaration(d, styleSheet); - } + } else this.applyStyleSheetMutation(d); break; } case IncrementalSource.CanvasMutation: { @@ -1763,6 +1724,21 @@ export class Replayer { } } + private applyStyleSheetMutation( + data: styleDeclarationData | styleSheetRuleData, + ) { + let styleSheet: CSSStyleSheet | null = null; + if (data.styleId) styleSheet = this.styleMirror.getStyle(data.styleId); + else if (data.id) + styleSheet = + (this.mirror.getNode(data.id) as HTMLStyleElement)?.sheet || null; + if (!styleSheet) return; + if (data.source === IncrementalSource.StyleSheetRule) + this.applyStyleSheetRule(data, styleSheet); + else if (data.source === IncrementalSource.StyleDeclaration) + this.applyStyleDeclaration(data, styleSheet); + } + private applyStyleSheetRule( data: styleSheetRuleData, styleSheet: CSSStyleSheet, @@ -1851,6 +1827,31 @@ export class Replayer { private applyAdoptedStyleSheet(data: adoptedStyleSheetData) { const targetHost = this.mirror.getNode(data.id); if (!targetHost) return; + // Create StyleSheet objects which will be adopted after. + data.styles?.forEach((style) => { + let newStyleSheet: CSSStyleSheet | null = null; + /** + * Constructed StyleSheet can't share across multiple documents. + * The replayer has to get the correct host window to recreate a StyleSheetObject. + */ + let hostWindow: IWindow | null = null; + if (hasShadowRoot(targetHost)) + hostWindow = targetHost.ownerDocument?.defaultView || null; + else if (targetHost.nodeName === '#document') + hostWindow = (targetHost as Document).defaultView; + + if (!hostWindow) return; + newStyleSheet = new hostWindow.CSSStyleSheet(); + this.styleMirror.add(newStyleSheet, style.styleId); + // To reuse the code of applying stylesheet rules + this.applyStyleSheetRule( + { + source: IncrementalSource.StyleSheetRule, + adds: style.rules, + }, + newStyleSheet, + ); + }); const stylesToAdopt = data.styleIds .map((styleId) => this.styleMirror.getStyle(styleId)) .filter((style) => style !== null) as CSSStyleSheet[]; @@ -1861,29 +1862,6 @@ export class Replayer { (targetHost as Document).adoptedStyleSheets = stylesToAdopt; } - /** - * Constructed StyleSheet can't share across multiple documents. - * The replayer has to get the correct host window to recreate a StyleSheetObject. - */ - private constructCSSStyleSheet( - hostId: number, - styleId: number, - ): CSSStyleSheet | null { - const host = this.mirror.getNode(hostId); - if (!host) return null; - let newStyleSheet: CSSStyleSheet | null = null; - let hostWindow: IWindow | null = null; - if (hasShadowRoot(host)) - hostWindow = host.ownerDocument?.defaultView || null; - else if (host.nodeName === '#document') - hostWindow = (host as Document).defaultView; - - if (!hostWindow) return null; - newStyleSheet = new hostWindow.CSSStyleSheet(); - this.styleMirror.add(newStyleSheet, styleId); - return newStyleSheet; - } - private legacy_resolveMissingNode( map: missingNodeMap, parent: Node | RRNode, diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 9a125869f8..e76db2a89c 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -524,8 +524,14 @@ export type styleSheetRuleParam = { export type styleSheetRuleCallback = (s: styleSheetRuleParam) => void; export type adoptedStyleSheetParam = { - // 0 indicates the outermost document, other numbers indicate id of IFrame elements or shadow DOMs' hosts. + // 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[]; + }[]; + // StyleSheet ids to be adopted. styleIds: number[]; }; diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index a0eec47c89..4081bbeb5d 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -564,20 +564,6 @@ exports[`record captures adopted stylesheets in nested shadow doms and iframes 1 \\"isAttachIframe\\": true } }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"adds\\": [ - { - \\"rule\\": \\"h1 { color: blue; }\\", - \\"index\\": 0 - } - ], - \\"styleId\\": 1, - \\"id\\": 29 - } - }, { \\"type\\": 3, \\"data\\": { @@ -585,21 +571,18 @@ exports[`record captures adopted stylesheets in nested shadow doms and iframes 1 \\"id\\": 29, \\"styleIds\\": [ 1 - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"adds\\": [ + ], + \\"styles\\": [ { - \\"rule\\": \\"div { font-size: large; }\\", - \\"index\\": 0 + \\"styleId\\": 1, + \\"rules\\": [ + { + \\"rule\\": \\"h1 { color: blue; }\\", + \\"index\\": 0 + } + ] } - ], - \\"styleId\\": 2, - \\"id\\": 33 + ] } }, { @@ -609,6 +592,17 @@ exports[`record captures adopted stylesheets in nested shadow doms and iframes 1 \\"id\\": 33, \\"styleIds\\": [ 2 + ], + \\"styles\\": [ + { + \\"styleId\\": 2, + \\"rules\\": [ + { + \\"rule\\": \\"div { font-size: large; }\\", + \\"index\\": 0 + } + ] + } ] } }, @@ -633,20 +627,6 @@ exports[`record captures adopted stylesheets in nested shadow doms and iframes 1 \\"replaceSync\\": \\"h1 { font-size: large; }\\" } }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"adds\\": [ - { - \\"rule\\": \\"span { background-color: red; }\\", - \\"index\\": 0 - } - ], - \\"styleId\\": 3, - \\"id\\": 29 - } - }, { \\"type\\": 3, \\"data\\": { @@ -655,6 +635,17 @@ exports[`record captures adopted stylesheets in nested shadow doms and iframes 1 \\"styleIds\\": [ 3, 2 + ], + \\"styles\\": [ + { + \\"styleId\\": 3, + \\"rules\\": [ + { + \\"rule\\": \\"span { background-color: red; }\\", + \\"index\\": 0 + } + ] + } ] } }, @@ -820,28 +811,6 @@ exports[`record captures adopted stylesheets in shadow doms and iframe 1`] = ` } } }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"adds\\": [ - { - \\"rule\\": \\"div { color: yellow; }\\", - \\"index\\": 0 - }, - { - \\"rule\\": \\"h2 { color: orange; }\\", - \\"index\\": 1 - }, - { - \\"rule\\": \\"h3 { font-size: larger; }\\", - \\"index\\": 2 - } - ], - \\"styleId\\": 1, - \\"id\\": 1 - } - }, { \\"type\\": 3, \\"data\\": { @@ -849,21 +818,26 @@ exports[`record captures adopted stylesheets in shadow doms and iframe 1`] = ` \\"id\\": 1, \\"styleIds\\": [ 1 - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"adds\\": [ + ], + \\"styles\\": [ { - \\"rule\\": \\"span { color: red; }\\", - \\"index\\": 0 + \\"styleId\\": 1, + \\"rules\\": [ + { + \\"rule\\": \\"div { color: yellow; }\\", + \\"index\\": 0 + }, + { + \\"rule\\": \\"h2 { color: orange; }\\", + \\"index\\": 1 + }, + { + \\"rule\\": \\"h3 { font-size: larger; }\\", + \\"index\\": 2 + } + ] } - ], - \\"styleId\\": 2, - \\"id\\": 10 + ] } }, { @@ -874,6 +848,17 @@ exports[`record captures adopted stylesheets in shadow doms and iframe 1`] = ` \\"styleIds\\": [ 1, 2 + ], + \\"styles\\": [ + { + \\"styleId\\": 2, + \\"rules\\": [ + { + \\"rule\\": \\"span { color: red; }\\", + \\"index\\": 0 + } + ] + } ] } }, @@ -941,20 +926,6 @@ exports[`record captures adopted stylesheets in shadow doms and iframe 1`] = ` \\"isAttachIframe\\": true } }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"adds\\": [ - { - \\"rule\\": \\"h1 { color: blue; }\\", - \\"index\\": 0 - } - ], - \\"styleId\\": 3, - \\"id\\": 20 - } - }, { \\"type\\": 3, \\"data\\": { @@ -962,21 +933,18 @@ exports[`record captures adopted stylesheets in shadow doms and iframe 1`] = ` \\"id\\": 20, \\"styleIds\\": [ 3 - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"adds\\": [ + ], + \\"styles\\": [ { - \\"rule\\": \\"span { color: green; }\\", - \\"index\\": 0 + \\"styleId\\": 3, + \\"rules\\": [ + { + \\"rule\\": \\"h1 { color: blue; }\\", + \\"index\\": 0 + } + ] } - ], - \\"styleId\\": 4, - \\"id\\": 1 + ] } }, { @@ -988,21 +956,18 @@ exports[`record captures adopted stylesheets in shadow doms and iframe 1`] = ` 4, 1, 2 - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"adds\\": [ + ], + \\"styles\\": [ { - \\"rule\\": \\"h2 { color: purple; }\\", - \\"index\\": 0 + \\"styleId\\": 4, + \\"rules\\": [ + { + \\"rule\\": \\"span { color: green; }\\", + \\"index\\": 0 + } + ] } - ], - \\"styleId\\": 5, - \\"id\\": 20 + ] } }, { @@ -1013,6 +978,17 @@ exports[`record captures adopted stylesheets in shadow doms and iframe 1`] = ` \\"styleIds\\": [ 5, 3 + ], + \\"styles\\": [ + { + \\"styleId\\": 5, + \\"rules\\": [ + { + \\"rule\\": \\"h2 { color: purple; }\\", + \\"index\\": 0 + } + ] + } ] } }, @@ -1303,15 +1279,6 @@ exports[`record captures mutations on adopted stylesheets 1`] = ` } } }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"adds\\": [], - \\"styleId\\": 1, - \\"id\\": 1 - } - }, { \\"type\\": 3, \\"data\\": { @@ -1319,6 +1286,12 @@ exports[`record captures mutations on adopted stylesheets 1`] = ` \\"id\\": 1, \\"styleIds\\": [ 1 + ], + \\"styles\\": [ + { + \\"styleId\\": 1, + \\"rules\\": [] + } ] } }, @@ -1386,15 +1359,6 @@ exports[`record captures mutations on adopted stylesheets 1`] = ` \\"isAttachIframe\\": true } }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"adds\\": [], - \\"styleId\\": 2, - \\"id\\": 12 - } - }, { \\"type\\": 3, \\"data\\": { @@ -1402,6 +1366,12 @@ exports[`record captures mutations on adopted stylesheets 1`] = ` \\"id\\": 12, \\"styleIds\\": [ 2 + ], + \\"styles\\": [ + { + \\"styleId\\": 2, + \\"rules\\": [] + } ] } }, diff --git a/packages/rrweb/test/events/adopted-style-sheet-modification.ts b/packages/rrweb/test/events/adopted-style-sheet-modification.ts index 570bb6851c..13d09eef0e 100644 --- a/packages/rrweb/test/events/adopted-style-sheet-modification.ts +++ b/packages/rrweb/test/events/adopted-style-sheet-modification.ts @@ -92,22 +92,18 @@ const events: eventWithTime[] = [ }, timestamp: now + 100, }, - { - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.StyleSheetRule, - adds: [], - styleId: 1, - id: 1, - }, - timestamp: now + 150, - }, { type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.AdoptedStyleSheet, id: 1, styleIds: [1], + styles: [ + { + styleId: 1, + rules: [], + }, + ], }, timestamp: now + 150, }, @@ -176,22 +172,18 @@ const events: eventWithTime[] = [ }, timestamp: now + 200, }, - { - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.StyleSheetRule, - adds: [], - styleId: 2, - id: 12, - }, - timestamp: now + 250, - }, { type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.AdoptedStyleSheet, id: 12, styleIds: [2], + styles: [ + { + rules: [], + styleId: 2, + }, + ], }, timestamp: now + 250, }, diff --git a/packages/rrweb/test/events/adopted-style-sheet.ts b/packages/rrweb/test/events/adopted-style-sheet.ts index 1098c96cee..72e2a48957 100644 --- a/packages/rrweb/test/events/adopted-style-sheet.ts +++ b/packages/rrweb/test/events/adopted-style-sheet.ts @@ -152,31 +152,26 @@ const events: eventWithTime[] = [ }, timestamp: now + 100, }, - // Create a unattached stylesheet #1 object at 200ms + // Adopt the stylesheet #1 on document at 200ms { type: EventType.IncrementalSnapshot, data: { - source: IncrementalSource.StyleSheetRule, - adds: [ + source: IncrementalSource.AdoptedStyleSheet, + id: 1, + styleIds: [1], + styles: [ { - rule: 'div { color: yellow; }', + rules: [ + { + rule: 'div { color: yellow; }', + }, + ], + styleId: 1, }, ], - styleId: 1, - id: 1, }, timestamp: now + 200, }, - // Adopt the stylesheet #1 on document - { - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.AdoptedStyleSheet, - id: 1, - styleIds: [1], - }, - timestamp: now + 210, - }, // Add an IFrame element { type: EventType.IncrementalSnapshot, @@ -243,42 +238,23 @@ const events: eventWithTime[] = [ }, timestamp: now + 250, }, - // Create a unattached stylesheet #2 object - { - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.StyleSheetRule, - adds: [ - { - rule: 'span { color: red; }', - }, - ], - styleId: 2, - id: 10, - }, - timestamp: now + 300, - }, - // Adopt the stylesheet #2 on a shadow root + // Adopt the stylesheet #2 on a shadow root at 300ms { type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.AdoptedStyleSheet, id: 10, styleIds: [1, 2], - }, - timestamp: now + 300, - }, - { - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.StyleSheetRule, - adds: [ + styles: [ { - rule: 'h1 { color: blue; }', + rules: [ + { + rule: 'span { color: red; }', + }, + ], + styleId: 2, }, ], - styleId: 3, - id: 20, }, timestamp: now + 300, }, @@ -289,6 +265,16 @@ const events: eventWithTime[] = [ source: IncrementalSource.AdoptedStyleSheet, id: 20, styleIds: [3], + styles: [ + { + rules: [ + { + rule: 'h1 { color: blue; }', + }, + ], + styleId: 3, + }, + ], }, timestamp: now + 300, }, @@ -347,16 +333,6 @@ const events: eventWithTime[] = [ }, timestamp: now + 500, }, - { - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.StyleSheetRule, - styleId: 4, - id: 16, - adds: [{ rule: 'span { color: green; }' }], - }, - timestamp: now + 550, - }, // Adopt the stylesheet #4 on the shadow dom { type: EventType.IncrementalSnapshot, @@ -364,6 +340,12 @@ const events: eventWithTime[] = [ source: IncrementalSource.AdoptedStyleSheet, id: 16, styleIds: [4], + styles: [ + { + rules: [{ rule: 'span { color: green; }' }], + styleId: 4, + }, + ], }, timestamp: now + 550, }, From 64d3c4ed2503af9772d91c54f593c8c76bd256f5 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 7 Sep 2022 16:24:59 +1000 Subject: [PATCH 29/43] improve the robustness in the live mode In the live mode where events are transferred over network without strict order guarantee, some newer events are applied before some old events and adopted stylesheets may haven't been created. Added a retry mechanism to solve this problem. --- packages/rrweb/src/replay/index.ts | 34 +++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 68ae34a86a..031a006893 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1852,14 +1852,32 @@ export class Replayer { newStyleSheet, ); }); - const stylesToAdopt = data.styleIds - .map((styleId) => this.styleMirror.getStyle(styleId)) - .filter((style) => style !== null) as CSSStyleSheet[]; - if (hasShadowRoot(targetHost)) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (targetHost as HTMLElement).shadowRoot!.adoptedStyleSheets = stylesToAdopt; - else if (targetHost.nodeName === '#document') - (targetHost as Document).adoptedStyleSheets = stylesToAdopt; + + const MAX_RETRY_TIME = 10; + let count = 0; + const adoptStyleSheets = (targetHost: Node, styleIds: number[]) => { + const stylesToAdopt = styleIds + .map((styleId) => this.styleMirror.getStyle(styleId)) + .filter((style) => style !== null) as CSSStyleSheet[]; + if (hasShadowRoot(targetHost)) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (targetHost as HTMLElement).shadowRoot!.adoptedStyleSheets = stylesToAdopt; + else if (targetHost.nodeName === '#document') + (targetHost as Document).adoptedStyleSheets = stylesToAdopt; + + /** + * In the live mode where events are transferred over network without strict order guarantee, some newer events are applied before some old events and adopted stylesheets may haven't been created. + * This retry mechanism can help resolve this situation. + */ + if (stylesToAdopt.length !== styleIds.length && count < MAX_RETRY_TIME) { + setTimeout( + () => adoptStyleSheets(targetHost, styleIds), + 0 + 100 * count, + ); + count++; + } + }; + adoptStyleSheets(targetHost, data.styleIds); } private legacy_resolveMissingNode( From ee4e879cd41741159dd41663f285970c220b0ca7 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 9 Sep 2022 01:08:52 +1000 Subject: [PATCH 30/43] apply Yanzhen's review suggestion --- packages/rrweb/src/record/observer.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 49a31ccd13..e575bf5fe7 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -502,7 +502,7 @@ function getNestedCSSRulePositions(rule: CSSRule): number[] { * For StyleSheets in Element, this function retrieves id of its host element. * For adopted StyleSheets, this function retrieves its styleId from a styleMirror. */ -function getIdOrStyleId( +function getIdAndStyleId( sheet: CSSStyleSheet | undefined | null, mirror: Mirror, styleMirror: StyleSheetMirror, @@ -531,7 +531,7 @@ function initStyleSheetObserver( rule: string, index?: number, ) { - const { id, styleId } = getIdOrStyleId( + const { id, styleId } = getIdAndStyleId( this, mirror, stylesheetManager.styleMirror, @@ -553,7 +553,7 @@ function initStyleSheetObserver( this: CSSStyleSheet, index: number, ) { - const { id, styleId } = getIdOrStyleId( + const { id, styleId } = getIdAndStyleId( this, mirror, stylesheetManager.styleMirror, @@ -575,7 +575,7 @@ function initStyleSheetObserver( this: CSSStyleSheet, text: string, ) { - const { id, styleId } = getIdOrStyleId( + const { id, styleId } = getIdAndStyleId( this, mirror, stylesheetManager.styleMirror, @@ -597,7 +597,7 @@ function initStyleSheetObserver( this: CSSStyleSheet, text: string, ) { - const { id, styleId } = getIdOrStyleId( + const { id, styleId } = getIdAndStyleId( this, mirror, stylesheetManager.styleMirror, @@ -654,7 +654,7 @@ function initStyleSheetObserver( rule: string, index?: number, ) { - const { id, styleId } = getIdOrStyleId( + const { id, styleId } = getIdAndStyleId( this.parentStyleSheet, mirror, stylesheetManager.styleMirror, @@ -682,7 +682,7 @@ function initStyleSheetObserver( this: CSSGroupingRule, index: number, ) { - const { id, styleId } = getIdOrStyleId( + const { id, styleId } = getIdAndStyleId( this.parentStyleSheet, mirror, stylesheetManager.styleMirror, @@ -797,7 +797,7 @@ function initStyleDeclarationObserver( if (ignoreCSSAttributes.has(property)) { return setProperty.apply(this, [property, value, priority]); } - const { id, styleId } = getIdOrStyleId( + const { id, styleId } = getIdAndStyleId( this.parentRule?.parentStyleSheet, mirror, stylesheetManager.styleMirror, @@ -828,7 +828,7 @@ function initStyleDeclarationObserver( if (ignoreCSSAttributes.has(property)) { return removeProperty.apply(this, [property]); } - const { id, styleId } = getIdOrStyleId( + const { id, styleId } = getIdAndStyleId( this.parentRule?.parentStyleSheet, mirror, stylesheetManager.styleMirror, From b4436455e7219eaac2997765302d7aa15b500f64 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 9 Sep 2022 13:32:57 +1000 Subject: [PATCH 31/43] update action name --- .github/workflows/style-check.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/style-check.yml b/.github/workflows/style-check.yml index b028a636b5..50498ee960 100644 --- a/.github/workflows/style-check.yml +++ b/.github/workflows/style-check.yml @@ -27,7 +27,7 @@ jobs: run: yarn lint:report # Continue to the next step even if this fails continue-on-error: true - - name: Upload ESLint report + - name: Upload ESLint Report uses: actions/upload-artifact@v3 with: name: eslint_report.json @@ -50,7 +50,7 @@ jobs: report-json: 'eslint_report.json' prettier_check: - # In the forked PR, it's hard to format code and push to the branch directly. + # In the forked PR, it's hard to format code and push to the branch directly, so the action only check the format correctness. if: github.event_name != 'push' && github.event.pull_request.head.repo.full_name != 'rrweb-io/rrweb' runs-on: ubuntu-latest name: Format Check @@ -66,7 +66,7 @@ jobs: cache: 'yarn' - name: Install Dependencies run: yarn - - name: Prettify code + - name: Prettier Check run: yarn prettier --check '**/*.{ts,md}' prettier: @@ -86,9 +86,9 @@ jobs: cache: 'yarn' - name: Install Dependencies run: yarn - - name: Prettify code + - name: Prettify Code run: yarn prettier --write '**/*.{ts,md}' - - name: Commit changes + - name: Commit Changes uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: Apply formatting changes From ca9da9a9e2fe2190768db364bf5f6a3a53673bf9 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 15 Sep 2022 20:27:30 +1000 Subject: [PATCH 32/43] test: make the test case more robust for travis CI --- packages/rrweb/test/record.test.ts | 4 ++-- packages/rrweb/test/replayer.test.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 916a79bfd5..d66302fd0c 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -564,9 +564,9 @@ describe('record', function (this: ISuite) { sheet3.replaceSync('span {background-color: red;}'); iframeDocument!.adoptedStyleSheets = [sheet3, sheet2]; shadowHost.shadowRoot!.adoptedStyleSheets = [sheet1, sheet3]; - }, 60); + }, 100); }); - await ctx.page.waitForTimeout(100); + await ctx.page.waitForTimeout(200); assertSnapshot(ctx.events); }); diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index 7255e27816..efe3e701df 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -934,19 +934,19 @@ describe('replayer', function () { // To test the correctness of replaying adopted stylesheet mutation events in the fast-forward mode. await page.evaluate('replayer.play(0);'); await waitForRAF(page); - await page.evaluate('replayer.pause(260);'); + await page.evaluate('replayer.pause(280);'); await check250ms(); - await page.evaluate('replayer.pause(310);'); + await page.evaluate('replayer.pause(330);'); await check300ms(); - await page.evaluate('replayer.pause(410);'); + await page.evaluate('replayer.pause(430);'); await check400ms(); - await page.evaluate('replayer.pause(510);'); + await page.evaluate('replayer.pause(530);'); await check500ms(); - await page.evaluate('replayer.pause(610);'); + await page.evaluate('replayer.pause(630);'); await check600ms(); }); }); From 7e633e5b7f07ba782713552f4631aa582bc1c18e Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 17 Sep 2022 09:34:37 +0800 Subject: [PATCH 33/43] Update packages/rrweb/src/record/constructableStyleSheets.d.ts Co-authored-by: Justin Halsall --- packages/rrweb/src/record/constructableStyleSheets.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/src/record/constructableStyleSheets.d.ts b/packages/rrweb/src/record/constructableStyleSheets.d.ts index df00485fb2..4209bd880a 100644 --- a/packages/rrweb/src/record/constructableStyleSheets.d.ts +++ b/packages/rrweb/src/record/constructableStyleSheets.d.ts @@ -1,7 +1,7 @@ // This informs the TS compiler about constructed stylesheets. // It can be removed when this is fixed: https://github.com/Microsoft/TypeScript/issues/30022 declare interface DocumentOrShadowRoot { - adoptedStyleSheets: CSSStyleSheet[]; + adoptedStyleSheets?: CSSStyleSheet[]; } declare interface CSSStyleSheet { From af458d360c1a80f876ec4aa51af8a06c704ad4fa Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 17 Sep 2022 09:35:41 +0800 Subject: [PATCH 34/43] Update packages/rrweb/src/record/constructableStyleSheets.d.ts Co-authored-by: Justin Halsall --- packages/rrweb/src/record/constructableStyleSheets.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rrweb/src/record/constructableStyleSheets.d.ts b/packages/rrweb/src/record/constructableStyleSheets.d.ts index 4209bd880a..43465f489c 100644 --- a/packages/rrweb/src/record/constructableStyleSheets.d.ts +++ b/packages/rrweb/src/record/constructableStyleSheets.d.ts @@ -5,6 +5,6 @@ declare interface DocumentOrShadowRoot { } declare interface CSSStyleSheet { - replace(text: string): Promise; - replaceSync(text: string): void; + replace(text: string)?: Promise; + replaceSync(text: string)?: void; } From 4fdb3e73353072b0fb2c9e18ae3cfd64ed81b858 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 17 Sep 2022 13:43:14 +1000 Subject: [PATCH 35/43] apply Justin's review suggestions add more browser compatibility checks --- packages/rrdom-nodejs/tsconfig.json | 2 +- packages/rrdom/tsconfig.json | 2 +- ...ts.d.ts => constructable-stylesheets.d.ts} | 4 +- packages/rrweb/src/record/iframe-manager.ts | 3 +- packages/rrweb/src/record/index.ts | 2 +- packages/rrweb/src/record/observer.ts | 90 ++++++++++--------- .../rrweb/src/record/shadow-dom-manager.ts | 5 +- packages/rrweb/src/replay/index.ts | 28 +++--- yarn.lock | 18 +--- 9 files changed, 79 insertions(+), 75 deletions(-) rename packages/rrweb/src/record/{constructableStyleSheets.d.ts => constructable-stylesheets.d.ts} (76%) diff --git a/packages/rrdom-nodejs/tsconfig.json b/packages/rrdom-nodejs/tsconfig.json index 0e7945b85b..0c2f119853 100644 --- a/packages/rrdom-nodejs/tsconfig.json +++ b/packages/rrdom-nodejs/tsconfig.json @@ -21,7 +21,7 @@ "src", "test.d.ts", "../rrweb/src/record/workers/workers.d.ts", - "../rrweb/src/record/constructableStyleSheets.d.ts" + "../rrweb/src/record/constructable-stylesheets.d.ts" ], "references": [ { diff --git a/packages/rrdom/tsconfig.json b/packages/rrdom/tsconfig.json index ce6872277c..4cf4a46fa7 100644 --- a/packages/rrdom/tsconfig.json +++ b/packages/rrdom/tsconfig.json @@ -25,6 +25,6 @@ "include": [ "src", "../rrweb/src/record/workers/workers.d.ts", - "../rrweb/src/record/constructableStyleSheets.d.ts" + "../rrweb/src/record/constructable-stylesheets.d.ts" ] } diff --git a/packages/rrweb/src/record/constructableStyleSheets.d.ts b/packages/rrweb/src/record/constructable-stylesheets.d.ts similarity index 76% rename from packages/rrweb/src/record/constructableStyleSheets.d.ts rename to packages/rrweb/src/record/constructable-stylesheets.d.ts index 43465f489c..8545dfdfd4 100644 --- a/packages/rrweb/src/record/constructableStyleSheets.d.ts +++ b/packages/rrweb/src/record/constructable-stylesheets.d.ts @@ -5,6 +5,6 @@ declare interface DocumentOrShadowRoot { } declare interface CSSStyleSheet { - replace(text: string)?: Promise; - replaceSync(text: string)?: void; + replace?(text: string): Promise; + replaceSync?(text: string): void; } diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index d1e431a01a..ed2bef8534 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -46,7 +46,8 @@ export class IframeManager { if ( iframeEl.contentDocument && - iframeEl.contentDocument.adoptedStyleSheets?.length > 0 + iframeEl.contentDocument.adoptedStyleSheets && + iframeEl.contentDocument.adoptedStyleSheets.length > 0 ) this.stylesheetManager.adoptStyleSheets( iframeEl.contentDocument.adoptedStyleSheets, diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 8b57f2c384..e50cffc909 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -361,7 +361,7 @@ function record( mutationBuffers.forEach((buf) => buf.unlock()); // generate & emit any mutations that happened during snapshotting, as can now apply against the newly built mirror // Some old browsers don't support adoptedStyleSheets. - document.adoptedStyleSheets?.length > 0 && + if (document.adoptedStyleSheets && document.adoptedStyleSheets.length > 0) stylesheetManager.adoptStyleSheets( document.adoptedStyleSheets, mirror.getId(document), diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index efe75c57aa..7e1588c64e 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -581,49 +581,55 @@ function initStyleSheetObserver( return deleteRule.apply(this, [index]); }; - // eslint-disable-next-line @typescript-eslint/unbound-method - const replace = win.CSSStyleSheet.prototype.replace; - win.CSSStyleSheet.prototype.replace = function ( - this: CSSStyleSheet, - text: string, - ) { - const { id, styleId } = getIdAndStyleId( - this, - mirror, - stylesheetManager.styleMirror, - ); + let replace: (text: string) => Promise; + if (win.CSSStyleSheet.prototype.replace) { + // eslint-disable-next-line @typescript-eslint/unbound-method + replace = win.CSSStyleSheet.prototype.replace; + win.CSSStyleSheet.prototype.replace = function ( + this: CSSStyleSheet, + text: string, + ) { + const { id, styleId } = getIdAndStyleId( + this, + mirror, + stylesheetManager.styleMirror, + ); - if ((id && id !== -1) || (styleId && styleId !== -1)) { - styleSheetRuleCb({ - id, - styleId, - replace: text, - }); - } - return replace.apply(this, [text]); - }; + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replace: text, + }); + } + return replace.apply(this, [text]); + }; + } - // eslint-disable-next-line @typescript-eslint/unbound-method - const replaceSync = win.CSSStyleSheet.prototype.replaceSync; - win.CSSStyleSheet.prototype.replaceSync = function ( - this: CSSStyleSheet, - text: string, - ) { - const { id, styleId } = getIdAndStyleId( - this, - mirror, - stylesheetManager.styleMirror, - ); + let replaceSync: (text: string) => void; + if (win.CSSStyleSheet.prototype.replaceSync) { + // eslint-disable-next-line @typescript-eslint/unbound-method + replaceSync = win.CSSStyleSheet.prototype.replaceSync; + win.CSSStyleSheet.prototype.replaceSync = function ( + this: CSSStyleSheet, + text: string, + ) { + const { id, styleId } = getIdAndStyleId( + this, + mirror, + stylesheetManager.styleMirror, + ); - if ((id && id !== -1) || (styleId && styleId !== -1)) { - styleSheetRuleCb({ - id, - styleId, - replaceSync: text, - }); - } - return replaceSync.apply(this, [text]); - }; + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replaceSync: text, + }); + } + return replaceSync.apply(this, [text]); + }; + } const supportedNestedCSSRuleTypes: { [key: string]: GroupingCSSRuleTypes; @@ -716,8 +722,8 @@ function initStyleSheetObserver( return () => { win.CSSStyleSheet.prototype.insertRule = insertRule; win.CSSStyleSheet.prototype.deleteRule = deleteRule; - win.CSSStyleSheet.prototype.replace = replace; - win.CSSStyleSheet.prototype.replaceSync = replaceSync; + replace && (win.CSSStyleSheet.prototype.replace = replace); + replaceSync && (win.CSSStyleSheet.prototype.replaceSync = replaceSync); Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule; type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule; diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 703a436615..31db827910 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -79,7 +79,10 @@ export class ShadowDomManager { }); // Defer this to avoid adoptedStyleSheet events being created before the full snapshot is created or attachShadow action is recorded. setTimeout(() => { - if (shadowRoot.adoptedStyleSheets?.length > 0) + if ( + shadowRoot.adoptedStyleSheets && + shadowRoot.adoptedStyleSheets.length > 0 + ) this.bypassOptions.stylesheetManager.adoptStyleSheets( shadowRoot.adoptedStyleSheets, this.mirror.getId(shadowRoot.host), diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 4e1e880aec..0a4ec31f81 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1813,14 +1813,14 @@ export class Replayer { if (data.replace) try { - void styleSheet.replace(data.replace); + void styleSheet.replace?.(data.replace); } catch (e) { // for safety } if (data.replaceSync) try { - styleSheet.replaceSync(data.replaceSync); + styleSheet.replaceSync?.(data.replaceSync); } catch (e) { // for safety } @@ -1868,16 +1868,20 @@ export class Replayer { hostWindow = (targetHost as Document).defaultView; if (!hostWindow) return; - newStyleSheet = new hostWindow.CSSStyleSheet(); - this.styleMirror.add(newStyleSheet, style.styleId); - // To reuse the code of applying stylesheet rules - this.applyStyleSheetRule( - { - source: IncrementalSource.StyleSheetRule, - adds: style.rules, - }, - newStyleSheet, - ); + try { + newStyleSheet = new hostWindow.CSSStyleSheet(); + this.styleMirror.add(newStyleSheet, style.styleId); + // To reuse the code of applying stylesheet rules + this.applyStyleSheetRule( + { + source: IncrementalSource.StyleSheetRule, + adds: style.rules, + }, + newStyleSheet, + ); + } catch (e) { + // In case some browsers don't support constructing StyleSheet. + } }); const MAX_RETRY_TIME = 10; diff --git a/yarn.lock b/yarn.lock index 222ff736a9..9e87ffd45b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3217,20 +3217,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001219: - version "1.0.30001246" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001246.tgz" - integrity sha512-Tc+ff0Co/nFNbLOrziBXmMVtpt9S2c2Y+Z9Nk9Khj09J+0zR9ejvIW5qkZAErCbOrVODCx/MN+GpB5FNBs5GFA== - -caniuse-lite@^1.0.30001264: - version "1.0.30001265" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz" - integrity sha512-YzBnspggWV5hep1m9Z6sZVLOt7vrju8xWooFAgN6BA5qvy98qPAPb7vNUzypFaoh2pb3vlfzbDO8tB57UPGbtw== - -caniuse-lite@^1.0.30001286: - version "1.0.30001297" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001297.tgz" - integrity sha512-6bbIbowYG8vFs/Lk4hU9jFt7NknGDleVAciK916tp6ft1j+D//ZwwL6LbF1wXMQ32DMSjeuUV8suhh6dlmFjcA== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001264, caniuse-lite@^1.0.30001286: + version "1.0.30001402" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001402.tgz" + integrity sha512-Mx4MlhXO5NwuvXGgVb+hg65HZ+bhUYsz8QtDGDo2QmaJS2GBX47Xfi2koL86lc8K+l+htXeTEB/Aeqvezoo6Ew== caseless@~0.12.0: version "0.12.0" From 2d9aa0baed9c8e25a5fec9b48054029e1749433b Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 17 Sep 2022 15:35:24 +1000 Subject: [PATCH 36/43] add eslint-plugin-compat and config --- .eslintrc.js | 3 +- package.json | 1 + packages/rrdom-nodejs/package.json | 5 +- packages/rrdom/package.json | 5 +- packages/rrweb-player/package.json | 5 +- packages/rrweb-player/src/utils.ts | 1 + packages/rrweb-snapshot/package.json | 6 +- packages/rrweb/package.json | 5 +- .../canvas-webrtc/simple-peer-light.d.ts | 1 + packages/rrweb/src/record/index.ts | 8 ++- .../rrweb/src/record/observers/canvas/2d.ts | 1 - .../record/observers/canvas/canvas-manager.ts | 1 - .../rrweb/src/record/stylesheet-manager.ts | 1 - .../workers/image-bitmap-data-url-worker.ts | 3 + packages/rrweb/src/replay/index.ts | 2 + packages/rrweb/src/utils.ts | 2 + yarn.lock | 70 ++++++++++++++++++- 17 files changed, 106 insertions(+), 14 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 2860afba73..8dac0c2368 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,6 +9,7 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:compat/recommended', ], parser: '@typescript-eslint/parser', parserOptions: { @@ -17,7 +18,7 @@ module.exports = { tsconfigRootDir: __dirname, project: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'], }, - plugins: ['@typescript-eslint', 'eslint-plugin-tsdoc', 'jest'], + plugins: ['@typescript-eslint', 'eslint-plugin-tsdoc', 'jest', 'compat'], rules: { 'tsdoc/syntax': 'warn', }, diff --git a/package.json b/package.json index 0b00efbd74..9e183bf1cf 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@typescript-eslint/parser": "^5.25.0", "concurrently": "^7.1.0", "eslint": "^8.19.0", + "eslint-plugin-compat": "^4.0.2", "eslint-plugin-jest": "^26.5.3", "eslint-plugin-tsdoc": "^0.2.16", "lerna": "^4.0.0", diff --git a/packages/rrdom-nodejs/package.json b/packages/rrdom-nodejs/package.json index fd00d34576..c26d19da80 100644 --- a/packages/rrdom-nodejs/package.json +++ b/packages/rrdom-nodejs/package.json @@ -51,5 +51,8 @@ "nwsapi": "^2.2.0", "rrdom": "^0.1.4", "rrweb-snapshot": "^2.0.0-alpha.1" - } + }, + "browserslist": [ + "supports es6-class" + ] } diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json index e59fa66139..63fb153e0a 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -47,5 +47,8 @@ }, "dependencies": { "rrweb-snapshot": "^2.0.0-alpha.1" - } + }, + "browserslist": [ + "supports es6-class" + ] } diff --git a/packages/rrweb-player/package.json b/packages/rrweb-player/package.json index 755350a567..11d9e34d70 100644 --- a/packages/rrweb-player/package.json +++ b/packages/rrweb-player/package.json @@ -57,5 +57,8 @@ "bugs": { "url": "https://github.com/rrweb-io/rrweb/issues" }, - "homepage": "https://github.com/rrweb-io/rrweb#readme" + "homepage": "https://github.com/rrweb-io/rrweb#readme", + "browserslist": [ + "supports promises and supports fullscreen" + ] } diff --git a/packages/rrweb-player/src/utils.ts b/packages/rrweb-player/src/utils.ts index 32f1faf522..acfd564f0b 100644 --- a/packages/rrweb-player/src/utils.ts +++ b/packages/rrweb-player/src/utils.ts @@ -84,6 +84,7 @@ export function exitFullscreen(): Promise { export function isFullscreen(): boolean { return ( + // eslint-disable-next-line compat/compat document.fullscreen || document.webkitIsFullScreen || document.mozFullScreen || diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index fd512672dc..7203e7abf9 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -57,5 +57,9 @@ "ts-node": "^7.0.1", "tslib": "^1.9.3", "typescript": "^4.7.3" - } + }, + "browserslist": [ + "cover 98%", + "not ie >= 0 and not ie_mob >= 0" + ] } diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 0ccab43829..0a9700c7e8 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -87,5 +87,8 @@ "mitt": "^3.0.0", "rrdom": "^0.1.4", "rrweb-snapshot": "^2.0.0-alpha.1" - } + }, + "browserslist": [ + "supports mutationobserver and supports es6-class and supports promises" + ] } diff --git a/packages/rrweb/src/plugins/canvas-webrtc/simple-peer-light.d.ts b/packages/rrweb/src/plugins/canvas-webrtc/simple-peer-light.d.ts index 203aa56421..9f7fc8687f 100644 --- a/packages/rrweb/src/plugins/canvas-webrtc/simple-peer-light.d.ts +++ b/packages/rrweb/src/plugins/canvas-webrtc/simple-peer-light.d.ts @@ -1,3 +1,4 @@ +/* eslint-disable */ /// declare module 'simple-peer-light' { import * as stream from 'stream'; diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index e50cffc909..3d7d6547b3 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -344,15 +344,19 @@ function record( window.pageXOffset !== undefined ? window.pageXOffset : document?.documentElement.scrollLeft || + // eslint-disable-next-line compat/compat document?.body?.parentElement?.scrollLeft || - document?.body.scrollLeft || + // eslint-disable-next-line compat/compat + document?.body?.scrollLeft || 0, top: window.pageYOffset !== undefined ? window.pageYOffset : document?.documentElement.scrollTop || + // eslint-disable-next-line compat/compat document?.body?.parentElement?.scrollTop || - document?.body.scrollTop || + // eslint-disable-next-line compat/compat + document?.body?.scrollTop || 0, }, }, diff --git a/packages/rrweb/src/record/observers/canvas/2d.ts b/packages/rrweb/src/record/observers/canvas/2d.ts index dd54e55452..8de2aa77c7 100644 --- a/packages/rrweb/src/record/observers/canvas/2d.ts +++ b/packages/rrweb/src/record/observers/canvas/2d.ts @@ -14,7 +14,6 @@ export default function initCanvas2DMutationObserver( win: IWindow, blockClass: blockClass, blockSelector: string | null, - mirror: Mirror, ): listenerHandler { const handlers: listenerHandler[] = []; const props2D = Object.getOwnPropertyNames( diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index 557826f077..c910a608ce 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -235,7 +235,6 @@ export class CanvasManager { win, blockClass, blockSelector, - this.mirror, ); const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver( diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts index 251d115498..1a2f6dc1a5 100644 --- a/packages/rrweb/src/record/stylesheet-manager.ts +++ b/packages/rrweb/src/record/stylesheet-manager.ts @@ -4,7 +4,6 @@ import type { adoptedStyleSheetCallback, adoptedStyleSheetParam, mutationCallBack, - styleSheetRuleCallback, } from '../types'; import { StyleSheetMirror } from '../utils'; diff --git a/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts b/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts index e5addc0b11..5aa593213b 100644 --- a/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts +++ b/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts @@ -28,6 +28,8 @@ async function getTransparentBlobFor( ): Promise { const id = `${width}-${height}`; if (transparentBlobMap.has(id)) return transparentBlobMap.get(id)!; + // offScreenCanvas will significantly increase the requirements for browser compatibility so that I disable this rule here. + // eslint-disable-next-line compat/compat const offscreen = new OffscreenCanvas(width, height); offscreen.getContext('2d'); // creates rendering context for `converToBlob` const blob = await offscreen.convertToBlob(); // takes a while @@ -49,6 +51,7 @@ worker.onmessage = async function (e) { const transparentBase64 = getTransparentBlobFor(width, height); + // eslint-disable-next-line compat/compat const offscreen = new OffscreenCanvas(width, height); const ctx = offscreen.getContext('2d')!; diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 0a4ec31f81..57081178ba 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -170,6 +170,7 @@ export class Replayer { const defaultConfig: playerConfig = { speed: 1, maxSpeed: 360, + // eslint-disable-next-line compat/compat root: document.body, loadTimeout: 0, skipInactive: false, @@ -1277,6 +1278,7 @@ export class Replayer { } case IncrementalSource.Font: { try { + // eslint-disable-next-line compat/compat const fontFace = new FontFace( d.family, d.buffer diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index b782bd52e2..f738f657fb 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -171,6 +171,7 @@ export function getWindowHeight(): number { return ( window.innerHeight || (document.documentElement && document.documentElement.clientHeight) || + // eslint-disable-next-line compat/compat (document.body && document.body.clientHeight) ); } @@ -179,6 +180,7 @@ export function getWindowWidth(): number { return ( window.innerWidth || (document.documentElement && document.documentElement.clientWidth) || + // eslint-disable-next-line compat/compat (document.body && document.body.clientWidth) ); } diff --git a/yarn.lock b/yarn.lock index 9e87ffd45b..b4faf584a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1618,6 +1618,16 @@ npmlog "^4.1.2" write-file-atomic "^3.0.3" +"@mdn/browser-compat-data@^3.3.14": + version "3.3.14" + resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-3.3.14.tgz#b72a37c654e598f9ae6f8335faaee182bebc6b28" + integrity sha512-n2RC9d6XatVbWFdHLimzzUJxJ1KY8LdjqrW6YvGPiRmsHkhOUx74/Ct10x5Yo7bC/Jvqx7cDEW8IMPv/+vwEzA== + +"@mdn/browser-compat-data@^4.1.5": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz#1fead437f3957ceebe2e8c3f46beccdb9bc575b8" + integrity sha512-EWUguj2kd7ldmrF9F+vI5hUOralPd+sdsUnYbRy33vZTuZkduC1shE9TtEMEjAQwyfyMb4ole5KtjF8MsnQOlA== + "@microsoft/tsdoc-config@0.16.1": version "0.16.1" resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.16.1.tgz#4de11976c1202854c4618f364bf499b4be33e657" @@ -2766,6 +2776,13 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= +ast-metadata-inferer@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/ast-metadata-inferer/-/ast-metadata-inferer-0.7.0.tgz#c45d874cbdecabea26dc5de11fc6fa1919807c66" + integrity sha512-OkMLzd8xelb3gmnp6ToFvvsHLtS6CbagTkFQvQ+ZYFe3/AIl9iKikNR9G7pY3GfOR/2Xc222hwBjzI7HLkE76Q== + dependencies: + "@mdn/browser-compat-data" "^3.3.14" + async-limiter@~1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz" @@ -3052,6 +3069,16 @@ browserslist@^4.16.6: node-releases "^1.1.77" picocolors "^0.2.1" +browserslist@^4.16.8: + version "4.21.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" + integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== + dependencies: + caniuse-lite "^1.0.30001400" + electron-to-chromium "^1.4.251" + node-releases "^2.0.6" + update-browserslist-db "^1.0.9" + browserslist@^4.17.5: version "4.19.1" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz" @@ -3217,7 +3244,7 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001264, caniuse-lite@^1.0.30001286: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001264, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001304, caniuse-lite@^1.0.30001400: version "1.0.30001402" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001402.tgz" integrity sha512-Mx4MlhXO5NwuvXGgVb+hg65HZ+bhUYsz8QtDGDo2QmaJS2GBX47Xfi2koL86lc8K+l+htXeTEB/Aeqvezoo6Ew== @@ -3719,6 +3746,11 @@ core-js@^2.4.0: resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== +core-js@^3.16.2: + version "3.25.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.25.1.tgz#5818e09de0db8956e16bf10e2a7141e931b7c69c" + integrity sha512-sr0FY4lnO1hkQ4gLDr24K0DGnweGO1QwSj5BpfQjpSJPdqWalja4cTps29Y/PJVG/P7FYlPDkH3hO+Tr0CvDgQ== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" @@ -4287,6 +4319,11 @@ electron-to-chromium@^1.4.17: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.37.tgz" integrity sha512-XIvFB1omSAxYgHYX48sC+HR8i/p7lx7R+0cX9faElg1g++h9IilCrJ12+bQuY+d96Wp7zkBiJwMOv+AhLtLrTg== +electron-to-chromium@^1.4.251: + version "1.4.254" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.254.tgz#c6203583890abf88dfc0be046cd72d3b48f8beb6" + integrity sha512-Sh/7YsHqQYkA6ZHuHMy24e6TE4eX6KZVsZb9E/DvU1nQRIrH4BflO/4k+83tfdYvDl+MObvlqHPRICzEdC9c6Q== + emittery@^0.8.1: version "0.8.1" resolved "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz" @@ -4610,6 +4647,20 @@ eslint-config-google@^0.14.0: resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a" integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== +eslint-plugin-compat@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-compat/-/eslint-plugin-compat-4.0.2.tgz#b058627a7d25d352adf0ec16dca8fcf92d9c7af7" + integrity sha512-xqvoO54CLTVaEYGMzhu35Wzwk/As7rCvz/2dqwnFiWi0OJccEtGIn+5qq3zqIu9nboXlpdBN579fZcItC73Ycg== + dependencies: + "@mdn/browser-compat-data" "^4.1.5" + ast-metadata-inferer "^0.7.0" + browserslist "^4.16.8" + caniuse-lite "^1.0.30001304" + core-js "^3.16.2" + find-up "^5.0.0" + lodash.memoize "4.1.2" + semver "7.3.5" + eslint-plugin-jest@^26.5.3: version "26.5.3" resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-26.5.3.tgz#a3ceeaf4a757878342b8b00eca92379b246e5505" @@ -7673,7 +7724,7 @@ lodash.ismatch@^4.4.0: resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= -lodash.memoize@4.x, lodash.memoize@^4.1.2: +lodash.memoize@4.1.2, lodash.memoize@4.x, lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= @@ -8266,6 +8317,11 @@ node-releases@^2.0.1: resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +node-releases@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" + integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + nopt@^4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz" @@ -10113,7 +10169,7 @@ semver-match@0.1.1: resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@7.x, semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: +semver@7.3.5, semver@7.x, semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: version "7.3.5" resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz" integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== @@ -11297,6 +11353,14 @@ upath@^2.0.1: resolved "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz" integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== +update-browserslist-db@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz#2924d3927367a38d5c555413a7ce138fc95fcb18" + integrity sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" From 9aa01bdcd6d306c85b1b72f0524b9ea59eeb60ea Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 17 Sep 2022 15:44:24 +1000 Subject: [PATCH 37/43] fix record test type errors --- packages/rrweb/test/record.test.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index d66302fd0c..f00ca4d0e3 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -489,13 +489,13 @@ describe('record', function (this: ISuite) { }); setTimeout(() => { - sheet.replace('div { color: yellow; }'); - sheet2.replace('h1 { color: blue; }'); + sheet.replace!('div { color: yellow; }'); + sheet2.replace!('h1 { color: blue; }'); }, 0); setTimeout(() => { - sheet.replaceSync('div { display: inline ; }'); - sheet2.replaceSync('h1 { font-size: large; }'); + sheet.replaceSync!('div { display: inline ; }'); + sheet2.replaceSync!('h1 { font-size: large; }'); }, 5); setTimeout(() => { @@ -543,10 +543,10 @@ describe('record', function (this: ISuite) { const iframeWin = iframeDocument!.defaultView!; const sheet1 = new iframeWin.CSSStyleSheet(); - sheet1.replaceSync('h1 {color: blue;}'); + sheet1.replaceSync!('h1 {color: blue;}'); iframeDocument!.adoptedStyleSheets = [sheet1]; const sheet2 = new iframeWin.CSSStyleSheet(); - sheet2.replaceSync('div {font-size: large;}'); + sheet2.replaceSync!('div {font-size: large;}'); shadowHost.shadowRoot!.adoptedStyleSheets = [sheet2]; const { record } = ((window as unknown) as IWindow).rrweb; @@ -555,13 +555,13 @@ describe('record', function (this: ISuite) { }); setTimeout(() => { - sheet1.insertRule('div { display: inline ; }', 1); - sheet2.replaceSync('h1 { font-size: large; }'); + sheet1.insertRule!('div { display: inline ; }', 1); + sheet2.replaceSync!('h1 { font-size: large; }'); }, 50); setTimeout(() => { const sheet3 = new iframeWin.CSSStyleSheet(); - sheet3.replaceSync('span {background-color: red;}'); + sheet3.replaceSync!('span {background-color: red;}'); iframeDocument!.adoptedStyleSheets = [sheet3, sheet2]; shadowHost.shadowRoot!.adoptedStyleSheets = [sheet1, sheet3]; }, 100); @@ -698,7 +698,7 @@ describe('record', function (this: ISuite) { `; const sheet = new CSSStyleSheet(); - sheet.replaceSync( + sheet.replaceSync!( 'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}', ); // Add stylesheet to a document. @@ -712,7 +712,7 @@ describe('record', function (this: ISuite) { '
div in shadow dom 1
span in shadow dom 1'; const sheet2 = new CSSStyleSheet(); - sheet2.replaceSync('span { color: red; }'); + sheet2.replaceSync!('span { color: red; }'); shadow.adoptedStyleSheets = [sheet, sheet2]; @@ -720,7 +720,7 @@ describe('record', function (this: ISuite) { const iframe = document.querySelector('iframe'); const sheet3 = new (iframe!.contentWindow! as IWindow & typeof globalThis).CSSStyleSheet(); - sheet3.replaceSync('h1 { color: blue; }'); + sheet3.replaceSync!('h1 { color: blue; }'); iframe!.contentDocument!.adoptedStyleSheets = [sheet3]; @@ -739,14 +739,14 @@ describe('record', function (this: ISuite) { shadow.innerHTML = '
div in shadow dom 2
span in shadow dom 2'; const sheet4 = new CSSStyleSheet(); - sheet4.replaceSync('span { color: green; }'); + sheet4.replaceSync!('span { color: green; }'); shadow.adoptedStyleSheets = [sheet, sheet4]; document.adoptedStyleSheets = [sheet4, sheet, sheet2]; const sheet5 = new (iframe!.contentWindow! as IWindow & typeof globalThis).CSSStyleSheet(); - sheet5.replaceSync('h2 { color: purple; }'); + sheet5.replaceSync!('h2 { color: purple; }'); iframe!.contentDocument!.adoptedStyleSheets = [sheet5, sheet3]; }, 10); }); From 1662f8f5dc1b1ebc13c437debdd96128b2bd9698 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 17 Sep 2022 17:36:22 +1000 Subject: [PATCH 38/43] make Mirror's replace function act the same with the original one when there's no existed node to replace --- packages/rrweb-snapshot/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 77019df12a..8f63e44f08 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -105,8 +105,8 @@ export class Mirror implements IMirror { if (oldNode) { const meta = this.nodeMetaMap.get(oldNode); if (meta) this.nodeMetaMap.set(n, meta); - this.idNodeMap.set(id, n); } + this.idNodeMap.set(id, n); } reset() { From f1c0406b6c70c68c9796e8272568d59625406cb9 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 23 Sep 2022 14:29:11 +1000 Subject: [PATCH 39/43] test: increase the robustness of test cases --- packages/rrweb/test/record.test.ts | 4 ++-- packages/rrweb/test/replayer.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index f00ca4d0e3..db7e0be4d4 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -557,14 +557,14 @@ describe('record', function (this: ISuite) { setTimeout(() => { sheet1.insertRule!('div { display: inline ; }', 1); sheet2.replaceSync!('h1 { font-size: large; }'); - }, 50); + }, 100); setTimeout(() => { const sheet3 = new iframeWin.CSSStyleSheet(); sheet3.replaceSync!('span {background-color: red;}'); iframeDocument!.adoptedStyleSheets = [sheet3, sheet2]; shadowHost.shadowRoot!.adoptedStyleSheets = [sheet1, sheet3]; - }, 100); + }, 150); }); await ctx.page.waitForTimeout(200); assertSnapshot(ctx.events); diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index efe3e701df..15a84c35b1 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -916,7 +916,7 @@ describe('replayer', function () { ).toBeTruthy(); }; - await page.waitForTimeout(250); + await page.waitForTimeout(240); await check250ms(); await page.waitForTimeout(50); From 4e7c29390690347b89076d0921a5ab808d615c4f Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 26 Sep 2022 17:13:33 +0200 Subject: [PATCH 40/43] remove eslint disable in favor of feature detection Early returns aren't supported yet unfortunately, otherwise this code would be cleaner https://github.com/amilajack/eslint-plugin-compat/issues/523 --- .../workers/image-bitmap-data-url-worker.ts | 84 ++++++++++--------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts b/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts index 171e6f49ea..51e75dbe06 100644 --- a/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts +++ b/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts @@ -29,16 +29,18 @@ async function getTransparentBlobFor( dataURLOptions: DataURLOptions, ): Promise { const id = `${width}-${height}`; - if (transparentBlobMap.has(id)) return transparentBlobMap.get(id)!; - // offScreenCanvas will significantly increase the requirements for browser compatibility so that I disable this rule here. - // eslint-disable-next-line compat/compat - const offscreen = new OffscreenCanvas(width, height); - offscreen.getContext('2d'); // creates rendering context for `converToBlob` - const blob = await offscreen.convertToBlob(dataURLOptions); // takes a while - const arrayBuffer = await blob.arrayBuffer(); - const base64 = encode(arrayBuffer); // cpu intensive - transparentBlobMap.set(id, base64); - return base64; + if ('OffscreenCanvas' in globalThis) { + if (transparentBlobMap.has(id)) return transparentBlobMap.get(id)!; + const offscreen = new OffscreenCanvas(width, height); + offscreen.getContext('2d'); // creates rendering context for `converToBlob` + const blob = await offscreen.convertToBlob(dataURLOptions); // takes a while + const arrayBuffer = await blob.arrayBuffer(); + const base64 = encode(arrayBuffer); // cpu intensive + transparentBlobMap.set(id, base64); + return base64; + } else { + return ''; + } } // `as any` because: https://github.com/Microsoft/TypeScript/issues/20595 @@ -46,42 +48,42 @@ const worker: ImageBitmapDataURLResponseWorker = self; // eslint-disable-next-line @typescript-eslint/no-misused-promises worker.onmessage = async function (e) { - if (!('OffscreenCanvas' in globalThis)) - return worker.postMessage({ id: e.data.id }); + if ('OffscreenCanvas' in globalThis) { + const { id, bitmap, width, height, dataURLOptions } = e.data; - const { id, bitmap, width, height, dataURLOptions } = e.data; + const transparentBase64 = getTransparentBlobFor( + width, + height, + dataURLOptions, + ); - const transparentBase64 = getTransparentBlobFor( - width, - height, - dataURLOptions, - ); + const offscreen = new OffscreenCanvas(width, height); + const ctx = offscreen.getContext('2d')!; - // eslint-disable-next-line compat/compat - const offscreen = new OffscreenCanvas(width, height); - const ctx = offscreen.getContext('2d')!; + ctx.drawImage(bitmap, 0, 0); + bitmap.close(); + const blob = await offscreen.convertToBlob(dataURLOptions); // takes a while + const type = blob.type; + const arrayBuffer = await blob.arrayBuffer(); + const base64 = encode(arrayBuffer); // cpu intensive - ctx.drawImage(bitmap, 0, 0); - bitmap.close(); - const blob = await offscreen.convertToBlob(dataURLOptions); // takes a while - const type = blob.type; - const arrayBuffer = await blob.arrayBuffer(); - const base64 = encode(arrayBuffer); // cpu intensive + // on first try we should check if canvas is transparent, + // no need to save it's contents in that case + if (!lastBlobMap.has(id) && (await transparentBase64) === base64) { + lastBlobMap.set(id, base64); + return worker.postMessage({ id }); + } - // on first try we should check if canvas is transparent, - // no need to save it's contents in that case - if (!lastBlobMap.has(id) && (await transparentBase64) === base64) { + if (lastBlobMap.get(id) === base64) return worker.postMessage({ id }); // unchanged + worker.postMessage({ + id, + type, + base64, + width, + height, + }); lastBlobMap.set(id, base64); - return worker.postMessage({ id }); + } else { + return worker.postMessage({ id: e.data.id }); } - - if (lastBlobMap.get(id) === base64) return worker.postMessage({ id }); // unchanged - worker.postMessage({ - id, - type, - base64, - width, - height, - }); - lastBlobMap.set(id, base64); }; From 6bd919ae2b2c391a99f734f26673859987b1f624 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 26 Sep 2022 17:24:22 +0200 Subject: [PATCH 41/43] Remove eslint-disable-next-line compat/compat --- packages/rrweb-player/src/utils.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/rrweb-player/src/utils.ts b/packages/rrweb-player/src/utils.ts index acfd564f0b..bee0bde5f8 100644 --- a/packages/rrweb-player/src/utils.ts +++ b/packages/rrweb-player/src/utils.ts @@ -83,13 +83,18 @@ export function exitFullscreen(): Promise { } export function isFullscreen(): boolean { - return ( - // eslint-disable-next-line compat/compat - document.fullscreen || - document.webkitIsFullScreen || - document.mozFullScreen || - document.msFullscreenElement - ); + let fullscreen = false; + [ + 'fullscreen', + 'webkitIsFullScreen', + 'mozFullScreen', + 'msFullscreenElement', + ].forEach((fullScreenAccessor) => { + if (fullScreenAccessor in document) { + fullscreen = fullscreen || Boolean(document[fullScreenAccessor]); + } + }); + return fullscreen; } export function onFullscreenChange(handler: () => unknown): () => void { From 7f0c3dfbf7876c988b05f2930c19a8535169dcc2 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 26 Sep 2022 17:36:11 +0200 Subject: [PATCH 42/43] Standardize browserslist and remove lint exceptions (#1010) --- package.json | 7 ++++++- packages/rrdom/package.json | 5 +---- packages/rrweb-player/package.json | 5 +---- packages/rrweb-snapshot/package.json | 6 +----- packages/rrweb/package.json | 5 +---- packages/rrweb/src/record/index.ts | 4 ---- packages/rrweb/src/replay/index.ts | 2 -- packages/rrweb/src/utils.ts | 2 -- yarn.lock | 2 +- 9 files changed, 11 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 9e183bf1cf..4b32ce3523 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@monorepo-utils/workspaces-to-typescript-project-references": "^2.8.2", "@typescript-eslint/eslint-plugin": "^5.25.0", "@typescript-eslint/parser": "^5.25.0", + "browserslist": "^4.21.4", "concurrently": "^7.1.0", "eslint": "^8.19.0", "eslint-plugin-compat": "^4.0.2", @@ -42,5 +43,9 @@ "live-stream": "cd packages/rrweb && yarn live-stream", "lint": "yarn run concurrently --success=all -r -m=1 'yarn run markdownlint docs' 'yarn eslint packages/*/src --ext .ts,.tsx,.js,.jsx,.svelte'", "lint:report": "yarn eslint --output-file eslint_report.json --format json packages/*/src --ext .ts,.tsx,.js,.jsx" - } + }, + "browserslist": [ + "defaults", + "not op_mini all" + ] } diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json index a2e1ff3eff..3b755cca81 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -47,8 +47,5 @@ }, "dependencies": { "rrweb-snapshot": "^2.0.0-alpha.2" - }, - "browserslist": [ - "supports es6-class" - ] + } } diff --git a/packages/rrweb-player/package.json b/packages/rrweb-player/package.json index d3b3f13d7f..04ff6b1f4a 100644 --- a/packages/rrweb-player/package.json +++ b/packages/rrweb-player/package.json @@ -57,8 +57,5 @@ "bugs": { "url": "https://github.com/rrweb-io/rrweb/issues" }, - "homepage": "https://github.com/rrweb-io/rrweb#readme", - "browserslist": [ - "supports promises and supports fullscreen" - ] + "homepage": "https://github.com/rrweb-io/rrweb#readme" } diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index 77dc38e76d..13e133e44c 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -57,9 +57,5 @@ "ts-node": "^7.0.1", "tslib": "^1.9.3", "typescript": "^4.7.3" - }, - "browserslist": [ - "cover 98%", - "not ie >= 0 and not ie_mob >= 0" - ] + } } diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index fef042cf09..d4cfcff22f 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -87,8 +87,5 @@ "mitt": "^3.0.0", "rrdom": "^0.1.5", "rrweb-snapshot": "^2.0.0-alpha.2" - }, - "browserslist": [ - "supports mutationobserver and supports es6-class and supports promises" - ] + } } diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 19e23a47d7..1718a036f4 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -348,18 +348,14 @@ function record( window.pageXOffset !== undefined ? window.pageXOffset : document?.documentElement.scrollLeft || - // eslint-disable-next-line compat/compat document?.body?.parentElement?.scrollLeft || - // eslint-disable-next-line compat/compat document?.body?.scrollLeft || 0, top: window.pageYOffset !== undefined ? window.pageYOffset : document?.documentElement.scrollTop || - // eslint-disable-next-line compat/compat document?.body?.parentElement?.scrollTop || - // eslint-disable-next-line compat/compat document?.body?.scrollTop || 0, }, diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 70cf228844..ae33316b2d 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -170,7 +170,6 @@ export class Replayer { const defaultConfig: playerConfig = { speed: 1, maxSpeed: 360, - // eslint-disable-next-line compat/compat root: document.body, loadTimeout: 0, skipInactive: false, @@ -1284,7 +1283,6 @@ export class Replayer { } case IncrementalSource.Font: { try { - // eslint-disable-next-line compat/compat const fontFace = new FontFace( d.family, d.buffer diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index f738f657fb..b782bd52e2 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -171,7 +171,6 @@ export function getWindowHeight(): number { return ( window.innerHeight || (document.documentElement && document.documentElement.clientHeight) || - // eslint-disable-next-line compat/compat (document.body && document.body.clientHeight) ); } @@ -180,7 +179,6 @@ export function getWindowWidth(): number { return ( window.innerWidth || (document.documentElement && document.documentElement.clientWidth) || - // eslint-disable-next-line compat/compat (document.body && document.body.clientWidth) ); } diff --git a/yarn.lock b/yarn.lock index b4faf584a1..caad917413 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3069,7 +3069,7 @@ browserslist@^4.16.6: node-releases "^1.1.77" picocolors "^0.2.1" -browserslist@^4.16.8: +browserslist@^4.16.8, browserslist@^4.21.4: version "4.21.4" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== From 9006abe9b82809d7a323f80fd2c68afd10b76e75 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 29 Sep 2022 11:42:24 +1000 Subject: [PATCH 43/43] test: revert deleting virtual style tests and rewrite them to fit the current code base --- package.json | 3 + packages/rrdom/test/diff.test.ts | 166 ++++++++++++++++++++++++++- packages/rrweb/test/replayer.test.ts | 2 +- yarn.lock | 7 +- 4 files changed, 172 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 4b32ce3523..66ae51cc19 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,9 @@ "lint": "yarn run concurrently --success=all -r -m=1 'yarn run markdownlint docs' 'yarn eslint packages/*/src --ext .ts,.tsx,.js,.jsx,.svelte'", "lint:report": "yarn eslint --output-file eslint_report.json --format json packages/*/src --ext .ts,.tsx,.js,.jsx" }, + "resolutions": { + "**/jsdom/cssom": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz" + }, "browserslist": [ "defaults", "not op_mini all" diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 4b6d1c3b85..723b2a3e6e 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -10,8 +10,14 @@ import { Mirror, } from 'rrweb-snapshot'; import type { IRRNode } from '../src/document'; -import type { canvasMutationData, styleSheetRuleData } from 'rrweb/src/types'; +import { Replayer } from 'rrweb'; +import type { + canvasMutationData, + styleDeclarationData, + styleSheetRuleData, +} from 'rrweb/src/types'; import { EventType, IncrementalSource } from 'rrweb/src/types'; +import type { eventWithTime } from 'rrweb/typings/types'; const elementSn = { type: RRNodeType.Element, @@ -1267,4 +1273,162 @@ describe('diff algorithm for rrdom', () => { expect(mirror.getMeta(result)).toEqual(mirror.getMeta(text)); }); }); + + describe('apply virtual style rules to node', () => { + beforeEach(() => { + const dummyReplayer = new Replayer(([ + { + type: EventType.DomContentLoaded, + timestamp: 0, + }, + { + type: EventType.Meta, + data: { + with: 1920, + height: 1080, + }, + timestamp: 0, + }, + ] as unknown) as eventWithTime[]); + replayer.applyStyleSheetMutation = ( + data: styleDeclarationData | styleSheetRuleData, + styleSheet: CSSStyleSheet, + ) => { + if (data.source === IncrementalSource.StyleSheetRule) + // Disable the ts check here because these two functions are private methods. + // @ts-ignore + dummyReplayer.applyStyleSheetRule(data, styleSheet); + else if (data.source === IncrementalSource.StyleDeclaration) + // @ts-ignore + dummyReplayer.applyStyleDeclaration(data, styleSheet); + }; + }); + + it('should insert rule at index 0 in empty sheet', () => { + document.write(''); + const styleEl = document.getElementsByTagName('style')[0]; + const cssText = '.added-rule {border: 1px solid yellow;}'; + + const styleRuleData: styleSheetRuleData = { + source: IncrementalSource.StyleSheetRule, + adds: [ + { + rule: cssText, + index: 0, + }, + ], + }; + replayer.applyStyleSheetMutation(styleRuleData, styleEl.sheet!); + + expect(styleEl.sheet?.cssRules?.length).toEqual(1); + expect(styleEl.sheet?.cssRules[0].cssText).toEqual(cssText); + }); + + it('should insert rule at index 0 and keep exsisting rules', () => { + document.write(` + + `); + const styleEl = document.getElementsByTagName('style')[0]; + + const cssText = '.added-rule {border: 1px solid yellow;}'; + const styleRuleData: styleSheetRuleData = { + source: IncrementalSource.StyleSheetRule, + adds: [ + { + rule: cssText, + index: 0, + }, + ], + }; + replayer.applyStyleSheetMutation(styleRuleData, styleEl.sheet!); + + expect(styleEl.sheet?.cssRules?.length).toEqual(3); + expect(styleEl.sheet?.cssRules[0].cssText).toEqual(cssText); + }); + + it('should delete rule at index 0', () => { + document.write(` + + `); + const styleEl = document.getElementsByTagName('style')[0]; + + const styleRuleData: styleSheetRuleData = { + source: IncrementalSource.StyleSheetRule, + removes: [ + { + index: 0, + }, + ], + }; + replayer.applyStyleSheetMutation(styleRuleData, styleEl.sheet!); + + expect(styleEl.sheet?.cssRules?.length).toEqual(1); + expect(styleEl.sheet?.cssRules[0].cssText).toEqual('div {color: black;}'); + }); + + it('should insert rule at index [0,0] and keep existing rules', () => { + document.write(` + + `); + const styleEl = document.getElementsByTagName('style')[0]; + + const cssText = '.added-rule {border: 1px solid yellow;}'; + const styleRuleData: styleSheetRuleData = { + source: IncrementalSource.StyleSheetRule, + adds: [ + { + rule: cssText, + index: [0, 0], + }, + ], + }; + replayer.applyStyleSheetMutation(styleRuleData, styleEl.sheet!); + + expect( + (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules?.length, + ).toEqual(3); + expect( + (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules[0].cssText, + ).toEqual(cssText); + }); + + it('should delete rule at index [0,1]', () => { + document.write(` + + `); + const styleEl = document.getElementsByTagName('style')[0]; + const styleRuleData: styleSheetRuleData = { + source: IncrementalSource.StyleSheetRule, + removes: [ + { + index: [0, 1], + }, + ], + }; + replayer.applyStyleSheetMutation(styleRuleData, styleEl.sheet!); + + expect( + (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules?.length, + ).toEqual(1); + expect( + (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules[0].cssText, + ).toEqual('a {color: blue;}'); + }); + }); }); diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index 15a84c35b1..7bc09ae2ca 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -916,7 +916,7 @@ describe('replayer', function () { ).toBeTruthy(); }; - await page.waitForTimeout(240); + await page.waitForTimeout(235); await check250ms(); await page.waitForTimeout(50); diff --git a/yarn.lock b/yarn.lock index caad917413..b2f2d2feaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3979,10 +3979,9 @@ csso@^4.0.2: dependencies: css-tree "^1.1.2" -cssom@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" - integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== +cssom@^0.4.4, "cssom@https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz": + version "0.6.0" + resolved "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" cssom@^0.5.0: version "0.5.0"