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:
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user