Fix: shadow dom bugs (#1049)
* Add test cases for bugs * Fix shadow dom recording When moving an element containing shadow dom When adding an element to shadow dom before its attached to the dom * Apply formatting changes * Refactor in dom checking code * Nodes don't get processed in more than one mutation buffer * Constrain node mutations to one mutation buffer per request animation frame * Make tests less flaky under heavy load * Apply suggestions from code review * Update packages/rrweb-snapshot/test/rebuild.test.ts * Remove unused nodeSet Co-authored-by: Yun Feng <yun.feng0817@gmail.com>
This commit is contained in:
@@ -47,6 +47,37 @@ describe('rebuild', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('shadowDom', function () {
|
||||
it('rebuild shadowRoot without siblings', function () {
|
||||
const node = buildNodeWithSN(
|
||||
{
|
||||
id: 1,
|
||||
tagName: 'div',
|
||||
type: NodeType.Element,
|
||||
attributes: {},
|
||||
childNodes: [
|
||||
{
|
||||
id: 2,
|
||||
tagName: 'div',
|
||||
type: NodeType.Element,
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
isShadow: true,
|
||||
},
|
||||
],
|
||||
isShadowHost: true,
|
||||
},
|
||||
{
|
||||
doc: document,
|
||||
mirror,
|
||||
hackCss: false,
|
||||
cache,
|
||||
},
|
||||
) as HTMLDivElement;
|
||||
expect(node.shadowRoot?.childNodes.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add hover class to hover selector related rules', function () {
|
||||
it('will do nothing to css text without :hover', () => {
|
||||
const cssText = 'body { color: white }';
|
||||
|
||||
@@ -4,7 +4,11 @@ import {
|
||||
SlimDOMOptions,
|
||||
createMirror,
|
||||
} from 'rrweb-snapshot';
|
||||
import { initObservers, mutationBuffers } from './observer';
|
||||
import {
|
||||
initObservers,
|
||||
mutationBuffers,
|
||||
processedNodeManager,
|
||||
} from './observer';
|
||||
import {
|
||||
on,
|
||||
getWindowWidth,
|
||||
@@ -316,6 +320,7 @@ function record<T = eventWithTime>(
|
||||
stylesheetManager,
|
||||
canvasManager,
|
||||
keepIframeSrcFn,
|
||||
processedNodeManager,
|
||||
},
|
||||
mirror,
|
||||
});
|
||||
@@ -528,6 +533,7 @@ function record<T = eventWithTime>(
|
||||
iframeManager,
|
||||
stylesheetManager,
|
||||
shadowDomManager,
|
||||
processedNodeManager,
|
||||
canvasManager,
|
||||
ignoreCSSAttributes,
|
||||
plugins:
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
hasShadowRoot,
|
||||
isSerializedIframe,
|
||||
isSerializedStylesheet,
|
||||
inDom,
|
||||
} from '../utils';
|
||||
|
||||
type DoubleLinkedListNode = {
|
||||
@@ -175,6 +176,7 @@ export default class MutationBuffer {
|
||||
private stylesheetManager: observerParam['stylesheetManager'];
|
||||
private shadowDomManager: observerParam['shadowDomManager'];
|
||||
private canvasManager: observerParam['canvasManager'];
|
||||
private processedNodeManager: observerParam['processedNodeManager'];
|
||||
|
||||
public init(options: MutationBufferParam) {
|
||||
([
|
||||
@@ -198,6 +200,7 @@ export default class MutationBuffer {
|
||||
'stylesheetManager',
|
||||
'shadowDomManager',
|
||||
'canvasManager',
|
||||
'processedNodeManager',
|
||||
] as const).forEach((key) => {
|
||||
// just a type trick, the runtime result is correct
|
||||
this[key] = options[key] as never;
|
||||
@@ -271,19 +274,8 @@ export default class MutationBuffer {
|
||||
(n.getRootNode() as ShadowRoot).host
|
||||
)
|
||||
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
|
||||
)
|
||||
rootShadowHost = (rootShadowHost.getRootNode() as ShadowRoot).host;
|
||||
// ensure contains is passed a Node, or it will throw an error
|
||||
const notInDoc =
|
||||
!this.doc.contains(n) &&
|
||||
(!rootShadowHost || !this.doc.contains(rootShadowHost));
|
||||
if (!n.parentNode || notInDoc) {
|
||||
|
||||
if (!n.parentNode || !inDom(n)) {
|
||||
return;
|
||||
}
|
||||
const parentId = isShadowRoot(n.parentNode)
|
||||
@@ -647,6 +639,9 @@ export default class MutationBuffer {
|
||||
* Make sure you check if `n`'s parent is blocked before calling this function
|
||||
* */
|
||||
private genAdds = (n: Node, target?: Node) => {
|
||||
// this node was already recorded in other buffer, ignore it
|
||||
if (this.processedNodeManager.inOtherBuffer(n, this)) return;
|
||||
|
||||
if (this.mirror.hasNode(n)) {
|
||||
if (isIgnored(n, this.mirror)) {
|
||||
return;
|
||||
@@ -666,8 +661,15 @@ export default class MutationBuffer {
|
||||
|
||||
// if this node is blocked `serializeNode` will turn it into a placeholder element
|
||||
// but we have to remove it's children otherwise they will be added as placeholders too
|
||||
if (!isBlocked(n, this.blockClass, this.blockSelector, false))
|
||||
if (!isBlocked(n, this.blockClass, this.blockSelector, false)) {
|
||||
n.childNodes.forEach((childN) => this.genAdds(childN));
|
||||
if (hasShadowRoot(n)) {
|
||||
n.shadowRoot.childNodes.forEach((childN) => {
|
||||
this.processedNodeManager.add(childN, this);
|
||||
this.genAdds(childN, n);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
selectionCallback,
|
||||
} from '@rrweb/types';
|
||||
import MutationBuffer from './mutation';
|
||||
import ProcessedNodeManager from './processed-node-manager';
|
||||
|
||||
type WindowWithStoredMutationObserver = IWindow & {
|
||||
__rrMutationObserver?: MutationObserver;
|
||||
@@ -51,6 +52,7 @@ type WindowWithAngularZone = IWindow & {
|
||||
};
|
||||
|
||||
export const mutationBuffers: MutationBuffer[] = [];
|
||||
export const processedNodeManager = new ProcessedNodeManager();
|
||||
|
||||
const isCSSGroupingRuleSupported = typeof CSSGroupingRule !== 'undefined';
|
||||
const isCSSMediaRuleSupported = typeof CSSMediaRule !== 'undefined';
|
||||
|
||||
34
packages/rrweb/src/record/processed-node-manager.ts
Normal file
34
packages/rrweb/src/record/processed-node-manager.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type MutationBuffer from './mutation';
|
||||
|
||||
/**
|
||||
* Keeps a log of nodes that could show up in multiple mutation buffer but shouldn't be handled twice.
|
||||
*/
|
||||
export default class ProcessedNodeManager {
|
||||
private nodeMap: WeakMap<Node, Set<MutationBuffer>> = new WeakMap();
|
||||
|
||||
constructor() {
|
||||
this.periodicallyClear();
|
||||
}
|
||||
|
||||
private periodicallyClear() {
|
||||
requestAnimationFrame(() => {
|
||||
this.clear();
|
||||
this.periodicallyClear();
|
||||
});
|
||||
}
|
||||
|
||||
public inOtherBuffer(node: Node, thisBuffer: MutationBuffer) {
|
||||
const buffers = this.nodeMap.get(node);
|
||||
return (
|
||||
buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer)
|
||||
);
|
||||
}
|
||||
|
||||
public add(node: Node, buffer: MutationBuffer) {
|
||||
this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer));
|
||||
}
|
||||
|
||||
private clear() {
|
||||
this.nodeMap = new WeakMap();
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
initScrollObserver,
|
||||
initAdoptedStyleSheetObserver,
|
||||
} from './observer';
|
||||
import { patch } from '../utils';
|
||||
import { patch, inDom } from '../utils';
|
||||
import type { Mirror } from 'rrweb-snapshot';
|
||||
import { isNativeShadowDom } from 'rrweb-snapshot';
|
||||
|
||||
@@ -49,7 +49,11 @@ export class ShadowDomManager {
|
||||
function (original: (init: ShadowRootInit) => ShadowRoot) {
|
||||
return function (this: HTMLElement, option: ShadowRootInit) {
|
||||
const shadowRoot = original.call(this, option);
|
||||
if (this.shadowRoot)
|
||||
|
||||
// For the shadow dom elements in the document, monitor their dom mutations.
|
||||
// For shadow dom elements that aren't in the document yet,
|
||||
// we start monitoring them once their shadow dom host is appended to the document.
|
||||
if (this.shadowRoot && inDom(this))
|
||||
manager.addShadowRoot(this.shadowRoot, this.ownerDocument);
|
||||
return shadowRoot;
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ import type {
|
||||
styleSheetRuleCallback,
|
||||
viewportResizeCallback,
|
||||
} from '@rrweb/types';
|
||||
import type ProcessedNodeManager from './record/processed-node-manager';
|
||||
|
||||
export type recordOptions<T> = {
|
||||
emit?: (e: T, isCheckout?: boolean) => void;
|
||||
@@ -105,6 +106,7 @@ export type observerParam = {
|
||||
stylesheetManager: StylesheetManager;
|
||||
shadowDomManager: ShadowDomManager;
|
||||
canvasManager: CanvasManager;
|
||||
processedNodeManager: ProcessedNodeManager;
|
||||
ignoreCSSAttributes: Set<string>;
|
||||
plugins: Array<{
|
||||
observer: (
|
||||
@@ -139,6 +141,7 @@ export type MutationBufferParam = Pick<
|
||||
| 'stylesheetManager'
|
||||
| 'shadowDomManager'
|
||||
| 'canvasManager'
|
||||
| 'processedNodeManager'
|
||||
>;
|
||||
|
||||
export type ReplayPlugin = {
|
||||
|
||||
@@ -518,3 +518,30 @@ export class StyleSheetMirror {
|
||||
return this.id++;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
rootShadowHost = (rootShadowHost.getRootNode() as ShadowRoot).host;
|
||||
|
||||
return rootShadowHost;
|
||||
}
|
||||
|
||||
export function shadowHostInDom(n: Node): boolean {
|
||||
const doc = n.ownerDocument;
|
||||
if (!doc) return false;
|
||||
const shadowHost = getRootShadowHost(n);
|
||||
return Boolean(shadowHost && doc.contains(shadowHost));
|
||||
}
|
||||
|
||||
export function inDom(n: Node): boolean {
|
||||
const doc = n.ownerDocument;
|
||||
if (!doc) return false;
|
||||
return doc.contains(n) || shadowHostInDom(n);
|
||||
}
|
||||
|
||||
@@ -11514,6 +11514,358 @@ exports[`record integration tests should record input userTriggered values if us
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record integration tests should record moved 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\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 3
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"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 \\\\n \\\\n\\\\n\\",
|
||||
\\"id\\": 8
|
||||
}
|
||||
],
|
||||
\\"id\\": 4
|
||||
}
|
||||
],
|
||||
\\"id\\": 2
|
||||
}
|
||||
],
|
||||
\\"compatMode\\": \\"BackCompat\\",
|
||||
\\"id\\": 1
|
||||
},
|
||||
\\"initialOffset\\": {
|
||||
\\"left\\": 0,
|
||||
\\"top\\": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 4,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 9,
|
||||
\\"isShadowHost\\": true
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 9,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"input\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 10,
|
||||
\\"isShadow\\": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [
|
||||
{
|
||||
\\"parentId\\": 4,
|
||||
\\"id\\": 9
|
||||
}
|
||||
],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 9,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"input\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 10,
|
||||
\\"isShadow\\": true
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 4,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 11
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 11,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 9,
|
||||
\\"isShadowHost\\": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record integration tests should record moved shadow DOM 2 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\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 3
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"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 \\\\n \\\\n\\\\n\\",
|
||||
\\"id\\": 8
|
||||
}
|
||||
],
|
||||
\\"id\\": 4
|
||||
}
|
||||
],
|
||||
\\"id\\": 2
|
||||
}
|
||||
],
|
||||
\\"compatMode\\": \\"BackCompat\\",
|
||||
\\"id\\": 1
|
||||
},
|
||||
\\"initialOffset\\": {
|
||||
\\"left\\": 0,
|
||||
\\"top\\": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 4,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {
|
||||
\\"id\\": \\"newEl\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 9
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 9,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {
|
||||
\\"id\\": \\"el\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 10,
|
||||
\\"isShadowHost\\": true
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 10,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"p\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 11,
|
||||
\\"isShadow\\": true
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 10,
|
||||
\\"nextId\\": 11,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"span\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 12,
|
||||
\\"isShadow\\": true
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 12,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"input\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 13
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [
|
||||
{
|
||||
\\"parentId\\": 12,
|
||||
\\"id\\": 13
|
||||
}
|
||||
],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 11,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"input\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 13
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record integration tests should record mutations in iframes accross pages 1`] = `
|
||||
"[
|
||||
{
|
||||
@@ -12620,6 +12972,242 @@ exports[`record integration tests should record shadow DOM 1`] = `
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record integration tests should record shadow DOM 2 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\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 3
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"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 \\\\n \\\\n\\\\n\\",
|
||||
\\"id\\": 8
|
||||
}
|
||||
],
|
||||
\\"id\\": 4
|
||||
}
|
||||
],
|
||||
\\"id\\": 2
|
||||
}
|
||||
],
|
||||
\\"compatMode\\": \\"BackCompat\\",
|
||||
\\"id\\": 1
|
||||
},
|
||||
\\"initialOffset\\": {
|
||||
\\"left\\": 0,
|
||||
\\"top\\": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 4,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 9,
|
||||
\\"isShadowHost\\": true
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 9,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"input\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 10,
|
||||
\\"isShadow\\": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record integration tests should record shadow DOM 3 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\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 3
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"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 \\\\n \\\\n\\\\n\\",
|
||||
\\"id\\": 8
|
||||
}
|
||||
],
|
||||
\\"id\\": 4
|
||||
}
|
||||
],
|
||||
\\"id\\": 2
|
||||
}
|
||||
],
|
||||
\\"compatMode\\": \\"BackCompat\\",
|
||||
\\"id\\": 1
|
||||
},
|
||||
\\"initialOffset\\": {
|
||||
\\"left\\": 0,
|
||||
\\"top\\": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 4,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 9,
|
||||
\\"isShadowHost\\": true
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 9,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"input\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 10,
|
||||
\\"isShadow\\": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record integration tests should record shadow doms polyfilled by shadydom 1`] = `
|
||||
"[
|
||||
{
|
||||
@@ -13121,18 +13709,7 @@ exports[`record integration tests should record shadow doms polyfilled by synthe
|
||||
\\"id\\": 31,
|
||||
\\"isShadowHost\\": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 31,
|
||||
\\"nextId\\": null,
|
||||
|
||||
3
packages/rrweb/test/html/blank.html
Normal file
3
packages/rrweb/test/html/blank.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<html>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -667,6 +667,119 @@ describe('record integration tests', function (this: ISuite) {
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('should record shadow DOM 2', async () => {
|
||||
const page: puppeteer.Page = await browser.newPage();
|
||||
await page.goto('about:blank');
|
||||
await page.setContent(getHtml.call(this, 'blank.html'));
|
||||
await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
const el = document.createElement('div') as HTMLDivElement;
|
||||
el.attachShadow({ mode: 'open' });
|
||||
(el.shadowRoot as ShadowRoot).appendChild(
|
||||
document.createElement('input'),
|
||||
);
|
||||
setTimeout(() => {
|
||||
document.body.append(el);
|
||||
resolve(null);
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
await waitForRAF(page);
|
||||
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('should record shadow DOM 3', async () => {
|
||||
const page: puppeteer.Page = await browser.newPage();
|
||||
await page.goto('about:blank');
|
||||
await page.setContent(getHtml.call(this, 'blank.html'));
|
||||
|
||||
await page.evaluate(() => {
|
||||
const el = document.createElement('div') as HTMLDivElement;
|
||||
el.attachShadow({ mode: 'open' });
|
||||
(el.shadowRoot as ShadowRoot).appendChild(
|
||||
document.createElement('input'),
|
||||
);
|
||||
document.body.append(el);
|
||||
});
|
||||
await waitForRAF(page);
|
||||
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('should record moved shadow DOM', async () => {
|
||||
const page: puppeteer.Page = await browser.newPage();
|
||||
await page.goto('about:blank');
|
||||
await page.setContent(getHtml.call(this, 'blank.html'));
|
||||
|
||||
await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
const el = document.createElement('div') as HTMLDivElement;
|
||||
el.attachShadow({ mode: 'open' });
|
||||
(el.shadowRoot as ShadowRoot).appendChild(
|
||||
document.createElement('input'),
|
||||
);
|
||||
document.body.append(el);
|
||||
setTimeout(() => {
|
||||
const newEl = document.createElement('div') as HTMLDivElement;
|
||||
document.body.append(newEl);
|
||||
newEl.append(el);
|
||||
resolve(null);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
await waitForRAF(page);
|
||||
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('should record moved shadow DOM 2', async () => {
|
||||
const page: puppeteer.Page = await browser.newPage();
|
||||
await page.goto('about:blank');
|
||||
await page.setContent(getHtml.call(this, 'blank.html'));
|
||||
|
||||
await page.evaluate(() => {
|
||||
const el = document.createElement('div') as HTMLDivElement;
|
||||
el.id = 'el';
|
||||
el.attachShadow({ mode: 'open' });
|
||||
(el.shadowRoot as ShadowRoot).appendChild(
|
||||
document.createElement('input'),
|
||||
);
|
||||
document.body.append(el);
|
||||
(el.shadowRoot as ShadowRoot).appendChild(document.createElement('span'));
|
||||
(el.shadowRoot as ShadowRoot).appendChild(document.createElement('p'));
|
||||
const newEl = document.createElement('div') as HTMLDivElement;
|
||||
newEl.id = 'newEl';
|
||||
document.body.append(newEl);
|
||||
newEl.append(el);
|
||||
const input = el.shadowRoot?.children[0] as HTMLInputElement;
|
||||
const span = el.shadowRoot?.children[1] as HTMLSpanElement;
|
||||
const p = el.shadowRoot?.children[2] as HTMLParagraphElement;
|
||||
input.remove();
|
||||
span.append(input);
|
||||
p.append(input);
|
||||
span.append(input);
|
||||
setTimeout(() => {
|
||||
p.append(input);
|
||||
}, 0);
|
||||
});
|
||||
await waitForRAF(page);
|
||||
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('should record nested iframes and shadow doms', async () => {
|
||||
const page: puppeteer.Page = await browser.newPage();
|
||||
await page.goto('about:blank');
|
||||
|
||||
Reference in New Issue
Block a user