impl shadow DOM manager

part of #38
1. observe DOM mutations in shadow DOM
2. rebuild DOM mutations in shadow DOM
This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 66c7c8f028
commit 0e688bba0c
12 changed files with 680 additions and 48 deletions

View File

@@ -6982,3 +6982,409 @@ exports[`serialize-before-record 1`] = `
}
]"
`;
exports[`shadow-dom 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\\": \\"meta\\",
\\"attributes\\": {
\\"charset\\": \\"UTF-8\\"
},
\\"childNodes\\": [],
\\"id\\": 6
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 7
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"name\\": \\"viewport\\",
\\"content\\": \\"width=device-width, initial-scale=1.0\\"
},
\\"childNodes\\": [],
\\"id\\": 8
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 9
},
{
\\"type\\": 2,
\\"tagName\\": \\"title\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"Shadow DOM Observer\\",
\\"id\\": 11
}
],
\\"id\\": 10
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 12
},
{
\\"type\\": 2,
\\"tagName\\": \\"style\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n .my-element {\\\\n margin: 0 0 1rem 0;\\\\n }\\\\n iframe {\\\\n border: 0;\\\\n width: 100%;\\\\n padding: 0;\\\\n }\\\\n\\\\n body {\\\\n max-width: 400px;\\\\n margin: 1rem auto;\\\\n padding: 0 1rem;\\\\n font-family: 'comic sans ms';\\\\n }\\\\n \\",
\\"isStyle\\": true,
\\"id\\": 14
}
],
\\"id\\": 13
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 15
}
],
\\"id\\": 4
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 16
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 18
},
{
\\"type\\": 2,
\\"tagName\\": \\"p\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repellat odit\\\\n officiis necessitatibus laborum asperiores et adipisci dolores corporis,\\\\n vero distinctio voluptas, suscipit commodi architecto, aliquam fugit.\\\\n Nesciunt labore reiciendis blanditiis!\\\\n \\",
\\"id\\": 20
}
],
\\"id\\": 19
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n \\",
\\"id\\": 21
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"class\\": \\"my-element\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 23
},
{
\\"type\\": 5,
\\"textContent\\": \\" Also could be a \\\\n <custom-element />\\\\n \\",
\\"id\\": 24
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 25
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 26,
\\"isShadow\\": true
},
{
\\"type\\": 2,
\\"tagName\\": \\"style\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n body { /* for fallback iframe */\\\\n margin: 0;\\\\n }\\\\n p { \\\\n border: 1px solid #ccc;\\\\n padding: 1rem;\\\\n color: red;\\\\n font-family: sans-serif;\\\\n }\\\\n \\",
\\"isStyle\\": true,
\\"id\\": 28
}
],
\\"id\\": 27,
\\"isShadow\\": true
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n \\",
\\"id\\": 29,
\\"isShadow\\": true
},
{
\\"type\\": 2,
\\"tagName\\": \\"p\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"Element with Shadow DOM\\",
\\"id\\": 31
}
],
\\"id\\": 30,
\\"isShadow\\": true
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\",
\\"id\\": 32,
\\"isShadow\\": true
}
],
\\"id\\": 22,
\\"isShadowHost\\": true
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n \\",
\\"id\\": 33
},
{
\\"type\\": 2,
\\"tagName\\": \\"p\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repellat odit\\\\n officiis necessitatibus laborum asperiores et adipisci dolores corporis,\\\\n vero distinctio voluptas, suscipit commodi architecto, aliquam fugit.\\\\n Nesciunt labore reiciendis blanditiis!\\\\n \\",
\\"id\\": 35
}
],
\\"id\\": 34
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 36
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 38
}
],
\\"id\\": 37
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
\\"id\\": 39
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 41
}
],
\\"id\\": 40
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
\\"id\\": 42
}
],
\\"id\\": 17
}
],
\\"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\\": \\"p\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 43,
\\"isShadow\\": true
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 43,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"p\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 44
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [
{
\\"parentId\\": 22,
\\"id\\": 30,
\\"isShadow\\": true
}
],
\\"adds\\": []
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 44,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"hi\\",
\\"id\\": 45
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [
{
\\"parentId\\": 44,
\\"id\\": 45
}
],
\\"adds\\": [
{
\\"parentId\\": 44,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"123\\",
\\"id\\": 46
}
}
]
}
}
]"
`;

83
test/html/shadow-dom.html Normal file
View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shadow DOM Observer</title>
<style>
.my-element {
margin: 0 0 1rem 0;
}
iframe {
border: 0;
width: 100%;
padding: 0;
}
body {
max-width: 400px;
margin: 1rem auto;
padding: 0 1rem;
font-family: 'comic sans ms';
}
</style>
</head>
<body>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repellat odit
officiis necessitatibus laborum asperiores et adipisci dolores corporis,
vero distinctio voluptas, suscipit commodi architecto, aliquam fugit.
Nesciunt labore reiciendis blanditiis!
</p>
<div class="my-element">
<!-- Also could be a
<custom-element />
-->
</div>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repellat odit
officiis necessitatibus laborum asperiores et adipisci dolores corporis,
vero distinctio voluptas, suscipit commodi architecto, aliquam fugit.
Nesciunt labore reiciendis blanditiis!
</p>
<script>
let content = `
<style>
body { /* for fallback iframe */
margin: 0;
}
p {
border: 1px solid #ccc;
padding: 1rem;
color: red;
font-family: sans-serif;
}
</style>
<p>Element with Shadow DOM</p>
`;
let myElements = document.querySelectorAll('.my-element');
if (document.body.attachShadow) {
myElements.forEach((el) => {
var shadow = el.attachShadow({
mode: 'open',
});
shadow.innerHTML = content;
});
} else {
let newiframe = document.createElement('iframe');
newiframe.srcdoc = content;
myElements.forEach((el) => {
let parent = el.parentNode;
parent.replaceChild(newiframe, el);
});
}
</script>
</script>
</body>
</html>

View File

@@ -419,4 +419,41 @@ describe('record integration tests', function (this: ISuite) {
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'iframe');
});
it('should record shadow DOM', async () => {
const page: puppeteer.Page = await this.browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'shadow-dom.html'));
await page.evaluate(() => {
const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
const el = document.querySelector('.my-element') as HTMLDivElement;
const shadowRoot = el.shadowRoot as ShadowRoot;
shadowRoot.appendChild(document.createElement('p'));
sleep(1)
.then(() => {
shadowRoot.lastChild!.appendChild(document.createElement('p'));
return sleep(1);
})
.then(() => {
const firstP = shadowRoot.querySelector('p') as HTMLParagraphElement;
shadowRoot.removeChild(firstP);
return sleep(1);
})
.then(() => {
(shadowRoot.lastChild!.childNodes[0] as HTMLElement).innerText = 'hi';
return sleep(1);
})
.then(() => {
(shadowRoot.lastChild!.childNodes[0] as HTMLElement).innerText =
'123';
});
});
await page.waitFor(50);
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'shadow-dom');
});
});