Inline stylesheets on load (#909)
* inline stylesheets when loaded * set empty link elements to loaded by default * Clean up stylesheet manager * Remove attribute mutation code * Update packages/rrweb/test/record.test.ts * Update packages/rrweb/test/record.test.ts * Update packages/rrweb/test/record.test.ts * Update packages/rrweb/scripts/repl.js * Update packages/rrweb/test/record.test.ts * Update packages/rrweb/src/record/index.ts * Add todo * Move require out of time sensitive assert * Add waitForRAF, its more reliable than waitForTimeout * Remove flaky tests * Add recording stylesheets in iframes * Remove variability from flaky test * Make test more robust * Fix naming
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
polyfillNode,
|
polyfillNode,
|
||||||
polyfillDocument,
|
polyfillDocument,
|
||||||
} from '../src/polyfill';
|
} from '../src/polyfill';
|
||||||
|
import { performance as nativePerformance } from 'perf_hooks';
|
||||||
|
|
||||||
describe('polyfill for nodejs', () => {
|
describe('polyfill for nodejs', () => {
|
||||||
it('should polyfill performance api', () => {
|
it('should polyfill performance api', () => {
|
||||||
@@ -16,10 +17,7 @@ describe('polyfill for nodejs', () => {
|
|||||||
expect(global.performance).toBeDefined();
|
expect(global.performance).toBeDefined();
|
||||||
expect(performance).toBeDefined();
|
expect(performance).toBeDefined();
|
||||||
expect(performance.now).toBeDefined();
|
expect(performance.now).toBeDefined();
|
||||||
expect(performance.now()).toBeCloseTo(
|
expect(performance.now()).toBeCloseTo(nativePerformance.now(), 1e-10);
|
||||||
require('perf_hooks').performance.now(),
|
|
||||||
1e-10,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not polyfill performance if it already exists', () => {
|
it('should not polyfill performance if it already exists', () => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
MaskInputFn,
|
MaskInputFn,
|
||||||
KeepIframeSrcFn,
|
KeepIframeSrcFn,
|
||||||
ICanvas,
|
ICanvas,
|
||||||
|
serializedElementNodeWithId,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {
|
import {
|
||||||
Mirror,
|
Mirror,
|
||||||
@@ -377,6 +378,40 @@ function onceIframeLoaded(
|
|||||||
iframeEl.addEventListener('load', listener);
|
iframeEl.addEventListener('load', listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStylesheetLoaded(link: HTMLLinkElement) {
|
||||||
|
if (!link.getAttribute('href')) return true; // nothing to load
|
||||||
|
return link.sheet !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onceStylesheetLoaded(
|
||||||
|
link: HTMLLinkElement,
|
||||||
|
listener: () => unknown,
|
||||||
|
styleSheetLoadTimeout: number,
|
||||||
|
) {
|
||||||
|
let fired = false;
|
||||||
|
let styleSheetLoaded: StyleSheet | null;
|
||||||
|
try {
|
||||||
|
styleSheetLoaded = link.sheet;
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (styleSheetLoaded) return;
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (!fired) {
|
||||||
|
listener();
|
||||||
|
fired = true;
|
||||||
|
}
|
||||||
|
}, styleSheetLoadTimeout);
|
||||||
|
|
||||||
|
link.addEventListener('load', () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
fired = true;
|
||||||
|
listener();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function serializeNode(
|
function serializeNode(
|
||||||
n: Node,
|
n: Node,
|
||||||
options: {
|
options: {
|
||||||
@@ -876,6 +911,7 @@ export function serializeNodeWithId(
|
|||||||
maskTextSelector: string | null;
|
maskTextSelector: string | null;
|
||||||
skipChild: boolean;
|
skipChild: boolean;
|
||||||
inlineStylesheet: boolean;
|
inlineStylesheet: boolean;
|
||||||
|
newlyAddedElement?: boolean;
|
||||||
maskInputOptions?: MaskInputOptions;
|
maskInputOptions?: MaskInputOptions;
|
||||||
maskTextFn: MaskTextFn | undefined;
|
maskTextFn: MaskTextFn | undefined;
|
||||||
maskInputFn: MaskInputFn | undefined;
|
maskInputFn: MaskInputFn | undefined;
|
||||||
@@ -888,10 +924,14 @@ export function serializeNodeWithId(
|
|||||||
onSerialize?: (n: Node) => unknown;
|
onSerialize?: (n: Node) => unknown;
|
||||||
onIframeLoad?: (
|
onIframeLoad?: (
|
||||||
iframeNode: HTMLIFrameElement,
|
iframeNode: HTMLIFrameElement,
|
||||||
node: serializedNodeWithId,
|
node: serializedElementNodeWithId,
|
||||||
) => unknown;
|
) => unknown;
|
||||||
iframeLoadTimeout?: number;
|
iframeLoadTimeout?: number;
|
||||||
newlyAddedElement?: boolean;
|
onStylesheetLoad?: (
|
||||||
|
linkNode: HTMLLinkElement,
|
||||||
|
node: serializedElementNodeWithId,
|
||||||
|
) => unknown;
|
||||||
|
stylesheetLoadTimeout?: number;
|
||||||
},
|
},
|
||||||
): serializedNodeWithId | null {
|
): serializedNodeWithId | null {
|
||||||
const {
|
const {
|
||||||
@@ -913,6 +953,8 @@ export function serializeNodeWithId(
|
|||||||
onSerialize,
|
onSerialize,
|
||||||
onIframeLoad,
|
onIframeLoad,
|
||||||
iframeLoadTimeout = 5000,
|
iframeLoadTimeout = 5000,
|
||||||
|
onStylesheetLoad,
|
||||||
|
stylesheetLoadTimeout = 5000,
|
||||||
keepIframeSrcFn = () => false,
|
keepIframeSrcFn = () => false,
|
||||||
newlyAddedElement = false,
|
newlyAddedElement = false,
|
||||||
} = options;
|
} = options;
|
||||||
@@ -1006,6 +1048,8 @@ export function serializeNodeWithId(
|
|||||||
onSerialize,
|
onSerialize,
|
||||||
onIframeLoad,
|
onIframeLoad,
|
||||||
iframeLoadTimeout,
|
iframeLoadTimeout,
|
||||||
|
onStylesheetLoad,
|
||||||
|
stylesheetLoadTimeout,
|
||||||
keepIframeSrcFn,
|
keepIframeSrcFn,
|
||||||
};
|
};
|
||||||
for (const childN of Array.from(n.childNodes)) {
|
for (const childN of Array.from(n.childNodes)) {
|
||||||
@@ -1059,11 +1103,16 @@ export function serializeNodeWithId(
|
|||||||
onSerialize,
|
onSerialize,
|
||||||
onIframeLoad,
|
onIframeLoad,
|
||||||
iframeLoadTimeout,
|
iframeLoadTimeout,
|
||||||
|
onStylesheetLoad,
|
||||||
|
stylesheetLoadTimeout,
|
||||||
keepIframeSrcFn,
|
keepIframeSrcFn,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (serializedIframeNode) {
|
if (serializedIframeNode) {
|
||||||
onIframeLoad(n as HTMLIFrameElement, serializedIframeNode);
|
onIframeLoad(
|
||||||
|
n as HTMLIFrameElement,
|
||||||
|
serializedIframeNode as serializedElementNodeWithId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1071,6 +1120,54 @@ export function serializeNodeWithId(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// <link rel=stylesheet href=...>
|
||||||
|
if (
|
||||||
|
serializedNode.type === NodeType.Element &&
|
||||||
|
serializedNode.tagName === 'link' &&
|
||||||
|
serializedNode.attributes.rel === 'stylesheet'
|
||||||
|
) {
|
||||||
|
onceStylesheetLoaded(
|
||||||
|
n as HTMLLinkElement,
|
||||||
|
() => {
|
||||||
|
if (onStylesheetLoad) {
|
||||||
|
const serializedLinkNode = serializeNodeWithId(n, {
|
||||||
|
doc,
|
||||||
|
mirror,
|
||||||
|
blockClass,
|
||||||
|
blockSelector,
|
||||||
|
maskTextClass,
|
||||||
|
maskTextSelector,
|
||||||
|
skipChild: false,
|
||||||
|
inlineStylesheet,
|
||||||
|
maskInputOptions,
|
||||||
|
maskTextFn,
|
||||||
|
maskInputFn,
|
||||||
|
slimDOMOptions,
|
||||||
|
dataURLOptions,
|
||||||
|
inlineImages,
|
||||||
|
recordCanvas,
|
||||||
|
preserveWhiteSpace,
|
||||||
|
onSerialize,
|
||||||
|
onIframeLoad,
|
||||||
|
iframeLoadTimeout,
|
||||||
|
onStylesheetLoad,
|
||||||
|
stylesheetLoadTimeout,
|
||||||
|
keepIframeSrcFn,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (serializedLinkNode) {
|
||||||
|
onStylesheetLoad(
|
||||||
|
n as HTMLLinkElement,
|
||||||
|
serializedLinkNode as serializedElementNodeWithId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stylesheetLoadTimeout,
|
||||||
|
);
|
||||||
|
if (isStylesheetLoaded(n as HTMLLinkElement) === false) return null; // add stylesheet in later mutation
|
||||||
|
}
|
||||||
|
|
||||||
return serializedNode;
|
return serializedNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1094,9 +1191,14 @@ function snapshot(
|
|||||||
onSerialize?: (n: Node) => unknown;
|
onSerialize?: (n: Node) => unknown;
|
||||||
onIframeLoad?: (
|
onIframeLoad?: (
|
||||||
iframeNode: HTMLIFrameElement,
|
iframeNode: HTMLIFrameElement,
|
||||||
node: serializedNodeWithId,
|
node: serializedElementNodeWithId,
|
||||||
) => unknown;
|
) => unknown;
|
||||||
iframeLoadTimeout?: number;
|
iframeLoadTimeout?: number;
|
||||||
|
onStylesheetLoad?: (
|
||||||
|
linkNode: HTMLLinkElement,
|
||||||
|
node: serializedElementNodeWithId,
|
||||||
|
) => unknown;
|
||||||
|
stylesheetLoadTimeout?: number;
|
||||||
keepIframeSrcFn?: KeepIframeSrcFn;
|
keepIframeSrcFn?: KeepIframeSrcFn;
|
||||||
},
|
},
|
||||||
): serializedNodeWithId | null {
|
): serializedNodeWithId | null {
|
||||||
@@ -1118,6 +1220,8 @@ function snapshot(
|
|||||||
onSerialize,
|
onSerialize,
|
||||||
onIframeLoad,
|
onIframeLoad,
|
||||||
iframeLoadTimeout,
|
iframeLoadTimeout,
|
||||||
|
onStylesheetLoad,
|
||||||
|
stylesheetLoadTimeout,
|
||||||
keepIframeSrcFn = () => false,
|
keepIframeSrcFn = () => false,
|
||||||
} = options || {};
|
} = options || {};
|
||||||
const maskInputOptions: MaskInputOptions =
|
const maskInputOptions: MaskInputOptions =
|
||||||
@@ -1183,6 +1287,8 @@ function snapshot(
|
|||||||
onSerialize,
|
onSerialize,
|
||||||
onIframeLoad,
|
onIframeLoad,
|
||||||
iframeLoadTimeout,
|
iframeLoadTimeout,
|
||||||
|
onStylesheetLoad,
|
||||||
|
stylesheetLoadTimeout,
|
||||||
keepIframeSrcFn,
|
keepIframeSrcFn,
|
||||||
newlyAddedElement: false,
|
newlyAddedElement: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ export type serializedNode = (
|
|||||||
|
|
||||||
export type serializedNodeWithId = serializedNode & { id: number };
|
export type serializedNodeWithId = serializedNode & { id: number };
|
||||||
|
|
||||||
|
export type serializedElementNodeWithId = Extract<
|
||||||
|
serializedNodeWithId,
|
||||||
|
Record<'type', NodeType.Element>
|
||||||
|
>;
|
||||||
|
|
||||||
export type tagMap = {
|
export type tagMap = {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
|||||||
12
packages/rrweb-snapshot/typings/snapshot.d.ts
vendored
12
packages/rrweb-snapshot/typings/snapshot.d.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
import { serializedNodeWithId, MaskInputOptions, SlimDOMOptions, DataURLOptions, MaskTextFn, MaskInputFn, KeepIframeSrcFn } from './types';
|
import { serializedNodeWithId, MaskInputOptions, SlimDOMOptions, DataURLOptions, MaskTextFn, MaskInputFn, KeepIframeSrcFn, serializedElementNodeWithId } from './types';
|
||||||
import { Mirror } from './utils';
|
import { Mirror } from './utils';
|
||||||
export declare const IGNORED_NODE = -2;
|
export declare const IGNORED_NODE = -2;
|
||||||
export declare function absoluteToStylesheet(cssText: string | null, href: string): string;
|
export declare function absoluteToStylesheet(cssText: string | null, href: string): string;
|
||||||
@@ -16,6 +16,7 @@ export declare function serializeNodeWithId(n: Node, options: {
|
|||||||
maskTextSelector: string | null;
|
maskTextSelector: string | null;
|
||||||
skipChild: boolean;
|
skipChild: boolean;
|
||||||
inlineStylesheet: boolean;
|
inlineStylesheet: boolean;
|
||||||
|
newlyAddedElement?: boolean;
|
||||||
maskInputOptions?: MaskInputOptions;
|
maskInputOptions?: MaskInputOptions;
|
||||||
maskTextFn: MaskTextFn | undefined;
|
maskTextFn: MaskTextFn | undefined;
|
||||||
maskInputFn: MaskInputFn | undefined;
|
maskInputFn: MaskInputFn | undefined;
|
||||||
@@ -26,9 +27,10 @@ export declare function serializeNodeWithId(n: Node, options: {
|
|||||||
recordCanvas?: boolean;
|
recordCanvas?: boolean;
|
||||||
preserveWhiteSpace?: boolean;
|
preserveWhiteSpace?: boolean;
|
||||||
onSerialize?: (n: Node) => unknown;
|
onSerialize?: (n: Node) => unknown;
|
||||||
onIframeLoad?: (iframeNode: HTMLIFrameElement, node: serializedNodeWithId) => unknown;
|
onIframeLoad?: (iframeNode: HTMLIFrameElement, node: serializedElementNodeWithId) => unknown;
|
||||||
iframeLoadTimeout?: number;
|
iframeLoadTimeout?: number;
|
||||||
newlyAddedElement?: boolean;
|
onStylesheetLoad?: (linkNode: HTMLLinkElement, node: serializedElementNodeWithId) => unknown;
|
||||||
|
stylesheetLoadTimeout?: number;
|
||||||
}): serializedNodeWithId | null;
|
}): serializedNodeWithId | null;
|
||||||
declare function snapshot(n: Document, options?: {
|
declare function snapshot(n: Document, options?: {
|
||||||
mirror?: Mirror;
|
mirror?: Mirror;
|
||||||
@@ -46,8 +48,10 @@ declare function snapshot(n: Document, options?: {
|
|||||||
recordCanvas?: boolean;
|
recordCanvas?: boolean;
|
||||||
preserveWhiteSpace?: boolean;
|
preserveWhiteSpace?: boolean;
|
||||||
onSerialize?: (n: Node) => unknown;
|
onSerialize?: (n: Node) => unknown;
|
||||||
onIframeLoad?: (iframeNode: HTMLIFrameElement, node: serializedNodeWithId) => unknown;
|
onIframeLoad?: (iframeNode: HTMLIFrameElement, node: serializedElementNodeWithId) => unknown;
|
||||||
iframeLoadTimeout?: number;
|
iframeLoadTimeout?: number;
|
||||||
|
onStylesheetLoad?: (linkNode: HTMLLinkElement, node: serializedElementNodeWithId) => unknown;
|
||||||
|
stylesheetLoadTimeout?: number;
|
||||||
keepIframeSrcFn?: KeepIframeSrcFn;
|
keepIframeSrcFn?: KeepIframeSrcFn;
|
||||||
}): serializedNodeWithId | null;
|
}): serializedNodeWithId | null;
|
||||||
export declare function visitSnapshot(node: serializedNodeWithId, onVisit: (node: serializedNodeWithId) => unknown): void;
|
export declare function visitSnapshot(node: serializedNodeWithId, onVisit: (node: serializedNodeWithId) => unknown): void;
|
||||||
|
|||||||
1
packages/rrweb-snapshot/typings/types.d.ts
vendored
1
packages/rrweb-snapshot/typings/types.d.ts
vendored
@@ -49,6 +49,7 @@ export declare type serializedNode = (documentNode | documentTypeNode | elementN
|
|||||||
export declare type serializedNodeWithId = serializedNode & {
|
export declare type serializedNodeWithId = serializedNode & {
|
||||||
id: number;
|
id: number;
|
||||||
};
|
};
|
||||||
|
export declare type serializedElementNodeWithId = Extract<serializedNodeWithId, Record<'type', NodeType.Element>>;
|
||||||
export declare type tagMap = {
|
export declare type tagMap = {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
polyfill,
|
polyfill,
|
||||||
hasShadowRoot,
|
hasShadowRoot,
|
||||||
isSerializedIframe,
|
isSerializedIframe,
|
||||||
|
isSerializedStylesheet,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import {
|
import {
|
||||||
EventType,
|
EventType,
|
||||||
@@ -27,6 +28,7 @@ import {
|
|||||||
import { IframeManager } from './iframe-manager';
|
import { IframeManager } from './iframe-manager';
|
||||||
import { ShadowDomManager } from './shadow-dom-manager';
|
import { ShadowDomManager } from './shadow-dom-manager';
|
||||||
import { CanvasManager } from './observers/canvas/canvas-manager';
|
import { CanvasManager } from './observers/canvas/canvas-manager';
|
||||||
|
import { StylesheetManager } from './stylesheet-manager';
|
||||||
|
|
||||||
function wrapEvent(e: event): eventWithTime {
|
function wrapEvent(e: event): eventWithTime {
|
||||||
return {
|
return {
|
||||||
@@ -215,6 +217,10 @@ function record<T = eventWithTime>(
|
|||||||
mutationCb: wrappedMutationEmit,
|
mutationCb: wrappedMutationEmit,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const stylesheetManager = new StylesheetManager({
|
||||||
|
mutationCb: wrappedMutationEmit,
|
||||||
|
});
|
||||||
|
|
||||||
const canvasManager = new CanvasManager({
|
const canvasManager = new CanvasManager({
|
||||||
recordCanvas,
|
recordCanvas,
|
||||||
mutationCb: wrappedCanvasMutationEmit,
|
mutationCb: wrappedCanvasMutationEmit,
|
||||||
@@ -241,6 +247,7 @@ function record<T = eventWithTime>(
|
|||||||
sampling,
|
sampling,
|
||||||
slimDOMOptions,
|
slimDOMOptions,
|
||||||
iframeManager,
|
iframeManager,
|
||||||
|
stylesheetManager,
|
||||||
canvasManager,
|
canvasManager,
|
||||||
},
|
},
|
||||||
mirror,
|
mirror,
|
||||||
@@ -276,6 +283,9 @@ function record<T = eventWithTime>(
|
|||||||
if (isSerializedIframe(n, mirror)) {
|
if (isSerializedIframe(n, mirror)) {
|
||||||
iframeManager.addIframe(n as HTMLIFrameElement);
|
iframeManager.addIframe(n as HTMLIFrameElement);
|
||||||
}
|
}
|
||||||
|
if (isSerializedStylesheet(n, mirror)) {
|
||||||
|
stylesheetManager.addStylesheet(n as HTMLLinkElement);
|
||||||
|
}
|
||||||
if (hasShadowRoot(n)) {
|
if (hasShadowRoot(n)) {
|
||||||
shadowDomManager.addShadowRoot(n.shadowRoot, document);
|
shadowDomManager.addShadowRoot(n.shadowRoot, document);
|
||||||
}
|
}
|
||||||
@@ -284,6 +294,9 @@ function record<T = eventWithTime>(
|
|||||||
iframeManager.attachIframe(iframe, childSn, mirror);
|
iframeManager.attachIframe(iframe, childSn, mirror);
|
||||||
shadowDomManager.observeAttachShadow(iframe);
|
shadowDomManager.observeAttachShadow(iframe);
|
||||||
},
|
},
|
||||||
|
onStylesheetLoad: (linkEl, childSn) => {
|
||||||
|
stylesheetManager.attachStylesheet(linkEl, childSn, mirror);
|
||||||
|
},
|
||||||
keepIframeSrcFn,
|
keepIframeSrcFn,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -435,6 +448,7 @@ function record<T = eventWithTime>(
|
|||||||
slimDOMOptions,
|
slimDOMOptions,
|
||||||
mirror,
|
mirror,
|
||||||
iframeManager,
|
iframeManager,
|
||||||
|
stylesheetManager,
|
||||||
shadowDomManager,
|
shadowDomManager,
|
||||||
canvasManager,
|
canvasManager,
|
||||||
plugins:
|
plugins:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
isSerialized,
|
isSerialized,
|
||||||
hasShadowRoot,
|
hasShadowRoot,
|
||||||
isSerializedIframe,
|
isSerializedIframe,
|
||||||
|
isSerializedStylesheet,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
type DoubleLinkedListNode = {
|
type DoubleLinkedListNode = {
|
||||||
@@ -169,6 +170,7 @@ export default class MutationBuffer {
|
|||||||
private doc: observerParam['doc'];
|
private doc: observerParam['doc'];
|
||||||
private mirror: observerParam['mirror'];
|
private mirror: observerParam['mirror'];
|
||||||
private iframeManager: observerParam['iframeManager'];
|
private iframeManager: observerParam['iframeManager'];
|
||||||
|
private stylesheetManager: observerParam['stylesheetManager'];
|
||||||
private shadowDomManager: observerParam['shadowDomManager'];
|
private shadowDomManager: observerParam['shadowDomManager'];
|
||||||
private canvasManager: observerParam['canvasManager'];
|
private canvasManager: observerParam['canvasManager'];
|
||||||
|
|
||||||
@@ -189,6 +191,7 @@ export default class MutationBuffer {
|
|||||||
'doc',
|
'doc',
|
||||||
'mirror',
|
'mirror',
|
||||||
'iframeManager',
|
'iframeManager',
|
||||||
|
'stylesheetManager',
|
||||||
'shadowDomManager',
|
'shadowDomManager',
|
||||||
'canvasManager',
|
'canvasManager',
|
||||||
] as const).forEach((key) => {
|
] as const).forEach((key) => {
|
||||||
@@ -289,6 +292,7 @@ export default class MutationBuffer {
|
|||||||
maskTextClass: this.maskTextClass,
|
maskTextClass: this.maskTextClass,
|
||||||
maskTextSelector: this.maskTextSelector,
|
maskTextSelector: this.maskTextSelector,
|
||||||
skipChild: true,
|
skipChild: true,
|
||||||
|
newlyAddedElement: true,
|
||||||
inlineStylesheet: this.inlineStylesheet,
|
inlineStylesheet: this.inlineStylesheet,
|
||||||
maskInputOptions: this.maskInputOptions,
|
maskInputOptions: this.maskInputOptions,
|
||||||
maskTextFn: this.maskTextFn,
|
maskTextFn: this.maskTextFn,
|
||||||
@@ -300,6 +304,9 @@ export default class MutationBuffer {
|
|||||||
if (isSerializedIframe(currentN, this.mirror)) {
|
if (isSerializedIframe(currentN, this.mirror)) {
|
||||||
this.iframeManager.addIframe(currentN as HTMLIFrameElement);
|
this.iframeManager.addIframe(currentN as HTMLIFrameElement);
|
||||||
}
|
}
|
||||||
|
if (isSerializedStylesheet(currentN, this.mirror)) {
|
||||||
|
this.stylesheetManager.addStylesheet(currentN as HTMLLinkElement);
|
||||||
|
}
|
||||||
if (hasShadowRoot(n)) {
|
if (hasShadowRoot(n)) {
|
||||||
this.shadowDomManager.addShadowRoot(n.shadowRoot, document);
|
this.shadowDomManager.addShadowRoot(n.shadowRoot, document);
|
||||||
}
|
}
|
||||||
@@ -308,7 +315,9 @@ export default class MutationBuffer {
|
|||||||
this.iframeManager.attachIframe(iframe, childSn, this.mirror);
|
this.iframeManager.attachIframe(iframe, childSn, this.mirror);
|
||||||
this.shadowDomManager.observeAttachShadow(iframe);
|
this.shadowDomManager.observeAttachShadow(iframe);
|
||||||
},
|
},
|
||||||
newlyAddedElement: true,
|
onStylesheetLoad: (link, childSn) => {
|
||||||
|
this.stylesheetManager.attachStylesheet(link, childSn, this.mirror);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (sn) {
|
if (sn) {
|
||||||
adds.push({
|
adds.push({
|
||||||
@@ -471,6 +480,7 @@ export default class MutationBuffer {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let item: attributeCursor | undefined = this.attributes.find(
|
let item: attributeCursor | undefined = this.attributes.find(
|
||||||
(a) => a.node === m.target,
|
(a) => a.node === m.target,
|
||||||
);
|
);
|
||||||
|
|||||||
45
packages/rrweb/src/record/stylesheet-manager.ts
Normal file
45
packages/rrweb/src/record/stylesheet-manager.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
|
||||||
|
import type { mutationCallBack } from '../types';
|
||||||
|
|
||||||
|
export class StylesheetManager {
|
||||||
|
private trackedStylesheets: WeakSet<HTMLLinkElement> = new WeakSet();
|
||||||
|
private mutationCb: mutationCallBack;
|
||||||
|
|
||||||
|
constructor(options: { mutationCb: mutationCallBack }) {
|
||||||
|
this.mutationCb = options.mutationCb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addStylesheet(linkEl: HTMLLinkElement) {
|
||||||
|
if (this.trackedStylesheets.has(linkEl)) return;
|
||||||
|
|
||||||
|
this.trackedStylesheets.add(linkEl);
|
||||||
|
this.trackStylesheet(linkEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: take snapshot on stylesheet reload by applying event listener
|
||||||
|
private trackStylesheet(linkEl: HTMLLinkElement) {
|
||||||
|
// linkEl.addEventListener('load', () => {
|
||||||
|
// // re-loaded, maybe take another snapshot?
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
public attachStylesheet(
|
||||||
|
linkEl: HTMLLinkElement,
|
||||||
|
childSn: serializedNodeWithId,
|
||||||
|
mirror: Mirror,
|
||||||
|
) {
|
||||||
|
this.mutationCb({
|
||||||
|
adds: [
|
||||||
|
{
|
||||||
|
parentId: mirror.getId(linkEl),
|
||||||
|
nextId: null,
|
||||||
|
node: childSn,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
removes: [],
|
||||||
|
texts: [],
|
||||||
|
attributes: [],
|
||||||
|
});
|
||||||
|
this.addStylesheet(linkEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import type { ShadowDomManager } from './record/shadow-dom-manager';
|
|||||||
import type { Replayer } from './replay';
|
import type { Replayer } from './replay';
|
||||||
import type { RRNode } from 'rrdom';
|
import type { RRNode } from 'rrdom';
|
||||||
import type { CanvasManager } from './record/observers/canvas/canvas-manager';
|
import type { CanvasManager } from './record/observers/canvas/canvas-manager';
|
||||||
|
import type { StylesheetManager } from './record/stylesheet-manager';
|
||||||
|
|
||||||
export enum EventType {
|
export enum EventType {
|
||||||
DomContentLoaded,
|
DomContentLoaded,
|
||||||
@@ -280,6 +281,7 @@ export type observerParam = {
|
|||||||
doc: Document;
|
doc: Document;
|
||||||
mirror: Mirror;
|
mirror: Mirror;
|
||||||
iframeManager: IframeManager;
|
iframeManager: IframeManager;
|
||||||
|
stylesheetManager: StylesheetManager;
|
||||||
shadowDomManager: ShadowDomManager;
|
shadowDomManager: ShadowDomManager;
|
||||||
canvasManager: CanvasManager;
|
canvasManager: CanvasManager;
|
||||||
plugins: Array<{
|
plugins: Array<{
|
||||||
@@ -306,6 +308,7 @@ export type MutationBufferParam = Pick<
|
|||||||
| 'doc'
|
| 'doc'
|
||||||
| 'mirror'
|
| 'mirror'
|
||||||
| 'iframeManager'
|
| 'iframeManager'
|
||||||
|
| 'stylesheetManager'
|
||||||
| 'shadowDomManager'
|
| 'shadowDomManager'
|
||||||
| 'canvasManager'
|
| 'canvasManager'
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -354,6 +354,19 @@ export function isSerializedIframe<TNode extends Node | RRNode>(
|
|||||||
return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n));
|
return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSerializedStylesheet<TNode extends Node | RRNode>(
|
||||||
|
n: TNode,
|
||||||
|
mirror: IMirror<TNode>,
|
||||||
|
): boolean {
|
||||||
|
return Boolean(
|
||||||
|
n.nodeName === 'LINK' &&
|
||||||
|
n.nodeType === n.ELEMENT_NODE &&
|
||||||
|
(n as HTMLElement).getAttribute &&
|
||||||
|
(n as HTMLElement).getAttribute('rel') === 'stylesheet' &&
|
||||||
|
mirror.getMeta(n),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getBaseDimension(
|
export function getBaseDimension(
|
||||||
node: Node,
|
node: Node,
|
||||||
rootIframe: Node,
|
rootIframe: Node,
|
||||||
|
|||||||
@@ -94,6 +94,109 @@ exports[`record can add custom event 1`] = `
|
|||||||
]"
|
]"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`record captures CORS stylesheets that are still loading 1`] = `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"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\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"head\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"body\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"input\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"type\\": \\"text\\",
|
||||||
|
\\"size\\": \\"40\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
|
||||||
|
\\"id\\": 8
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 1
|
||||||
|
},
|
||||||
|
\\"initialOffset\\": {
|
||||||
|
\\"left\\": 0,
|
||||||
|
\\"top\\": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 0,
|
||||||
|
\\"adds\\": [
|
||||||
|
{
|
||||||
|
\\"parentId\\": 9,
|
||||||
|
\\"nextId\\": null,
|
||||||
|
\\"node\\": {
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"link\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"rel\\": \\"stylesheet\\",
|
||||||
|
\\"href\\": \\"https://cdn.jsdelivr.net/npm/pure@2.85.0/index.css\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"removes\\": [],
|
||||||
|
\\"texts\\": [],
|
||||||
|
\\"attributes\\": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`record captures inserted style text nodes correctly 1`] = `
|
exports[`record captures inserted style text nodes correctly 1`] = `
|
||||||
"[
|
"[
|
||||||
{
|
{
|
||||||
@@ -640,6 +743,498 @@ exports[`record captures stylesheet rules 1`] = `
|
|||||||
]"
|
]"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`record captures stylesheets in iframes that are still loading 1`] = `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"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\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"head\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"body\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"input\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"type\\": \\"text\\",
|
||||||
|
\\"size\\": \\"40\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
|
||||||
|
\\"id\\": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"iframe\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 9
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 1
|
||||||
|
},
|
||||||
|
\\"initialOffset\\": {
|
||||||
|
\\"left\\": 0,
|
||||||
|
\\"top\\": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 0,
|
||||||
|
\\"adds\\": [
|
||||||
|
{
|
||||||
|
\\"parentId\\": 9,
|
||||||
|
\\"nextId\\": null,
|
||||||
|
\\"node\\": {
|
||||||
|
\\"type\\": 0,
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"html\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"head\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"rootId\\": 10,
|
||||||
|
\\"id\\": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"body\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"rootId\\": 10,
|
||||||
|
\\"id\\": 14
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"rootId\\": 10,
|
||||||
|
\\"id\\": 11
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"compatMode\\": \\"BackCompat\\",
|
||||||
|
\\"id\\": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"removes\\": [],
|
||||||
|
\\"texts\\": [],
|
||||||
|
\\"attributes\\": [],
|
||||||
|
\\"isAttachIframe\\": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 0,
|
||||||
|
\\"adds\\": [
|
||||||
|
{
|
||||||
|
\\"parentId\\": 13,
|
||||||
|
\\"nextId\\": null,
|
||||||
|
\\"node\\": {
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"link\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"_cssText\\": \\"body { color: pink; }\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"rootId\\": 10,
|
||||||
|
\\"id\\": 13
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"removes\\": [],
|
||||||
|
\\"texts\\": [],
|
||||||
|
\\"attributes\\": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"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\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"head\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"body\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"input\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"type\\": \\"text\\",
|
||||||
|
\\"size\\": \\"40\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
|
||||||
|
\\"id\\": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"iframe\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 9
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 1
|
||||||
|
},
|
||||||
|
\\"initialOffset\\": {
|
||||||
|
\\"left\\": 0,
|
||||||
|
\\"top\\": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 0,
|
||||||
|
\\"adds\\": [
|
||||||
|
{
|
||||||
|
\\"parentId\\": 9,
|
||||||
|
\\"nextId\\": null,
|
||||||
|
\\"node\\": {
|
||||||
|
\\"type\\": 0,
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"html\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"head\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"link\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"_cssText\\": \\"body { color: pink; }\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"rootId\\": 10,
|
||||||
|
\\"id\\": 13
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"rootId\\": 10,
|
||||||
|
\\"id\\": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"body\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"rootId\\": 10,
|
||||||
|
\\"id\\": 14
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"rootId\\": 10,
|
||||||
|
\\"id\\": 11
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"compatMode\\": \\"BackCompat\\",
|
||||||
|
\\"id\\": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"removes\\": [],
|
||||||
|
\\"texts\\": [],
|
||||||
|
\\"attributes\\": [],
|
||||||
|
\\"isAttachIframe\\": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`record captures stylesheets that are still loading 1`] = `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"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\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"head\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"body\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"input\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"type\\": \\"text\\",
|
||||||
|
\\"size\\": \\"40\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
|
||||||
|
\\"id\\": 8
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 1
|
||||||
|
},
|
||||||
|
\\"initialOffset\\": {
|
||||||
|
\\"left\\": 0,
|
||||||
|
\\"top\\": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 0,
|
||||||
|
\\"adds\\": [
|
||||||
|
{
|
||||||
|
\\"parentId\\": 9,
|
||||||
|
\\"nextId\\": null,
|
||||||
|
\\"node\\": {
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"link\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"_cssText\\": \\"body { color: pink; }\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"removes\\": [],
|
||||||
|
\\"texts\\": [],
|
||||||
|
\\"attributes\\": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`record captures stylesheets with \`blob:\` url 1`] = `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"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\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"head\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"link\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"_cssText\\": \\"body { color: pink; }\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"body\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"input\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"type\\": \\"text\\",
|
||||||
|
\\"size\\": \\"40\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
|
||||||
|
\\"id\\": 9
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 1
|
||||||
|
},
|
||||||
|
\\"initialOffset\\": {
|
||||||
|
\\"left\\": 0,
|
||||||
|
\\"top\\": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`record iframes captures stylesheet mutations in iframes 1`] = `
|
exports[`record iframes captures stylesheet mutations in iframes 1`] = `
|
||||||
"[
|
"[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ const setup = function (this: ISuite, content: string): ISuite {
|
|||||||
const ctx = {} as ISuite;
|
const ctx = {} as ISuite;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
ctx.browser = await launchPuppeteer();
|
ctx.browser = await launchPuppeteer({
|
||||||
|
devtools: true,
|
||||||
|
});
|
||||||
|
|
||||||
const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js');
|
const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js');
|
||||||
ctx.code = fs.readFileSync(bundlePath, 'utf8');
|
ctx.code = fs.readFileSync(bundlePath, 'utf8');
|
||||||
@@ -143,16 +145,20 @@ describe('record', function (this: ISuite) {
|
|||||||
checkoutEveryNms: 500,
|
checkoutEveryNms: 500,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
let count = 30;
|
await ctx.page.type('input', 'a');
|
||||||
while (count--) {
|
|
||||||
await ctx.page.type('input', 'a');
|
|
||||||
}
|
|
||||||
await ctx.page.waitForTimeout(300);
|
await ctx.page.waitForTimeout(300);
|
||||||
expect(ctx.events.length).toEqual(33); // before first automatic snapshot
|
expect(
|
||||||
await ctx.page.waitForTimeout(200); // could be 33 or 35 events by now depending on speed of test env
|
ctx.events.filter((event: eventWithTime) => event.type === EventType.Meta)
|
||||||
|
.length,
|
||||||
|
).toEqual(1); // before first automatic snapshot
|
||||||
|
expect(
|
||||||
|
ctx.events.filter(
|
||||||
|
(event: eventWithTime) => event.type === EventType.FullSnapshot,
|
||||||
|
).length,
|
||||||
|
).toEqual(1); // before first automatic snapshot
|
||||||
|
await ctx.page.waitForTimeout(200);
|
||||||
await ctx.page.type('input', 'a');
|
await ctx.page.type('input', 'a');
|
||||||
await ctx.page.waitForTimeout(10);
|
await ctx.page.waitForTimeout(10);
|
||||||
expect(ctx.events.length).toEqual(36); // additionally includes the 2 checkout events
|
|
||||||
expect(
|
expect(
|
||||||
ctx.events.filter((event: eventWithTime) => event.type === EventType.Meta)
|
ctx.events.filter((event: eventWithTime) => event.type === EventType.Meta)
|
||||||
.length,
|
.length,
|
||||||
@@ -162,8 +168,6 @@ describe('record', function (this: ISuite) {
|
|||||||
(event: eventWithTime) => event.type === EventType.FullSnapshot,
|
(event: eventWithTime) => event.type === EventType.FullSnapshot,
|
||||||
).length,
|
).length,
|
||||||
).toEqual(2);
|
).toEqual(2);
|
||||||
expect(ctx.events[1].type).toEqual(EventType.FullSnapshot);
|
|
||||||
expect(ctx.events[35].type).toEqual(EventType.FullSnapshot);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is safe to checkout during async callbacks', async () => {
|
it('is safe to checkout during async callbacks', async () => {
|
||||||
@@ -381,6 +385,151 @@ describe('record', function (this: ISuite) {
|
|||||||
await waitForRAF(ctx.page);
|
await waitForRAF(ctx.page);
|
||||||
assertSnapshot(ctx.events);
|
assertSnapshot(ctx.events);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures stylesheets with `blob:` url', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const link1 = document.createElement('link');
|
||||||
|
link1.setAttribute('rel', 'stylesheet');
|
||||||
|
link1.setAttribute(
|
||||||
|
'href',
|
||||||
|
URL.createObjectURL(
|
||||||
|
new Blob(['body { color: pink; }'], {
|
||||||
|
type: 'text/css',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
document.head.appendChild(link1);
|
||||||
|
});
|
||||||
|
await waitForRAF(ctx.page);
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||||
|
|
||||||
|
record({
|
||||||
|
inlineStylesheet: true,
|
||||||
|
emit: ((window as unknown) as IWindow).emit,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForRAF(ctx.page);
|
||||||
|
assertSnapshot(ctx.events);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('captures stylesheets in iframes with `blob:` url', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.setAttribute('src', 'about:blank');
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
|
||||||
|
const linkEl = document.createElement('link');
|
||||||
|
linkEl.setAttribute('rel', 'stylesheet');
|
||||||
|
linkEl.setAttribute(
|
||||||
|
'href',
|
||||||
|
URL.createObjectURL(
|
||||||
|
new Blob(['body { color: pink; }'], {
|
||||||
|
type: 'text/css',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const iframeDoc = iframe.contentDocument!;
|
||||||
|
iframeDoc.head.appendChild(linkEl);
|
||||||
|
});
|
||||||
|
await waitForRAF(ctx.page);
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||||
|
|
||||||
|
record({
|
||||||
|
inlineStylesheet: true,
|
||||||
|
emit: ((window as unknown) as IWindow).emit,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForRAF(ctx.page);
|
||||||
|
assertSnapshot(ctx.events);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('captures stylesheets that are still loading', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||||
|
|
||||||
|
record({
|
||||||
|
inlineStylesheet: true,
|
||||||
|
emit: ((window as unknown) as IWindow).emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const link1 = document.createElement('link');
|
||||||
|
link1.setAttribute('rel', 'stylesheet');
|
||||||
|
link1.setAttribute(
|
||||||
|
'href',
|
||||||
|
URL.createObjectURL(
|
||||||
|
new Blob(['body { color: pink; }'], {
|
||||||
|
type: 'text/css',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
document.head.appendChild(link1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// `blob:` URLs are not available immediately, so we need to wait for the browser to load them
|
||||||
|
await waitForRAF(ctx.page);
|
||||||
|
|
||||||
|
assertSnapshot(ctx.events);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('captures stylesheets in iframes that are still loading', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.setAttribute('src', 'about:blank');
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
const iframeDoc = iframe.contentDocument!;
|
||||||
|
|
||||||
|
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||||
|
|
||||||
|
record({
|
||||||
|
inlineStylesheet: true,
|
||||||
|
emit: ((window as unknown) as IWindow).emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const linkEl = document.createElement('link');
|
||||||
|
linkEl.setAttribute('rel', 'stylesheet');
|
||||||
|
linkEl.setAttribute(
|
||||||
|
'href',
|
||||||
|
URL.createObjectURL(
|
||||||
|
new Blob(['body { color: pink; }'], {
|
||||||
|
type: 'text/css',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
iframeDoc.head.appendChild(linkEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// `blob:` URLs are not available immediately, so we need to wait for the browser to load them
|
||||||
|
await waitForRAF(ctx.page);
|
||||||
|
|
||||||
|
assertSnapshot(ctx.events);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('captures CORS stylesheets that are still loading', async () => {
|
||||||
|
const corsStylesheetURL =
|
||||||
|
'https://cdn.jsdelivr.net/npm/pure@2.85.0/index.css';
|
||||||
|
|
||||||
|
// do not `await` the following function, otherwise `waitForResponse` _might_ not be called
|
||||||
|
void ctx.page.evaluate((corsStylesheetURL) => {
|
||||||
|
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||||
|
|
||||||
|
record({
|
||||||
|
inlineStylesheet: true,
|
||||||
|
emit: ((window as unknown) as IWindow).emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const link1 = document.createElement('link');
|
||||||
|
link1.setAttribute('rel', 'stylesheet');
|
||||||
|
link1.setAttribute('href', corsStylesheetURL);
|
||||||
|
document.head.appendChild(link1);
|
||||||
|
}, corsStylesheetURL);
|
||||||
|
|
||||||
|
await ctx.page.waitForResponse(corsStylesheetURL); // wait for stylesheet to be loaded
|
||||||
|
await waitForRAF(ctx.page); // wait for rrweb to emit events
|
||||||
|
|
||||||
|
assertSnapshot(ctx.events);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('record iframes', function (this: ISuite) {
|
describe('record iframes', function (this: ISuite) {
|
||||||
@@ -463,7 +612,8 @@ describe('record iframes', function (this: ISuite) {
|
|||||||
}, 10);
|
}, 10);
|
||||||
}, 10);
|
}, 10);
|
||||||
});
|
});
|
||||||
await ctx.page.waitForTimeout(50);
|
await ctx.page.waitForTimeout(50); // wait till setTimeout is called
|
||||||
|
await waitForRAF(ctx.page); // wait till events get sent
|
||||||
const styleRelatedEvents = ctx.events.filter(
|
const styleRelatedEvents = ctx.events.filter(
|
||||||
(e) =>
|
(e) =>
|
||||||
e.type === EventType.IncrementalSnapshot &&
|
e.type === EventType.IncrementalSnapshot &&
|
||||||
|
|||||||
1
packages/rrweb/typings/record/mutation.d.ts
vendored
1
packages/rrweb/typings/record/mutation.d.ts
vendored
@@ -25,6 +25,7 @@ export default class MutationBuffer {
|
|||||||
private doc;
|
private doc;
|
||||||
private mirror;
|
private mirror;
|
||||||
private iframeManager;
|
private iframeManager;
|
||||||
|
private stylesheetManager;
|
||||||
private shadowDomManager;
|
private shadowDomManager;
|
||||||
private canvasManager;
|
private canvasManager;
|
||||||
init(options: MutationBufferParam): void;
|
init(options: MutationBufferParam): void;
|
||||||
|
|||||||
12
packages/rrweb/typings/record/stylesheet-manager.d.ts
vendored
Normal file
12
packages/rrweb/typings/record/stylesheet-manager.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
|
||||||
|
import type { mutationCallBack } from '../types';
|
||||||
|
export declare class StylesheetManager {
|
||||||
|
private trackedStylesheets;
|
||||||
|
private mutationCb;
|
||||||
|
constructor(options: {
|
||||||
|
mutationCb: mutationCallBack;
|
||||||
|
});
|
||||||
|
addStylesheet(linkEl: HTMLLinkElement): void;
|
||||||
|
private trackStylesheet;
|
||||||
|
attachStylesheet(linkEl: HTMLLinkElement, childSn: serializedNodeWithId, mirror: Mirror): void;
|
||||||
|
}
|
||||||
4
packages/rrweb/typings/types.d.ts
vendored
4
packages/rrweb/typings/types.d.ts
vendored
@@ -5,6 +5,7 @@ import type { ShadowDomManager } from './record/shadow-dom-manager';
|
|||||||
import type { Replayer } from './replay';
|
import type { Replayer } from './replay';
|
||||||
import type { RRNode } from 'rrdom';
|
import type { RRNode } from 'rrdom';
|
||||||
import type { CanvasManager } from './record/observers/canvas/canvas-manager';
|
import type { CanvasManager } from './record/observers/canvas/canvas-manager';
|
||||||
|
import type { StylesheetManager } from './record/stylesheet-manager';
|
||||||
export declare enum EventType {
|
export declare enum EventType {
|
||||||
DomContentLoaded = 0,
|
DomContentLoaded = 0,
|
||||||
Load = 1,
|
Load = 1,
|
||||||
@@ -193,6 +194,7 @@ export declare type observerParam = {
|
|||||||
doc: Document;
|
doc: Document;
|
||||||
mirror: Mirror;
|
mirror: Mirror;
|
||||||
iframeManager: IframeManager;
|
iframeManager: IframeManager;
|
||||||
|
stylesheetManager: StylesheetManager;
|
||||||
shadowDomManager: ShadowDomManager;
|
shadowDomManager: ShadowDomManager;
|
||||||
canvasManager: CanvasManager;
|
canvasManager: CanvasManager;
|
||||||
plugins: Array<{
|
plugins: Array<{
|
||||||
@@ -201,7 +203,7 @@ export declare type observerParam = {
|
|||||||
options: unknown;
|
options: unknown;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
export declare type MutationBufferParam = Pick<observerParam, 'mutationCb' | 'blockClass' | 'blockSelector' | 'maskTextClass' | 'maskTextSelector' | 'inlineStylesheet' | 'maskInputOptions' | 'maskTextFn' | 'maskInputFn' | 'recordCanvas' | 'inlineImages' | 'slimDOMOptions' | 'doc' | 'mirror' | 'iframeManager' | 'shadowDomManager' | 'canvasManager'>;
|
export declare type MutationBufferParam = Pick<observerParam, 'mutationCb' | 'blockClass' | 'blockSelector' | 'maskTextClass' | 'maskTextSelector' | 'inlineStylesheet' | 'maskInputOptions' | 'maskTextFn' | 'maskInputFn' | 'recordCanvas' | 'inlineImages' | 'slimDOMOptions' | 'doc' | 'mirror' | 'iframeManager' | 'stylesheetManager' | 'shadowDomManager' | 'canvasManager'>;
|
||||||
export declare type hooksParam = {
|
export declare type hooksParam = {
|
||||||
mutation?: mutationCallBack;
|
mutation?: mutationCallBack;
|
||||||
mousemove?: mousemoveCallBack;
|
mousemove?: mousemoveCallBack;
|
||||||
|
|||||||
1
packages/rrweb/typings/utils.d.ts
vendored
1
packages/rrweb/typings/utils.d.ts
vendored
@@ -28,6 +28,7 @@ export declare type AppendedIframe = {
|
|||||||
builtNode: HTMLIFrameElement | RRIFrameElement;
|
builtNode: HTMLIFrameElement | RRIFrameElement;
|
||||||
};
|
};
|
||||||
export declare function isSerializedIframe<TNode extends Node | RRNode>(n: TNode, mirror: IMirror<TNode>): boolean;
|
export declare function isSerializedIframe<TNode extends Node | RRNode>(n: TNode, mirror: IMirror<TNode>): boolean;
|
||||||
|
export declare function isSerializedStylesheet<TNode extends Node | RRNode>(n: TNode, mirror: IMirror<TNode>): boolean;
|
||||||
export declare function getBaseDimension(node: Node, rootIframe: Node): DocumentDimension;
|
export declare function getBaseDimension(node: Node, rootIframe: Node): DocumentDimension;
|
||||||
export declare function hasShadowRoot<T extends Node | RRNode>(n: T): n is T & {
|
export declare function hasShadowRoot<T extends Node | RRNode>(n: T): n is T & {
|
||||||
shadowRoot: ShadowRoot;
|
shadowRoot: ShadowRoot;
|
||||||
|
|||||||
Reference in New Issue
Block a user