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:
Justin Halsall
2022-07-01 07:29:09 +02:00
committed by GitHub
parent a31e272bf2
commit d5d877e380
16 changed files with 985 additions and 25 deletions

View File

@@ -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,
});

View File

@@ -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;
};