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 @@ + + + This is a robot + + + 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;