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

@@ -66,6 +66,6 @@
"@xstate/fsm": "^1.4.0",
"fflate": "^0.4.4",
"mitt": "^1.1.3",
"rrweb-snapshot": "^1.0.7"
"rrweb-snapshot": "^1.1.1"
}
}

View File

@@ -7,6 +7,7 @@ import {
getWindowHeight,
polyfill,
isIframeINode,
hasShadowRoot,
} from '../utils';
import {
EventType,
@@ -16,8 +17,10 @@ import {
IncrementalSource,
listenerHandler,
LogRecordOptions,
mutationCallbackParam,
} from '../types';
import { IframeManager } from './iframe-manager';
import { ShadowDomManager } from './shadow-dom-manager';
function wrapEvent(e: event): eventWithTime {
return {
@@ -179,17 +182,33 @@ function record<T = eventWithTime>(
}
};
const wrappedMutationEmit = (m: mutationCallbackParam) => {
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
...m,
},
}),
);
};
const iframeManager = new IframeManager({
mutationCb: (m) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
...m,
},
}),
),
mutationCb: wrappedMutationEmit,
});
const shadowDomManager = new ShadowDomManager({
mutationCb: wrappedMutationEmit,
bypassOptions: {
blockClass,
blockSelector,
inlineStylesheet,
maskInputOptions,
recordCanvas,
slimDOMOptions,
iframeManager,
},
});
takeFullSnapshot = (isCheckout = false) => {
@@ -217,6 +236,9 @@ function record<T = eventWithTime>(
if (isIframeINode(n)) {
iframeManager.addIframe(n);
}
if (hasShadowRoot(n)) {
shadowDomManager.addShadowRoot(n.shadowRoot, document);
}
},
onIframeLoad: (iframe, childSn) => {
iframeManager.attachIframe(iframe, childSn);
@@ -271,16 +293,7 @@ function record<T = eventWithTime>(
const observe = (doc: Document) => {
return initObservers(
{
mutationCb: (m) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
...m,
},
}),
),
mutationCb: wrappedMutationEmit,
mousemoveCb: (positions, source) =>
wrappedEmit(
wrapEvent({
@@ -394,6 +407,7 @@ function record<T = eventWithTime>(
blockSelector,
slimDOMOptions,
iframeManager,
shadowDomManager,
},
hooks,
);

View File

@@ -5,7 +5,7 @@ import {
MaskInputOptions,
SlimDOMOptions,
IGNORED_NODE,
NodeType,
isShadowRoot,
} from 'rrweb-snapshot';
import {
mutationRecord,
@@ -16,8 +16,16 @@ import {
removedNodeMutation,
addedNodeMutation,
} from '../types';
import { mirror, isBlocked, isAncestorRemoved, isIgnored } from '../utils';
import {
mirror,
isBlocked,
isAncestorRemoved,
isIgnored,
isIframeINode,
hasShadowRoot,
} from '../utils';
import { IframeManager } from './iframe-manager';
import { ShadowDomManager } from './shadow-dom-manager';
type DoubleLinkedListNode = {
previous: DoubleLinkedListNode | null;
@@ -158,6 +166,7 @@ export default class MutationBuffer {
private doc: Document;
private iframeManager: IframeManager;
private shadowDomManager: ShadowDomManager;
public init(
cb: mutationCallBack,
@@ -169,6 +178,7 @@ export default class MutationBuffer {
slimDOMOptions: SlimDOMOptions,
doc: Document,
iframeManager: IframeManager,
shadowDomManager: ShadowDomManager,
) {
this.blockClass = blockClass;
this.blockSelector = blockSelector;
@@ -179,6 +189,7 @@ export default class MutationBuffer {
this.emissionCallback = cb;
this.doc = doc;
this.iframeManager = iframeManager;
this.shadowDomManager = shadowDomManager;
}
public freeze() {
@@ -236,10 +247,14 @@ export default class MutationBuffer {
return nextId;
};
const pushAdd = (n: Node) => {
if (!n.parentNode || !this.doc.contains(n)) {
const shadowHost: Element | null = (n.getRootNode() as ShadowRoot)?.host;
const notInDoc = !this.doc.contains(n) && !this.doc.contains(shadowHost);
if (!n.parentNode || notInDoc) {
return;
}
const parentId = mirror.getId((n.parentNode as Node) as INode);
const parentId = isShadowRoot(n.parentNode)
? mirror.getId((shadowHost as unknown) as INode)
: mirror.getId((n.parentNode as Node) as INode);
const nextId = getNextId(n);
if (parentId === -1 || nextId === -1) {
return addList.addNode(n);
@@ -255,13 +270,11 @@ export default class MutationBuffer {
slimDOMOptions: this.slimDOMOptions,
recordCanvas: this.recordCanvas,
onSerialize: (currentN) => {
if (
currentN.__sn.type === NodeType.Element &&
currentN.__sn.tagName === 'iframe'
) {
this.iframeManager.addIframe(
(currentN as unknown) as HTMLIFrameElement,
);
if (isIframeINode(currentN)) {
this.iframeManager.addIframe(currentN);
}
if (hasShadowRoot(n)) {
this.shadowDomManager.addShadowRoot(n.shadowRoot, document);
}
},
onIframeLoad: (iframe, childSn) => {
@@ -418,6 +431,7 @@ export default class MutationBuffer {
// overwrite attribute if the mutations was triggered in same time
item.attributes[m.attributeName!] = transformAttribute(
this.doc,
(m.target as HTMLElement).tagName,
m.attributeName!,
value!,
);
@@ -427,7 +441,9 @@ export default class MutationBuffer {
m.addedNodes.forEach((n) => this.genAdds(n, m.target));
m.removedNodes.forEach((n) => {
const nodeId = mirror.getId(n as INode);
const parentId = mirror.getId(m.target as INode);
const parentId = isShadowRoot(m.target)
? mirror.getId((m.target.host as unknown) as INode)
: mirror.getId(m.target as INode);
if (
isBlocked(n, this.blockClass) ||
isBlocked(m.target, this.blockClass) ||
@@ -463,6 +479,7 @@ export default class MutationBuffer {
this.removes.push({
parentId,
id: nodeId,
isShadow: isShadowRoot(m.target) ? true : undefined,
});
}
this.mapRemoves.push(n);

View File

@@ -44,6 +44,7 @@ import {
import MutationBuffer from './mutation';
import { stringify } from './stringify';
import { IframeManager } from './iframe-manager';
import { ShadowDomManager } from './shadow-dom-manager';
type WindowWithStoredMutationObserver = Window & {
__rrMutationObserver?: MutationObserver;
@@ -56,7 +57,7 @@ type WindowWithAngularZone = Window & {
export const mutationBuffers: MutationBuffer[] = [];
function initMutationObserver(
export function initMutationObserver(
cb: mutationCallBack,
doc: Document,
blockClass: blockClass,
@@ -66,6 +67,8 @@ function initMutationObserver(
recordCanvas: boolean,
slimDOMOptions: SlimDOMOptions,
iframeManager: IframeManager,
shadowDomManager: ShadowDomManager,
rootEl: Node,
): MutationObserver {
const mutationBuffer = new MutationBuffer();
mutationBuffers.push(mutationBuffer);
@@ -80,6 +83,7 @@ function initMutationObserver(
slimDOMOptions,
doc,
iframeManager,
shadowDomManager,
);
let mutationObserverCtor =
window.MutationObserver ||
@@ -109,7 +113,7 @@ function initMutationObserver(
const observer = new mutationObserverCtor(
mutationBuffer.processMutations.bind(mutationBuffer),
);
observer.observe(doc, {
observer.observe(rootEl, {
attributes: true,
attributeOldValue: true,
characterData: true,
@@ -763,6 +767,8 @@ export function initObservers(
o.recordCanvas,
o.slimDOMOptions,
o.iframeManager,
o.shadowDomManager,
o.doc,
);
const mousemoveHandler = initMoveObserver(o.mousemoveCb, o.sampling, o.doc);
const mouseInteractionHandler = initMouseInteractionObserver(

View File

@@ -0,0 +1,43 @@
import { mutationCallBack, blockClass } from '../types';
import { MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot';
import { IframeManager } from './iframe-manager';
import { initMutationObserver } from './observer';
type BypassOptions = {
blockClass: blockClass;
blockSelector: string | null;
inlineStylesheet: boolean;
maskInputOptions: MaskInputOptions;
recordCanvas: boolean;
slimDOMOptions: SlimDOMOptions;
iframeManager: IframeManager;
};
export class ShadowDomManager {
private mutationCb: mutationCallBack;
private bypassOptions: BypassOptions;
constructor(options: {
mutationCb: mutationCallBack;
bypassOptions: BypassOptions;
}) {
this.mutationCb = options.mutationCb;
this.bypassOptions = options.bypassOptions;
}
public addShadowRoot(shadowRoot: ShadowRoot, doc: Document) {
initMutationObserver(
this.mutationCb,
doc,
this.bypassOptions.blockClass,
this.bypassOptions.blockSelector,
this.bypassOptions.inlineStylesheet,
this.bypassOptions.maskInputOptions,
this.bypassOptions.recordCanvas,
this.bypassOptions.slimDOMOptions,
this.bypassOptions.iframeManager,
this,
shadowRoot,
);
}
}

View File

@@ -40,6 +40,7 @@ import {
AppendedIframe,
isIframeINode,
getBaseDimension,
hasShadowRoot,
} from '../utils';
import getInjectStyleRules from './styles/inject-style';
import './styles/style.css';
@@ -1048,14 +1049,18 @@ export class Replayer {
if (!target) {
return this.warnNodeNotFound(d, mutation.id);
}
const parent = mirror.getNode(mutation.parentId);
let parent: INode | null | ShadowRoot = mirror.getNode(mutation.parentId);
if (!parent) {
return this.warnNodeNotFound(d, mutation.parentId);
}
if (mutation.isShadow && hasShadowRoot(parent)) {
parent = parent.shadowRoot;
}
// target may be removed with its parents before
mirror.removeNodeFromMap(target);
if (parent) {
const realParent = this.fragmentParentMap.get(parent);
const realParent =
'__sn' in parent ? this.fragmentParentMap.get(parent) : undefined;
if (realParent && realParent.contains(target)) {
realParent.removeChild(target);
} else if (this.fragmentParentMap.has(target)) {
@@ -1100,7 +1105,7 @@ export class Replayer {
if (!this.iframe.contentDocument) {
return console.warn('Looks like your replayer has been destroyed.');
}
let parent = mirror.getNode(mutation.parentId);
let parent: INode | null | ShadowRoot = mirror.getNode(mutation.parentId);
if (!parent) {
if (mutation.node.type === NodeType.Document) {
// is newly added document, maybe the document node of an iframe
@@ -1133,6 +1138,10 @@ export class Replayer {
parent = virtualParent;
}
if (mutation.node.isShadow && hasShadowRoot(parent)) {
parent = parent.shadowRoot;
}
let previous: Node | null = null;
let next: Node | null = null;
if (mutation.previousId) {

View File

@@ -8,6 +8,7 @@ import {
import { PackFn, UnpackFn } from './packer/base';
import { FontFaceDescriptors } from 'css-font-loading-module';
import { IframeManager } from './record/iframe-manager';
import { ShadowDomManager } from './record/shadow-dom-manager';
export enum EventType {
DomContentLoaded,
@@ -231,6 +232,7 @@ export type observerParam = {
slimDOMOptions: SlimDOMOptions;
doc: Document;
iframeManager: IframeManager;
shadowDomManager: ShadowDomManager;
};
export type hooksParam = {
@@ -282,6 +284,7 @@ export type attributeMutation = {
export type removedNodeMutation = {
parentId: number;
id: number;
isShadow?: boolean;
};
export type addedNodeMutation = {
@@ -292,7 +295,7 @@ export type addedNodeMutation = {
node: serializedNodeWithId;
};
type mutationCallbackParam = {
export type mutationCallbackParam = {
texts: textMutation[];
attributes: attributeMutation[];
removes: removedNodeMutation[];

View File

@@ -21,6 +21,7 @@ import {
IGNORED_NODE,
serializedNodeWithId,
NodeType,
isShadowRoot,
} from 'rrweb-snapshot';
export function on(
@@ -213,6 +214,9 @@ export function isIgnored(n: Node | INode): boolean {
}
export function isAncestorRemoved(target: INode): boolean {
if (isShadowRoot(target)) {
return false;
}
const id = mirror.getId(target);
if (!mirror.has(id)) {
return true;
@@ -542,12 +546,16 @@ export type AppendedIframe = {
builtNode: HTMLIFrameINode;
};
export function isIframeINode(node: INode): node is HTMLIFrameINode {
// node can be document fragment when using the virtual parent feature
if (!node.__sn) {
return false;
export function isIframeINode(
node: INode | ShadowRoot,
): node is HTMLIFrameINode {
if ('__sn' in node) {
return (
node.__sn.type === NodeType.Element && node.__sn.tagName === 'iframe'
);
}
return node.__sn.type === NodeType.Element && node.__sn.tagName === 'iframe';
// node can be document fragment when using the virtual parent feature
return false;
}
export function getBaseDimension(
@@ -579,3 +587,9 @@ export function getBaseDimension(
absoluteScale: frameBaseDimension.absoluteScale * relativeScale,
};
}
export function hasShadowRoot<T extends Node>(
n: T,
): n is T & { shadowRoot: ShadowRoot } {
return Boolean(((n as unknown) as Element)?.shadowRoot);
}

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');
});
});

View File

@@ -2767,10 +2767,10 @@ rollup@^2.3.3:
optionalDependencies:
fsevents "~2.1.2"
rrweb-snapshot@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.0.7.tgz#9d334590089af4a857970ef4e9e978d986a122d1"
integrity sha512-6fu9+KiQlFPkFk2SdahIDsV+yu1juiAR/o+kOiwKPbXur1TiFGMPAfaQNCkqLc8Nvyx3ItkJmrIldyxnAalEag==
rrweb-snapshot@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.1.tgz#71da8792f43b8bd7017851edcd02e3d7c7cfef9f"
integrity sha512-xRX7s2/MA/Ifnul4ImAquD1w/Nkz6WOACm3xdKDdQrCD/xKdgcu1yWoJ8eSIXyfVSuIt4VfrhxJdeHyhC1gmGQ==
run-async@^2.2.0:
version "2.4.1"