Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: regression of issue: ShadowHost can't be a string (issue 941) #1092

Merged
merged 2 commits into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
isSerializedIframe,
isSerializedStylesheet,
inDom,
getShadowHost,
} from '../utils';

type DoubleLinkedListNode = {
Expand Down Expand Up @@ -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) {
Expand Down
33 changes: 23 additions & 10 deletions packages/rrweb/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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 {
Expand Down
70 changes: 69 additions & 1 deletion packages/rrweb/test/util.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
});