diff --git a/packages/rrdom-nodejs/test/polyfill.test.ts b/packages/rrdom-nodejs/test/polyfill.test.ts
index a9d5f381f1..240976ad83 100644
--- a/packages/rrdom-nodejs/test/polyfill.test.ts
+++ b/packages/rrdom-nodejs/test/polyfill.test.ts
@@ -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', () => {
diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts
index 73850d6453..c3c9acd544 100644
--- a/packages/rrdom/src/diff.ts
+++ b/packages/rrdom/src/diff.ts
@@ -316,7 +316,7 @@ function diffChildren(
* We should delete it before insert a serialized one. Otherwise, an error 'Only one element on document allowed' will be thrown.
*/
if (
- replayer.mirror.getMeta(parentNode)?.type === RRNodeType.Document &&
+ parentNode.nodeName === '#document' &&
replayer.mirror.getMeta(newNode)?.type === RRNodeType.Element &&
(parentNode as Document).documentElement
) {
diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts
index bb4544e888..efb5aaaa8f 100644
--- a/packages/rrdom/test/diff.test.ts
+++ b/packages/rrdom/test/diff.test.ts
@@ -1056,6 +1056,24 @@ describe('diff algorithm for rrdom', () => {
expect(element.tagName).toBe('DIV');
expect(mirror.getId(element)).toEqual(2);
});
+
+ it('should remove children from document before adding new nodes', () => {
+ document.write(''); // old document with elements that need removing
+
+ const rrDocument = new RRDocument();
+ const docType = rrDocument.createDocumentType('html', '', '');
+ rrDocument.mirror.add(docType, getDefaultSN(docType, 1));
+ rrDocument.appendChild(docType);
+ const htmlEl = rrDocument.createElement('html');
+ rrDocument.mirror.add(htmlEl, getDefaultSN(htmlEl, 2));
+ rrDocument.appendChild(htmlEl);
+
+ diff(document, rrDocument, replayer);
+ expect(document.childNodes.length).toBe(2);
+ const element = document.childNodes[0] as HTMLElement;
+ expect(element.nodeType).toBe(element.DOCUMENT_TYPE_NODE);
+ expect(mirror.getId(element)).toEqual(1);
+ });
});
describe('create or get a Node', () => {
diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts
index 1694d3f222..b415208272 100644
--- a/packages/rrweb-snapshot/src/snapshot.ts
+++ b/packages/rrweb-snapshot/src/snapshot.ts
@@ -10,6 +10,7 @@ import {
MaskInputFn,
KeepIframeSrcFn,
ICanvas,
+ serializedElementNodeWithId,
} from './types';
import {
Mirror,
@@ -371,12 +372,47 @@ function onceIframeLoaded(
// iframe was already loaded, make sure we wait to trigger the listener
// till _after_ the mutation that found this iframe has had time to process
setTimeout(listener, 0);
- return;
+
+ return iframeEl.addEventListener('load', listener); // keep listing for future loads
}
// use default 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(
n: Node,
options: {
@@ -876,6 +912,7 @@ export function serializeNodeWithId(
maskTextSelector: string | null;
skipChild: boolean;
inlineStylesheet: boolean;
+ newlyAddedElement?: boolean;
maskInputOptions?: MaskInputOptions;
maskTextFn: MaskTextFn | undefined;
maskInputFn: MaskInputFn | undefined;
@@ -888,10 +925,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 +954,8 @@ export function serializeNodeWithId(
onSerialize,
onIframeLoad,
iframeLoadTimeout = 5000,
+ onStylesheetLoad,
+ stylesheetLoadTimeout = 5000,
keepIframeSrcFn = () => false,
newlyAddedElement = false,
} = options;
@@ -1006,6 +1049,8 @@ export function serializeNodeWithId(
onSerialize,
onIframeLoad,
iframeLoadTimeout,
+ onStylesheetLoad,
+ stylesheetLoadTimeout,
keepIframeSrcFn,
};
for (const childN of Array.from(n.childNodes)) {
@@ -1059,11 +1104,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 +1121,54 @@ export function serializeNodeWithId(
);
}
+ //
+ 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 +1192,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 +1221,8 @@ function snapshot(
onSerialize,
onIframeLoad,
iframeLoadTimeout,
+ onStylesheetLoad,
+ stylesheetLoadTimeout,
keepIframeSrcFn = () => false,
} = options || {};
const maskInputOptions: MaskInputOptions =
@@ -1183,6 +1288,8 @@ function snapshot(
onSerialize,
onIframeLoad,
iframeLoadTimeout,
+ onStylesheetLoad,
+ stylesheetLoadTimeout,
keepIframeSrcFn,
newlyAddedElement: false,
});
diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts
index 6a09c633b4..488be6eaa1 100644
--- a/packages/rrweb-snapshot/src/types.ts
+++ b/packages/rrweb-snapshot/src/types.ts
@@ -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;
};
diff --git a/packages/rrweb-snapshot/test/html/picture-blob-in-frame.html b/packages/rrweb-snapshot/test/html/picture-blob-in-frame.html
new file mode 100644
index 0000000000..f6237f22b2
--- /dev/null
+++ b/packages/rrweb-snapshot/test/html/picture-blob-in-frame.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/packages/rrweb-snapshot/test/html/picture-blob.html b/packages/rrweb-snapshot/test/html/picture-blob.html
new file mode 100644
index 0000000000..d2a32658a1
--- /dev/null
+++ b/packages/rrweb-snapshot/test/html/picture-blob.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/packages/rrweb-snapshot/test/html/picture-in-frame.html b/packages/rrweb-snapshot/test/html/picture-in-frame.html
new file mode 100644
index 0000000000..31684d2cda
--- /dev/null
+++ b/packages/rrweb-snapshot/test/html/picture-in-frame.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts
index e34432fd7b..b6a0c85193 100644
--- a/packages/rrweb-snapshot/test/integration.test.ts
+++ b/packages/rrweb-snapshot/test/integration.test.ts
@@ -6,6 +6,7 @@ import * as puppeteer from 'puppeteer';
import * as rollup from 'rollup';
import * as typescript from 'rollup-plugin-typescript2';
import * as assert from 'assert';
+import { waitForRAF } from './utils';
const _typescript = (typescript as unknown) as () => rollup.Plugin;
@@ -209,6 +210,74 @@ iframe.contentDocument.querySelector('center').clientHeight
assert(snapshot.includes('"rr_dataURL"'));
assert(snapshot.includes('data:image/webp;base64,'));
});
+
+ it('correctly saves blob:images offline', async () => {
+ const page: puppeteer.Page = await browser.newPage();
+
+ await page.goto('http://localhost:3030/html/picture-blob.html', {
+ waitUntil: 'load',
+ });
+ await page.waitForSelector('img', { timeout: 1000 });
+ await page.evaluate(`${code}var snapshot = rrweb.snapshot(document, {
+ dataURLOptions: { type: "image/webp", quality: 0.8 },
+ inlineImages: true,
+ inlineStylesheet: false
+ })`);
+ await page.waitFor(100);
+ const snapshot = await page.evaluate('JSON.stringify(snapshot, null, 2);');
+ assert(snapshot.includes('"rr_dataURL"'));
+ assert(snapshot.includes('data:image/webp;base64,'));
+ });
+
+ it('correctly saves images in iframes offline', async () => {
+ const page: puppeteer.Page = await browser.newPage();
+
+ await page.goto('http://localhost:3030/html/picture-in-frame.html', {
+ waitUntil: 'load',
+ });
+ await page.waitForSelector('iframe', { timeout: 1000 });
+ await waitForRAF(page); // wait for page to render
+ await page.evaluate(`${code}
+ rrweb.snapshot(document, {
+ dataURLOptions: { type: "image/webp", quality: 0.8 },
+ inlineImages: true,
+ inlineStylesheet: false,
+ onIframeLoad: function(iframe, sn) {
+ window.snapshot = sn;
+ }
+ })`);
+ await page.waitFor(100);
+ const snapshot = await page.evaluate(
+ 'JSON.stringify(window.snapshot, null, 2);',
+ );
+ assert(snapshot.includes('"rr_dataURL"'));
+ assert(snapshot.includes('data:image/webp;base64,'));
+ });
+
+ it('correctly saves blob:images in iframes offline', async () => {
+ const page: puppeteer.Page = await browser.newPage();
+
+ await page.goto('http://localhost:3030/html/picture-blob-in-frame.html', {
+ waitUntil: 'load',
+ });
+ await page.waitForSelector('iframe', { timeout: 1000 });
+ await waitForRAF(page); // wait for page to render
+ await page.evaluate(`${code}
+ rrweb.snapshot(document, {
+ dataURLOptions: { type: "image/webp", quality: 0.8 },
+ inlineImages: true,
+ inlineStylesheet: false,
+ onIframeLoad: function(iframe, sn) {
+ window.snapshot = sn;
+ }
+ })`);
+ await page.waitFor(100);
+ const snapshot = await page.evaluate(
+ 'JSON.stringify(window.snapshot, null, 2);',
+ );
+ assert(snapshot.includes('"rr_dataURL"'));
+ assert(snapshot.includes('data:image/webp;base64,'));
+ });
});
describe('iframe integration tests', function (this: ISuite) {
diff --git a/packages/rrweb-snapshot/test/utils.ts b/packages/rrweb-snapshot/test/utils.ts
new file mode 100644
index 0000000000..43d4484bb4
--- /dev/null
+++ b/packages/rrweb-snapshot/test/utils.ts
@@ -0,0 +1,11 @@
+import * as puppeteer from 'puppeteer';
+
+export async function waitForRAF(page: puppeteer.Page) {
+ return await page.evaluate(() => {
+ return new Promise((resolve) => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(resolve);
+ });
+ });
+ });
+}
diff --git a/packages/rrweb-snapshot/typings/snapshot.d.ts b/packages/rrweb-snapshot/typings/snapshot.d.ts
index 72a7c4ca20..da722cd397 100644
--- a/packages/rrweb-snapshot/typings/snapshot.d.ts
+++ b/packages/rrweb-snapshot/typings/snapshot.d.ts
@@ -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;
diff --git a/packages/rrweb-snapshot/typings/types.d.ts b/packages/rrweb-snapshot/typings/types.d.ts
index bac3fcfb1f..5282993e41 100644
--- a/packages/rrweb-snapshot/typings/types.d.ts
+++ b/packages/rrweb-snapshot/typings/types.d.ts
@@ -49,6 +49,7 @@ export declare type serializedNode = (documentNode | documentTypeNode | elementN
export declare type serializedNodeWithId = serializedNode & {
id: number;
};
+export declare type serializedElementNodeWithId = Extract>;
export declare type tagMap = {
[key: string]: string;
};
diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts
index 3240e2f42a..955e20b101 100644
--- a/packages/rrweb/src/record/index.ts
+++ b/packages/rrweb/src/record/index.ts
@@ -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(
mutationCb: wrappedMutationEmit,
});
+ const stylesheetManager = new StylesheetManager({
+ mutationCb: wrappedMutationEmit,
+ });
+
const canvasManager = new CanvasManager({
recordCanvas,
mutationCb: wrappedCanvasMutationEmit,
@@ -241,7 +247,9 @@ function record(
sampling,
slimDOMOptions,
iframeManager,
+ stylesheetManager,
canvasManager,
+ keepIframeSrcFn,
},
mirror,
});
@@ -276,6 +284,9 @@ function record(
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 +295,9 @@ function record(
iframeManager.attachIframe(iframe, childSn, mirror);
shadowDomManager.observeAttachShadow(iframe);
},
+ onStylesheetLoad: (linkEl, childSn) => {
+ stylesheetManager.attachStylesheet(linkEl, childSn, mirror);
+ },
keepIframeSrcFn,
});
@@ -431,10 +445,12 @@ function record(
doc,
maskInputFn,
maskTextFn,
+ keepIframeSrcFn,
blockSelector,
slimDOMOptions,
mirror,
iframeManager,
+ stylesheetManager,
shadowDomManager,
canvasManager,
plugins:
diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts
index aef3aa4cb4..06247692f8 100644
--- a/packages/rrweb/src/record/mutation.ts
+++ b/packages/rrweb/src/record/mutation.ts
@@ -25,6 +25,7 @@ import {
isSerialized,
hasShadowRoot,
isSerializedIframe,
+ isSerializedStylesheet,
} from '../utils';
type DoubleLinkedListNode = {
@@ -163,12 +164,14 @@ export default class MutationBuffer {
private maskInputOptions: observerParam['maskInputOptions'];
private maskTextFn: observerParam['maskTextFn'];
private maskInputFn: observerParam['maskInputFn'];
+ private keepIframeSrcFn: observerParam['keepIframeSrcFn'];
private recordCanvas: observerParam['recordCanvas'];
private inlineImages: observerParam['inlineImages'];
private slimDOMOptions: observerParam['slimDOMOptions'];
private doc: observerParam['doc'];
private mirror: observerParam['mirror'];
private iframeManager: observerParam['iframeManager'];
+ private stylesheetManager: observerParam['stylesheetManager'];
private shadowDomManager: observerParam['shadowDomManager'];
private canvasManager: observerParam['canvasManager'];
@@ -183,12 +186,14 @@ export default class MutationBuffer {
'maskInputOptions',
'maskTextFn',
'maskInputFn',
+ 'keepIframeSrcFn',
'recordCanvas',
'inlineImages',
'slimDOMOptions',
'doc',
'mirror',
'iframeManager',
+ 'stylesheetManager',
'shadowDomManager',
'canvasManager',
] as const).forEach((key) => {
@@ -289,6 +294,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 +306,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 +317,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,9 +482,23 @@ export default class MutationBuffer {
) {
return;
}
+
let item: attributeCursor | undefined = this.attributes.find(
(a) => a.node === m.target,
);
+ if (
+ target.tagName === 'IFRAME' &&
+ m.attributeName === 'src' &&
+ !this.keepIframeSrcFn(value as string)
+ ) {
+ if (!(target as HTMLIFrameElement).contentDocument) {
+ // we can't record it directly as we can't see into it
+ // preserve the src attribute so a decision can be taken at replay time
+ m.attributeName = 'rr_src';
+ } else {
+ return;
+ }
+ }
if (!item) {
item = {
node: m.target,
@@ -517,7 +542,7 @@ export default class MutationBuffer {
// overwrite attribute if the mutations was triggered in same time
item.attributes[m.attributeName!] = transformAttribute(
this.doc,
- (m.target as HTMLElement).tagName,
+ target.tagName,
m.attributeName!,
value!,
);
diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts
new file mode 100644
index 0000000000..01a69744d7
--- /dev/null
+++ b/packages/rrweb/src/record/stylesheet-manager.ts
@@ -0,0 +1,45 @@
+import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
+import type { mutationCallBack } from '../types';
+
+export class StylesheetManager {
+ private trackedStylesheets: WeakSet = 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);
+ }
+}
diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts
index 63a10943dd..6440eab005 100644
--- a/packages/rrweb/src/types.ts
+++ b/packages/rrweb/src/types.ts
@@ -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,
@@ -266,6 +267,7 @@ export type observerParam = {
maskInputOptions: MaskInputOptions;
maskInputFn?: MaskInputFn;
maskTextFn?: MaskTextFn;
+ keepIframeSrcFn: KeepIframeSrcFn;
inlineStylesheet: boolean;
styleSheetRuleCb: styleSheetRuleCallback;
styleDeclarationCb: styleDeclarationCallback;
@@ -280,6 +282,7 @@ export type observerParam = {
doc: Document;
mirror: Mirror;
iframeManager: IframeManager;
+ stylesheetManager: StylesheetManager;
shadowDomManager: ShadowDomManager;
canvasManager: CanvasManager;
plugins: Array<{
@@ -300,12 +303,14 @@ export type MutationBufferParam = Pick<
| 'maskInputOptions'
| 'maskTextFn'
| 'maskInputFn'
+ | 'keepIframeSrcFn'
| 'recordCanvas'
| 'inlineImages'
| 'slimDOMOptions'
| 'doc'
| 'mirror'
| 'iframeManager'
+ | 'stylesheetManager'
| 'shadowDomManager'
| 'canvasManager'
>;
diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts
index 81108b58b5..d3c16a8919 100644
--- a/packages/rrweb/src/utils.ts
+++ b/packages/rrweb/src/utils.ts
@@ -354,6 +354,19 @@ export function isSerializedIframe(
return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n));
}
+export function isSerializedStylesheet(
+ n: TNode,
+ mirror: IMirror,
+): 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,
diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap
index 5af8b6dcf1..657094725f 100644
--- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap
+++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap
@@ -9096,7 +9096,7 @@ exports[`record integration tests should record dynamic CSS changes 1`] = `
]"
`;
-exports[`record integration tests should record input userTriggered values if userTriggeredOnInput is enabled 1`] = `
+exports[`record integration tests should record images inside iframe with blob url 1`] = `
"[
{
\\"type\\": 0,
@@ -9173,21 +9173,6 @@ exports[`record integration tests should record input userTriggered values if us
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 9
},
- {
- \\"type\\": 2,
- \\"tagName\\": \\"meta\\",
- \\"attributes\\": {
- \\"http-equiv\\": \\"X-UA-Compatible\\",
- \\"content\\": \\"ie=edge\\"
- },
- \\"childNodes\\": [],
- \\"id\\": 10
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 11
- },
{
\\"type\\": 2,
\\"tagName\\": \\"title\\",
@@ -9195,24 +9180,24 @@ exports[`record integration tests should record input userTriggered values if us
\\"childNodes\\": [
{
\\"type\\": 3,
- \\"textContent\\": \\"form fields\\",
- \\"id\\": 13
+ \\"textContent\\": \\"Frame with image\\",
+ \\"id\\": 11
}
],
- \\"id\\": 12
+ \\"id\\": 10
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
- \\"id\\": 14
+ \\"id\\": 12
}
],
\\"id\\": 4
},
{
\\"type\\": 3,
- \\"textContent\\": \\"\\\\n\\\\n \\",
- \\"id\\": 15
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 13
},
{
\\"type\\": 2,
@@ -9222,620 +9207,1737 @@ exports[`record integration tests should record input userTriggered values if us
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 15
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"iframe\\",
+ \\"attributes\\": {
+ \\"id\\": \\"four\\",
+ \\"frameborder\\": \\"0\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 16
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\",
\\"id\\": 17
},
{
\\"type\\": 2,
- \\"tagName\\": \\"form\\",
+ \\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 19
+ }
+ ],
+ \\"id\\": 18
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
+ \\"id\\": 20
+ }
+ ],
+ \\"id\\": 14
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"adds\\": [
+ {
+ \\"parentId\\": 16,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"rootId\\": 21,
+ \\"id\\": 22
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 21,
+ \\"id\\": 25
},
{
\\"type\\": 2,
- \\"tagName\\": \\"label\\",
+ \\"tagName\\": \\"meta\\",
\\"attributes\\": {
- \\"for\\": \\"text\\"
+ \\"charset\\": \\"UTF-8\\"
},
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 21
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"input\\",
- \\"attributes\\": {
- \\"type\\": \\"text\\"
- },
- \\"childNodes\\": [],
- \\"id\\": 22
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 23
- }
- ],
- \\"id\\": 20
+ \\"childNodes\\": [],
+ \\"rootId\\": 21,
+ \\"id\\": 26
},
{
\\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 24
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 21,
+ \\"id\\": 27
},
{
\\"type\\": 2,
- \\"tagName\\": \\"label\\",
- \\"attributes\\": {},
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 26
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"input\\",
- \\"attributes\\": {
- \\"type\\": \\"radio\\",
- \\"name\\": \\"toggle\\",
- \\"value\\": \\"on\\"
- },
- \\"childNodes\\": [],
- \\"id\\": 27
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 28
- }
- ],
- \\"id\\": 25
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"http-equiv\\": \\"X-UA-Compatible\\",
+ \\"content\\": \\"IE=edge\\"
+ },
+ \\"childNodes\\": [],
+ \\"rootId\\": 21,
+ \\"id\\": 28
},
{
\\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 21,
\\"id\\": 29
},
{
\\"type\\": 2,
- \\"tagName\\": \\"label\\",
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"rootId\\": 21,
+ \\"id\\": 30
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 21,
+ \\"id\\": 31
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 31
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"input\\",
- \\"attributes\\": {
- \\"type\\": \\"radio\\",
- \\"name\\": \\"toggle\\",
- \\"value\\": \\"off\\",
- \\"checked\\": true
- },
- \\"childNodes\\": [],
- \\"id\\": 32
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
+ \\"textContent\\": \\"Image with blob:url\\",
+ \\"rootId\\": 21,
\\"id\\": 33
}
],
- \\"id\\": 30
+ \\"rootId\\": 21,
+ \\"id\\": 32
},
{
\\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 21,
\\"id\\": 34
+ }
+ ],
+ \\"rootId\\": 21,
+ \\"id\\": 24
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 21,
+ \\"id\\": 35
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 21,
+ \\"id\\": 37
},
{
\\"type\\": 2,
- \\"tagName\\": \\"label\\",
- \\"attributes\\": {
- \\"for\\": \\"checkbox\\"
- },
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 36
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"input\\",
- \\"attributes\\": {
- \\"type\\": \\"checkbox\\"
- },
- \\"childNodes\\": [],
- \\"id\\": 37
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 38
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"rootId\\": 21,
+ \\"id\\": 39
}
],
- \\"id\\": 35
+ \\"rootId\\": 21,
+ \\"id\\": 38
},
{
\\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 39
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"label\\",
- \\"attributes\\": {
- \\"for\\": \\"textarea\\"
- },
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 41
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"textarea\\",
- \\"attributes\\": {
- \\"name\\": \\"\\",
- \\"id\\": \\"\\",
- \\"cols\\": \\"30\\",
- \\"rows\\": \\"10\\"
- },
- \\"childNodes\\": [],
- \\"id\\": 42
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 43
- }
- ],
+ \\"textContent\\": \\"\\\\n \\\\n\\\\n\\",
+ \\"rootId\\": 21,
\\"id\\": 40
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 44
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"label\\",
- \\"attributes\\": {
- \\"for\\": \\"select\\"
- },
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 46
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"select\\",
- \\"attributes\\": {
- \\"name\\": \\"\\",
- \\"id\\": \\"\\",
- \\"value\\": \\"1\\"
- },
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 48
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"option\\",
- \\"attributes\\": {
- \\"value\\": \\"1\\",
- \\"selected\\": true
- },
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"1\\",
- \\"id\\": 50
- }
- ],
- \\"id\\": 49
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 51
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"option\\",
- \\"attributes\\": {
- \\"value\\": \\"2\\"
- },
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"2\\",
- \\"id\\": 53
- }
- ],
- \\"id\\": 52
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 54
- }
- ],
- \\"id\\": 47
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 55
- }
- ],
- \\"id\\": 45
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 56
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"label\\",
- \\"attributes\\": {
- \\"for\\": \\"password\\"
- },
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 58
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"input\\",
- \\"attributes\\": {
- \\"type\\": \\"password\\"
- },
- \\"childNodes\\": [],
- \\"id\\": 59
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 60
- }
- ],
- \\"id\\": 57
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 61
- }
- ],
- \\"id\\": 18
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\\\n \\",
- \\"id\\": 62
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"script\\",
- \\"attributes\\": {},
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
- \\"id\\": 64
}
],
- \\"id\\": 63
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
- \\"id\\": 65
+ \\"rootId\\": 21,
+ \\"id\\": 36
}
],
- \\"id\\": 16
+ \\"rootId\\": 21,
+ \\"id\\": 23
}
],
- \\"id\\": 3
+ \\"id\\": 21
}
- ],
- \\"id\\": 1
- },
- \\"initialOffset\\": {
- \\"left\\": 0,
- \\"top\\": 0
- }
+ }
+ ],
+ \\"removes\\": [],
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 2,
- \\"type\\": 5,
- \\"id\\": 22
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 36,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"img\\",
+ \\"attributes\\": {
+ \\"src\\": \\"blob:http://localhost:3030/...\\",
+ \\"rr_dataURL\\": \\"data:image/png;base64,...\\"
+ },
+ \\"childNodes\\": [],
+ \\"rootId\\": 21,
+ \\"id\\": 41
+ }
+ }
+ ]
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"t\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 22
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 41,
+ \\"attributes\\": {
+ \\"crossorigin\\": \\"anonymous\\"
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"te\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 22
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 41,
+ \\"attributes\\": {
+ \\"crossorigin\\": null
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
}
- },
+ }
+]"
+`;
+
+exports[`record integration tests should record images inside iframe with blob url after iframe was reloaded 1`] = `
+"[
{
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"tes\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 22
- }
+ \\"type\\": 0,
+ \\"data\\": {}
},
{
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"test\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 22
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 2,
- \\"type\\": 1,
- \\"id\\": 27
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 2,
- \\"type\\": 6,
- \\"id\\": 22
- }
+ \\"type\\": 1,
+ \\"data\\": {}
},
{
- \\"type\\": 3,
+ \\"type\\": 4,
\\"data\\": {
- \\"source\\": 2,
- \\"type\\": 5,
- \\"id\\": 27
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
}
},
{
- \\"type\\": 3,
+ \\"type\\": 2,
\\"data\\": {
- \\"source\\": 2,
- \\"type\\": 0,
- \\"id\\": 27
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 5
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"charset\\": \\"UTF-8\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 8
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 9
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"Frame 2\\",
+ \\"id\\": 11
+ }
+ ],
+ \\"id\\": 10
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 12
+ }
+ ],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 13
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n frame 2\\\\n \\\\n \\",
+ \\"id\\": 15
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 17
+ }
+ ],
+ \\"id\\": 16
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
+ \\"id\\": 18
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 20
+ }
+ ],
+ \\"id\\": 19
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n\\\\n\\",
+ \\"id\\": 21
+ }
+ ],
+ \\"id\\": 14
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 2,
- \\"type\\": 2,
- \\"id\\": 27
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 14,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"iframe\\",
+ \\"attributes\\": {
+ \\"id\\": \\"five\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 22
+ }
+ }
+ ]
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"on\\",
- \\"isChecked\\": true,
- \\"userTriggered\\": true,
- \\"id\\": 27
+ \\"source\\": 0,
+ \\"adds\\": [
+ {
+ \\"parentId\\": 22,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"rootId\\": 23,
+ \\"id\\": 25
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"rootId\\": 23,
+ \\"id\\": 26
+ }
+ ],
+ \\"rootId\\": 23,
+ \\"id\\": 24
+ }
+ ],
+ \\"compatMode\\": \\"BackCompat\\",
+ \\"id\\": 23
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"off\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": false,
- \\"id\\": 32
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 2,
- \\"type\\": 1,
- \\"id\\": 37
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 2,
- \\"type\\": 6,
- \\"id\\": 27
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 2,
- \\"type\\": 5,
- \\"id\\": 37
+ \\"source\\": 0,
+ \\"adds\\": [
+ {
+ \\"parentId\\": 22,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"rootId\\": 27,
+ \\"id\\": 28
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 27,
+ \\"id\\": 31
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"charset\\": \\"UTF-8\\"
+ },
+ \\"childNodes\\": [],
+ \\"rootId\\": 27,
+ \\"id\\": 32
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 27,
+ \\"id\\": 33
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"http-equiv\\": \\"X-UA-Compatible\\",
+ \\"content\\": \\"IE=edge\\"
+ },
+ \\"childNodes\\": [],
+ \\"rootId\\": 27,
+ \\"id\\": 34
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 27,
+ \\"id\\": 35
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"rootId\\": 27,
+ \\"id\\": 36
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 27,
+ \\"id\\": 37
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"Image with blob:url\\",
+ \\"rootId\\": 27,
+ \\"id\\": 39
+ }
+ ],
+ \\"rootId\\": 27,
+ \\"id\\": 38
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 27,
+ \\"id\\": 40
+ }
+ ],
+ \\"rootId\\": 27,
+ \\"id\\": 30
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 27,
+ \\"id\\": 41
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"rootId\\": 27,
+ \\"id\\": 43
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"rootId\\": 27,
+ \\"id\\": 45
+ }
+ ],
+ \\"rootId\\": 27,
+ \\"id\\": 44
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n\\\\n\\",
+ \\"rootId\\": 27,
+ \\"id\\": 46
+ }
+ ],
+ \\"rootId\\": 27,
+ \\"id\\": 42
+ }
+ ],
+ \\"rootId\\": 27,
+ \\"id\\": 29
+ }
+ ],
+ \\"id\\": 27
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 2,
- \\"type\\": 0,
- \\"id\\": 37
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 42,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"img\\",
+ \\"attributes\\": {
+ \\"src\\": \\"blob:http://localhost:3030/...\\",
+ \\"rr_dataURL\\": \\"data:image/png;base64,...\\"
+ },
+ \\"childNodes\\": [],
+ \\"rootId\\": 27,
+ \\"id\\": 47
+ }
+ }
+ ]
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 2,
- \\"type\\": 2,
- \\"id\\": 37
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 47,
+ \\"attributes\\": {
+ \\"crossorigin\\": \\"anonymous\\"
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"on\\",
- \\"isChecked\\": true,
- \\"userTriggered\\": true,
- \\"id\\": 37
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 47,
+ \\"attributes\\": {
+ \\"crossorigin\\": null
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
}
- },
+ }
+]"
+`;
+
+exports[`record integration tests should record images with blob url 1`] = `
+"[
{
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 2,
- \\"type\\": 6,
- \\"id\\": 37
- }
+ \\"type\\": 0,
+ \\"data\\": {}
},
{
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 2,
- \\"type\\": 5,
- \\"id\\": 59
- }
+ \\"type\\": 1,
+ \\"data\\": {}
},
{
- \\"type\\": 3,
+ \\"type\\": 4,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"*\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 59
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
}
},
{
- \\"type\\": 3,
+ \\"type\\": 2,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"**\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 59
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"***\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 59
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 5
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"charset\\": \\"UTF-8\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"http-equiv\\": \\"X-UA-Compatible\\",
+ \\"content\\": \\"IE=edge\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 8
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 9
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 10
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 11
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"Image with blob:url\\",
+ \\"id\\": 13
+ }
+ ],
+ \\"id\\": 12
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 14
+ }
+ ],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 15
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 17
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 19
+ }
+ ],
+ \\"id\\": 18
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\",
+ \\"id\\": 20
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 22
+ }
+ ],
+ \\"id\\": 21
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
+ \\"id\\": 23
+ }
+ ],
+ \\"id\\": 16
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"****\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 59
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 16,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"img\\",
+ \\"attributes\\": {
+ \\"src\\": \\"blob:http://localhost:3030/...\\",
+ \\"rr_dataURL\\": \\"data:image/png;base64,...\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 24
+ }
+ }
+ ]
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"*****\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 59
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 24,
+ \\"attributes\\": {
+ \\"crossorigin\\": \\"anonymous\\"
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"******\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 59
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 24,
+ \\"attributes\\": {
+ \\"crossorigin\\": null
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
}
- },
+ }
+]"
+`;
+
+exports[`record integration tests should record input userTriggered values if userTriggeredOnInput is enabled 1`] = `
+"[
{
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"*******\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 59
- }
+ \\"type\\": 0,
+ \\"data\\": {}
},
{
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"********\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 59
- }
+ \\"type\\": 1,
+ \\"data\\": {}
},
{
- \\"type\\": 3,
+ \\"type\\": 4,
\\"data\\": {
- \\"source\\": 2,
- \\"type\\": 6,
- \\"id\\": 59
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
}
},
{
- \\"type\\": 3,
+ \\"type\\": 2,
\\"data\\": {
- \\"source\\": 2,
- \\"type\\": 5,
- \\"id\\": 42
- }
- },
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 5
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"charset\\": \\"UTF-8\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 8
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 9
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"http-equiv\\": \\"X-UA-Compatible\\",
+ \\"content\\": \\"ie=edge\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 10
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 11
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"form fields\\",
+ \\"id\\": 13
+ }
+ ],
+ \\"id\\": 12
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 14
+ }
+ ],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n\\\\n \\",
+ \\"id\\": 15
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 17
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"form\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 19
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"label\\",
+ \\"attributes\\": {
+ \\"for\\": \\"text\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 21
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"input\\",
+ \\"attributes\\": {
+ \\"type\\": \\"text\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 22
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 23
+ }
+ ],
+ \\"id\\": 20
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 24
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"label\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 26
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"input\\",
+ \\"attributes\\": {
+ \\"type\\": \\"radio\\",
+ \\"name\\": \\"toggle\\",
+ \\"value\\": \\"on\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 27
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 28
+ }
+ ],
+ \\"id\\": 25
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 29
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"label\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 31
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"input\\",
+ \\"attributes\\": {
+ \\"type\\": \\"radio\\",
+ \\"name\\": \\"toggle\\",
+ \\"value\\": \\"off\\",
+ \\"checked\\": true
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 32
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 33
+ }
+ ],
+ \\"id\\": 30
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 34
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"label\\",
+ \\"attributes\\": {
+ \\"for\\": \\"checkbox\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 36
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"input\\",
+ \\"attributes\\": {
+ \\"type\\": \\"checkbox\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 37
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 38
+ }
+ ],
+ \\"id\\": 35
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 39
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"label\\",
+ \\"attributes\\": {
+ \\"for\\": \\"textarea\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 41
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"textarea\\",
+ \\"attributes\\": {
+ \\"name\\": \\"\\",
+ \\"id\\": \\"\\",
+ \\"cols\\": \\"30\\",
+ \\"rows\\": \\"10\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 42
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 43
+ }
+ ],
+ \\"id\\": 40
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 44
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"label\\",
+ \\"attributes\\": {
+ \\"for\\": \\"select\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 46
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"select\\",
+ \\"attributes\\": {
+ \\"name\\": \\"\\",
+ \\"id\\": \\"\\",
+ \\"value\\": \\"1\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 48
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"option\\",
+ \\"attributes\\": {
+ \\"value\\": \\"1\\",
+ \\"selected\\": true
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"1\\",
+ \\"id\\": 50
+ }
+ ],
+ \\"id\\": 49
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 51
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"option\\",
+ \\"attributes\\": {
+ \\"value\\": \\"2\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"2\\",
+ \\"id\\": 53
+ }
+ ],
+ \\"id\\": 52
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 54
+ }
+ ],
+ \\"id\\": 47
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 55
+ }
+ ],
+ \\"id\\": 45
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 56
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"label\\",
+ \\"attributes\\": {
+ \\"for\\": \\"password\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 58
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"input\\",
+ \\"attributes\\": {
+ \\"type\\": \\"password\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 59
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 60
+ }
+ ],
+ \\"id\\": 57
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 61
+ }
+ ],
+ \\"id\\": 18
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\",
+ \\"id\\": 62
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 64
+ }
+ ],
+ \\"id\\": 63
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
+ \\"id\\": 65
+ }
+ ],
+ \\"id\\": 16
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 5,
+ \\"id\\": 22
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"t\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 22
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"te\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 22
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"tes\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 22
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"test\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 22
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 1,
+ \\"id\\": 27
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 6,
+ \\"id\\": 22
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 5,
+ \\"id\\": 27
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 0,
+ \\"id\\": 27
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 2,
+ \\"id\\": 27
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"on\\",
+ \\"isChecked\\": true,
+ \\"userTriggered\\": true,
+ \\"id\\": 27
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"off\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": false,
+ \\"id\\": 32
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 1,
+ \\"id\\": 37
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 6,
+ \\"id\\": 27
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 5,
+ \\"id\\": 37
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 0,
+ \\"id\\": 37
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 2,
+ \\"id\\": 37
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"on\\",
+ \\"isChecked\\": true,
+ \\"userTriggered\\": true,
+ \\"id\\": 37
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 6,
+ \\"id\\": 37
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 5,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"*\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"**\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"***\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"****\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"*****\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"******\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"*******\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"********\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 6,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 5,
+ \\"id\\": 42
+ }
+ },
{
\\"type\\": 3,
\\"data\\": {
@@ -9939,41 +11041,350 @@ exports[`record integration tests should record input userTriggered values if us
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"textarea te\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 42
+ \\"source\\": 5,
+ \\"text\\": \\"textarea te\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"textarea tes\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"textarea test\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": true,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"1\\",
+ \\"isChecked\\": false,
+ \\"userTriggered\\": false,
+ \\"id\\": 47
+ }
+ }
+]"
+`;
+
+exports[`record integration tests should record mutations in iframes accross pages 1`] = `
+"[
+ {
+ \\"type\\": 0,
+ \\"data\\": {}
+ },
+ {
+ \\"type\\": 1,
+ \\"data\\": {}
+ },
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 5
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"charset\\": \\"UTF-8\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 8
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 9
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"Frame 2\\",
+ \\"id\\": 11
+ }
+ ],
+ \\"id\\": 10
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 12
+ }
+ ],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 13
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n frame 2\\\\n \\\\n \\",
+ \\"id\\": 15
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 17
+ }
+ ],
+ \\"id\\": 16
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
+ \\"id\\": 18
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 20
+ }
+ ],
+ \\"id\\": 19
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n\\\\n\\",
+ \\"id\\": 21
+ }
+ ],
+ \\"id\\": 14
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 14,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"iframe\\",
+ \\"attributes\\": {
+ \\"id\\": \\"five\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 22
+ }
+ }
+ ]
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"textarea tes\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 42
+ \\"source\\": 0,
+ \\"adds\\": [
+ {
+ \\"parentId\\": 22,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"rootId\\": 23,
+ \\"id\\": 25
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"rootId\\": 23,
+ \\"id\\": 26
+ }
+ ],
+ \\"rootId\\": 23,
+ \\"id\\": 24
+ }
+ ],
+ \\"compatMode\\": \\"BackCompat\\",
+ \\"id\\": 23
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"textarea test\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": true,
- \\"id\\": 42
+ \\"source\\": 0,
+ \\"adds\\": [
+ {
+ \\"parentId\\": 22,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"rootId\\": 27,
+ \\"id\\": 29
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"rootId\\": 27,
+ \\"id\\": 30
+ }
+ ],
+ \\"rootId\\": 27,
+ \\"id\\": 28
+ }
+ ],
+ \\"id\\": 27
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"1\\",
- \\"isChecked\\": false,
- \\"userTriggered\\": false,
- \\"id\\": 47
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 30,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"div\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"rootId\\": 27,
+ \\"id\\": 31
+ }
+ }
+ ]
}
}
]"
diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap
index e65b2372c8..2c2b7c8d0d 100644
--- a/packages/rrweb/test/__snapshots__/record.test.ts.snap
+++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap
@@ -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`] = `
"[
{
diff --git a/packages/rrweb/test/html/assets/robot.png b/packages/rrweb/test/html/assets/robot.png
new file mode 100644
index 0000000000..cc486cc8b7
Binary files /dev/null and b/packages/rrweb/test/html/assets/robot.png differ
diff --git a/packages/rrweb/test/html/frame-image-blob-url.html b/packages/rrweb/test/html/frame-image-blob-url.html
new file mode 100644
index 0000000000..038ced1655
--- /dev/null
+++ b/packages/rrweb/test/html/frame-image-blob-url.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Frame with image
+
+
+
+
+
diff --git a/packages/rrweb/test/html/image-blob-url.html b/packages/rrweb/test/html/image-blob-url.html
new file mode 100644
index 0000000000..4dd3f60855
--- /dev/null
+++ b/packages/rrweb/test/html/image-blob-url.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Image with blob:url
+
+
+
+
+
diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts
index fbb23378e5..a5c667d2c3 100644
--- a/packages/rrweb/test/integration.test.ts
+++ b/packages/rrweb/test/integration.test.ts
@@ -495,6 +495,57 @@ describe('record integration tests', function (this: ISuite) {
assertSnapshot(snapshots);
});
+ it('should record images with blob url', async () => {
+ const page: puppeteer.Page = await browser.newPage();
+ page.on('console', (msg) => console.log(msg.text()));
+ await page.goto(`${serverURL}/html`);
+ page.setContent(
+ getHtml.call(this, 'image-blob-url.html', { inlineImages: true }),
+ );
+ await page.waitForResponse(`${serverURL}/html/assets/robot.png`);
+ await page.waitForSelector('img'); // wait for image to get added
+ await waitForRAF(page); // wait for image to be captured
+
+ const snapshots = await page.evaluate('window.snapshots');
+ assertSnapshot(snapshots);
+ });
+
+ it('should record images inside iframe with blob url', async () => {
+ const page: puppeteer.Page = await browser.newPage();
+ page.on('console', (msg) => console.log(msg.text()));
+ await page.goto(`${serverURL}/html`);
+ await page.setContent(
+ getHtml.call(this, 'frame-image-blob-url.html', { inlineImages: true }),
+ );
+ await page.waitForResponse(`${serverURL}/html/assets/robot.png`);
+ await page.waitForTimeout(50); // wait for image to get added
+ await waitForRAF(page); // wait for image to be captured
+
+ const snapshots = await page.evaluate('window.snapshots');
+ assertSnapshot(snapshots);
+ });
+
+ it('should record images inside iframe with blob url after iframe was reloaded', async () => {
+ const page: puppeteer.Page = await browser.newPage();
+ page.on('console', (msg) => console.log(msg.text()));
+ await page.goto(`${serverURL}/html`);
+ await page.setContent(
+ getHtml.call(this, 'frame2.html', { inlineImages: true }),
+ );
+ await page.waitForSelector('iframe'); // wait for iframe to get added
+ await waitForRAF(page); // wait for iframe to load
+ page.evaluate(() => {
+ const iframe = document.querySelector('iframe')!;
+ iframe.setAttribute('src', '/html/image-blob-url.html');
+ });
+ await page.waitForResponse(`${serverURL}/html/assets/robot.png`); // wait for image to get loaded
+ await page.waitForTimeout(50); // wait for image to get added
+ await waitForRAF(page); // wait for image to be captured
+
+ const snapshots = await page.evaluate('window.snapshots');
+ assertSnapshot(snapshots);
+ });
+
it('should record shadow DOM', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
@@ -585,6 +636,34 @@ describe('record integration tests', function (this: ISuite) {
assertSnapshot(snapshots);
});
+ it('should record mutations in iframes accross pages', async () => {
+ const page: puppeteer.Page = await browser.newPage();
+ await page.goto(`${serverURL}/html`);
+ page.on('console', (msg) => console.log(msg.text()));
+ await page.setContent(getHtml.call(this, 'frame2.html'));
+
+ await page.waitForSelector('iframe'); // wait for iframe to get added
+ await waitForRAF(page); // wait for iframe to load
+
+ page.evaluate((serverURL) => {
+ const iframe = document.querySelector('iframe')!;
+ iframe.setAttribute('src', `${serverURL}/html`); // load new page
+ }, serverURL);
+
+ await page.waitForResponse(`${serverURL}/html`); // wait for iframe to load pt1
+ await waitForRAF(page); // wait for iframe to load pt2
+
+ await page.evaluate(() => {
+ const iframeDocument = document.querySelector('iframe')!.contentDocument!;
+ const div = iframeDocument.createElement('div');
+ iframeDocument.body.appendChild(div);
+ });
+
+ await waitForRAF(page); // wait for snapshot to be updated
+ const snapshots = await page.evaluate('window.snapshots');
+ assertSnapshot(snapshots);
+ });
+
it('should mask texts', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts
index 969f11ff5d..b45a6ce46c 100644
--- a/packages/rrweb/test/record.test.ts
+++ b/packages/rrweb/test/record.test.ts
@@ -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 &&
diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts
index 89bc28453b..487f1aab48 100644
--- a/packages/rrweb/test/utils.ts
+++ b/packages/rrweb/test/utils.ts
@@ -153,20 +153,54 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript
}
}
+
+ // strip blob:urls as they are different every time
+ console.log(
+ a.attributes.src,
+ 'src' in a.attributes &&
+ a.attributes.src &&
+ typeof a.attributes.src === 'string',
+ );
});
s.data.adds.forEach((add) => {
- if (
- add.node.type === NodeType.Element &&
- 'style' in add.node.attributes &&
- typeof add.node.attributes.style === 'string' &&
- coordinatesReg.test(add.node.attributes.style)
- ) {
- add.node.attributes.style = add.node.attributes.style.replace(
- coordinatesReg,
- '$1: Npx',
- );
+ if (add.node.type === NodeType.Element) {
+ if (
+ 'style' in add.node.attributes &&
+ typeof add.node.attributes.style === 'string' &&
+ coordinatesReg.test(add.node.attributes.style)
+ ) {
+ add.node.attributes.style = add.node.attributes.style.replace(
+ coordinatesReg,
+ '$1: Npx',
+ );
+ }
+ coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript
+
+ // strip blob:urls as they are different every time
+ if (
+ 'src' in add.node.attributes &&
+ add.node.attributes.src &&
+ typeof add.node.attributes.src === 'string' &&
+ add.node.attributes.src.startsWith('blob:')
+ ) {
+ add.node.attributes.src = add.node.attributes.src.replace(
+ /[\w-]+$/,
+ '...',
+ );
+ }
+
+ // strip rr_dataURL as they are not consistent
+ if (
+ 'rr_dataURL' in add.node.attributes &&
+ add.node.attributes.rr_dataURL &&
+ typeof add.node.attributes.rr_dataURL === 'string'
+ ) {
+ add.node.attributes.rr_dataURL = add.node.attributes.rr_dataURL.replace(
+ /,.+$/,
+ ',...',
+ );
+ }
}
- coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript
});
}
delete (s as Optional).timestamp;
@@ -556,6 +590,7 @@ export function generateRecordSnippet(options: recordOptions) {
userTriggeredOnInput: ${options.userTriggeredOnInput},
maskTextFn: ${options.maskTextFn},
recordCanvas: ${options.recordCanvas},
+ inlineImages: ${options.inlineImages},
plugins: ${options.plugins}
});
`;
diff --git a/packages/rrweb/typings/record/mutation.d.ts b/packages/rrweb/typings/record/mutation.d.ts
index 930d247848..c2108a959c 100644
--- a/packages/rrweb/typings/record/mutation.d.ts
+++ b/packages/rrweb/typings/record/mutation.d.ts
@@ -19,12 +19,14 @@ export default class MutationBuffer {
private maskInputOptions;
private maskTextFn;
private maskInputFn;
+ private keepIframeSrcFn;
private recordCanvas;
private inlineImages;
private slimDOMOptions;
private doc;
private mirror;
private iframeManager;
+ private stylesheetManager;
private shadowDomManager;
private canvasManager;
init(options: MutationBufferParam): void;
diff --git a/packages/rrweb/typings/record/stylesheet-manager.d.ts b/packages/rrweb/typings/record/stylesheet-manager.d.ts
new file mode 100644
index 0000000000..022fc54445
--- /dev/null
+++ b/packages/rrweb/typings/record/stylesheet-manager.d.ts
@@ -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;
+}
diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts
index 50a7109a75..f3ed7615b5 100644
--- a/packages/rrweb/typings/types.d.ts
+++ b/packages/rrweb/typings/types.d.ts
@@ -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,
@@ -179,6 +180,7 @@ export declare type observerParam = {
maskInputOptions: MaskInputOptions;
maskInputFn?: MaskInputFn;
maskTextFn?: MaskTextFn;
+ keepIframeSrcFn: KeepIframeSrcFn;
inlineStylesheet: boolean;
styleSheetRuleCb: styleSheetRuleCallback;
styleDeclarationCb: styleDeclarationCallback;
@@ -193,6 +195,7 @@ export declare type observerParam = {
doc: Document;
mirror: Mirror;
iframeManager: IframeManager;
+ stylesheetManager: StylesheetManager;
shadowDomManager: ShadowDomManager;
canvasManager: CanvasManager;
plugins: Array<{
@@ -201,7 +204,7 @@ export declare type observerParam = {
options: unknown;
}>;
};
-export declare type MutationBufferParam = Pick;
+export declare type MutationBufferParam = Pick;
export declare type hooksParam = {
mutation?: mutationCallBack;
mousemove?: mousemoveCallBack;
diff --git a/packages/rrweb/typings/utils.d.ts b/packages/rrweb/typings/utils.d.ts
index 0fabf6f199..ed87d5fee1 100644
--- a/packages/rrweb/typings/utils.d.ts
+++ b/packages/rrweb/typings/utils.d.ts
@@ -28,6 +28,7 @@ export declare type AppendedIframe = {
builtNode: HTMLIFrameElement | RRIFrameElement;
};
export declare function isSerializedIframe(n: TNode, mirror: IMirror): boolean;
+export declare function isSerializedStylesheet(n: TNode, mirror: IMirror): boolean;
export declare function getBaseDimension(node: Node, rootIframe: Node): DocumentDimension;
export declare function hasShadowRoot(n: T): n is T & {
shadowRoot: ShadowRoot;