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 () {
|
describe('add hover class to hover selector related rules', function () {
|
||||||
it('will do nothing to css text without :hover', () => {
|
it('will do nothing to css text without :hover', () => {
|
||||||
const cssText = 'body { color: white }';
|
const cssText = 'body { color: white }';
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import {
|
|||||||
SlimDOMOptions,
|
SlimDOMOptions,
|
||||||
createMirror,
|
createMirror,
|
||||||
} from 'rrweb-snapshot';
|
} from 'rrweb-snapshot';
|
||||||
import { initObservers, mutationBuffers } from './observer';
|
import {
|
||||||
|
initObservers,
|
||||||
|
mutationBuffers,
|
||||||
|
processedNodeManager,
|
||||||
|
} from './observer';
|
||||||
import {
|
import {
|
||||||
on,
|
on,
|
||||||
getWindowWidth,
|
getWindowWidth,
|
||||||
@@ -316,6 +320,7 @@ function record<T = eventWithTime>(
|
|||||||
stylesheetManager,
|
stylesheetManager,
|
||||||
canvasManager,
|
canvasManager,
|
||||||
keepIframeSrcFn,
|
keepIframeSrcFn,
|
||||||
|
processedNodeManager,
|
||||||
},
|
},
|
||||||
mirror,
|
mirror,
|
||||||
});
|
});
|
||||||
@@ -528,6 +533,7 @@ function record<T = eventWithTime>(
|
|||||||
iframeManager,
|
iframeManager,
|
||||||
stylesheetManager,
|
stylesheetManager,
|
||||||
shadowDomManager,
|
shadowDomManager,
|
||||||
|
processedNodeManager,
|
||||||
canvasManager,
|
canvasManager,
|
||||||
ignoreCSSAttributes,
|
ignoreCSSAttributes,
|
||||||
plugins:
|
plugins:
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
hasShadowRoot,
|
hasShadowRoot,
|
||||||
isSerializedIframe,
|
isSerializedIframe,
|
||||||
isSerializedStylesheet,
|
isSerializedStylesheet,
|
||||||
|
inDom,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
type DoubleLinkedListNode = {
|
type DoubleLinkedListNode = {
|
||||||
@@ -175,6 +176,7 @@ export default class MutationBuffer {
|
|||||||
private stylesheetManager: observerParam['stylesheetManager'];
|
private stylesheetManager: observerParam['stylesheetManager'];
|
||||||
private shadowDomManager: observerParam['shadowDomManager'];
|
private shadowDomManager: observerParam['shadowDomManager'];
|
||||||
private canvasManager: observerParam['canvasManager'];
|
private canvasManager: observerParam['canvasManager'];
|
||||||
|
private processedNodeManager: observerParam['processedNodeManager'];
|
||||||
|
|
||||||
public init(options: MutationBufferParam) {
|
public init(options: MutationBufferParam) {
|
||||||
([
|
([
|
||||||
@@ -198,6 +200,7 @@ export default class MutationBuffer {
|
|||||||
'stylesheetManager',
|
'stylesheetManager',
|
||||||
'shadowDomManager',
|
'shadowDomManager',
|
||||||
'canvasManager',
|
'canvasManager',
|
||||||
|
'processedNodeManager',
|
||||||
] as const).forEach((key) => {
|
] as const).forEach((key) => {
|
||||||
// just a type trick, the runtime result is correct
|
// just a type trick, the runtime result is correct
|
||||||
this[key] = options[key] as never;
|
this[key] = options[key] as never;
|
||||||
@@ -271,19 +274,8 @@ export default class MutationBuffer {
|
|||||||
(n.getRootNode() as ShadowRoot).host
|
(n.getRootNode() as ShadowRoot).host
|
||||||
)
|
)
|
||||||
shadowHost = (n.getRootNode() as ShadowRoot).host;
|
shadowHost = (n.getRootNode() as ShadowRoot).host;
|
||||||
// If n is in a nested shadow dom.
|
|
||||||
let rootShadowHost = shadowHost;
|
if (!n.parentNode || !inDom(n)) {
|
||||||
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) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parentId = isShadowRoot(n.parentNode)
|
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
|
* Make sure you check if `n`'s parent is blocked before calling this function
|
||||||
* */
|
* */
|
||||||
private genAdds = (n: Node, target?: Node) => {
|
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 (this.mirror.hasNode(n)) {
|
||||||
if (isIgnored(n, this.mirror)) {
|
if (isIgnored(n, this.mirror)) {
|
||||||
return;
|
return;
|
||||||
@@ -666,8 +661,15 @@ export default class MutationBuffer {
|
|||||||
|
|
||||||
// if this node is blocked `serializeNode` will turn it into a placeholder element
|
// 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
|
// 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));
|
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,
|
selectionCallback,
|
||||||
} from '@rrweb/types';
|
} from '@rrweb/types';
|
||||||
import MutationBuffer from './mutation';
|
import MutationBuffer from './mutation';
|
||||||
|
import ProcessedNodeManager from './processed-node-manager';
|
||||||
|
|
||||||
type WindowWithStoredMutationObserver = IWindow & {
|
type WindowWithStoredMutationObserver = IWindow & {
|
||||||
__rrMutationObserver?: MutationObserver;
|
__rrMutationObserver?: MutationObserver;
|
||||||
@@ -51,6 +52,7 @@ type WindowWithAngularZone = IWindow & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const mutationBuffers: MutationBuffer[] = [];
|
export const mutationBuffers: MutationBuffer[] = [];
|
||||||
|
export const processedNodeManager = new ProcessedNodeManager();
|
||||||
|
|
||||||
const isCSSGroupingRuleSupported = typeof CSSGroupingRule !== 'undefined';
|
const isCSSGroupingRuleSupported = typeof CSSGroupingRule !== 'undefined';
|
||||||
const isCSSMediaRuleSupported = typeof CSSMediaRule !== '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,
|
initScrollObserver,
|
||||||
initAdoptedStyleSheetObserver,
|
initAdoptedStyleSheetObserver,
|
||||||
} from './observer';
|
} from './observer';
|
||||||
import { patch } from '../utils';
|
import { patch, inDom } from '../utils';
|
||||||
import type { Mirror } from 'rrweb-snapshot';
|
import type { Mirror } from 'rrweb-snapshot';
|
||||||
import { isNativeShadowDom } from 'rrweb-snapshot';
|
import { isNativeShadowDom } from 'rrweb-snapshot';
|
||||||
|
|
||||||
@@ -49,7 +49,11 @@ export class ShadowDomManager {
|
|||||||
function (original: (init: ShadowRootInit) => ShadowRoot) {
|
function (original: (init: ShadowRootInit) => ShadowRoot) {
|
||||||
return function (this: HTMLElement, option: ShadowRootInit) {
|
return function (this: HTMLElement, option: ShadowRootInit) {
|
||||||
const shadowRoot = original.call(this, option);
|
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);
|
manager.addShadowRoot(this.shadowRoot, this.ownerDocument);
|
||||||
return shadowRoot;
|
return shadowRoot;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import type {
|
|||||||
styleSheetRuleCallback,
|
styleSheetRuleCallback,
|
||||||
viewportResizeCallback,
|
viewportResizeCallback,
|
||||||
} from '@rrweb/types';
|
} from '@rrweb/types';
|
||||||
|
import type ProcessedNodeManager from './record/processed-node-manager';
|
||||||
|
|
||||||
export type recordOptions<T> = {
|
export type recordOptions<T> = {
|
||||||
emit?: (e: T, isCheckout?: boolean) => void;
|
emit?: (e: T, isCheckout?: boolean) => void;
|
||||||
@@ -105,6 +106,7 @@ export type observerParam = {
|
|||||||
stylesheetManager: StylesheetManager;
|
stylesheetManager: StylesheetManager;
|
||||||
shadowDomManager: ShadowDomManager;
|
shadowDomManager: ShadowDomManager;
|
||||||
canvasManager: CanvasManager;
|
canvasManager: CanvasManager;
|
||||||
|
processedNodeManager: ProcessedNodeManager;
|
||||||
ignoreCSSAttributes: Set<string>;
|
ignoreCSSAttributes: Set<string>;
|
||||||
plugins: Array<{
|
plugins: Array<{
|
||||||
observer: (
|
observer: (
|
||||||
@@ -139,6 +141,7 @@ export type MutationBufferParam = Pick<
|
|||||||
| 'stylesheetManager'
|
| 'stylesheetManager'
|
||||||
| 'shadowDomManager'
|
| 'shadowDomManager'
|
||||||
| 'canvasManager'
|
| 'canvasManager'
|
||||||
|
| 'processedNodeManager'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type ReplayPlugin = {
|
export type ReplayPlugin = {
|
||||||
|
|||||||
@@ -518,3 +518,30 @@ export class StyleSheetMirror {
|
|||||||
return this.id++;
|
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`] = `
|
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`] = `
|
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,
|
\\"id\\": 31,
|
||||||
\\"isShadowHost\\": true
|
\\"isShadowHost\\": true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\\"type\\": 3,
|
|
||||||
\\"data\\": {
|
|
||||||
\\"source\\": 0,
|
|
||||||
\\"texts\\": [],
|
|
||||||
\\"attributes\\": [],
|
|
||||||
\\"removes\\": [],
|
|
||||||
\\"adds\\": [
|
|
||||||
{
|
{
|
||||||
\\"parentId\\": 31,
|
\\"parentId\\": 31,
|
||||||
\\"nextId\\": null,
|
\\"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);
|
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 () => {
|
it('should record nested iframes and shadow doms', async () => {
|
||||||
const page: puppeteer.Page = await browser.newPage();
|
const page: puppeteer.Page = await browser.newPage();
|
||||||
await page.goto('about:blank');
|
await page.goto('about:blank');
|
||||||
|
|||||||
Reference in New Issue
Block a user