From 70e6155be99c4b3c6127b48714d1d359f3f743d1 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 13 Jan 2023 16:19:16 +1100 Subject: [PATCH 1/2] fix: regression of issue: ShadowHost can't be a string (issue 941) --- packages/rrweb/src/utils.ts | 11 +++--- packages/rrweb/test/util.test.ts | 63 +++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 1d5ec83855..8e399d6b08 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -519,13 +519,12 @@ 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; +export function getRootShadowHost(n: Node): Node { + let rootShadowHost: Node = n; + // If n is in a nested shadow dom. while ( - rootShadowHost?.getRootNode?.()?.nodeType === Node.DOCUMENT_FRAGMENT_NODE && + rootShadowHost.getRootNode?.()?.nodeType === Node.DOCUMENT_FRAGMENT_NODE && (rootShadowHost.getRootNode() as ShadowRoot).host ) rootShadowHost = (rootShadowHost.getRootNode() as ShadowRoot).host; @@ -537,7 +536,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..2110f01e71 100644 --- a/packages/rrweb/test/util.test.ts +++ b/packages/rrweb/test/util.test.ts @@ -1,7 +1,12 @@ /** * @jest-environment jsdom */ -import { StyleSheetMirror } from '../src/utils'; +import { + getRootShadowHost, + StyleSheetMirror, + inDom, + shadowHostInDom, +} from '../src/utils'; describe('Utilities for other modules', () => { describe('StyleSheetMirror', () => { @@ -75,4 +80,60 @@ 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(getRootShadowHost(div)).toBe(shadowHost); + expect(shadowHostInDom(div)).toBeFalsy(); + expect(inDom(div)).toBeFalsy(); + + // Added to the Dom. + document.body.appendChild(shadowHost); + 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(getRootShadowHost(div)).toBe(div); + expect(shadowHostInDom(div)).toBeFalsy(); + expect(inDom(div)).toBeFalsy(); + + // Added to the Dom. + document.body.appendChild(div); + 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(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(getRootShadowHost(a.childNodes[0])).toBe(a.childNodes[0]); + expect(shadowHostInDom(a.childNodes[0])).toBeTruthy(); + expect(inDom(a.childNodes[0])).toBeTruthy(); + }); + }); }); From 407a48a0dd9af4b3de083cc218883371c082ac94 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 13 Jan 2023 16:43:28 +1100 Subject: [PATCH 2/2] refactor shadow dom recording to make tests cover key code --- packages/rrweb/src/record/mutation.ts | 10 ++-------- packages/rrweb/src/utils.ts | 24 +++++++++++++++++++----- packages/rrweb/test/util.test.ts | 7 +++++++ 3 files changed, 28 insertions(+), 13 deletions(-) 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 8e399d6b08..8703191f6d 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -519,15 +519,29 @@ export class StyleSheetMirror { } } +/** + * 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 + ) + 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 ( - rootShadowHost.getRootNode?.()?.nodeType === Node.DOCUMENT_FRAGMENT_NODE && - (rootShadowHost.getRootNode() as ShadowRoot).host - ) - rootShadowHost = (rootShadowHost.getRootNode() as ShadowRoot).host; + while ((shadowHost = getShadowHost(rootShadowHost))) + rootShadowHost = shadowHost; return rootShadowHost; } diff --git a/packages/rrweb/test/util.test.ts b/packages/rrweb/test/util.test.ts index 2110f01e71..46ae81db2a 100644 --- a/packages/rrweb/test/util.test.ts +++ b/packages/rrweb/test/util.test.ts @@ -6,6 +6,7 @@ import { StyleSheetMirror, inDom, shadowHostInDom, + getShadowHost, } from '../src/utils'; describe('Utilities for other modules', () => { @@ -91,12 +92,14 @@ describe('Utilities for other modules', () => { 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(); @@ -105,12 +108,14 @@ describe('Utilities for other modules', () => { 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(); @@ -125,12 +130,14 @@ describe('Utilities for other modules', () => { 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();