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,
|
||||
polyfillDocument,
|
||||
} from '../src/polyfill';
|
||||
import { performance as nativePerformance } from 'perf_hooks';
|
||||
|
||||
describe('polyfill for nodejs', () => {
|
||||
it('should polyfill performance api', () => {
|
||||
@@ -16,10 +17,7 @@ describe('polyfill for nodejs', () => {
|
||||
expect(global.performance).toBeDefined();
|
||||
expect(performance).toBeDefined();
|
||||
expect(performance.now).toBeDefined();
|
||||
expect(performance.now()).toBeCloseTo(
|
||||
require('perf_hooks').performance.now(),
|
||||
1e-10,
|
||||
);
|
||||
expect(performance.now()).toBeCloseTo(nativePerformance.now(), 1e-10);
|
||||
});
|
||||
|
||||
it('should not polyfill performance if it already exists', () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
MaskInputFn,
|
||||
KeepIframeSrcFn,
|
||||
ICanvas,
|
||||
serializedElementNodeWithId,
|
||||
} from './types';
|
||||
import {
|
||||
Mirror,
|
||||
@@ -377,6 +378,40 @@ function onceIframeLoaded(
|
||||
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(
|
||||
n: Node,
|
||||
options: {
|
||||
@@ -876,6 +911,7 @@ export function serializeNodeWithId(
|
||||
maskTextSelector: string | null;
|
||||
skipChild: boolean;
|
||||
inlineStylesheet: boolean;
|
||||
newlyAddedElement?: boolean;
|
||||
maskInputOptions?: MaskInputOptions;
|
||||
maskTextFn: MaskTextFn | undefined;
|
||||
maskInputFn: MaskInputFn | undefined;
|
||||
@@ -888,10 +924,14 @@ export function serializeNodeWithId(
|
||||
onSerialize?: (n: Node) => unknown;
|
||||
onIframeLoad?: (
|
||||
iframeNode: HTMLIFrameElement,
|
||||
node: serializedNodeWithId,
|
||||
node: serializedElementNodeWithId,
|
||||
) => unknown;
|
||||
iframeLoadTimeout?: number;
|
||||
newlyAddedElement?: boolean;
|
||||
onStylesheetLoad?: (
|
||||
linkNode: HTMLLinkElement,
|
||||
node: serializedElementNodeWithId,
|
||||
) => unknown;
|
||||
stylesheetLoadTimeout?: number;
|
||||
},
|
||||
): serializedNodeWithId | null {
|
||||
const {
|
||||
@@ -913,6 +953,8 @@ export function serializeNodeWithId(
|
||||
onSerialize,
|
||||
onIframeLoad,
|
||||
iframeLoadTimeout = 5000,
|
||||
onStylesheetLoad,
|
||||
stylesheetLoadTimeout = 5000,
|
||||
keepIframeSrcFn = () => false,
|
||||
newlyAddedElement = false,
|
||||
} = options;
|
||||
@@ -1006,6 +1048,8 @@ export function serializeNodeWithId(
|
||||
onSerialize,
|
||||
onIframeLoad,
|
||||
iframeLoadTimeout,
|
||||
onStylesheetLoad,
|
||||
stylesheetLoadTimeout,
|
||||
keepIframeSrcFn,
|
||||
};
|
||||
for (const childN of Array.from(n.childNodes)) {
|
||||
@@ -1059,11 +1103,16 @@ export function serializeNodeWithId(
|
||||
onSerialize,
|
||||
onIframeLoad,
|
||||
iframeLoadTimeout,
|
||||
onStylesheetLoad,
|
||||
stylesheetLoadTimeout,
|
||||
keepIframeSrcFn,
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1094,9 +1191,14 @@ function snapshot(
|
||||
onSerialize?: (n: Node) => unknown;
|
||||
onIframeLoad?: (
|
||||
iframeNode: HTMLIFrameElement,
|
||||
node: serializedNodeWithId,
|
||||
node: serializedElementNodeWithId,
|
||||
) => unknown;
|
||||
iframeLoadTimeout?: number;
|
||||
onStylesheetLoad?: (
|
||||
linkNode: HTMLLinkElement,
|
||||
node: serializedElementNodeWithId,
|
||||
) => unknown;
|
||||
stylesheetLoadTimeout?: number;
|
||||
keepIframeSrcFn?: KeepIframeSrcFn;
|
||||
},
|
||||
): serializedNodeWithId | null {
|
||||
@@ -1118,6 +1220,8 @@ function snapshot(
|
||||
onSerialize,
|
||||
onIframeLoad,
|
||||
iframeLoadTimeout,
|
||||
onStylesheetLoad,
|
||||
stylesheetLoadTimeout,
|
||||
keepIframeSrcFn = () => false,
|
||||
} = options || {};
|
||||
const maskInputOptions: MaskInputOptions =
|
||||
@@ -1183,6 +1287,8 @@ function snapshot(
|
||||
onSerialize,
|
||||
onIframeLoad,
|
||||
iframeLoadTimeout,
|
||||
onStylesheetLoad,
|
||||
stylesheetLoadTimeout,
|
||||
keepIframeSrcFn,
|
||||
newlyAddedElement: false,
|
||||
});
|
||||
|
||||
@@ -63,6 +63,11 @@ export type serializedNode = (
|
||||
|
||||
export type serializedNodeWithId = serializedNode & { id: number };
|
||||
|
||||
export type serializedElementNodeWithId = Extract<
|
||||
serializedNodeWithId,
|
||||
Record<'type', NodeType.Element>
|
||||
>;
|
||||
|
||||
export type tagMap = {
|
||||
[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';
|
||||
export declare const IGNORED_NODE = -2;
|
||||
export declare function absoluteToStylesheet(cssText: string | null, href: string): string;
|
||||
@@ -16,6 +16,7 @@ export declare function serializeNodeWithId(n: Node, options: {
|
||||
maskTextSelector: string | null;
|
||||
skipChild: boolean;
|
||||
inlineStylesheet: boolean;
|
||||
newlyAddedElement?: boolean;
|
||||
maskInputOptions?: MaskInputOptions;
|
||||
maskTextFn: MaskTextFn | undefined;
|
||||
maskInputFn: MaskInputFn | undefined;
|
||||
@@ -26,9 +27,10 @@ export declare function serializeNodeWithId(n: Node, options: {
|
||||
recordCanvas?: boolean;
|
||||
preserveWhiteSpace?: boolean;
|
||||
onSerialize?: (n: Node) => unknown;
|
||||
onIframeLoad?: (iframeNode: HTMLIFrameElement, node: serializedNodeWithId) => unknown;
|
||||
onIframeLoad?: (iframeNode: HTMLIFrameElement, node: serializedElementNodeWithId) => unknown;
|
||||
iframeLoadTimeout?: number;
|
||||
newlyAddedElement?: boolean;
|
||||
onStylesheetLoad?: (linkNode: HTMLLinkElement, node: serializedElementNodeWithId) => unknown;
|
||||
stylesheetLoadTimeout?: number;
|
||||
}): serializedNodeWithId | null;
|
||||
declare function snapshot(n: Document, options?: {
|
||||
mirror?: Mirror;
|
||||
@@ -46,8 +48,10 @@ declare function snapshot(n: Document, options?: {
|
||||
recordCanvas?: boolean;
|
||||
preserveWhiteSpace?: boolean;
|
||||
onSerialize?: (n: Node) => unknown;
|
||||
onIframeLoad?: (iframeNode: HTMLIFrameElement, node: serializedNodeWithId) => unknown;
|
||||
onIframeLoad?: (iframeNode: HTMLIFrameElement, node: serializedElementNodeWithId) => unknown;
|
||||
iframeLoadTimeout?: number;
|
||||
onStylesheetLoad?: (linkNode: HTMLLinkElement, node: serializedElementNodeWithId) => unknown;
|
||||
stylesheetLoadTimeout?: number;
|
||||
keepIframeSrcFn?: KeepIframeSrcFn;
|
||||
}): serializedNodeWithId | null;
|
||||
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 & {
|
||||
id: number;
|
||||
};
|
||||
export declare type serializedElementNodeWithId = Extract<serializedNodeWithId, Record<'type', NodeType.Element>>;
|
||||
export declare type tagMap = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
polyfill,
|
||||
hasShadowRoot,
|
||||
isSerializedIframe,
|
||||
isSerializedStylesheet,
|
||||
} from '../utils';
|
||||
import {
|
||||
EventType,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
import { IframeManager } from './iframe-manager';
|
||||
import { ShadowDomManager } from './shadow-dom-manager';
|
||||
import { CanvasManager } from './observers/canvas/canvas-manager';
|
||||
import { StylesheetManager } from './stylesheet-manager';
|
||||
|
||||
function wrapEvent(e: event): eventWithTime {
|
||||
return {
|
||||
@@ -215,6 +217,10 @@ function record<T = eventWithTime>(
|
||||
mutationCb: wrappedMutationEmit,
|
||||
});
|
||||
|
||||
const stylesheetManager = new StylesheetManager({
|
||||
mutationCb: wrappedMutationEmit,
|
||||
});
|
||||
|
||||
const canvasManager = new CanvasManager({
|
||||
recordCanvas,
|
||||
mutationCb: wrappedCanvasMutationEmit,
|
||||
@@ -241,6 +247,7 @@ function record<T = eventWithTime>(
|
||||
sampling,
|
||||
slimDOMOptions,
|
||||
iframeManager,
|
||||
stylesheetManager,
|
||||
canvasManager,
|
||||
},
|
||||
mirror,
|
||||
@@ -276,6 +283,9 @@ function record<T = eventWithTime>(
|
||||
if (isSerializedIframe(n, mirror)) {
|
||||
iframeManager.addIframe(n as HTMLIFrameElement);
|
||||
}
|
||||
if (isSerializedStylesheet(n, mirror)) {
|
||||
stylesheetManager.addStylesheet(n as HTMLLinkElement);
|
||||
}
|
||||
if (hasShadowRoot(n)) {
|
||||
shadowDomManager.addShadowRoot(n.shadowRoot, document);
|
||||
}
|
||||
@@ -284,6 +294,9 @@ function record<T = eventWithTime>(
|
||||
iframeManager.attachIframe(iframe, childSn, mirror);
|
||||
shadowDomManager.observeAttachShadow(iframe);
|
||||
},
|
||||
onStylesheetLoad: (linkEl, childSn) => {
|
||||
stylesheetManager.attachStylesheet(linkEl, childSn, mirror);
|
||||
},
|
||||
keepIframeSrcFn,
|
||||
});
|
||||
|
||||
@@ -435,6 +448,7 @@ function record<T = eventWithTime>(
|
||||
slimDOMOptions,
|
||||
mirror,
|
||||
iframeManager,
|
||||
stylesheetManager,
|
||||
shadowDomManager,
|
||||
canvasManager,
|
||||
plugins:
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
isSerialized,
|
||||
hasShadowRoot,
|
||||
isSerializedIframe,
|
||||
isSerializedStylesheet,
|
||||
} from '../utils';
|
||||
|
||||
type DoubleLinkedListNode = {
|
||||
@@ -169,6 +170,7 @@ export default class MutationBuffer {
|
||||
private doc: observerParam['doc'];
|
||||
private mirror: observerParam['mirror'];
|
||||
private iframeManager: observerParam['iframeManager'];
|
||||
private stylesheetManager: observerParam['stylesheetManager'];
|
||||
private shadowDomManager: observerParam['shadowDomManager'];
|
||||
private canvasManager: observerParam['canvasManager'];
|
||||
|
||||
@@ -189,6 +191,7 @@ export default class MutationBuffer {
|
||||
'doc',
|
||||
'mirror',
|
||||
'iframeManager',
|
||||
'stylesheetManager',
|
||||
'shadowDomManager',
|
||||
'canvasManager',
|
||||
] as const).forEach((key) => {
|
||||
@@ -289,6 +292,7 @@ export default class MutationBuffer {
|
||||
maskTextClass: this.maskTextClass,
|
||||
maskTextSelector: this.maskTextSelector,
|
||||
skipChild: true,
|
||||
newlyAddedElement: true,
|
||||
inlineStylesheet: this.inlineStylesheet,
|
||||
maskInputOptions: this.maskInputOptions,
|
||||
maskTextFn: this.maskTextFn,
|
||||
@@ -300,6 +304,9 @@ export default class MutationBuffer {
|
||||
if (isSerializedIframe(currentN, this.mirror)) {
|
||||
this.iframeManager.addIframe(currentN as HTMLIFrameElement);
|
||||
}
|
||||
if (isSerializedStylesheet(currentN, this.mirror)) {
|
||||
this.stylesheetManager.addStylesheet(currentN as HTMLLinkElement);
|
||||
}
|
||||
if (hasShadowRoot(n)) {
|
||||
this.shadowDomManager.addShadowRoot(n.shadowRoot, document);
|
||||
}
|
||||
@@ -308,7 +315,9 @@ export default class MutationBuffer {
|
||||
this.iframeManager.attachIframe(iframe, childSn, this.mirror);
|
||||
this.shadowDomManager.observeAttachShadow(iframe);
|
||||
},
|
||||
newlyAddedElement: true,
|
||||
onStylesheetLoad: (link, childSn) => {
|
||||
this.stylesheetManager.attachStylesheet(link, childSn, this.mirror);
|
||||
},
|
||||
});
|
||||
if (sn) {
|
||||
adds.push({
|
||||
@@ -471,6 +480,7 @@ export default class MutationBuffer {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let item: attributeCursor | undefined = this.attributes.find(
|
||||
(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 { RRNode } from 'rrdom';
|
||||
import type { CanvasManager } from './record/observers/canvas/canvas-manager';
|
||||
import type { StylesheetManager } from './record/stylesheet-manager';
|
||||
|
||||
export enum EventType {
|
||||
DomContentLoaded,
|
||||
@@ -280,6 +281,7 @@ export type observerParam = {
|
||||
doc: Document;
|
||||
mirror: Mirror;
|
||||
iframeManager: IframeManager;
|
||||
stylesheetManager: StylesheetManager;
|
||||
shadowDomManager: ShadowDomManager;
|
||||
canvasManager: CanvasManager;
|
||||
plugins: Array<{
|
||||
@@ -306,6 +308,7 @@ export type MutationBufferParam = Pick<
|
||||
| 'doc'
|
||||
| 'mirror'
|
||||
| 'iframeManager'
|
||||
| 'stylesheetManager'
|
||||
| 'shadowDomManager'
|
||||
| 'canvasManager'
|
||||
>;
|
||||
|
||||
@@ -354,6 +354,19 @@ export function isSerializedIframe<TNode extends Node | RRNode>(
|
||||
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(
|
||||
node: 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`] = `
|
||||
"[
|
||||
{
|
||||
@@ -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`] = `
|
||||
"[
|
||||
{
|
||||
|
||||
@@ -34,7 +34,9 @@ const setup = function (this: ISuite, content: string): ISuite {
|
||||
const ctx = {} as ISuite;
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx.browser = await launchPuppeteer();
|
||||
ctx.browser = await launchPuppeteer({
|
||||
devtools: true,
|
||||
});
|
||||
|
||||
const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js');
|
||||
ctx.code = fs.readFileSync(bundlePath, 'utf8');
|
||||
@@ -143,16 +145,20 @@ describe('record', function (this: ISuite) {
|
||||
checkoutEveryNms: 500,
|
||||
});
|
||||
});
|
||||
let count = 30;
|
||||
while (count--) {
|
||||
await ctx.page.type('input', 'a');
|
||||
}
|
||||
await ctx.page.type('input', 'a');
|
||||
await ctx.page.waitForTimeout(300);
|
||||
expect(ctx.events.length).toEqual(33); // before first automatic snapshot
|
||||
await ctx.page.waitForTimeout(200); // could be 33 or 35 events by now depending on speed of test env
|
||||
expect(
|
||||
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.waitForTimeout(10);
|
||||
expect(ctx.events.length).toEqual(36); // additionally includes the 2 checkout events
|
||||
expect(
|
||||
ctx.events.filter((event: eventWithTime) => event.type === EventType.Meta)
|
||||
.length,
|
||||
@@ -162,8 +168,6 @@ describe('record', function (this: ISuite) {
|
||||
(event: eventWithTime) => event.type === EventType.FullSnapshot,
|
||||
).length,
|
||||
).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 () => {
|
||||
@@ -381,6 +385,151 @@ describe('record', function (this: ISuite) {
|
||||
await waitForRAF(ctx.page);
|
||||
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) {
|
||||
@@ -463,7 +612,8 @@ describe('record iframes', function (this: ISuite) {
|
||||
}, 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(
|
||||
(e) =>
|
||||
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 mirror;
|
||||
private iframeManager;
|
||||
private stylesheetManager;
|
||||
private shadowDomManager;
|
||||
private canvasManager;
|
||||
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 { RRNode } from 'rrdom';
|
||||
import type { CanvasManager } from './record/observers/canvas/canvas-manager';
|
||||
import type { StylesheetManager } from './record/stylesheet-manager';
|
||||
export declare enum EventType {
|
||||
DomContentLoaded = 0,
|
||||
Load = 1,
|
||||
@@ -193,6 +194,7 @@ export declare type observerParam = {
|
||||
doc: Document;
|
||||
mirror: Mirror;
|
||||
iframeManager: IframeManager;
|
||||
stylesheetManager: StylesheetManager;
|
||||
shadowDomManager: ShadowDomManager;
|
||||
canvasManager: CanvasManager;
|
||||
plugins: Array<{
|
||||
@@ -201,7 +203,7 @@ export declare type observerParam = {
|
||||
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 = {
|
||||
mutation?: mutationCallBack;
|
||||
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;
|
||||
};
|
||||
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 hasShadowRoot<T extends Node | RRNode>(n: T): n is T & {
|
||||
shadowRoot: ShadowRoot;
|
||||
|
||||
Reference in New Issue
Block a user