* fix: style not applied to polyfillled shadow dom

* test: add integration test for shadydom and @lwc/synthetic-shadow

* improve the implementation of function isNativeShadowDom

* apply lele0108's review suggestion
This commit is contained in:
Yun Feng
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 5c1c104073
commit b60ad44a19
8 changed files with 658 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ import {
isElement,
isShadowRoot,
maskInputValue,
isNativeShadowDom,
} from './utils';
let _id = 1;
@@ -248,7 +249,10 @@ export function transformAttribute(
value: string,
): string {
// relative path in attribute
if (name === 'src' || (name === 'href' && value && !(tagName === 'use' && value[0] === '#'))) {
if (
name === 'src' ||
(name === 'href' && value && !(tagName === 'use' && value[0] === '#'))
) {
// href starts with a # is an id pointer for svg
return absoluteToDoc(doc, value);
} else if (name === 'xlink:href' && value && value[0] !== '#') {
@@ -1025,7 +1029,9 @@ export function serializeNodeWithId(
recordChild = recordChild && !serializedNode.needBlock;
// this property was not needed in replay side
delete serializedNode.needBlock;
if ((n as HTMLElement).shadowRoot) serializedNode.isShadowHost = true;
const shadowRoot = (n as HTMLElement).shadowRoot;
if (shadowRoot && isNativeShadowDom(shadowRoot))
serializedNode.isShadowHost = true;
}
if (
(serializedNode.type === NodeType.Document ||
@@ -1075,14 +1081,19 @@ export function serializeNodeWithId(
for (const childN of Array.from(n.shadowRoot.childNodes)) {
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
if (serializedChildNode) {
serializedChildNode.isShadow = true;
isNativeShadowDom(n.shadowRoot) &&
(serializedChildNode.isShadow = true);
serializedNode.childNodes.push(serializedChildNode);
}
}
}
}
if (n.parentNode && isShadowRoot(n.parentNode)) {
if (
n.parentNode &&
isShadowRoot(n.parentNode) &&
isNativeShadowDom(n.parentNode)
) {
serializedNode.isShadow = true;
}

View File

@@ -16,6 +16,14 @@ export function isShadowRoot(n: Node): n is ShadowRoot {
return Boolean(host?.shadowRoot === n);
}
/**
* To fix the issue https://github.com/rrweb-io/rrweb/issues/933.
* Some websites use polyfilled shadow dom and this function is used to detect this situation.
*/
export function isNativeShadowDom(shadowRoot: ShadowRoot) {
return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]';
}
export class Mirror implements IMirror<Node> {
private idNodeMap: idNodeMap = new Map();
private nodeMetaMap: nodeMetaMap = new WeakMap();

View File

@@ -1,6 +1,7 @@
import { MaskInputFn, MaskInputOptions, IMirror, serializedNodeWithId } from './types';
export declare function isElement(n: Node): n is Element;
export declare function isShadowRoot(n: Node): n is ShadowRoot;
export declare function isNativeShadowDom(shadowRoot: ShadowRoot): boolean;
export declare class Mirror implements IMirror<Node> {
private idNodeMap;
private nodeMetaMap;

View File

@@ -6,6 +6,7 @@ import {
needMaskingText,
maskInputValue,
Mirror,
isNativeShadowDom,
} from 'rrweb-snapshot';
import type {
mutationRecord,
@@ -581,7 +582,10 @@ export default class MutationBuffer {
this.removes.push({
parentId,
id: nodeId,
isShadow: isShadowRoot(m.target) ? true : undefined,
isShadow:
isShadowRoot(m.target) && isNativeShadowDom(m.target)
? true
: undefined,
});
}
this.mapRemoves.push(n);

View File

@@ -7,6 +7,7 @@ import type {
import { initMutationObserver, initScrollObserver } from './observer';
import { patch } from '../utils';
import type { Mirror } from 'rrweb-snapshot';
import { isNativeShadowDom } from 'rrweb-snapshot';
type BypassOptions = Omit<
MutationBufferParam,
@@ -53,6 +54,7 @@ export class ShadowDomManager {
}
public addShadowRoot(shadowRoot: ShadowRoot, doc: Document) {
if (!isNativeShadowDom(shadowRoot)) return;
initMutationObserver(
{
...this.bypassOptions,

View File

@@ -10764,6 +10764,537 @@ exports[`record integration tests should record shadow DOM 1`] = `
]"
`;
exports[`record integration tests should record shadow doms polyfilled by shadydom 1`] = `
"[
{
\\"type\\": 0,
\\"data\\": {}
},
{
\\"type\\": 1,
\\"data\\": {}
},
{
\\"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\\": {
\\"lang\\": \\"en\\"
},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 5
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 7
}
],
\\"id\\": 6
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 8
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {
\\"src\\": \\"https://cdn.jsdelivr.net/npm/@webcomponents/shadydom@1.9.0/shadydom.min.js\\"
},
\\"childNodes\\": [],
\\"id\\": 9
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\",
\\"id\\": 10
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"charset\\": \\"UTF-8\\"
},
\\"childNodes\\": [],
\\"id\\": 11
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 12
}
],
\\"id\\": 4
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 13
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 15
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"id\\": \\"target1\\"
},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 17
}
],
\\"id\\": 16
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 18
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"id\\": \\"target2\\"
},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"p\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 20
}
],
\\"id\\": 19
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 21
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"id\\": \\"target3\\"
},
\\"childNodes\\": [],
\\"id\\": 22
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 23
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 25
}
],
\\"id\\": 24
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\",
\\"id\\": 26
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 28
}
],
\\"id\\": 27
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
\\"id\\": 29
}
],
\\"id\\": 14
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 22,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"span\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 30
}
}
]
}
}
]"
`;
exports[`record integration tests should record shadow doms polyfilled by synthetic-shadow 1`] = `
"[
{
\\"type\\": 0,
\\"data\\": {}
},
{
\\"type\\": 1,
\\"data\\": {}
},
{
\\"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\\": {
\\"lang\\": \\"en\\"
},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 5
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 7
}
],
\\"id\\": 6
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 8
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {
\\"src\\": \\"https://cdn.jsdelivr.net/npm/@lwc/synthetic-shadow@2.20.3/dist/synthetic-shadow.js\\"
},
\\"childNodes\\": [],
\\"id\\": 9
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\",
\\"id\\": 10
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"charset\\": \\"UTF-8\\"
},
\\"childNodes\\": [],
\\"id\\": 11
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 12
}
],
\\"id\\": 4
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 13
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 15
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"id\\": \\"target1\\"
},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 17,
\\"isShadow\\": true
}
],
\\"id\\": 16,
\\"isShadowHost\\": true
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 18
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"id\\": \\"target2\\"
},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"p\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 20
}
],
\\"id\\": 19
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 21
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"id\\": \\"target3\\"
},
\\"childNodes\\": [],
\\"id\\": 22
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 23
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 25
}
],
\\"id\\": 24
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\",
\\"id\\": 26
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 28
}
],
\\"id\\": 27
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
\\"id\\": 29
}
],
\\"id\\": 14
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 22,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"span\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 30
}
},
{
\\"parentId\\": 14,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"id\\": \\"target4\\"
},
\\"childNodes\\": [],
\\"id\\": 31,
\\"isShadowHost\\": true
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 31,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"ul\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 32,
\\"isShadow\\": true
}
}
]
}
}
]"
`;
exports[`record integration tests should record webgl canvas mutations 1`] = `
"[
{

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="target1"></div>
<div id="target2"></div>
<div id="target3"></div>
<script>
const target1 = document.querySelector('#target1');
target1.attachShadow({
mode: 'open',
});
target1.shadowRoot.appendChild(document.createElement('div'));
const target2 = document.querySelector('#target2');
target2.attachShadow({
mode: 'open',
'$$lwc-synthetic-mode': true,
});
target2.shadowRoot.appendChild(document.createElement('p'));
</script>
</body>
</html>

View File

@@ -589,6 +589,78 @@ describe('record integration tests', function (this: ISuite) {
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();
await page.goto('about:blank');
await page.setContent(
// insert shadydom script
replaceLast(
getHtml.call(this, 'polyfilled-shadowdom-mutation.html'),
'<head>',
`
<head>
<script>
// To force ShadyDOM to be used even when native ShadowDOM is available, set the ShadyDOM = {force: true} in a script prior to loading the polyfill.
window.ShadyDOM = { force: true };
</script>
<script src="https://cdn.jsdelivr.net/npm/@webcomponents/shadydom@1.9.0/shadydom.min.js"></script>
`,
),
);
await page.evaluate(() => {
const target3 = document.querySelector('#target3');
target3?.attachShadow({
mode: 'open',
});
target3?.shadowRoot?.appendChild(document.createElement('span'));
});
await waitForRAF(page); // wait till browser sent snapshots
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots);
});
// https://github.com/salesforce/lwc/tree/master/packages/%40lwc/synthetic-shadow
it('should record shadow doms polyfilled by synthetic-shadow', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(
// insert lwc's synthetic-shadow script
replaceLast(
getHtml.call(this, 'polyfilled-shadowdom-mutation.html'),
'<head>',
`
<head>
<script>var process = {env: {NODE_ENV: "production"}};</script>
<script src="https://cdn.jsdelivr.net/npm/@lwc/synthetic-shadow@2.20.3/dist/synthetic-shadow.js"></script>
`,
),
);
await page.evaluate(() => {
const target3 = document.querySelector('#target3');
// create a shadow dom with synthetic shadow
// https://github.com/salesforce/lwc/blob/v2.20.3/packages/@lwc/synthetic-shadow/src/faux-shadow/element.ts#L81-L87
target3?.attachShadow({
mode: 'open',
'$$lwc-synthetic-mode': true,
} as ShadowRootInit);
target3?.shadowRoot?.appendChild(document.createElement('span'));
const target4 = document.createElement('div');
target4.id = 'target4';
// create a native shadow dom
document.body.appendChild(target4);
target4.attachShadow({
mode: 'open',
});
target4.shadowRoot?.appendChild(document.createElement('ul'));
});
await waitForRAF(page); // wait till browser sent snapshots
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots);
});
it('should mask texts', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');