diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index bcc316ea1d..d9e27496a2 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -27,6 +27,7 @@ import { isSerializedIframe, isSerializedStylesheet, inDom, + getShadowHost, } from '../utils'; type DoubleLinkedListNode = { @@ -268,18 +269,11 @@ export default class MutationBuffer { return nextId; }; const pushAdd = (n: Node) => { - let shadowHost: Element | null = null; - if ( - n.getRootNode?.()?.nodeType === Node.DOCUMENT_FRAGMENT_NODE && - (n.getRootNode() as ShadowRoot).host - ) - shadowHost = (n.getRootNode() as ShadowRoot).host; - if (!n.parentNode || !inDom(n)) { return; } const parentId = isShadowRoot(n.parentNode) - ? this.mirror.getId(shadowHost) + ? this.mirror.getId(getShadowHost(n)) : this.mirror.getId(n.parentNode); const nextId = getNextId(n); if (parentId === -1 || nextId === -1) { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 1d5ec83855..8703191f6d 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -519,16 +519,29 @@ export class StyleSheetMirror { } } -export function getRootShadowHost(n: Node): Node | null { - const shadowHost = (n.getRootNode() as ShadowRoot).host; - // If n is in a nested shadow dom. - let rootShadowHost = shadowHost; - - while ( - rootShadowHost?.getRootNode?.()?.nodeType === Node.DOCUMENT_FRAGMENT_NODE && - (rootShadowHost.getRootNode() as ShadowRoot).host +/** + * Get the direct shadow host of a node in shadow dom. Returns null if it is not in a shadow dom. + */ +export function getShadowHost(n: Node): Element | null { + let shadowHost: Element | null = null; + if ( + n.getRootNode?.()?.nodeType === Node.DOCUMENT_FRAGMENT_NODE && + (n.getRootNode() as ShadowRoot).host ) - rootShadowHost = (rootShadowHost.getRootNode() as ShadowRoot).host; + shadowHost = (n.getRootNode() as ShadowRoot).host; + return shadowHost; +} + +/** + * Get the root shadow host of a node in nested shadow doms. Returns the node itself if it is not in a shadow dom. + */ +export function getRootShadowHost(n: Node): Node { + let rootShadowHost: Node = n; + + let shadowHost: Element | null; + // If n is in a nested shadow dom. + while ((shadowHost = getShadowHost(rootShadowHost))) + rootShadowHost = shadowHost; return rootShadowHost; } @@ -537,7 +550,7 @@ export function shadowHostInDom(n: Node): boolean { const doc = n.ownerDocument; if (!doc) return false; const shadowHost = getRootShadowHost(n); - return Boolean(shadowHost && doc.contains(shadowHost)); + return doc.contains(shadowHost); } export function inDom(n: Node): boolean { diff --git a/packages/rrweb/test/util.test.ts b/packages/rrweb/test/util.test.ts index 964005c8c6..46ae81db2a 100644 --- a/packages/rrweb/test/util.test.ts +++ b/packages/rrweb/test/util.test.ts @@ -1,7 +1,13 @@ /** * @jest-environment jsdom */ -import { StyleSheetMirror } from '../src/utils'; +import { + getRootShadowHost, + StyleSheetMirror, + inDom, + shadowHostInDom, + getShadowHost, +} from '../src/utils'; describe('Utilities for other modules', () => { describe('StyleSheetMirror', () => { @@ -75,4 +81,66 @@ describe('Utilities for other modules', () => { expect(mirror.add(new CSSStyleSheet())).toBe(1); }); }); + + describe('inDom()', () => { + it('should get correct result given nested shadow doms', () => { + const shadowHost = document.createElement('div'); + const shadowRoot = shadowHost.attachShadow({ mode: 'open' }); + const shadowHost2 = document.createElement('div'); + const shadowRoot2 = shadowHost2.attachShadow({ mode: 'open' }); + const div = document.createElement('div'); + shadowRoot.appendChild(shadowHost2); + shadowRoot2.appendChild(div); + // Not in Dom yet. + expect(getShadowHost(div)).toBe(shadowHost2); + expect(getRootShadowHost(div)).toBe(shadowHost); + expect(shadowHostInDom(div)).toBeFalsy(); + expect(inDom(div)).toBeFalsy(); + + // Added to the Dom. + document.body.appendChild(shadowHost); + expect(getShadowHost(div)).toBe(shadowHost2); + expect(getRootShadowHost(div)).toBe(shadowHost); + expect(shadowHostInDom(div)).toBeTruthy(); + expect(inDom(div)).toBeTruthy(); + }); + + it('should get correct result given a normal node', () => { + const div = document.createElement('div'); + // Not in Dom yet. + expect(getShadowHost(div)).toBeNull(); + expect(getRootShadowHost(div)).toBe(div); + expect(shadowHostInDom(div)).toBeFalsy(); + expect(inDom(div)).toBeFalsy(); + + // Added to the Dom. + document.body.appendChild(div); + expect(getShadowHost(div)).toBeNull(); + expect(getRootShadowHost(div)).toBe(div); + expect(shadowHostInDom(div)).toBeTruthy(); + expect(inDom(div)).toBeTruthy(); + }); + + /** + * Given the textNode of a detached HTMLAnchorElement, getRootNode() will return the anchor element itself and its host property is a string. + * This corner case may cause an error in getRootShadowHost(). + */ + it('should get correct result given the textNode of a detached HTMLAnchorElement', () => { + const a = document.createElement('a'); + a.href = 'example.com'; + a.textContent = 'something'; + // Not in Dom yet. + expect(getShadowHost(a.childNodes[0])).toBeNull(); + expect(getRootShadowHost(a.childNodes[0])).toBe(a.childNodes[0]); + expect(shadowHostInDom(a.childNodes[0])).toBeFalsy(); + expect(inDom(a.childNodes[0])).toBeFalsy(); + + // Added to the Dom. + document.body.appendChild(a); + expect(getShadowHost(a.childNodes[0])).toBeNull(); + expect(getRootShadowHost(a.childNodes[0])).toBe(a.childNodes[0]); + expect(shadowHostInDom(a.childNodes[0])).toBeTruthy(); + expect(inDom(a.childNodes[0])).toBeTruthy(); + }); + }); });