From 1cabfbf71e2cfdb5b86402b250a49422cfeb4ba0 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 24 Oct 2022 16:42:05 +0200 Subject: [PATCH 01/61] Add `recordCrossOriginIframe` setting --- packages/rrweb/src/record/index.ts | 20 ++- packages/rrweb/src/types.ts | 1 + .../test/record/cross-origin-iframes.test.ts | 156 ++++++++++++++++++ 3 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 packages/rrweb/test/record/cross-origin-iframes.test.ts diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 26e07fc83a..ee4a193631 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -68,6 +68,7 @@ function record( dataURLOptions = {}, mousemoveWait, recordCanvas = false, + recordCrossOriginIframes = false, userTriggeredOnInput = false, collectFonts = false, inlineImages = false, @@ -76,8 +77,12 @@ function record( ignoreCSSAttributes = new Set([]), } = options; + const inEmittingFrame = recordCrossOriginIframes + ? location.ancestorOrigins.length === 0 + : true; + // runtime checks for user options - if (!emit) { + if (inEmittingFrame && !emit) { throw new Error('emit function is required'); } // move departed options to new options @@ -169,7 +174,18 @@ function record( mutationBuffers.forEach((buf) => buf.unfreeze()); } - emit(eventProcessor(e), isCheckout); + if (inEmittingFrame) { + emit!(eventProcessor(e), isCheckout); + } else { + window.parent.postMessage( + { + type: 'rrweb', + data: eventProcessor(e), + }, + '*', + ); + } + if (e.type === EventType.FullSnapshot) { lastFullSnapshotEvent = e; incrementalSnapshotCount = 0; diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 235005073a..04085a0487 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -260,6 +260,7 @@ export type recordOptions = { sampling?: SamplingStrategy; dataURLOptions?: DataURLOptions; recordCanvas?: boolean; + recordCrossOriginIframes?: boolean; userTriggeredOnInput?: boolean; collectFonts?: boolean; inlineImages?: boolean; diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts new file mode 100644 index 0000000000..d8e13fb15e --- /dev/null +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -0,0 +1,156 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import type * as puppeteer from 'puppeteer'; +import type { + recordOptions, + listenerHandler, + eventWithTime, +} from '../../src/types'; +import { EventType, IncrementalSource } from '../../src/types'; +import { + assertSnapshot, + getServerURL, + launchPuppeteer, + startServer, + stripBase64, + waitForRAF, +} from '../utils'; +import type * as http from 'http'; + +interface ISuite { + code: string; + browser: puppeteer.Browser; + page: puppeteer.Page; + events: eventWithTime[]; + server: http.Server; + serverURL: string; +} + +interface IWindow extends Window { + rrweb: { + record: ( + options: recordOptions, + ) => listenerHandler | undefined; + addCustomEvent(tag: string, payload: T): void; + }; + emit: (e: eventWithTime) => undefined; + snapshots: eventWithTime[]; +} + +const setup = function (this: ISuite, content: string): ISuite { + const ctx = {} as ISuite; + + beforeAll(async () => { + ctx.browser = await launchPuppeteer(); + ctx.server = await startServer(); + ctx.serverURL = getServerURL(ctx.server); + // ctx.serverB = await startServer(); + // ctx.serverBURL = getServerURL(ctx.serverB); + + const bundlePath = path.resolve(__dirname, '../../dist/rrweb.js'); + ctx.code = fs.readFileSync(bundlePath, 'utf8'); + }); + + beforeEach(async () => { + ctx.page = await ctx.browser.newPage(); + await ctx.page.goto('about:blank'); + await ctx.page.setContent( + content.replace(/\{SERVER_URL\}/g, ctx.serverURL), + ); + // await ctx.page.evaluate(ctx.code); + ctx.events = []; + await ctx.page.exposeFunction('emit', (e: eventWithTime) => { + if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) { + return; + } + ctx.events.push(e); + }); + + ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); + await injectRecordScript(ctx.page.mainFrame()); + + async function injectRecordScript(frame: puppeteer.Frame) { + await frame.addScriptTag({ + path: path.resolve(__dirname, '../../dist/rrweb.js'), + }); + await frame.evaluate(() => { + ((window as unknown) as IWindow).snapshots = []; + const { record } = ((window as unknown) as IWindow).rrweb; + record({ + recordCrossOriginIframes: true, + emit(event) { + ((window as unknown) as IWindow).snapshots.push(event); + ((window as unknown) as IWindow).emit(event); + }, + }); + }); + + for (const child of frame.childFrames()) { + await injectRecordScript(child); + } + } + }); + + afterEach(async () => { + await ctx.page.close(); + }); + + afterAll(async () => { + await ctx.browser.close(); + ctx.server.close(); + // ctx.serverB.close(); + }); + + return ctx; +}; + +describe('record webgl', function (this: ISuite) { + jest.setTimeout(100_000); + + const ctx: ISuite = setup.call( + this, + ` + + + + + + + `, + ); + + it("won't emit events if it isn't in the top level iframe", async () => { + const el = (await ctx.page.$( + 'body > iframe', + )) as puppeteer.ElementHandle; + + const frame = await el.contentFrame(); + const events = await frame?.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + expect(events).toMatchObject([]); + // await ctx.page.waitForTimeout(25_000); + // await ctx.page.waitForTimeout(50); + + // const lastEvent = ctx.events[ctx.events.length - 1]; + // expect(lastEvent).toMatchObject({ + // data: { + // source: IncrementalSource.CanvasMutation, + // type: CanvasContext.WebGL, + // commands: [ + // { + // args: [16384], + // property: 'clear', + // }, + // ], + // }, + // }); + // assertSnapshot(ctx.events); + }); + it('will emit events if it is in the top level iframe', async () => { + const events = await ctx.page.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + expect(events.length).not.toBe(0); + }); +}); From bd0d6332432409d76945dc241544832755d3701f Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 25 Oct 2022 12:41:57 +0200 Subject: [PATCH 02/61] Set up messaging between iframes --- packages/rrweb/src/record/iframe-manager.ts | 37 ++++++++++++++++++- packages/rrweb/src/record/index.ts | 16 ++++---- packages/rrweb/src/types.ts | 7 ++++ .../test/record/cross-origin-iframes.test.ts | 9 +++++ 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index ed2bef8534..00529f6485 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -1,23 +1,42 @@ import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot'; -import type { mutationCallBack } from '../types'; +import type { + CrossOriginIframeMessageEvent, + eventWithTime, + mutationCallBack, +} from '../types'; import type { StylesheetManager } from './stylesheet-manager'; export class IframeManager { private iframes: WeakMap = new WeakMap(); + private crossOriginIframeMap: WeakMap< + MessageEventSource, + HTMLIFrameElement + > = new WeakMap(); private mutationCb: mutationCallBack; + private wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void; private loadListener?: (iframeEl: HTMLIFrameElement) => unknown; private stylesheetManager: StylesheetManager; + private recordCrossOriginIframes: boolean; constructor(options: { mutationCb: mutationCallBack; stylesheetManager: StylesheetManager; + recordCrossOriginIframes: boolean; + wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void; }) { this.mutationCb = options.mutationCb; + this.wrappedEmit = options.wrappedEmit; this.stylesheetManager = options.stylesheetManager; + this.recordCrossOriginIframes = options.recordCrossOriginIframes; + if (this.recordCrossOriginIframes) { + window.addEventListener('message', this.handleMessage.bind(this)); + } } public addIframe(iframeEl: HTMLIFrameElement) { this.iframes.set(iframeEl, true); + if (iframeEl.contentWindow) + this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl); } public addLoadListener(cb: (iframeEl: HTMLIFrameElement) => unknown) { @@ -54,4 +73,20 @@ export class IframeManager { mirror.getId(iframeEl.contentDocument), ); } + private handleMessage(message: MessageEvent | CrossOriginIframeMessageEvent) { + if ((message as CrossOriginIframeMessageEvent).data.type === 'rrweb') { + const iframeSourceWindow = message.source; + + console.log( + 'frameElement', + iframeSourceWindow && + this.crossOriginIframeMap.get(iframeSourceWindow)?.outerHTML, + ); + + this.wrappedEmit( + (message as CrossOriginIframeMessageEvent).data.event, + (message as CrossOriginIframeMessageEvent).data.isCheckout, + ); + } + } } diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index ee4a193631..4f8bdb0c3c 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -25,6 +25,7 @@ import { scrollCallback, canvasMutationParam, adoptedStyleSheetParam, + CrossOriginIframeMessageEventContent, } from '../types'; import { IframeManager } from './iframe-manager'; import { ShadowDomManager } from './shadow-dom-manager'; @@ -177,13 +178,12 @@ function record( if (inEmittingFrame) { emit!(eventProcessor(e), isCheckout); } else { - window.parent.postMessage( - { - type: 'rrweb', - data: eventProcessor(e), - }, - '*', - ); + const message: CrossOriginIframeMessageEventContent = { + type: 'rrweb', + event: eventProcessor(e), + isCheckout, + }; + window.parent.postMessage(message, '*'); } if (e.type === EventType.FullSnapshot) { @@ -261,6 +261,8 @@ function record( const iframeManager = new IframeManager({ mutationCb: wrappedMutationEmit, stylesheetManager: stylesheetManager, + recordCrossOriginIframes, + wrappedEmit, }); canvasManager = new CanvasManager({ diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 04085a0487..68e549bee8 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -797,3 +797,10 @@ declare global { export type IWindow = Window & typeof globalThis; export type Optional = Pick, K> & Omit; + +export type CrossOriginIframeMessageEventContent = { + type: 'rrweb'; + event: T; + isCheckout?: boolean; +}; +export type CrossOriginIframeMessageEvent = MessageEvent; diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index d8e13fb15e..3d899f0181 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -153,4 +153,13 @@ describe('record webgl', function (this: ISuite) { ); expect(events.length).not.toBe(0); }); + + it.only('should emit contents of iframe', async () => { + const events = await ctx.page.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + await waitForRAF(ctx.page); + // two events from main frame, and two from iframe + expect(events.length).toBe(4); + }); }); From 017bd3ee339f0186c8f3d73302e964d54a78a8c0 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 25 Oct 2022 14:36:49 +0200 Subject: [PATCH 03/61] should emit full snapshot event from iframe as mutation event --- packages/rrweb/src/record/iframe-manager.ts | 48 +++++++++++++++++-- packages/rrweb/src/record/index.ts | 3 +- .../test/record/cross-origin-iframes.test.ts | 43 ++++++++++------- 3 files changed, 70 insertions(+), 24 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 00529f6485..8cc378c9c0 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -1,7 +1,9 @@ import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot'; -import type { +import { CrossOriginIframeMessageEvent, + EventType, eventWithTime, + IncrementalSource, mutationCallBack, } from '../types'; import type { StylesheetManager } from './stylesheet-manager'; @@ -12,6 +14,7 @@ export class IframeManager { MessageEventSource, HTMLIFrameElement > = new WeakMap(); + private mirror: Mirror; private mutationCb: mutationCallBack; private wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void; private loadListener?: (iframeEl: HTMLIFrameElement) => unknown; @@ -19,6 +22,7 @@ export class IframeManager { private recordCrossOriginIframes: boolean; constructor(options: { + mirror: Mirror; mutationCb: mutationCallBack; stylesheetManager: StylesheetManager; recordCrossOriginIframes: boolean; @@ -28,6 +32,7 @@ export class IframeManager { this.wrappedEmit = options.wrappedEmit; this.stylesheetManager = options.stylesheetManager; this.recordCrossOriginIframes = options.recordCrossOriginIframes; + this.mirror = options.mirror; if (this.recordCrossOriginIframes) { window.addEventListener('message', this.handleMessage.bind(this)); } @@ -46,12 +51,11 @@ export class IframeManager { public attachIframe( iframeEl: HTMLIFrameElement, childSn: serializedNodeWithId, - mirror: Mirror, ) { this.mutationCb({ adds: [ { - parentId: mirror.getId(iframeEl), + parentId: this.mirror.getId(iframeEl), nextId: null, node: childSn, }, @@ -70,12 +74,13 @@ export class IframeManager { ) this.stylesheetManager.adoptStyleSheets( iframeEl.contentDocument.adoptedStyleSheets, - mirror.getId(iframeEl.contentDocument), + this.mirror.getId(iframeEl.contentDocument), ); } private handleMessage(message: MessageEvent | CrossOriginIframeMessageEvent) { if ((message as CrossOriginIframeMessageEvent).data.type === 'rrweb') { const iframeSourceWindow = message.source; + if (!iframeSourceWindow) return; console.log( 'frameElement', @@ -83,10 +88,43 @@ export class IframeManager { this.crossOriginIframeMap.get(iframeSourceWindow)?.outerHTML, ); + const iframeEl = this.crossOriginIframeMap.get(message.source); + if (!iframeEl) return; + this.wrappedEmit( - (message as CrossOriginIframeMessageEvent).data.event, + this.transformCrossOriginEvent( + iframeEl, + (message as CrossOriginIframeMessageEvent).data.event, + ), (message as CrossOriginIframeMessageEvent).data.isCheckout, ); } } + + private transformCrossOriginEvent( + iframeEl: HTMLIFrameElement, + e: eventWithTime, + ): eventWithTime { + if (e.type === EventType.FullSnapshot) { + return { + timestamp: e.timestamp, + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: e.data.node, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + }; + } + return e; + } } diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 4f8bdb0c3c..8254297851 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -259,6 +259,7 @@ function record( }); const iframeManager = new IframeManager({ + mirror, mutationCb: wrappedMutationEmit, stylesheetManager: stylesheetManager, recordCrossOriginIframes, @@ -343,7 +344,7 @@ function record( } }, onIframeLoad: (iframe, childSn) => { - iframeManager.attachIframe(iframe, childSn, mirror); + iframeManager.attachIframe(iframe, childSn); shadowDomManager.observeAttachShadow(iframe); }, onStylesheetLoad: (linkEl, childSn) => { diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index 3d899f0181..acfc0623e8 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -104,7 +104,7 @@ const setup = function (this: ISuite, content: string): ISuite { return ctx; }; -describe('record webgl', function (this: ISuite) { +describe('cross origin iframes', function (this: ISuite) { jest.setTimeout(100_000); const ctx: ISuite = setup.call( @@ -129,22 +129,7 @@ describe('record webgl', function (this: ISuite) { () => ((window as unknown) as IWindow).snapshots, ); expect(events).toMatchObject([]); - // await ctx.page.waitForTimeout(25_000); - // await ctx.page.waitForTimeout(50); - - // const lastEvent = ctx.events[ctx.events.length - 1]; - // expect(lastEvent).toMatchObject({ - // data: { - // source: IncrementalSource.CanvasMutation, - // type: CanvasContext.WebGL, - // commands: [ - // { - // args: [16384], - // property: 'clear', - // }, - // ], - // }, - // }); + // assertSnapshot(ctx.events); }); it('will emit events if it is in the top level iframe', async () => { @@ -154,7 +139,7 @@ describe('record webgl', function (this: ISuite) { expect(events.length).not.toBe(0); }); - it.only('should emit contents of iframe', async () => { + it('should emit contents of iframe', async () => { const events = await ctx.page.evaluate( () => ((window as unknown) as IWindow).snapshots, ); @@ -162,4 +147,26 @@ describe('record webgl', function (this: ISuite) { // two events from main frame, and two from iframe expect(events.length).toBe(4); }); + + it('should emit full snapshot event from iframe as mutation event', async () => { + const events = await ctx.page.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + await waitForRAF(ctx.page); + // two events from main frame, and two from iframe + expect(events[events.length - 1]).toMatchObject({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: expect.any(Number), + node: { + id: expect.any(Number), + }, + }, + ], + }, + }); + }); }); From f1c8d0b66345b2d82ebc252db5c8886cba21eb61 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 25 Oct 2022 15:19:31 +0200 Subject: [PATCH 04/61] this.mirror was dropped on attachIframe --- packages/rrweb/src/record/mutation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 5f54842b95..2a56a51740 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -325,7 +325,7 @@ export default class MutationBuffer { } }, onIframeLoad: (iframe, childSn) => { - this.iframeManager.attachIframe(iframe, childSn, this.mirror); + this.iframeManager.attachIframe(iframe, childSn); this.shadowDomManager.observeAttachShadow(iframe); }, onStylesheetLoad: (link, childSn) => { From 580ed6acae7102b65be734114207b7bd87091dc6 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 25 Oct 2022 15:20:27 +0200 Subject: [PATCH 05/61] should use unique id for child of iframe --- packages/rrweb-snapshot/src/index.ts | 2 + packages/rrweb-snapshot/src/snapshot.ts | 2 +- packages/rrweb/src/record/iframe-manager.ts | 40 ++++++++++++++++--- .../test/record/cross-origin-iframes.test.ts | 11 +++++ 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/rrweb-snapshot/src/index.ts b/packages/rrweb-snapshot/src/index.ts index 82dd6a42c3..b2417187ca 100644 --- a/packages/rrweb-snapshot/src/index.ts +++ b/packages/rrweb-snapshot/src/index.ts @@ -6,6 +6,7 @@ import snapshot, { needMaskingText, classMatchesRegex, IGNORED_NODE, + genId, } from './snapshot'; import rebuild, { buildNodeWithSN, @@ -28,4 +29,5 @@ export { needMaskingText, classMatchesRegex, IGNORED_NODE, + genId, }; diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 3708001599..99a23ff7be 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -27,7 +27,7 @@ const tagNameRegex = new RegExp('[^a-z0-9-_:]'); export const IGNORED_NODE = -2; -function genId(): number { +export function genId(): number { return _id++; } diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 8cc378c9c0..6a0c7c2b23 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -1,4 +1,5 @@ import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot'; +import { genId } from 'rrweb-snapshot'; import { CrossOriginIframeMessageEvent, EventType, @@ -14,6 +15,10 @@ export class IframeManager { MessageEventSource, HTMLIFrameElement > = new WeakMap(); + private iframeIdMap: WeakMap< + HTMLIFrameElement, + Map + > = new WeakMap(); private mirror: Mirror; private mutationCb: mutationCallBack; private wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void; @@ -82,12 +87,6 @@ export class IframeManager { const iframeSourceWindow = message.source; if (!iframeSourceWindow) return; - console.log( - 'frameElement', - iframeSourceWindow && - this.crossOriginIframeMap.get(iframeSourceWindow)?.outerHTML, - ); - const iframeEl = this.crossOriginIframeMap.get(message.source); if (!iframeEl) return; @@ -106,6 +105,11 @@ export class IframeManager { e: eventWithTime, ): eventWithTime { if (e.type === EventType.FullSnapshot) { + this.iframeIdMap.set(iframeEl, new Map()); + /** + * Replaces the original id of the iframe with a new set of unique ids + */ + this.replaceIdOnNode(e.data.node, iframeEl); return { timestamp: e.timestamp, type: EventType.IncrementalSnapshot, @@ -127,4 +131,28 @@ export class IframeManager { } return e; } + + private replaceIdOnNode( + node: serializedNodeWithId, + iframeEl: HTMLIFrameElement, + ) { + let idMap = this.iframeIdMap.get(iframeEl); + if (!idMap) { + idMap = new Map(); + this.iframeIdMap.set(iframeEl, idMap); + } + + let newId = idMap.get(node.id); + if (newId === undefined) { + newId = genId(); + idMap.set(node.id, newId); + } + + node.id = newId; + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.replaceIdOnNode(child, iframeEl); + }); + } + } } diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index acfc0623e8..0c521e1067 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -5,6 +5,7 @@ import type { recordOptions, listenerHandler, eventWithTime, + mutationData, } from '../../src/types'; import { EventType, IncrementalSource } from '../../src/types'; import { @@ -169,4 +170,14 @@ describe('cross origin iframes', function (this: ISuite) { }, }); }); + + it('should use unique id for child of iframes', async () => { + const events: eventWithTime[] = await ctx.page.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + await waitForRAF(ctx.page); + expect( + (events[events.length - 1].data as mutationData).adds[0].node.id, + ).not.toBe(1); + }); }); From f248392217a597c79d9ef856da56da556cd13e81 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 25 Oct 2022 17:38:16 +0200 Subject: [PATCH 06/61] Cross origin iframe recording in `yarn live-stream` --- packages/rrweb/scripts/stream.js | 66 +++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/rrweb/scripts/stream.js b/packages/rrweb/scripts/stream.js index 394c23e9e0..9d143eebb9 100644 --- a/packages/rrweb/scripts/stream.js +++ b/packages/rrweb/scripts/stream.js @@ -18,13 +18,40 @@ const __dirname = path.dirname(__filename); const emitter = new EventEmitter(); -async function startRecording(page, serverURL) { - try { - await page.addScriptTag({ url: `${serverURL}/rrweb.js` }); - await page.addScriptTag({ - url: `${serverURL}/plugins/canvas-webrtc-record.js`, - }); - await page.evaluate((serverURL) => { +async function injectRecording(page, serverURL) { + await page.evaluateOnNewDocument((serverURL) => { + (async () => { + function loadScript(src) { + return new Promise(function (resolve, reject) { + const s = document.createElement('script'); + let r = false; + s.type = 'text/javascript'; + s.src = src; + s.async = true; + s.onerror = function (err) { + reject(err, s); + }; + s.onload = s.onreadystatechange = function () { + // console.log(this.readyState); // uncomment this line to see which ready states are called. + if (!r && (!this.readyState || this.readyState == 'complete')) { + r = true; + resolve(); + } + }; + if (document.head) { + document.head.append(s); + } else { + requestAnimationFrame(() => { + document.head.append(s); + }); + } + }); + } + await Promise.all([ + loadScript(`${serverURL}/rrweb.js`), + loadScript(`${serverURL}/plugins/canvas-webrtc-record.js`), + ]); + const win = window; win.__IS_RECORDING__ = true; win.events = []; @@ -45,13 +72,12 @@ async function startRecording(page, serverURL) { }, plugins: [window.plugin.initPlugin()], recordCanvas: false, + recordCrossOriginIframes: true, collectFonts: true, inlineImages: true, }); - }); - } catch (error) { - console.error(error); - } + })(); + }, serverURL); } async function startReplay(page, serverURL, recordedPage) { @@ -165,6 +191,11 @@ void (async () => { '--start-maximized', '--ignore-certificate-errors', '--no-sandbox', + // evaluateOnNewDocument doesn't work on cross-origin iframes without this + // more info: + // https://github.com/puppeteer/puppeteer/issues/7353 + // https://stackoverflow.com/questions/60375593/puppeteer-and-dynamically-added-iframe-element + '--disable-features=site-per-process', ], }); @@ -209,6 +240,7 @@ void (async () => { resizeWindow(replayerPage, 0, 800, 800, 800), ]); + await injectRecording(recordedPage, serverURL); await recordedPage.goto(url, { waitUntil: 'domcontentloaded', timeout: 300000, @@ -221,18 +253,6 @@ void (async () => { window.replayer.addEvent(event); }, event); }); - await startRecording(recordedPage, serverURL); - recordedPage.on('framenavigated', async () => { - const isRecording = await recordedPage.evaluate( - 'window.__IS_RECORDING__', - ); - if (!isRecording) { - // When the page navigates, I notice this event is emitted twice so that there are two recording processes running in a single page. - // Set recording flag True ASAP to prevent recording twice. - await recordedPage.evaluate('window.__IS_RECORDING__ = true'); - await startRecording(recordedPage, serverURL); - } - }); emitter.once('done', async () => { const pages = [ From e34e85673252cf188d6af9062a5aa9d39e2d9b16 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 11:18:38 +0200 Subject: [PATCH 07/61] Root iframe check thats supported by firefox --- packages/rrweb/src/record/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 8254297851..6170aef496 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -79,7 +79,7 @@ function record( } = options; const inEmittingFrame = recordCrossOriginIframes - ? location.ancestorOrigins.length === 0 + ? window.parent === window : true; // runtime checks for user options From 7e6a5eefd46aaff91b8b755d8d85c4d0147e3207 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 16:11:22 +0200 Subject: [PATCH 08/61] Live stream: Inject script in all frames --- packages/rrweb/scripts/stream.js | 115 +++++++++++++++---------------- 1 file changed, 55 insertions(+), 60 deletions(-) diff --git a/packages/rrweb/scripts/stream.js b/packages/rrweb/scripts/stream.js index 9d143eebb9..f54908ca0e 100644 --- a/packages/rrweb/scripts/stream.js +++ b/packages/rrweb/scripts/stream.js @@ -17,27 +17,25 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const emitter = new EventEmitter(); +const code = fs.readFileSync(path.join(__dirname, '../dist/rrweb.js'), 'utf8'); +const pluginCode = fs.readFileSync( + path.join(__dirname, '../dist/plugins/canvas-webrtc-record.js'), + 'utf8', +); + +async function injectRecording(frame) { + await frame.evaluate( + (rrwebCode, pluginCode) => { + const win = window; + if (win.__IS_RECORDING__) return; + win.__IS_RECORDING__ = true; -async function injectRecording(page, serverURL) { - await page.evaluateOnNewDocument((serverURL) => { - (async () => { - function loadScript(src) { - return new Promise(function (resolve, reject) { + (async () => { + function loadScript(code) { const s = document.createElement('script'); let r = false; s.type = 'text/javascript'; - s.src = src; - s.async = true; - s.onerror = function (err) { - reject(err, s); - }; - s.onload = s.onreadystatechange = function () { - // console.log(this.readyState); // uncomment this line to see which ready states are called. - if (!r && (!this.readyState || this.readyState == 'complete')) { - r = true; - resolve(); - } - }; + s.innerHTML = code; if (document.head) { document.head.append(s); } else { @@ -45,39 +43,37 @@ async function injectRecording(page, serverURL) { document.head.append(s); }); } - }); - } - await Promise.all([ - loadScript(`${serverURL}/rrweb.js`), - loadScript(`${serverURL}/plugins/canvas-webrtc-record.js`), - ]); + } + loadScript(rrwebCode); + loadScript(pluginCode); + + win.events = []; + window.record = win.rrweb.record; + window.plugin = new rrwebCanvasWebRTCRecord.RRWebPluginCanvasWebRTCRecord( + { + signalSendCallback: (msg) => { + // [record#callback] provides canvas id, stream, and webrtc sdpOffer signal & connect message + _signal(msg); + }, + }, + ); - const win = window; - win.__IS_RECORDING__ = true; - win.events = []; - window.record = win.rrweb.record; - window.plugin = new rrwebCanvasWebRTCRecord.RRWebPluginCanvasWebRTCRecord( - { - signalSendCallback: (msg) => { - // [record#callback] provides canvas id, stream, and webrtc sdpOffer signal & connect message - _signal(msg); + window.record({ + emit: (event) => { + win.events.push(event); + win._captureEvent(event); }, - }, - ); - - window.record({ - emit: (event) => { - win.events.push(event); - win._captureEvent(event); - }, - plugins: [window.plugin.initPlugin()], - recordCanvas: false, - recordCrossOriginIframes: true, - collectFonts: true, - inlineImages: true, - }); - })(); - }, serverURL); + plugins: [window.plugin.initPlugin()], + recordCanvas: false, + recordCrossOriginIframes: true, + collectFonts: true, + inlineImages: true, + }); + })(); + }, + code, + pluginCode, + ); } async function startReplay(page, serverURL, recordedPage) { @@ -191,11 +187,6 @@ void (async () => { '--start-maximized', '--ignore-certificate-errors', '--no-sandbox', - // evaluateOnNewDocument doesn't work on cross-origin iframes without this - // more info: - // https://github.com/puppeteer/puppeteer/issues/7353 - // https://stackoverflow.com/questions/60375593/puppeteer-and-dynamically-added-iframe-element - '--disable-features=site-per-process', ], }); @@ -240,7 +231,17 @@ void (async () => { resizeWindow(replayerPage, 0, 800, 800, 800), ]); - await injectRecording(recordedPage, serverURL); + await recordedPage.exposeFunction('_captureEvent', (event) => { + replayerPage.evaluate((event) => { + window.replayer.addEvent(event); + }, event); + }); + + recordedPage.on('framenavigated', async (frame) => { + console.log('framenavigated'); + await injectRecording(frame, serverURL); + }); + await recordedPage.goto(url, { waitUntil: 'domcontentloaded', timeout: 300000, @@ -248,12 +249,6 @@ void (async () => { if (!replayerPage) throw new Error('No replayer page found'); - await recordedPage.exposeFunction('_captureEvent', (event) => { - replayerPage.evaluate((event) => { - window.replayer.addEvent(event); - }, event); - }); - emitter.once('done', async () => { const pages = [ ...(await recordingBrowser.pages()), From a8542b8c22c538698ca06cdcb026634588e92d66 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 16:26:36 +0200 Subject: [PATCH 09/61] Record same origin and cross origin iframes differently --- packages/rrweb/package.json | 2 +- packages/rrweb/src/record/index.ts | 12 ++- .../test/record/cross-origin-iframes.test.ts | 88 +++++++++++---- yarn.lock | 102 ++++++++++++------ 4 files changed, 151 insertions(+), 53 deletions(-) diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index efc065233d..10b08d0ca7 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -66,7 +66,7 @@ "jest-image-snapshot": "^4.5.1", "jest-snapshot": "^23.6.0", "prettier": "2.2.1", - "puppeteer": "^9.1.1", + "puppeteer": "^16.1.1", "rollup": "^2.68.0", "rollup-plugin-esbuild": "^4.9.1", "rollup-plugin-postcss": "^3.1.1", diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 6170aef496..1592fb0c6e 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -82,6 +82,16 @@ function record( ? window.parent === window : true; + let passEmitsToParent = false; + if (!inEmittingFrame) { + try { + window.parent.document; // throws if parent is cross-origin + passEmitsToParent = false; // if parent is same origin we collect iframe events from the parent + } catch (e) { + passEmitsToParent = true; + } + } + // runtime checks for user options if (inEmittingFrame && !emit) { throw new Error('emit function is required'); @@ -177,7 +187,7 @@ function record( if (inEmittingFrame) { emit!(eventProcessor(e), isCheckout); - } else { + } else if (passEmitsToParent) { const message: CrossOriginIframeMessageEventContent = { type: 'rrweb', event: eventProcessor(e), diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index 0c521e1067..d47409590d 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -38,6 +38,27 @@ interface IWindow extends Window { snapshots: eventWithTime[]; } +async function injectRecordScript(frame: puppeteer.Frame) { + await frame.addScriptTag({ + path: path.resolve(__dirname, '../../dist/rrweb.js'), + }); + await frame.evaluate(() => { + ((window as unknown) as IWindow).snapshots = []; + const { record } = ((window as unknown) as IWindow).rrweb; + record({ + recordCrossOriginIframes: true, + emit(event) { + ((window as unknown) as IWindow).snapshots.push(event); + ((window as unknown) as IWindow).emit(event); + }, + }); + }); + + for (const child of frame.childFrames()) { + await injectRecordScript(child); + } +} + const setup = function (this: ISuite, content: string): ISuite { const ctx = {} as ISuite; @@ -69,27 +90,6 @@ const setup = function (this: ISuite, content: string): ISuite { ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); await injectRecordScript(ctx.page.mainFrame()); - - async function injectRecordScript(frame: puppeteer.Frame) { - await frame.addScriptTag({ - path: path.resolve(__dirname, '../../dist/rrweb.js'), - }); - await frame.evaluate(() => { - ((window as unknown) as IWindow).snapshots = []; - const { record } = ((window as unknown) as IWindow).rrweb; - record({ - recordCrossOriginIframes: true, - emit(event) { - ((window as unknown) as IWindow).snapshots.push(event); - ((window as unknown) as IWindow).emit(event); - }, - }); - }); - - for (const child of frame.childFrames()) { - await injectRecordScript(child); - } - } }); afterEach(async () => { @@ -133,6 +133,7 @@ describe('cross origin iframes', function (this: ISuite) { // assertSnapshot(ctx.events); }); + it('will emit events if it is in the top level iframe', async () => { const events = await ctx.page.evaluate( () => ((window as unknown) as IWindow).snapshots, @@ -180,4 +181,49 @@ describe('cross origin iframes', function (this: ISuite) { (events[events.length - 1].data as mutationData).adds[0].node.id, ).not.toBe(1); }); + + it('should replace the existing DOM nodes on iframe navigation with `isAttachIframe`', async () => { + await ctx.page.evaluate((url) => { + const iframe = document.querySelector('iframe') as HTMLIFrameElement; + iframe.src = `${url}/html/form.html?2`; + }, ctx.serverURL); + await waitForRAF(ctx.page); // loads iframe + + await injectRecordScript(ctx.page.mainFrame().childFrames()[0]); // injects script into new iframe + + const events: eventWithTime[] = await ctx.page.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + expect( + (events[events.length - 1].data as mutationData).removes, + ).toMatchObject([]); + expect( + (events[events.length - 1].data as mutationData).isAttachIframe, + ).toBeTruthy(); + }); +}); + +describe('same origin iframes', function (this: ISuite) { + jest.setTimeout(100_000); + + const ctx: ISuite = setup.call( + this, + ` + + + + + + + `, + ); + + it('should emit contents of iframe once', async () => { + const events = await ctx.page.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + await waitForRAF(ctx.page); + // two events from main frame, and two from iframe + expect(events.length).toBe(4); + }); }); diff --git a/yarn.lock b/yarn.lock index b2f2d2feaa..3bc6cf3615 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3794,6 +3794,13 @@ cross-env@^5.2.0: dependencies: cross-spawn "^6.0.5" +cross-fetch@3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz" @@ -4045,6 +4052,13 @@ debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: dependencies: ms "2.1.2" +debug@4.3.4, debug@^4.3.3, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debug@^3.1.0: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" @@ -4052,13 +4066,6 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debug@^4.3.3, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debuglog@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz" @@ -4161,6 +4168,11 @@ detect-newline@^3.0.0: resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +devtools-protocol@0.0.1019158: + version "0.0.1019158" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1019158.tgz#4b08d06108a784a2134313149626ba55f030a86f" + integrity sha512-wvq+KscQ7/6spEV7czhnZc9RM/woz1AY+/Vpd8/h2HFMwJSdTliu7f/yr1A6vDdJfKICZsShqsYpEQbdhg8AFQ== + devtools-protocol@0.0.869402: version "0.0.869402" resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz" @@ -4976,17 +4988,7 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" -extract-zip@^1.6.6: - version "1.7.0" - resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== - dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" - yauzl "^2.10.0" - -extract-zip@^2.0.0: +extract-zip@2.0.1, extract-zip@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== @@ -4997,6 +4999,16 @@ extract-zip@^2.0.0: optionalDependencies: "@types/yauzl" "^2.9.1" +extract-zip@^1.6.6: + version "1.7.0" + resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== + dependencies: + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" @@ -5788,6 +5800,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-proxy-agent@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + https-proxy-agent@^2.2.1: version "2.2.4" resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz" @@ -8251,7 +8271,7 @@ nice-try@^1.0.4: resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-fetch@^2.6.1: +node-fetch@2.6.7, node-fetch@^2.6.1: version "2.6.7" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -9452,7 +9472,7 @@ process-nextick-args@~2.0.0: resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -progress@^2.0.1: +progress@2.0.3, progress@^2.0.1: version "2.0.3" resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -9513,7 +9533,7 @@ proxy-addr@~2.0.5: forwarded "0.2.0" ipaddr.js "1.9.1" -proxy-from-env@^1.0.0, proxy-from-env@^1.1.0: +proxy-from-env@1.1.0, proxy-from-env@^1.0.0, proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -9550,6 +9570,23 @@ puppeteer@^1.15.0: rimraf "^2.6.1" ws "^6.1.0" +puppeteer@^16.1.1: + version "16.2.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-16.2.0.tgz#3fc9cc0c461d3166a98562a7dcd0bf3424a0523c" + integrity sha512-7Au6iC98rS6WEAD110V4Bxd0iIbqoFtzz9XzkG1BSofidS1VAJ881E1+GFR7Xn2Yea0hbj8n0ErzRyseMp1Ctg== + dependencies: + cross-fetch "3.1.5" + debug "4.3.4" + devtools-protocol "0.0.1019158" + extract-zip "2.0.1" + https-proxy-agent "5.0.1" + progress "2.0.3" + proxy-from-env "1.1.0" + rimraf "3.0.2" + tar-fs "2.1.1" + unbzip2-stream "1.4.3" + ws "8.8.1" + puppeteer@^9.1.1: version "9.1.1" resolved "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz" @@ -9944,6 +9981,13 @@ rgba-regex@^1.0.0: resolved "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" @@ -9951,13 +9995,6 @@ rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rimraf@^3.0.0, rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - rollup-plugin-css-only@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz" @@ -10764,7 +10801,7 @@ symbol-tree@^3.2.4: resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -tar-fs@^2.0.0: +tar-fs@2.1.1, tar-fs@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz" integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== @@ -11281,7 +11318,7 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" -unbzip2-stream@^1.3.3: +unbzip2-stream@1.4.3, unbzip2-stream@^1.3.3: version "1.4.3" resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz" integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== @@ -11652,6 +11689,11 @@ write-pkg@^4.0.0: type-fest "^0.4.1" write-json-file "^3.2.0" +ws@8.8.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" + integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== + ws@^6.1.0: version "6.2.2" resolved "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz" From 6b5a9c6616210bec576f9a656e8b21ab07f3dec4 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 16:55:06 +0200 Subject: [PATCH 10/61] Should map Input events correctly --- packages/rrweb/src/record/iframe-manager.ts | 92 +- .../cross-origin-iframes.test.ts.snap | 957 ++++++++++++++++++ .../test/record/cross-origin-iframes.test.ts | 15 + 3 files changed, 1058 insertions(+), 6 deletions(-) create mode 100644 packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 6a0c7c2b23..c902ba8601 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -128,27 +128,107 @@ export class IframeManager { isAttachIframe: true, }, }; + } else if (e.type === EventType.IncrementalSnapshot) { + switch (e.data.source) { + case IncrementalSource.Mutation: { + // TODO + break; + } + case IncrementalSource.MouseMove: { + // TODO + break; + } + case IncrementalSource.MouseInteraction: { + // TODO + break; + } + case IncrementalSource.Scroll: { + // TODO + break; + } + case IncrementalSource.ViewportResize: { + // TODO + break; + } + case IncrementalSource.Input: { + this.replaceId(e.data, iframeEl); + break; + } + case IncrementalSource.TouchMove: { + // TODO + break; + } + case IncrementalSource.MediaInteraction: { + // TODO + break; + } + case IncrementalSource.StyleSheetRule: { + // TODO + break; + } + case IncrementalSource.CanvasMutation: { + // TODO + break; + } + case IncrementalSource.Font: { + // TODO + break; + } + // case IncrementalSource.Log: { + // // TODO + // break; + // } + case IncrementalSource.Drag: { + // TODO + break; + } + case IncrementalSource.StyleDeclaration: { + // TODO + break; + } + case IncrementalSource.Selection: { + // TODO + break; + } + case IncrementalSource.AdoptedStyleSheet: { + // TODO + break; + } + default: { + break; + } + } + return e; } return e; } - private replaceIdOnNode( - node: serializedNodeWithId, + private replaceId( + obj: T, iframeEl: HTMLIFrameElement, - ) { + ): T { let idMap = this.iframeIdMap.get(iframeEl); if (!idMap) { idMap = new Map(); this.iframeIdMap.set(iframeEl, idMap); } - let newId = idMap.get(node.id); + let newId = idMap.get(obj.id); if (newId === undefined) { newId = genId(); - idMap.set(node.id, newId); + idMap.set(obj.id, newId); } - node.id = newId; + obj.id = newId; + + return obj; + } + + private replaceIdOnNode( + node: serializedNodeWithId, + iframeEl: HTMLIFrameElement, + ) { + this.replaceId(node, iframeEl); if ('childNodes' in node) { node.childNodes.forEach((child) => { this.replaceIdOnNode(child, iframeEl); diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap new file mode 100644 index 0000000000..ede606b1a0 --- /dev/null +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -0,0 +1,957 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`cross origin iframes should map input events correctly 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\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"rr_src\\": \\"http://localhost:3030/html/form.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 300, + \\"height\\": 150 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"form fields\\", + \\"id\\": 23 + } + ], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 26 + } + ], + \\"id\\": 25 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"form\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"text\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\" + }, + \\"childNodes\\": [], + \\"id\\": 34 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 35 + } + ], + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 38 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"on\\" + }, + \\"childNodes\\": [], + \\"id\\": 39 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 40 + } + ], + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 43 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"off\\", + \\"checked\\": true + }, + \\"childNodes\\": [], + \\"id\\": 44 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 45 + } + ], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 46 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"checkbox\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 48 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"checkbox\\" + }, + \\"childNodes\\": [], + \\"id\\": 49 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 50 + } + ], + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 51 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"textarea\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 53 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"textarea\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"cols\\": \\"30\\", + \\"rows\\": \\"10\\" + }, + \\"childNodes\\": [], + \\"id\\": 54 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 55 + } + ], + \\"id\\": 52 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 56 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"select\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 58 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"select\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"value\\": \\"1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 60 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"1\\", + \\"selected\\": true + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 62 + } + ], + \\"id\\": 61 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 63 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"2\\", + \\"id\\": 65 + } + ], + \\"id\\": 64 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 66 + } + ], + \\"id\\": 59 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 67 + } + ], + \\"id\\": 57 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 68 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"password\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 70 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"password\\" + }, + \\"childNodes\\": [], + \\"id\\": 71 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 72 + } + ], + \\"id\\": 69 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 73 + } + ], + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 74 + } + ], + \\"id\\": 28 + } + ], + \\"id\\": 13 + } + ], + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 24 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"t\\", + \\"isChecked\\": false, + \\"id\\": 34 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"te\\", + \\"isChecked\\": false, + \\"id\\": 34 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"tes\\", + \\"isChecked\\": false, + \\"id\\": 34 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"test\\", + \\"isChecked\\": false, + \\"id\\": 34 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 29 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 24 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 29 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 29 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 29 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"off\\", + \\"isChecked\\": false, + \\"id\\": 44 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 29 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 49 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 61 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 3, + \\"id\\": 1, + \\"x\\": 0, + \\"y\\": 70 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*\\", + \\"isChecked\\": false, + \\"id\\": 71 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**\\", + \\"isChecked\\": false, + \\"id\\": 71 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***\\", + \\"isChecked\\": false, + \\"id\\": 71 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****\\", + \\"isChecked\\": false, + \\"id\\": 71 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*****\\", + \\"isChecked\\": false, + \\"id\\": 71 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"******\\", + \\"isChecked\\": false, + \\"id\\": 71 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*******\\", + \\"isChecked\\": false, + \\"id\\": 71 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"********\\", + \\"isChecked\\": false, + \\"id\\": 71 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 61 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 44 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"t\\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"te\\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"tex\\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"text\\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"texta\\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textar\\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textare\\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea\\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea \\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea t\\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea te\\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea tes\\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea test\\", + \\"isChecked\\": false, + \\"id\\": 54 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 3, + \\"id\\": 1, + \\"x\\": 0, + \\"y\\": 0 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"1\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + } +]" +`; diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index d47409590d..45126c3bc8 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -201,6 +201,21 @@ describe('cross origin iframes', function (this: ISuite) { (events[events.length - 1].data as mutationData).isAttachIframe, ).toBeTruthy(); }); + + it.only('should map input events correctly', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + await frame.type('input[type="text"]', 'test'); + await frame.click('input[type="radio"]'); + await frame.click('input[type="checkbox"]'); + await frame.type('input[type="password"]', 'password'); + await frame.type('textarea', 'textarea test'); + await frame.select('select', '1'); + + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); }); describe('same origin iframes', function (this: ISuite) { From fee185a2b283633abb63ffb8a1eec64edb5c113d Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 17:02:14 +0200 Subject: [PATCH 11/61] Turn on other tests --- packages/rrweb/test/record/cross-origin-iframes.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index 45126c3bc8..20bf5d5c27 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -202,7 +202,7 @@ describe('cross origin iframes', function (this: ISuite) { ).toBeTruthy(); }); - it.only('should map input events correctly', async () => { + it('should map input events correctly', async () => { const frame = ctx.page.mainFrame().childFrames()[0]; await frame.type('input[type="text"]', 'test'); await frame.click('input[type="radio"]'); From cb1e065a84d80235922ee0c3610c946f922cd6f5 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 17:07:12 +0200 Subject: [PATCH 12/61] Fix compatibility with newer puppeteer --- packages/rrweb/test/e2e/webgl.test.ts | 8 +- packages/rrweb/test/integration.test.ts | 144 ++++++++++++++++++------ 2 files changed, 114 insertions(+), 38 deletions(-) diff --git a/packages/rrweb/test/e2e/webgl.test.ts b/packages/rrweb/test/e2e/webgl.test.ts index 8480eb0f87..e42cbc4fe6 100644 --- a/packages/rrweb/test/e2e/webgl.test.ts +++ b/packages/rrweb/test/e2e/webgl.test.ts @@ -89,7 +89,9 @@ describe('e2e webgl', () => { await waitForRAF(page); - const snapshots: eventWithTime[] = await page.evaluate('window.snapshots'); + const snapshots: eventWithTime[] = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; page = await browser.newPage(); @@ -122,7 +124,9 @@ describe('e2e webgl', () => { ); await page.waitForTimeout(100); - const snapshots: eventWithTime[] = await page.evaluate('window.snapshots'); + const snapshots: eventWithTime[] = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; page = await browser.newPage(); diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 8b5524bbde..b2ab5624f7 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -77,7 +77,9 @@ describe('record integration tests', function (this: ISuite) { await page.type('textarea', 'textarea test'); await page.select('select', '1'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -95,7 +97,9 @@ describe('record integration tests', function (this: ISuite) { p.appendChild(document.createElement('span')); }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -115,7 +119,9 @@ describe('record integration tests', function (this: ISuite) { p.innerText = 'mutated'; }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -133,7 +139,9 @@ describe('record integration tests', function (this: ISuite) { document.body.setAttribute('test', 'true'); }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -150,7 +158,9 @@ describe('record integration tests', function (this: ISuite) { await page.evaluate( 'document.getElementById("select2-drop").setAttribute("style", document.getElementById("select2-drop").style.cssText + "color:black !important")', ); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -184,7 +194,9 @@ describe('record integration tests', function (this: ISuite) { await waitForRAF(page); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -195,7 +207,9 @@ describe('record integration tests', function (this: ISuite) { await page.type('.rr-ignore', 'secret'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -213,7 +227,9 @@ describe('record integration tests', function (this: ISuite) { await page.type('textarea', 'textarea test'); await page.select('select', '1'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -237,7 +253,9 @@ describe('record integration tests', function (this: ISuite) { await page.type('input[type="password"]', 'password'); await page.select('select', '1'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -254,7 +272,9 @@ describe('record integration tests', function (this: ISuite) { await page.type('input[type="password"]', 'secr3t'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -272,7 +292,9 @@ describe('record integration tests', function (this: ISuite) { await page.type('textarea', 'textarea test'); await page.select('select', '1'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -285,7 +307,9 @@ describe('record integration tests', function (this: ISuite) { await page.evaluate(`document.getElementById('text').innerText = '1'`); await page.click('#text'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -305,7 +329,9 @@ describe('record integration tests', function (this: ISuite) { nextElement.parentNode!.insertBefore(el, nextElement); }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -314,13 +340,19 @@ describe('record integration tests', function (this: ISuite) { await page.goto('about: blank'); await page.setContent(getHtml.call(this, 'blocked-unblocked.html')); - const elements1 = await page.$x('/html/body/div[1]/button'); + const elements1 = (await page.$x( + '/html/body/div[1]/button', + )) as puppeteer.ElementHandle[]; await elements1[0].click(); - const elements2 = await page.$x('/html/body/div[2]/button'); + const elements2 = (await page.$x( + '/html/body/div[2]/button', + )) as puppeteer.ElementHandle[]; await elements2[0].click(); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -338,7 +370,9 @@ describe('record integration tests', function (this: ISuite) { p.removeChild(span); div.appendChild(span); }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -353,7 +387,9 @@ describe('record integration tests', function (this: ISuite) { document.body.appendChild(div); div.appendChild(span); }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -362,7 +398,9 @@ describe('record integration tests', function (this: ISuite) { await page.goto('about:blank'); await page.setContent(getHtml.call(this, 'react-styled-components.html')); await page.click('.toggle'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -375,7 +413,9 @@ describe('record integration tests', function (this: ISuite) { }), ); await waitForRAF(page); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; for (const event of snapshots) { if (event.type === EventType.FullSnapshot) { visitSnapshot(event.data.node, (n) => { @@ -397,7 +437,9 @@ describe('record integration tests', function (this: ISuite) { }), ); await page.waitForTimeout(50); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -410,7 +452,9 @@ describe('record integration tests', function (this: ISuite) { }), ); await waitForRAF(page); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -429,7 +473,9 @@ describe('record integration tests', function (this: ISuite) { } }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -491,7 +537,9 @@ describe('record integration tests', function (this: ISuite) { console.log('from iframe'); }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -508,7 +556,9 @@ describe('record integration tests', function (this: ISuite) { await page.waitForTimeout(50); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -523,7 +573,9 @@ describe('record integration tests', function (this: ISuite) { 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'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -538,7 +590,9 @@ describe('record integration tests', function (this: ISuite) { 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'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -559,7 +613,9 @@ describe('record integration tests', function (this: ISuite) { 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'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -607,7 +663,9 @@ describe('record integration tests', function (this: ISuite) { }); await page.waitForTimeout(50); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -650,7 +708,9 @@ describe('record integration tests', function (this: ISuite) { }); await waitForRAF(page); // wait till browser sent snapshots - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -678,7 +738,9 @@ describe('record integration tests', function (this: ISuite) { }); await waitForRAF(page); // wait for snapshot to be updated - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -710,7 +772,9 @@ describe('record integration tests', function (this: ISuite) { }); await waitForRAF(page); // wait till browser sent snapshots - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -750,7 +814,9 @@ describe('record integration tests', function (this: ISuite) { }); await waitForRAF(page); // wait till browser sent snapshots - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -763,7 +829,9 @@ describe('record integration tests', function (this: ISuite) { }), ); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -777,7 +845,9 @@ describe('record integration tests', function (this: ISuite) { }), ); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -798,7 +868,9 @@ describe('record integration tests', function (this: ISuite) { p.innerText = 'mutated'; }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); }); From c54b98196d7d35e912411bbd9b7173514f65e034 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 17:17:01 +0200 Subject: [PATCH 13/61] puppeteer vs 12 seems stable without to many changes needed --- packages/rrweb/package.json | 2 +- .../cross-origin-iframes.test.ts.snap | 11 +-- yarn.lock | 99 +++++++++---------- 3 files changed, 48 insertions(+), 64 deletions(-) diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 10b08d0ca7..818af72767 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -66,7 +66,7 @@ "jest-image-snapshot": "^4.5.1", "jest-snapshot": "^23.6.0", "prettier": "2.2.1", - "puppeteer": "^16.1.1", + "puppeteer": "^12.0.0", "rollup": "^2.68.0", "rollup-plugin-esbuild": "^4.9.1", "rollup-plugin-postcss": "^3.1.1", diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index ede606b1a0..374a1ec15b 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -727,7 +727,7 @@ exports[`cross origin iframes should map input events correctly 1`] = ` \\"source\\": 3, \\"id\\": 1, \\"x\\": 0, - \\"y\\": 70 + \\"y\\": 69 } }, { @@ -935,15 +935,6 @@ exports[`cross origin iframes should map input events correctly 1`] = ` \\"id\\": 54 } }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 3, - \\"id\\": 1, - \\"x\\": 0, - \\"y\\": 0 - } - }, { \\"type\\": 3, \\"data\\": { diff --git a/yarn.lock b/yarn.lock index 3bc6cf3615..7986cd83fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3794,13 +3794,6 @@ cross-env@^5.2.0: dependencies: cross-spawn "^6.0.5" -cross-fetch@3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz" @@ -4045,20 +4038,13 @@ debug@2.6.9, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: +debug@4, debug@4.3.2, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: version "4.3.2" resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== dependencies: ms "2.1.2" -debug@4.3.4, debug@^4.3.3, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debug@^3.1.0: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" @@ -4066,6 +4052,13 @@ debug@^3.1.0: dependencies: ms "^2.1.1" +debug@^4.3.3, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz" @@ -4168,16 +4161,16 @@ detect-newline@^3.0.0: resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -devtools-protocol@0.0.1019158: - version "0.0.1019158" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1019158.tgz#4b08d06108a784a2134313149626ba55f030a86f" - integrity sha512-wvq+KscQ7/6spEV7czhnZc9RM/woz1AY+/Vpd8/h2HFMwJSdTliu7f/yr1A6vDdJfKICZsShqsYpEQbdhg8AFQ== - devtools-protocol@0.0.869402: version "0.0.869402" resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz" integrity sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA== +devtools-protocol@0.0.937139: + version "0.0.937139" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.937139.tgz#bdee3751fdfdb81cb701fd3afa94b1065dafafcf" + integrity sha512-daj+rzR3QSxsPRy5vjjthn58axO8c11j58uY0lG5vvlJk/EiOdCWOptGdkXDjtuRHr78emKq0udHCXM4trhoDQ== + dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz" @@ -5800,10 +5793,10 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -https-proxy-agent@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== +https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== dependencies: agent-base "6" debug "4" @@ -5816,14 +5809,6 @@ https-proxy-agent@^2.2.1: agent-base "^4.3.0" debug "^3.1.0" -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" @@ -8271,7 +8256,14 @@ nice-try@^1.0.4: resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-fetch@2.6.7, node-fetch@^2.6.1: +node-fetch@2.6.5: + version "2.6.5" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd" + integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^2.6.1: version "2.6.7" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -9022,6 +9014,13 @@ pixelmatch@^5.1.0: dependencies: pngjs "^4.0.1" +pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + "pkg-dir@< 6 >= 5": version "5.0.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz" @@ -9029,13 +9028,6 @@ pixelmatch@^5.1.0: dependencies: find-up "^5.0.0" -pkg-dir@^4.1.0, pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - pngjs@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz" @@ -9570,22 +9562,23 @@ puppeteer@^1.15.0: rimraf "^2.6.1" ws "^6.1.0" -puppeteer@^16.1.1: - version "16.2.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-16.2.0.tgz#3fc9cc0c461d3166a98562a7dcd0bf3424a0523c" - integrity sha512-7Au6iC98rS6WEAD110V4Bxd0iIbqoFtzz9XzkG1BSofidS1VAJ881E1+GFR7Xn2Yea0hbj8n0ErzRyseMp1Ctg== +puppeteer@^12.0.0: + version "12.0.1" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-12.0.1.tgz#ae79d0e174a07563e0bf2e05c94ccafce3e70033" + integrity sha512-YQ3GRiyZW0ddxTW+iiQcv2/8TT5c3+FcRUCg7F8q2gHqxd5akZN400VRXr9cHQKLWGukmJLDiE72MrcLK9tFHQ== dependencies: - cross-fetch "3.1.5" - debug "4.3.4" - devtools-protocol "0.0.1019158" + debug "4.3.2" + devtools-protocol "0.0.937139" extract-zip "2.0.1" - https-proxy-agent "5.0.1" + https-proxy-agent "5.0.0" + node-fetch "2.6.5" + pkg-dir "4.2.0" progress "2.0.3" proxy-from-env "1.1.0" rimraf "3.0.2" tar-fs "2.1.1" unbzip2-stream "1.4.3" - ws "8.8.1" + ws "8.2.3" puppeteer@^9.1.1: version "9.1.1" @@ -11689,10 +11682,10 @@ write-pkg@^4.0.0: type-fest "^0.4.1" write-json-file "^3.2.0" -ws@8.8.1: - version "8.8.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" - integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== +ws@8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== ws@^6.1.0: version "6.2.2" From 505ba2c7649b77e86d358e20d6b05b8f662e4a96 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 17:21:16 +0200 Subject: [PATCH 14/61] normalize port numbers in snapshots --- packages/rrweb/test/utils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 818298b713..1eff8ce180 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -191,6 +191,11 @@ function stringifySnapshots(snapshots: eventWithTime[]): string { }), null, 2, + ).replace( + // servers might get run on a random port, + // so we need to normalize the port number + /http:\/\/localhost:\d+/g, + 'http://localhost:3030', ); } From 15c2cc0bda10f193ffc33eb1e3f530fb4aea28fe Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 17:42:39 +0200 Subject: [PATCH 15/61] Handle scroll and ViewportResize events in cross origin iframe --- packages/rrweb/src/record/iframe-manager.ts | 23 +- .../cross-origin-iframes.test.ts.snap | 609 +++++++++++++++++- .../test/record/cross-origin-iframes.test.ts | 25 +- 3 files changed, 635 insertions(+), 22 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index c902ba8601..43efce11ec 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -90,20 +90,23 @@ export class IframeManager { const iframeEl = this.crossOriginIframeMap.get(message.source); if (!iframeEl) return; - this.wrappedEmit( - this.transformCrossOriginEvent( - iframeEl, - (message as CrossOriginIframeMessageEvent).data.event, - ), - (message as CrossOriginIframeMessageEvent).data.isCheckout, + const transformedEvent = this.transformCrossOriginEvent( + iframeEl, + (message as CrossOriginIframeMessageEvent).data.event, ); + + if (transformedEvent) + this.wrappedEmit( + transformedEvent, + (message as CrossOriginIframeMessageEvent).data.isCheckout, + ); } } private transformCrossOriginEvent( iframeEl: HTMLIFrameElement, e: eventWithTime, - ): eventWithTime { + ): eventWithTime | void { if (e.type === EventType.FullSnapshot) { this.iframeIdMap.set(iframeEl, new Map()); /** @@ -143,12 +146,12 @@ export class IframeManager { break; } case IncrementalSource.Scroll: { - // TODO + this.replaceId(e.data, iframeEl); break; } case IncrementalSource.ViewportResize: { - // TODO - break; + // can safely ignore these events + return; } case IncrementalSource.Input: { this.replaceId(e.data, iframeEl); diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index 374a1ec15b..36527b11fe 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -65,6 +65,7 @@ exports[`cross origin iframes should map input events correctly 1`] = ` \\"type\\": 2, \\"tagName\\": \\"iframe\\", \\"attributes\\": { + \\"style\\": \\"width: 400px; height: 400px;\\", \\"rr_src\\": \\"http://localhost:3030/html/form.html\\" }, \\"childNodes\\": [], @@ -94,8 +95,8 @@ exports[`cross origin iframes should map input events correctly 1`] = ` \\"type\\": 4, \\"data\\": { \\"href\\": \\"about:blank\\", - \\"width\\": 300, - \\"height\\": 150 + \\"width\\": 400, + \\"height\\": 400 } }, { @@ -721,15 +722,6 @@ exports[`cross origin iframes should map input events correctly 1`] = ` \\"id\\": 61 } }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 3, - \\"id\\": 1, - \\"x\\": 0, - \\"y\\": 69 - } - }, { \\"type\\": 3, \\"data\\": { @@ -946,3 +938,598 @@ exports[`cross origin iframes should map input events correctly 1`] = ` } ]" `; + +exports[`cross origin iframes should map scroll events correctly 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\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"style\\": \\"width: 400px; height: 400px;\\", + \\"rr_src\\": \\"http://localhost:3030/html/form.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 400, + \\"height\\": 400 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"form fields\\", + \\"id\\": 23 + } + ], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 26 + } + ], + \\"id\\": 25 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"form\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"text\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\" + }, + \\"childNodes\\": [], + \\"id\\": 34 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 35 + } + ], + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 38 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"on\\" + }, + \\"childNodes\\": [], + \\"id\\": 39 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 40 + } + ], + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 43 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"off\\", + \\"checked\\": true + }, + \\"childNodes\\": [], + \\"id\\": 44 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 45 + } + ], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 46 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"checkbox\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 48 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"checkbox\\" + }, + \\"childNodes\\": [], + \\"id\\": 49 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 50 + } + ], + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 51 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"textarea\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 53 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"textarea\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"cols\\": \\"30\\", + \\"rows\\": \\"10\\" + }, + \\"childNodes\\": [], + \\"id\\": 54 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 55 + } + ], + \\"id\\": 52 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 56 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"select\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 58 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"select\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"value\\": \\"1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 60 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"1\\", + \\"selected\\": true + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 62 + } + ], + \\"id\\": 61 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 63 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"2\\", + \\"id\\": 65 + } + ], + \\"id\\": 64 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 66 + } + ], + \\"id\\": 59 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 67 + } + ], + \\"id\\": 57 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 68 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"password\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 70 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"password\\" + }, + \\"childNodes\\": [], + \\"id\\": 71 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 72 + } + ], + \\"id\\": 69 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 73 + } + ], + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 74 + } + ], + \\"id\\": 28 + } + ], + \\"id\\": 13 + } + ], + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 9, + \\"attributes\\": { + \\"style\\": { + \\"width\\": \\"Npx\\", + \\"height\\": \\"Npx\\" + } + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 4, + \\"width\\": 50, + \\"height\\": 50 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 3, + \\"id\\": 11, + \\"x\\": 0, + \\"y\\": 10 + } + } +]" +`; diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index 20bf5d5c27..08acee810b 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -114,7 +114,7 @@ describe('cross origin iframes', function (this: ISuite) { - + `, @@ -216,6 +216,29 @@ describe('cross origin iframes', function (this: ISuite) { )) as eventWithTime[]; assertSnapshot(snapshots); }); + + it('should map scroll events correctly', async () => { + // force scrollbars in iframe + ctx.page.evaluate(() => { + const iframe = document.querySelector('iframe') as HTMLIFrameElement; + iframe.style.width = '50px'; + iframe.style.height = '50px'; + }); + + await waitForRAF(ctx.page); + const frame = ctx.page.mainFrame().childFrames()[0]; + + // scroll a little + frame.evaluate(() => { + window.scrollTo(0, 10); + }); + await waitForRAF(ctx.page); + + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); }); describe('same origin iframes', function (this: ISuite) { From 9febf1cae1a203ee144146bb7dc1eb7f2b69a3c0 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 20:21:27 +0200 Subject: [PATCH 16/61] Correctly map cross origin mutations --- packages/rrweb/src/record/iframe-manager.ts | 38 +- .../cross-origin-iframes.test.ts.snap | 1202 ++++++++++++++++- .../test/record/cross-origin-iframes.test.ts | 286 ++-- 3 files changed, 1391 insertions(+), 135 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 43efce11ec..3d31e735d3 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -134,7 +134,19 @@ export class IframeManager { } else if (e.type === EventType.IncrementalSnapshot) { switch (e.data.source) { case IncrementalSource.Mutation: { - // TODO + e.data.adds.forEach((n) => { + this.replaceIds(n, iframeEl, ['parentId', 'nextId', 'previousId']); + this.replaceIdOnNode(n.node, iframeEl); + }); + e.data.removes.forEach((n) => { + this.replaceIds(n, iframeEl, ['parentId', 'id']); + }); + e.data.attributes.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + e.data.texts.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); break; } case IncrementalSource.MouseMove: { @@ -146,7 +158,7 @@ export class IframeManager { break; } case IncrementalSource.Scroll: { - this.replaceId(e.data, iframeEl); + this.replaceIds(e.data, iframeEl, ['id']); break; } case IncrementalSource.ViewportResize: { @@ -154,7 +166,7 @@ export class IframeManager { return; } case IncrementalSource.Input: { - this.replaceId(e.data, iframeEl); + this.replaceIds(e.data, iframeEl, ['id']); break; } case IncrementalSource.TouchMove: { @@ -206,9 +218,10 @@ export class IframeManager { return e; } - private replaceId( + private replaceIds>( obj: T, iframeEl: HTMLIFrameElement, + keys: Array, ): T { let idMap = this.iframeIdMap.get(iframeEl); if (!idMap) { @@ -216,14 +229,17 @@ export class IframeManager { this.iframeIdMap.set(iframeEl, idMap); } - let newId = idMap.get(obj.id); - if (newId === undefined) { - newId = genId(); - idMap.set(obj.id, newId); + for (const key of keys) { + if (typeof obj[key] !== 'number') continue; + const originalId = obj[key] as number; + let newId = idMap.get(originalId); + if (!newId) { + newId = genId(); + idMap.set(originalId, newId); + } + (obj[key] as number) = newId; } - obj.id = newId; - return obj; } @@ -231,7 +247,7 @@ export class IframeManager { node: serializedNodeWithId, iframeEl: HTMLIFrameElement, ) { - this.replaceId(node, iframeEl); + this.replaceIds(node, iframeEl, ['id']); if ('childNodes' in node) { node.childNodes.forEach((child) => { this.replaceIdOnNode(child, iframeEl); diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index 36527b11fe..dc50463809 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`cross origin iframes should map input events correctly 1`] = ` +exports[`cross origin iframes form.html should map input events correctly 1`] = ` "[ { \\"type\\": 4, @@ -58,7 +58,7 @@ exports[`cross origin iframes should map input events correctly 1`] = ` \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", + \\"textContent\\": \\"\\\\n \\", \\"id\\": 8 }, { @@ -73,7 +73,7 @@ exports[`cross origin iframes should map input events correctly 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", \\"id\\": 10 } ], @@ -939,7 +939,7 @@ exports[`cross origin iframes should map input events correctly 1`] = ` ]" `; -exports[`cross origin iframes should map scroll events correctly 1`] = ` +exports[`cross origin iframes form.html should map scroll events correctly 1`] = ` "[ { \\"type\\": 4, @@ -997,7 +997,7 @@ exports[`cross origin iframes should map scroll events correctly 1`] = ` \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", + \\"textContent\\": \\"\\\\n \\", \\"id\\": 8 }, { @@ -1012,7 +1012,7 @@ exports[`cross origin iframes should map scroll events correctly 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", \\"id\\": 10 } ], @@ -1517,18 +1517,1194 @@ exports[`cross origin iframes should map scroll events correctly 1`] = ` { \\"type\\": 3, \\"data\\": { - \\"source\\": 4, - \\"width\\": 50, - \\"height\\": 50 + \\"source\\": 3, + \\"id\\": 11, + \\"x\\": 0, + \\"y\\": 10 + } + } +]" +`; + +exports[`cross origin iframes move-node.html should record DOM attribute changes 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\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"rr_src\\": \\"http://localhost:3030/html/move-node.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 300, + \\"height\\": 150 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 16 + } + ], + \\"id\\": 15 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 29 + } + ], + \\"id\\": 28 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + } + ], + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 32 + } + ], + \\"id\\": 17 + } + ], + \\"id\\": 13 + } + ], + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 24, + \\"attributes\\": { + \\"class\\": \\"added-class-name\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +exports[`cross origin iframes move-node.html should record DOM node movement 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\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"rr_src\\": \\"http://localhost:3030/html/move-node.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 300, + \\"height\\": 150 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 16 + } + ], + \\"id\\": 15 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 29 + } + ], + \\"id\\": 28 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + } + ], + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 32 + } + ], + \\"id\\": 17 + } + ], + \\"id\\": 13 + } + ], + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 17, + \\"id\\": 24 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 24, + \\"nextId\\": 26, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + } + }, + { + \\"parentId\\": 24, + \\"nextId\\": 31, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 26 + } + }, + { + \\"parentId\\": 26, + \\"nextId\\": 28, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 27 + } + }, + { + \\"parentId\\": 26, + \\"nextId\\": 30, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 28 + } + }, + { + \\"parentId\\": 28, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 29 + } + }, + { + \\"parentId\\": 26, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + } + }, + { + \\"parentId\\": 24, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + } + }, + { + \\"parentId\\": 17, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 33 + } + }, + { + \\"parentId\\": 33, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 24 + } + } + ] + } + } +]" +`; + +exports[`cross origin iframes move-node.html should record DOM node removal 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\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"rr_src\\": \\"http://localhost:3030/html/move-node.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 300, + \\"height\\": 150 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 16 + } + ], + \\"id\\": 15 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 29 + } + ], + \\"id\\": 28 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + } + ], + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 32 + } + ], + \\"id\\": 17 + } + ], + \\"id\\": 13 + } + ], + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true } }, { \\"type\\": 3, \\"data\\": { - \\"source\\": 3, - \\"id\\": 11, - \\"x\\": 0, - \\"y\\": 10 + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 17, + \\"id\\": 24 + } + ], + \\"adds\\": [] + } + } +]" +`; + +exports[`cross origin iframes move-node.html should record DOM text changes 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\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"rr_src\\": \\"http://localhost:3030/html/move-node.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 300, + \\"height\\": 150 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 16 + } + ], + \\"id\\": 15 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 29 + } + ], + \\"id\\": 28 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + } + ], + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 32 + } + ], + \\"id\\": 17 + } + ], + \\"id\\": 13 + } + ], + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [ + { + \\"id\\": 29, + \\"value\\": \\"replaced text\\" + } + ], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [] } } ]" diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index 08acee810b..7d31d117c0 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -108,136 +108,200 @@ const setup = function (this: ISuite, content: string): ISuite { describe('cross origin iframes', function (this: ISuite) { jest.setTimeout(100_000); - const ctx: ISuite = setup.call( - this, - ` - - - - - - - `, - ); - - it("won't emit events if it isn't in the top level iframe", async () => { - const el = (await ctx.page.$( - 'body > iframe', - )) as puppeteer.ElementHandle; - - const frame = await el.contentFrame(); - const events = await frame?.evaluate( - () => ((window as unknown) as IWindow).snapshots, + describe('form.html', function (this: ISuite) { + const ctx: ISuite = setup.call( + this, + ` + + + + + + + `, ); - expect(events).toMatchObject([]); - // assertSnapshot(ctx.events); - }); + it("won't emit events if it isn't in the top level iframe", async () => { + const el = (await ctx.page.$( + 'body > iframe', + )) as puppeteer.ElementHandle; - it('will emit events if it is in the top level iframe', async () => { - const events = await ctx.page.evaluate( - () => ((window as unknown) as IWindow).snapshots, - ); - expect(events.length).not.toBe(0); - }); + const frame = await el.contentFrame(); + const events = await frame?.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + expect(events).toMatchObject([]); + }); - it('should emit contents of iframe', async () => { - const events = await ctx.page.evaluate( - () => ((window as unknown) as IWindow).snapshots, - ); - await waitForRAF(ctx.page); - // two events from main frame, and two from iframe - expect(events.length).toBe(4); - }); + it('will emit events if it is in the top level iframe', async () => { + const events = await ctx.page.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + expect(events.length).not.toBe(0); + }); - it('should emit full snapshot event from iframe as mutation event', async () => { - const events = await ctx.page.evaluate( - () => ((window as unknown) as IWindow).snapshots, - ); - await waitForRAF(ctx.page); - // two events from main frame, and two from iframe - expect(events[events.length - 1]).toMatchObject({ - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.Mutation, - adds: [ - { - parentId: expect.any(Number), - node: { - id: expect.any(Number), + it('should emit contents of iframe', async () => { + const events = await ctx.page.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + await waitForRAF(ctx.page); + // two events from main frame, and two from iframe + expect(events.length).toBe(4); + }); + + it('should emit full snapshot event from iframe as mutation event', async () => { + const events = await ctx.page.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + await waitForRAF(ctx.page); + // two events from main frame, and two from iframe + expect(events[events.length - 1]).toMatchObject({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: expect.any(Number), + node: { + id: expect.any(Number), + }, }, - }, - ], - }, + ], + }, + }); }); - }); - it('should use unique id for child of iframes', async () => { - const events: eventWithTime[] = await ctx.page.evaluate( - () => ((window as unknown) as IWindow).snapshots, - ); - await waitForRAF(ctx.page); - expect( - (events[events.length - 1].data as mutationData).adds[0].node.id, - ).not.toBe(1); - }); + it('should use unique id for child of iframes', async () => { + const events: eventWithTime[] = await ctx.page.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + await waitForRAF(ctx.page); + expect( + (events[events.length - 1].data as mutationData).adds[0].node.id, + ).not.toBe(1); + }); - it('should replace the existing DOM nodes on iframe navigation with `isAttachIframe`', async () => { - await ctx.page.evaluate((url) => { - const iframe = document.querySelector('iframe') as HTMLIFrameElement; - iframe.src = `${url}/html/form.html?2`; - }, ctx.serverURL); - await waitForRAF(ctx.page); // loads iframe + it('should replace the existing DOM nodes on iframe navigation with `isAttachIframe`', async () => { + await ctx.page.evaluate((url) => { + const iframe = document.querySelector('iframe') as HTMLIFrameElement; + iframe.src = `${url}/html/form.html?2`; + }, ctx.serverURL); + await waitForRAF(ctx.page); // loads iframe + + await injectRecordScript(ctx.page.mainFrame().childFrames()[0]); // injects script into new iframe + + const events: eventWithTime[] = await ctx.page.evaluate( + () => ((window as unknown) as IWindow).snapshots, + ); + expect( + (events[events.length - 1].data as mutationData).removes, + ).toMatchObject([]); + expect( + (events[events.length - 1].data as mutationData).isAttachIframe, + ).toBeTruthy(); + }); - await injectRecordScript(ctx.page.mainFrame().childFrames()[0]); // injects script into new iframe + it('should map input events correctly', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + await frame.type('input[type="text"]', 'test'); + await frame.click('input[type="radio"]'); + await frame.click('input[type="checkbox"]'); + await frame.type('input[type="password"]', 'password'); + await frame.type('textarea', 'textarea test'); + await frame.select('select', '1'); + + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); - const events: eventWithTime[] = await ctx.page.evaluate( - () => ((window as unknown) as IWindow).snapshots, - ); - expect( - (events[events.length - 1].data as mutationData).removes, - ).toMatchObject([]); - expect( - (events[events.length - 1].data as mutationData).isAttachIframe, - ).toBeTruthy(); + it('should map scroll events correctly', async () => { + // force scrollbars in iframe + ctx.page.evaluate(() => { + const iframe = document.querySelector('iframe') as HTMLIFrameElement; + iframe.style.width = '50px'; + iframe.style.height = '50px'; + }); + + await waitForRAF(ctx.page); + const frame = ctx.page.mainFrame().childFrames()[0]; + + // scroll a little + frame.evaluate(() => { + window.scrollTo(0, 10); + }); + await waitForRAF(ctx.page); + + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); }); - it('should map input events correctly', async () => { - const frame = ctx.page.mainFrame().childFrames()[0]; - await frame.type('input[type="text"]', 'test'); - await frame.click('input[type="radio"]'); - await frame.click('input[type="checkbox"]'); - await frame.type('input[type="password"]', 'password'); - await frame.type('textarea', 'textarea test'); - await frame.select('select', '1'); - - const snapshots = (await ctx.page.evaluate( - 'window.snapshots', - )) as eventWithTime[]; - assertSnapshot(snapshots); - }); + describe('move-node.html', function (this: ISuite) { + const ctx: ISuite = setup.call( + this, + ` + + + + + + + `, + ); - it('should map scroll events correctly', async () => { - // force scrollbars in iframe - ctx.page.evaluate(() => { - const iframe = document.querySelector('iframe') as HTMLIFrameElement; - iframe.style.width = '50px'; - iframe.style.height = '50px'; + it('should record DOM node movement', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + await frame.evaluate(() => { + const div = document.createElement('div'); + const span = document.querySelector('span')!; + document.body.appendChild(div); + div.appendChild(span); + }); + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); }); - await waitForRAF(ctx.page); - const frame = ctx.page.mainFrame().childFrames()[0]; + it('should record DOM node removal', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + await frame.evaluate(() => { + const span = document.querySelector('span')!; + span.remove(); + }); + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); - // scroll a little - frame.evaluate(() => { - window.scrollTo(0, 10); + it('should record DOM attribute changes', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + await frame.evaluate(() => { + const span = document.querySelector('span')!; + span.className = 'added-class-name'; + }); + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); }); - await waitForRAF(ctx.page); - const snapshots = (await ctx.page.evaluate( - 'window.snapshots', - )) as eventWithTime[]; - assertSnapshot(snapshots); + it('should record DOM text changes', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + await frame.evaluate(() => { + const b = document.querySelector('b')!; + b.childNodes[0].textContent = 'replaced text'; + }); + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); }); }); From 5fa4634bbafeb48c4a78f82bfaadbc50f10e0414 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 22:01:28 +0200 Subject: [PATCH 17/61] Map selection events for cross origin iframes --- packages/rrweb/src/record/iframe-manager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 3d31e735d3..06b62bf8d2 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -202,7 +202,9 @@ export class IframeManager { break; } case IncrementalSource.Selection: { - // TODO + e.data.ranges.forEach((range) => { + this.replaceIds(range, iframeEl, ['start', 'end']); + }); break; } case IncrementalSource.AdoptedStyleSheet: { From 05a1fbc8c694385bddc9dd471d2b8de3f13741db Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 22:32:58 +0200 Subject: [PATCH 18/61] Map canvas mutations for cross origin iframes --- packages/rrweb/src/record/iframe-manager.ts | 10 +- .../cross-origin-iframes.test.ts.snap | 280 ++++++++++++++++++ .../test/record/cross-origin-iframes.test.ts | 19 ++ 3 files changed, 301 insertions(+), 8 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 06b62bf8d2..1983bfe738 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -157,14 +157,12 @@ export class IframeManager { // TODO break; } - case IncrementalSource.Scroll: { - this.replaceIds(e.data, iframeEl, ['id']); - break; - } case IncrementalSource.ViewportResize: { // can safely ignore these events return; } + case IncrementalSource.Scroll: + case IncrementalSource.CanvasMutation: case IncrementalSource.Input: { this.replaceIds(e.data, iframeEl, ['id']); break; @@ -181,10 +179,6 @@ export class IframeManager { // TODO break; } - case IncrementalSource.CanvasMutation: { - // TODO - break; - } case IncrementalSource.Font: { // TODO break; diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index dc50463809..b71979a946 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -2709,3 +2709,283 @@ exports[`cross origin iframes move-node.html should record DOM text changes 1`] } ]" `; + +exports[`cross origin iframes move-node.html should record canvas elements 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\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"rr_src\\": \\"http://localhost:3030/html/move-node.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 300, + \\"height\\": 150 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 16 + } + ], + \\"id\\": 15 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 29 + } + ], + \\"id\\": 28 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + } + ], + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 32 + } + ], + \\"id\\": 17 + } + ], + \\"id\\": 13 + } + ], + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 17, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 33 + } + } + ] + } + } +]" +`; diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index 7d31d117c0..395f04048b 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -47,6 +47,7 @@ async function injectRecordScript(frame: puppeteer.Frame) { const { record } = ((window as unknown) as IWindow).rrweb; record({ recordCrossOriginIframes: true, + recordCanvas: true, emit(event) { ((window as unknown) as IWindow).snapshots.push(event); ((window as unknown) as IWindow).emit(event); @@ -302,6 +303,24 @@ describe('cross origin iframes', function (this: ISuite) { )) as eventWithTime[]; assertSnapshot(snapshots); }); + + it('should record canvas elements', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + await frame.evaluate(() => { + var canvas = document.createElement('canvas'); + var gl = canvas.getContext('webgl')!; + var program = gl.createProgram()!; + gl.linkProgram(program); + gl.clear(gl.COLOR_BUFFER_BIT); + document.body.appendChild(canvas); + }); + await waitForRAF(ctx.page); + await waitForRAF(ctx.page); + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); }); }); From 8ea496cbf10b8ab8d80ccde572725415bd8d3999 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 22:38:54 +0200 Subject: [PATCH 19/61] Update snapshot to include canvas events --- .../cross-origin-iframes.test.ts.snap | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index b71979a946..bc47b467cd 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -2986,6 +2986,35 @@ exports[`cross origin iframes move-node.html should record canvas elements 1`] = } ] } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 9, + \\"id\\": 33, + \\"type\\": 1, + \\"commands\\": [ + { + \\"property\\": \\"createProgram\\", + \\"args\\": [] + }, + { + \\"property\\": \\"linkProgram\\", + \\"args\\": [ + { + \\"rr_type\\": \\"WebGLProgram\\", + \\"index\\": 0 + } + ] + }, + { + \\"property\\": \\"clear\\", + \\"args\\": [ + 16384 + ] + } + ] + } } ]" `; From 8ac677b2a8913736ad5adaa5db299c09953a0f28 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 22:49:43 +0200 Subject: [PATCH 20/61] Skip all meta events --- packages/rrweb/src/record/iframe-manager.ts | 3 + .../cross-origin-iframes.test.ts.snap | 230 +++++++++++++----- .../test/record/cross-origin-iframes.test.ts | 8 +- 3 files changed, 182 insertions(+), 59 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 1983bfe738..45dc46e195 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -210,6 +210,9 @@ export class IframeManager { } } return e; + } else if (e.type === EventType.Meta) { + // skip meta events + return; } return e; } diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index bc47b467cd..7ddc52a793 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -91,14 +91,6 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = } } }, - { - \\"type\\": 4, - \\"data\\": { - \\"href\\": \\"about:blank\\", - \\"width\\": 400, - \\"height\\": 400 - } - }, { \\"type\\": 3, \\"data\\": { @@ -1030,14 +1022,6 @@ exports[`cross origin iframes form.html should map scroll events correctly 1`] = } } }, - { - \\"type\\": 4, - \\"data\\": { - \\"href\\": \\"about:blank\\", - \\"width\\": 400, - \\"height\\": 400 - } - }, { \\"type\\": 3, \\"data\\": { @@ -1616,14 +1600,6 @@ exports[`cross origin iframes move-node.html should record DOM attribute changes } } }, - { - \\"type\\": 4, - \\"data\\": { - \\"href\\": \\"about:blank\\", - \\"width\\": 300, - \\"height\\": 150 - } - }, { \\"type\\": 3, \\"data\\": { @@ -1891,14 +1867,6 @@ exports[`cross origin iframes move-node.html should record DOM node movement 1`] } } }, - { - \\"type\\": 4, - \\"data\\": { - \\"href\\": \\"about:blank\\", - \\"width\\": 300, - \\"height\\": 150 - } - }, { \\"type\\": 3, \\"data\\": { @@ -2254,14 +2222,6 @@ exports[`cross origin iframes move-node.html should record DOM node removal 1`] } } }, - { - \\"type\\": 4, - \\"data\\": { - \\"href\\": \\"about:blank\\", - \\"width\\": 300, - \\"height\\": 150 - } - }, { \\"type\\": 3, \\"data\\": { @@ -2527,14 +2487,6 @@ exports[`cross origin iframes move-node.html should record DOM text changes 1`] } } }, - { - \\"type\\": 4, - \\"data\\": { - \\"href\\": \\"about:blank\\", - \\"width\\": 300, - \\"height\\": 150 - } - }, { \\"type\\": 3, \\"data\\": { @@ -2800,14 +2752,6 @@ exports[`cross origin iframes move-node.html should record canvas elements 1`] = } } }, - { - \\"type\\": 4, - \\"data\\": { - \\"href\\": \\"about:blank\\", - \\"width\\": 300, - \\"height\\": 150 - } - }, { \\"type\\": 3, \\"data\\": { @@ -3018,3 +2962,177 @@ exports[`cross origin iframes move-node.html should record canvas elements 1`] = } ]" `; + +exports[`same origin iframes should emit contents of iframe once 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\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"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\\": 11, + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 11, + \\"id\\": 14 + } + ], + \\"rootId\\": 11, + \\"id\\": 12 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 13, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 11, + \\"id\\": 15 + } + }, + { + \\"parentId\\": 15, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"rootId\\": 11, + \\"id\\": 16 + } + } + ] + } + } +]" +`; diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index 395f04048b..f257072499 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -146,8 +146,8 @@ describe('cross origin iframes', function (this: ISuite) { () => ((window as unknown) as IWindow).snapshots, ); await waitForRAF(ctx.page); - // two events from main frame, and two from iframe - expect(events.length).toBe(4); + // two events (full snapshot + meta) from main frame, and one full snapshot from iframe + expect(events.length).toBe(3); }); it('should emit full snapshot event from iframe as mutation event', async () => { @@ -344,7 +344,9 @@ describe('same origin iframes', function (this: ISuite) { () => ((window as unknown) as IWindow).snapshots, ); await waitForRAF(ctx.page); - // two events from main frame, and two from iframe + // two events (full snapshot + meta) from main frame, + // and two (full snapshot + mutation) from iframe expect(events.length).toBe(4); + assertSnapshot(events); }); }); From 92b9594f8c3517c4625613c23f5cac82c08068ad Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 23:20:49 +0200 Subject: [PATCH 21/61] Support custom events as best we can in cross origin iframes --- packages/rrweb/src/record/iframe-manager.ts | 29 +- .../cross-origin-iframes.test.ts.snap | 261 ++++++++++++++++++ .../test/record/cross-origin-iframes.test.ts | 15 + 3 files changed, 298 insertions(+), 7 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 45dc46e195..33c6950125 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -180,19 +180,16 @@ export class IframeManager { break; } case IncrementalSource.Font: { - // TODO + // fine as-is no modification needed break; } - // case IncrementalSource.Log: { - // // TODO - // break; - // } case IncrementalSource.Drag: { // TODO break; } case IncrementalSource.StyleDeclaration: { // TODO + // setup a map for the stylemirror, it has its own ids that need mapping break; } case IncrementalSource.Selection: { @@ -210,9 +207,27 @@ export class IframeManager { } } return e; - } else if (e.type === EventType.Meta) { - // skip meta events + } else if ( + e.type === EventType.Meta || + e.type === EventType.Load || + e.type === EventType.DomContentLoaded + ) { + // skip meta and load events return; + } else if (e.type === EventType.Plugin) { + return e; + } else if (e.type === EventType.Custom) { + this.replaceIds( + e.data.payload as { + id?: unknown; + parentId?: unknown; + previousId?: unknown; + nextId?: unknown; + }, + iframeEl, + ['id', 'parentId', 'previousId', 'nextId'], + ); + return e; } return e; } diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index 7ddc52a793..7a802abb53 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -2963,6 +2963,267 @@ exports[`cross origin iframes move-node.html should record canvas elements 1`] = ]" `; +exports[`cross origin iframes move-node.html should record custom events 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\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"rr_src\\": \\"http://localhost:3030/html/move-node.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 16 + } + ], + \\"id\\": 15 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 29 + } + ], + \\"id\\": 28 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + } + ], + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 32 + } + ], + \\"id\\": 17 + } + ], + \\"id\\": 13 + } + ], + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 5, + \\"data\\": { + \\"tag\\": \\"test\\", + \\"payload\\": { + \\"id\\": 11, + \\"parentId\\": 11, + \\"nextId\\": 12 + } + } + } +]" +`; + exports[`same origin iframes should emit contents of iframe once 1`] = ` "[ { diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index f257072499..49562f6589 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -315,6 +315,21 @@ describe('cross origin iframes', function (this: ISuite) { document.body.appendChild(canvas); }); await waitForRAF(ctx.page); + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + + it('should record custom events', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + await frame.evaluate(() => { + ((window as unknown) as IWindow).rrweb.addCustomEvent('test', { + id: 1, + parentId: 1, + nextId: 2, + }); + }); await waitForRAF(ctx.page); const snapshots = (await ctx.page.evaluate( 'window.snapshots', From 20a39e6b9c8d1f68d6f92d24682f9e71d3208308 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Oct 2022 23:35:21 +0200 Subject: [PATCH 22/61] Use earliest version of puppeteer that works with cross origin live-stream --- packages/rrweb/package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 818af72767..64bf1c3ed9 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -66,7 +66,7 @@ "jest-image-snapshot": "^4.5.1", "jest-snapshot": "^23.6.0", "prettier": "2.2.1", - "puppeteer": "^12.0.0", + "puppeteer": "^11.0.0", "rollup": "^2.68.0", "rollup-plugin-esbuild": "^4.9.1", "rollup-plugin-postcss": "^3.1.1", diff --git a/yarn.lock b/yarn.lock index 7986cd83fb..e5e5dc816d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4166,10 +4166,10 @@ devtools-protocol@0.0.869402: resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz" integrity sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA== -devtools-protocol@0.0.937139: - version "0.0.937139" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.937139.tgz#bdee3751fdfdb81cb701fd3afa94b1065dafafcf" - integrity sha512-daj+rzR3QSxsPRy5vjjthn58axO8c11j58uY0lG5vvlJk/EiOdCWOptGdkXDjtuRHr78emKq0udHCXM4trhoDQ== +devtools-protocol@0.0.901419: + version "0.0.901419" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd" + integrity sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ== dezalgo@^1.0.0: version "1.0.3" @@ -9562,13 +9562,13 @@ puppeteer@^1.15.0: rimraf "^2.6.1" ws "^6.1.0" -puppeteer@^12.0.0: - version "12.0.1" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-12.0.1.tgz#ae79d0e174a07563e0bf2e05c94ccafce3e70033" - integrity sha512-YQ3GRiyZW0ddxTW+iiQcv2/8TT5c3+FcRUCg7F8q2gHqxd5akZN400VRXr9cHQKLWGukmJLDiE72MrcLK9tFHQ== +puppeteer@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-11.0.0.tgz#0808719c38e15315ecc1b1c28911f1c9054d201f" + integrity sha512-6rPFqN1ABjn4shgOICGDBITTRV09EjXVqhDERBDKwCLz0UyBxeeBH6Ay0vQUJ84VACmlxwzOIzVEJXThcF3aNg== dependencies: debug "4.3.2" - devtools-protocol "0.0.937139" + devtools-protocol "0.0.901419" extract-zip "2.0.1" https-proxy-agent "5.0.0" node-fetch "2.6.5" From 738a1fc657899b85d6c877f3fa6afe8515ee7ad4 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 27 Oct 2022 10:03:33 +0200 Subject: [PATCH 23/61] Map mouse/touch interaction events --- packages/rrweb/src/record/iframe-manager.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 33c6950125..a3627da837 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -149,28 +149,25 @@ export class IframeManager { }); break; } + case IncrementalSource.Drag: + case IncrementalSource.TouchMove: case IncrementalSource.MouseMove: { - // TODO - break; - } - case IncrementalSource.MouseInteraction: { - // TODO + e.data.positions.forEach((p) => { + this.replaceIds(p, iframeEl, ['id']); + }); break; } case IncrementalSource.ViewportResize: { // can safely ignore these events return; } + case IncrementalSource.MouseInteraction: case IncrementalSource.Scroll: case IncrementalSource.CanvasMutation: case IncrementalSource.Input: { this.replaceIds(e.data, iframeEl, ['id']); break; } - case IncrementalSource.TouchMove: { - // TODO - break; - } case IncrementalSource.MediaInteraction: { // TODO break; @@ -183,10 +180,6 @@ export class IframeManager { // fine as-is no modification needed break; } - case IncrementalSource.Drag: { - // TODO - break; - } case IncrementalSource.StyleDeclaration: { // TODO // setup a map for the stylemirror, it has its own ids that need mapping From 66353909a36dce409401d91e471e31446df013e1 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 27 Oct 2022 10:08:32 +0200 Subject: [PATCH 24/61] Update snapshots for correctly mapped click events --- .../cross-origin-iframes.test.ts.snap | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index 7a802abb53..4ad2480e07 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -552,7 +552,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 5, - \\"id\\": 24 + \\"id\\": 34 } }, { @@ -596,7 +596,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 1, - \\"id\\": 29 + \\"id\\": 39 } }, { @@ -604,7 +604,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 6, - \\"id\\": 24 + \\"id\\": 34 } }, { @@ -612,7 +612,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 5, - \\"id\\": 29 + \\"id\\": 39 } }, { @@ -620,7 +620,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 0, - \\"id\\": 29 + \\"id\\": 39 } }, { @@ -628,7 +628,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 2, - \\"id\\": 29 + \\"id\\": 39 } }, { @@ -654,7 +654,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 1, - \\"id\\": 39 + \\"id\\": 49 } }, { @@ -662,7 +662,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 6, - \\"id\\": 29 + \\"id\\": 39 } }, { @@ -670,7 +670,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 5, - \\"id\\": 39 + \\"id\\": 49 } }, { @@ -678,7 +678,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 0, - \\"id\\": 39 + \\"id\\": 49 } }, { @@ -686,7 +686,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 2, - \\"id\\": 39 + \\"id\\": 49 } }, { @@ -703,7 +703,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 6, - \\"id\\": 39 + \\"id\\": 49 } }, { @@ -711,7 +711,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 5, - \\"id\\": 61 + \\"id\\": 71 } }, { @@ -791,7 +791,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 6, - \\"id\\": 61 + \\"id\\": 71 } }, { @@ -799,7 +799,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] = \\"data\\": { \\"source\\": 2, \\"type\\": 5, - \\"id\\": 44 + \\"id\\": 54 } }, { From f88487896cad8431a594a81a9f485532f1622008 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 27 Oct 2022 10:18:59 +0200 Subject: [PATCH 25/61] Tweak tests for new puppeteer version --- packages/rrweb/test/__snapshots__/integration.test.ts.snap | 4 ++-- packages/rrweb/test/integration.test.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 61c6d0b465..7107527c5a 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -4177,7 +4177,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"attributes\\": { \\"class\\": \\"rr-block\\", \\"rr_width\\": \\"1904px\\", - \\"rr_height\\": \\"21px\\" + \\"rr_height\\": \\"21.5px\\" }, \\"childNodes\\": [], \\"id\\": 33 @@ -4349,7 +4349,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"attributes\\": { \\"class\\": \\"rr-block\\", \\"rr_width\\": \\"1904px\\", - \\"rr_height\\": \\"21px\\" + \\"rr_height\\": \\"21.5px\\" }, \\"childNodes\\": [], \\"id\\": 59 diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index b2ab5624f7..9fd83a46be 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -536,6 +536,7 @@ describe('record integration tests', function (this: ISuite) { await page.frames()[1].evaluate(() => { console.log('from iframe'); }); + await waitForRAF(page); const snapshots = (await page.evaluate( 'window.snapshots', From a5984292fc93f0eb78e245efc0d5f8399be96a92 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 27 Oct 2022 10:36:35 +0200 Subject: [PATCH 26/61] Map MediaInteraction correctly for cross origin iframes --- packages/rrweb/src/record/iframe-manager.ts | 5 +- .../test/html/assets/1-minute-of-silence.mp3 | Bin 0 -> 96246 bytes packages/rrweb/test/html/audio.html | 16 + .../cross-origin-iframes.test.ts.snap | 307 ++++++++++++++++++ .../test/record/cross-origin-iframes.test.ts | 29 ++ 5 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 packages/rrweb/test/html/assets/1-minute-of-silence.mp3 create mode 100644 packages/rrweb/test/html/audio.html diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index a3627da837..abfa40d223 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -161,6 +161,7 @@ export class IframeManager { // can safely ignore these events return; } + case IncrementalSource.MediaInteraction: case IncrementalSource.MouseInteraction: case IncrementalSource.Scroll: case IncrementalSource.CanvasMutation: @@ -168,10 +169,6 @@ export class IframeManager { this.replaceIds(e.data, iframeEl, ['id']); break; } - case IncrementalSource.MediaInteraction: { - // TODO - break; - } case IncrementalSource.StyleSheetRule: { // TODO break; diff --git a/packages/rrweb/test/html/assets/1-minute-of-silence.mp3 b/packages/rrweb/test/html/assets/1-minute-of-silence.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..a0f601c79af9a8d447699803914630a1942d859c GIT binary patch literal 96246 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

@Rk`@T?tKZGs(?ng2tw{8YX^1|Bz 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 literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/html/audio.html b/packages/rrweb/test/html/audio.html new file mode 100644 index 0000000000..9f8c231102 --- /dev/null +++ b/packages/rrweb/test/html/audio.html @@ -0,0 +1,16 @@ + + + + + + + Audio + + +

1 minute of silence

+ + + diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index 4ad2480e07..b06beb0a7f 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -1,5 +1,312 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`cross origin iframes audio.html should emit contents of iframe once 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\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"rr_src\\": \\"http://localhost:3030/html/audio.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Audio\\", + \\"id\\": 23 + } + ], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 26 + } + ], + \\"id\\": 25 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"h1\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1 minute of silence\\", + \\"id\\": 31 + } + ], + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 32 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"audio\\", + \\"attributes\\": { + \\"controls\\": \\"\\", + \\"rr_mediaState\\": \\"paused\\", + \\"rr_mediaCurrentTime\\": 0 + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"source\\", + \\"attributes\\": { + \\"src\\": \\"http://localhost:3030/html/assets/1-minute-of-silence.mp3\\", + \\"type\\": \\"audio/mpeg\\" + }, + \\"childNodes\\": [], + \\"id\\": 35 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n Your browser does not support the audio element.\\\\n \\", + \\"id\\": 36 + } + ], + \\"id\\": 33 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 37 + } + ], + \\"id\\": 28 + } + ], + \\"id\\": 13 + } + ], + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 7, + \\"type\\": 0, + \\"id\\": 33, + \\"currentTime\\": 0, + \\"volume\\": 1, + \\"muted\\": false, + \\"playbackRate\\": 1 + } + } +]" +`; + exports[`cross origin iframes form.html should map input events correctly 1`] = ` "[ { diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index 49562f6589..b626e54d8d 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -337,6 +337,35 @@ describe('cross origin iframes', function (this: ISuite) { assertSnapshot(snapshots); }); }); + + describe('audio.html', function (this: ISuite) { + jest.setTimeout(100_000); + + const ctx: ISuite = setup.call( + this, + ` + + + + + + + `, + ); + + it('should emit contents of iframe once', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + await frame.evaluate(() => { + const el = document.querySelector('audio')!; + el.play(); + }); + await waitForRAF(ctx.page); + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + }); }); describe('same origin iframes', function (this: ISuite) { From 0f91e987ff27b3d590aebad1505e9fb121814d54 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 27 Oct 2022 10:47:54 +0200 Subject: [PATCH 27/61] Make tests consistent between high and low dpi devices --- .../__snapshots__/integration.test.ts.snap | 241 ++++++++++-------- .../rrweb/test/html/blocked-unblocked.html | 108 ++++---- 2 files changed, 186 insertions(+), 163 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 7107527c5a..f2d86c86bb 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -4014,8 +4014,27 @@ exports[`record integration tests mutations should work when blocked class is un }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n\\\\n\\", + \\"textContent\\": \\"\\\\n \\", \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"#b-class, #b-class-2 { height: 33px; width: 200px; }\\", + \\"isStyle\\": true, + \\"id\\": 9 + } + ], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\", + \\"id\\": 10 } ], \\"id\\": 3 @@ -4023,7 +4042,7 @@ exports[`record integration tests mutations should work when blocked class is un { \\"type\\": 3, \\"textContent\\": \\"\\\\n\\\\n\\", - \\"id\\": 8 + \\"id\\": 11 }, { \\"type\\": 2, @@ -4033,7 +4052,7 @@ exports[`record integration tests mutations should work when blocked class is un { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 10 + \\"id\\": 13 }, { \\"type\\": 2, @@ -4043,27 +4062,27 @@ exports[`record integration tests mutations should work when blocked class is un { \\"type\\": 3, \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 12 + \\"id\\": 15 } ], - \\"id\\": 11 + \\"id\\": 14 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 13 + \\"id\\": 16 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 14 + \\"id\\": 17 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 15 + \\"id\\": 18 }, { \\"type\\": 2, @@ -4072,28 +4091,28 @@ exports[`record integration tests mutations should work when blocked class is un \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"\\\\n Verify that block class bugs are fixed\\\\n \\", - \\"id\\": 17 + \\"textContent\\": \\"Verify that block class bugs are fixed\\", + \\"id\\": 20 } ], - \\"id\\": 16 + \\"id\\": 19 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 18 + \\"id\\": 21 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 19 + \\"id\\": 22 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 20 + \\"id\\": 23 }, { \\"type\\": 2, @@ -4105,7 +4124,7 @@ exports[`record integration tests mutations should work when blocked class is un { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 22 + \\"id\\": 25 }, { \\"type\\": 2, @@ -4117,7 +4136,7 @@ exports[`record integration tests mutations should work when blocked class is un { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 24 + \\"id\\": 27 }, { \\"type\\": 2, @@ -4127,91 +4146,91 @@ exports[`record integration tests mutations should work when blocked class is un { \\"type\\": 3, \\"textContent\\": \\"VISIBLE\\", - \\"id\\": 26 + \\"id\\": 29 } ], - \\"id\\": 25 + \\"id\\": 28 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 27 + \\"id\\": 30 } ], - \\"id\\": 23 + \\"id\\": 26 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 28 + \\"id\\": 31 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 29 + \\"id\\": 32 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 30 + \\"id\\": 33 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 31 + \\"id\\": 34 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 32 + \\"id\\": 35 }, { \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": { \\"class\\": \\"rr-block\\", - \\"rr_width\\": \\"1904px\\", - \\"rr_height\\": \\"21.5px\\" + \\"rr_width\\": \\"200px\\", + \\"rr_height\\": \\"33px\\" }, \\"childNodes\\": [], - \\"id\\": 33 + \\"id\\": 36 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 34 + \\"id\\": 37 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 35 + \\"id\\": 38 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 36 + \\"id\\": 39 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 37 + \\"id\\": 40 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 38 + \\"id\\": 41 }, { \\"type\\": 2, @@ -4223,49 +4242,49 @@ exports[`record integration tests mutations should work when blocked class is un { \\"type\\": 3, \\"textContent\\": \\"MUTATE\\", - \\"id\\": 40 + \\"id\\": 43 } ], - \\"id\\": 39 + \\"id\\": 42 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 41 + \\"id\\": 44 } ], - \\"id\\": 21 + \\"id\\": 24 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 42 + \\"id\\": 45 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 43 + \\"id\\": 46 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 44 + \\"id\\": 47 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 45 + \\"id\\": 48 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 46 + \\"id\\": 49 }, { \\"type\\": 2, @@ -4277,7 +4296,7 @@ exports[`record integration tests mutations should work when blocked class is un { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 48 + \\"id\\": 51 }, { \\"type\\": 2, @@ -4289,7 +4308,7 @@ exports[`record integration tests mutations should work when blocked class is un { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 50 + \\"id\\": 53 }, { \\"type\\": 2, @@ -4299,91 +4318,91 @@ exports[`record integration tests mutations should work when blocked class is un { \\"type\\": 3, \\"textContent\\": \\"VISIBLE\\", - \\"id\\": 52 + \\"id\\": 55 } ], - \\"id\\": 51 + \\"id\\": 54 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 53 + \\"id\\": 56 } ], - \\"id\\": 49 + \\"id\\": 52 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 54 + \\"id\\": 57 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 55 + \\"id\\": 58 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 56 + \\"id\\": 59 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 57 + \\"id\\": 60 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 58 + \\"id\\": 61 }, { \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": { \\"class\\": \\"rr-block\\", - \\"rr_width\\": \\"1904px\\", - \\"rr_height\\": \\"21.5px\\" + \\"rr_width\\": \\"200px\\", + \\"rr_height\\": \\"33px\\" }, \\"childNodes\\": [], - \\"id\\": 59 + \\"id\\": 62 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 60 + \\"id\\": 63 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 61 + \\"id\\": 64 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 62 + \\"id\\": 65 }, { \\"type\\": 2, \\"tagName\\": \\"br\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 63 + \\"id\\": 66 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 64 + \\"id\\": 67 }, { \\"type\\": 2, @@ -4395,23 +4414,23 @@ exports[`record integration tests mutations should work when blocked class is un { \\"type\\": 3, \\"textContent\\": \\"MUTATE\\", - \\"id\\": 66 + \\"id\\": 69 } ], - \\"id\\": 65 + \\"id\\": 68 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 67 + \\"id\\": 70 } ], - \\"id\\": 47 + \\"id\\": 50 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n\\\\n \\", - \\"id\\": 68 + \\"id\\": 71 }, { \\"type\\": 2, @@ -4421,18 +4440,18 @@ exports[`record integration tests mutations should work when blocked class is un { \\"type\\": 3, \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 70 + \\"id\\": 73 } ], - \\"id\\": 69 + \\"id\\": 72 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\\\n \\\\n\\", - \\"id\\": 71 + \\"id\\": 74 } ], - \\"id\\": 9 + \\"id\\": 12 } ], \\"id\\": 2 @@ -4452,7 +4471,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"data\\": { \\"source\\": 2, \\"type\\": 1, - \\"id\\": 39 + \\"id\\": 42 } }, { @@ -4460,7 +4479,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"data\\": { \\"source\\": 2, \\"type\\": 5, - \\"id\\": 39 + \\"id\\": 42 } }, { @@ -4468,7 +4487,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"data\\": { \\"source\\": 2, \\"type\\": 0, - \\"id\\": 39 + \\"id\\": 42 } }, { @@ -4476,7 +4495,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"data\\": { \\"source\\": 2, \\"type\\": 2, - \\"id\\": 39 + \\"id\\": 42 } }, { @@ -4486,7 +4505,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"texts\\": [], \\"attributes\\": [ { - \\"id\\": 33, + \\"id\\": 36, \\"attributes\\": { \\"class\\": \\"notB\\" } @@ -4495,76 +4514,76 @@ exports[`record integration tests mutations should work when blocked class is un \\"removes\\": [], \\"adds\\": [ { - \\"parentId\\": 23, + \\"parentId\\": 26, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 72 + \\"id\\": 75 } }, { - \\"parentId\\": 72, + \\"parentId\\": 75, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 73 + \\"id\\": 76 } }, { - \\"parentId\\": 73, + \\"parentId\\": 76, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"button\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 74 + \\"id\\": 77 } }, { - \\"parentId\\": 74, + \\"parentId\\": 77, \\"nextId\\": null, \\"node\\": { \\"type\\": 3, \\"textContent\\": \\"I1I2 VISIBLE\\", - \\"id\\": 75 + \\"id\\": 78 } }, { - \\"parentId\\": 72, - \\"nextId\\": 73, + \\"parentId\\": 75, + \\"nextId\\": 76, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 76 + \\"id\\": 79 } }, { - \\"parentId\\": 76, + \\"parentId\\": 79, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"button\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 77 + \\"id\\": 80 } }, { - \\"parentId\\": 77, + \\"parentId\\": 80, \\"nextId\\": null, \\"node\\": { \\"type\\": 3, \\"textContent\\": \\"I1I1 VISIBLE\\", - \\"id\\": 78 + \\"id\\": 81 } } ] @@ -4575,7 +4594,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"data\\": { \\"source\\": 2, \\"type\\": 1, - \\"id\\": 65 + \\"id\\": 68 } }, { @@ -4583,7 +4602,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"data\\": { \\"source\\": 2, \\"type\\": 6, - \\"id\\": 39 + \\"id\\": 42 } }, { @@ -4591,7 +4610,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"data\\": { \\"source\\": 2, \\"type\\": 5, - \\"id\\": 65 + \\"id\\": 68 } }, { @@ -4599,7 +4618,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"data\\": { \\"source\\": 2, \\"type\\": 0, - \\"id\\": 65 + \\"id\\": 68 } }, { @@ -4607,7 +4626,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"data\\": { \\"source\\": 2, \\"type\\": 2, - \\"id\\": 65 + \\"id\\": 68 } }, { @@ -4617,7 +4636,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"texts\\": [], \\"attributes\\": [ { - \\"id\\": 59, + \\"id\\": 62, \\"attributes\\": { \\"class\\": \\"notB\\" } @@ -4626,76 +4645,76 @@ exports[`record integration tests mutations should work when blocked class is un \\"removes\\": [], \\"adds\\": [ { - \\"parentId\\": 49, + \\"parentId\\": 52, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 79 + \\"id\\": 82 } }, { - \\"parentId\\": 79, + \\"parentId\\": 82, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 80 + \\"id\\": 83 } }, { - \\"parentId\\": 80, + \\"parentId\\": 83, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"button\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 81 + \\"id\\": 84 } }, { - \\"parentId\\": 81, + \\"parentId\\": 84, \\"nextId\\": null, \\"node\\": { \\"type\\": 3, \\"textContent\\": \\"I1I2 VISIBLE\\", - \\"id\\": 82 + \\"id\\": 85 } }, { - \\"parentId\\": 79, - \\"nextId\\": 80, + \\"parentId\\": 82, + \\"nextId\\": 83, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 83 + \\"id\\": 86 } }, { - \\"parentId\\": 83, + \\"parentId\\": 86, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"button\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 84 + \\"id\\": 87 } }, { - \\"parentId\\": 84, + \\"parentId\\": 87, \\"nextId\\": null, \\"node\\": { \\"type\\": 3, \\"textContent\\": \\"I1I1 VISIBLE\\", - \\"id\\": 85 + \\"id\\": 88 } } ] diff --git a/packages/rrweb/test/html/blocked-unblocked.html b/packages/rrweb/test/html/blocked-unblocked.html index ff87e546dd..5d82c6cd79 100644 --- a/packages/rrweb/test/html/blocked-unblocked.html +++ b/packages/rrweb/test/html/blocked-unblocked.html @@ -1,88 +1,92 @@ Uber Application for Codegen Testing - + -
-

- Verify that block class bugs are fixed -

-
+
+

Verify that block class bugs are fixed

+
-


+


-


+


-


+


-


+


-


+


From 784c2da7c155cd1ea7ffd8a2846943c8da91b891 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 27 Oct 2022 10:49:19 +0200 Subject: [PATCH 28/61] Make test less flaky --- packages/rrweb/test/integration.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 9fd83a46be..cdabd48b88 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -533,6 +533,7 @@ describe('record integration tests', function (this: ISuite) { document.body.appendChild(iframe); }); + await waitForRAF(page); await page.frames()[1].evaluate(() => { console.log('from iframe'); }); From 50c907e08ec8ac6b2bdd2d41d509d6fdbe99806c Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 27 Oct 2022 10:54:50 +0200 Subject: [PATCH 29/61] Make test less flaky --- packages/rrweb/test/record.test.ts | 81 +++++++++++++++++------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index e353559a26..fbb77b11c0 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -465,54 +465,63 @@ describe('record', function (this: ISuite) { it('captures mutations on adopted stylesheets', async () => { await ctx.page.evaluate(() => { - document.body.innerHTML = ` + return new Promise((resolve) => { + document.body.innerHTML = `
div in outermost document
`; - const sheet = new CSSStyleSheet(); - // Add stylesheet to a document. + const sheet = new CSSStyleSheet(); + // Add stylesheet to a document. - document.adoptedStyleSheets = [sheet]; + document.adoptedStyleSheets = [sheet]; - const iframe = document.querySelector('iframe'); - const sheet2 = new (iframe!.contentWindow! as Window & - typeof globalThis).CSSStyleSheet(); + const iframe = document.querySelector('iframe'); + const sheet2 = new (iframe!.contentWindow! as Window & + typeof globalThis).CSSStyleSheet(); - // Add stylesheet to an IFrame document. - iframe!.contentDocument!.adoptedStyleSheets = [sheet2]; - iframe!.contentDocument!.body.innerHTML = '

h1 in iframe

'; + // Add stylesheet to an IFrame document. + iframe!.contentDocument!.adoptedStyleSheets = [sheet2]; + iframe!.contentDocument!.body.innerHTML = '

h1 in iframe

'; - const { record } = ((window as unknown) as IWindow).rrweb; - record({ - emit: ((window as unknown) as IWindow).emit, - }); + const { record } = ((window as unknown) as IWindow).rrweb; + record({ + emit: ((window as unknown) as IWindow).emit, + }); - setTimeout(() => { - sheet.replace!('div { color: yellow; }'); - sheet2.replace!('h1 { color: blue; }'); - }, 0); + setTimeout(() => { + sheet.replace!('div { color: yellow; }'); + sheet2.replace!('h1 { color: blue; }'); + }, 0); - setTimeout(() => { - sheet.replaceSync!('div { display: inline ; }'); - sheet2.replaceSync!('h1 { font-size: large; }'); - }, 5); + setTimeout(() => { + sheet.replaceSync!('div { display: inline ; }'); + sheet2.replaceSync!('h1 { font-size: large; }'); + }, 5); - setTimeout(() => { - (sheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green'); - (sheet.cssRules[0] as CSSStyleRule).style.removeProperty('display'); - (sheet2.cssRules[0] as CSSStyleRule).style.setProperty( - 'font-size', - 'medium', - 'important', - ); - sheet2.insertRule('h2 { color: red; }'); - }, 10); + setTimeout(() => { + (sheet.cssRules[0] as CSSStyleRule).style.setProperty( + 'color', + 'green', + ); + (sheet.cssRules[0] as CSSStyleRule).style.removeProperty('display'); + (sheet2.cssRules[0] as CSSStyleRule).style.setProperty( + 'font-size', + 'medium', + 'important', + ); + sheet2.insertRule('h2 { color: red; }'); + }, 10); - setTimeout(() => { - sheet.insertRule('body { border: 2px solid blue; }', 1); - sheet2.deleteRule(0); - }, 15); + setTimeout(() => { + sheet.insertRule('body { border: 2px solid blue; }', 1); + sheet2.deleteRule(0); + }, 15); + + setTimeout(() => { + resolve(undefined); + }, 20); + }); }); await waitForRAF(ctx.page); assertSnapshot(ctx.events); From 97b15ed6cc7f49bd289d560b11ca102c2cd69737 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 27 Oct 2022 11:00:18 +0200 Subject: [PATCH 30/61] Make test less flaky --- packages/rrweb/test/record.test.ts | 93 ++++++++++++++++-------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index fbb77b11c0..1efadb546c 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -671,6 +671,7 @@ describe('record', function (this: ISuite) { // `blob:` URLs are not available immediately, so we need to wait for the browser to load them await waitForRAF(ctx.page); + await waitForRAF(ctx.page); const filteredEvents = JSON.parse( JSON.stringify(ctx.events).replace(/blob\:[\w\d-/]+"/, 'blob:null"'), ); @@ -704,65 +705,71 @@ describe('record', function (this: ISuite) { it('captures adopted stylesheets in shadow doms and iframe', async () => { await ctx.page.evaluate(() => { - document.body.innerHTML = ` + return new Promise((resolve) => { + document.body.innerHTML = `
div in outermost document
`; - const sheet = new CSSStyleSheet(); - sheet.replaceSync!( - 'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}', - ); - // Add stylesheet to a document. - - document.adoptedStyleSheets = [sheet]; + const sheet = new CSSStyleSheet(); + sheet.replaceSync!( + 'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}', + ); + // Add stylesheet to a document. - // Add stylesheet to a shadow host. - const host = document.querySelector('#shadow-host1'); - const shadow = host!.attachShadow({ mode: 'open' }); - shadow.innerHTML = - '
div in shadow dom 1
span in shadow dom 1'; - const sheet2 = new CSSStyleSheet(); + document.adoptedStyleSheets = [sheet]; - sheet2.replaceSync!('span { color: red; }'); + // Add stylesheet to a shadow host. + const host = document.querySelector('#shadow-host1'); + const shadow = host!.attachShadow({ mode: 'open' }); + shadow.innerHTML = + '
div in shadow dom 1
span in shadow dom 1'; + const sheet2 = new CSSStyleSheet(); - shadow.adoptedStyleSheets = [sheet, sheet2]; + sheet2.replaceSync!('span { color: red; }'); - // Add stylesheet to an IFrame document. - const iframe = document.querySelector('iframe'); - const sheet3 = new (iframe!.contentWindow! as IWindow & - typeof globalThis).CSSStyleSheet(); - sheet3.replaceSync!('h1 { color: blue; }'); + shadow.adoptedStyleSheets = [sheet, sheet2]; - iframe!.contentDocument!.adoptedStyleSheets = [sheet3]; + // Add stylesheet to an IFrame document. + const iframe = document.querySelector('iframe'); + const sheet3 = new (iframe!.contentWindow! as IWindow & + typeof globalThis).CSSStyleSheet(); + sheet3.replaceSync!('h1 { color: blue; }'); - const ele = iframe!.contentDocument!.createElement('h1'); - ele.innerText = 'h1 in iframe'; - iframe!.contentDocument!.body.appendChild(ele); + iframe!.contentDocument!.adoptedStyleSheets = [sheet3]; - ((window as unknown) as IWindow).rrweb.record({ - emit: ((window.top as unknown) as IWindow).emit, - }); + const ele = iframe!.contentDocument!.createElement('h1'); + ele.innerText = 'h1 in iframe'; + iframe!.contentDocument!.body.appendChild(ele); - // Make incremental changes to shadow dom. - setTimeout(() => { - const host = document.querySelector('#shadow-host2'); - const shadow = host!.attachShadow({ mode: 'open' }); - shadow.innerHTML = - '
div in shadow dom 2
span in shadow dom 2'; - const sheet4 = new CSSStyleSheet(); - sheet4.replaceSync!('span { color: green; }'); - shadow.adoptedStyleSheets = [sheet, sheet4]; + ((window as unknown) as IWindow).rrweb.record({ + emit: ((window.top as unknown) as IWindow).emit, + }); - document.adoptedStyleSheets = [sheet4, sheet, sheet2]; + // Make incremental changes to shadow dom. + setTimeout(() => { + const host = document.querySelector('#shadow-host2'); + const shadow = host!.attachShadow({ mode: 'open' }); + shadow.innerHTML = + '
div in shadow dom 2
span in shadow dom 2'; + const sheet4 = new CSSStyleSheet(); + sheet4.replaceSync!('span { color: green; }'); + shadow.adoptedStyleSheets = [sheet, sheet4]; + + document.adoptedStyleSheets = [sheet4, sheet, sheet2]; + + const sheet5 = new (iframe!.contentWindow! as IWindow & + typeof globalThis).CSSStyleSheet(); + sheet5.replaceSync!('h2 { color: purple; }'); + iframe!.contentDocument!.adoptedStyleSheets = [sheet5, sheet3]; + }, 10); - const sheet5 = new (iframe!.contentWindow! as IWindow & - typeof globalThis).CSSStyleSheet(); - sheet5.replaceSync!('h2 { color: purple; }'); - iframe!.contentDocument!.adoptedStyleSheets = [sheet5, sheet3]; - }, 10); + setTimeout(() => { + resolve(null); + }, 20); + }); }); await waitForRAF(ctx.page); // wait till events get sent From 89b14bd12b38cf3077f5d8d9f1ddc64f71b24188 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 27 Oct 2022 11:07:51 +0200 Subject: [PATCH 31/61] Make test less flaky --- packages/rrweb/test/record.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 1efadb546c..8347825935 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -645,7 +645,6 @@ describe('record', function (this: ISuite) { 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!; @@ -670,8 +669,11 @@ describe('record', function (this: ISuite) { }); // `blob:` URLs are not available immediately, so we need to wait for the browser to load them + // the following is a bit overkill but this test is super flaky on CI await waitForRAF(ctx.page); + await ctx.page.waitForTimeout(50); await waitForRAF(ctx.page); + const filteredEvents = JSON.parse( JSON.stringify(ctx.events).replace(/blob\:[\w\d-/]+"/, 'blob:null"'), ); From cb261536c81abd67c90ff6e7a6d82e7fcb3b4aa1 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 27 Oct 2022 14:24:22 +0200 Subject: [PATCH 32/61] Add support for styles in cross origin iframes --- packages/rrweb/src/record/iframe-manager.ts | 76 +++- packages/rrweb/src/utils.ts | 4 + .../cross-origin-iframes.test.ts.snap | 393 ++++++++++++++++++ .../test/record/cross-origin-iframes.test.ts | 64 +++ 4 files changed, 519 insertions(+), 18 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index abfa40d223..4369221a15 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -19,6 +19,10 @@ export class IframeManager { HTMLIFrameElement, Map > = new WeakMap(); + private iframeStyleIdMap: WeakMap< + HTMLIFrameElement, + Map + > = new WeakMap(); private mirror: Mirror; private mutationCb: mutationCallBack; private wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void; @@ -169,19 +173,15 @@ export class IframeManager { this.replaceIds(e.data, iframeEl, ['id']); break; } - case IncrementalSource.StyleSheetRule: { - // TODO + case IncrementalSource.StyleSheetRule: + case IncrementalSource.StyleDeclaration: { + this.replaceStyleIds(e.data, iframeEl, ['styleId']); break; } case IncrementalSource.Font: { // fine as-is no modification needed break; } - case IncrementalSource.StyleDeclaration: { - // TODO - // setup a map for the stylemirror, it has its own ids that need mapping - break; - } case IncrementalSource.Selection: { e.data.ranges.forEach((range) => { this.replaceIds(range, iframeEl, ['start', 'end']); @@ -189,7 +189,11 @@ export class IframeManager { break; } case IncrementalSource.AdoptedStyleSheet: { - // TODO + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleIds']); + e.data.styles?.forEach((style) => { + this.replaceStyleIds(style, iframeEl, ['styleId']); + }); break; } default: { @@ -222,31 +226,67 @@ export class IframeManager { return e; } - private replaceIds>( + private replace>( + map: WeakMap>, + generateIdFn: () => number, obj: T, iframeEl: HTMLIFrameElement, keys: Array, ): T { - let idMap = this.iframeIdMap.get(iframeEl); + let idMap = map.get(iframeEl); if (!idMap) { idMap = new Map(); - this.iframeIdMap.set(iframeEl, idMap); + map.set(iframeEl, idMap); } for (const key of keys) { - if (typeof obj[key] !== 'number') continue; - const originalId = obj[key] as number; - let newId = idMap.get(originalId); - if (!newId) { - newId = genId(); - idMap.set(originalId, newId); + if (!Array.isArray(obj[key]) && typeof obj[key] !== 'number') continue; + if (Array.isArray(obj[key])) { + obj[key] = (obj[key] as unknown[]).map((id) => { + if (typeof id !== 'number') return id; + if (idMap!.has(id)) return idMap!.get(id); + const newId = generateIdFn(); + idMap!.set(id, newId); + return newId; + }) as T[keyof T]; + } else { + const originalId = obj[key] as number; + let newId = idMap.get(originalId); + if (!newId) { + newId = generateIdFn(); + idMap.set(originalId, newId); + } + (obj[key] as number) = newId; } - (obj[key] as number) = newId; } return obj; } + private replaceIds>( + obj: T, + iframeEl: HTMLIFrameElement, + keys: Array, + ): T { + return this.replace(this.iframeIdMap, genId, obj, iframeEl, keys); + } + + private replaceStyleIds>( + obj: T, + iframeEl: HTMLIFrameElement, + keys: Array, + ): T { + return this.replace( + this.iframeStyleIdMap, + this.stylesheetManager.styleMirror.generateId.bind( + this.stylesheetManager.styleMirror, + ), + obj, + iframeEl, + keys, + ); + } + private replaceIdOnNode( node: serializedNodeWithId, iframeEl: HTMLIFrameElement, diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index b782bd52e2..3b6b3feaf0 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -491,4 +491,8 @@ export class StyleSheetMirror { this.idStyleMap = new Map(); this.id = 1; } + + generateId(): number { + return this.id++; + } } diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index b06beb0a7f..13ec85f993 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -1817,6 +1817,399 @@ exports[`cross origin iframes form.html should map scroll events correctly 1`] = ]" `; +exports[`cross origin iframes move-node.html captures mutations on adopted stylesheets 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\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"rr_src\\": \\"http://localhost:3030/html/move-node.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 16 + } + ], + \\"id\\": 15 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 29 + } + ], + \\"id\\": 28 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + } + ], + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 32 + } + ], + \\"id\\": 17 + } + ], + \\"id\\": 13 + } + ], + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 1, + \\"styleIds\\": [ + 1 + ], + \\"styles\\": [ + { + \\"styleId\\": 1, + \\"rules\\": [] + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 15, + \\"id\\": 11, + \\"styleIds\\": [ + 2 + ], + \\"styles\\": [ + { + \\"styleId\\": 2, + \\"rules\\": [] + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 1, + \\"replace\\": \\"div { color: yellow; }\\" + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 2, + \\"replace\\": \\"h1 { color: blue; }\\" + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 1, + \\"replaceSync\\": \\"div { display: inline ; }\\" + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 2, + \\"replaceSync\\": \\"h1 { font-size: large; }\\" + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"styleId\\": 1, + \\"set\\": { + \\"property\\": \\"color\\", + \\"value\\": \\"green\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"styleId\\": 1, + \\"remove\\": { + \\"property\\": \\"display\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"styleId\\": 2, + \\"set\\": { + \\"property\\": \\"font-size\\", + \\"value\\": \\"medium\\", + \\"priority\\": \\"important\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 2, + \\"adds\\": [ + { + \\"rule\\": \\"h2 { color: red; }\\" + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 1, + \\"adds\\": [ + { + \\"rule\\": \\"body { border: 2px solid blue; }\\", + \\"index\\": 1 + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"styleId\\": 2, + \\"removes\\": [ + { + \\"index\\": 0 + } + ] + } + } +]" +`; + exports[`cross origin iframes move-node.html should record DOM attribute changes 1`] = ` "[ { diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index b626e54d8d..f8a829a78b 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -336,6 +336,70 @@ describe('cross origin iframes', function (this: ISuite) { )) as eventWithTime[]; assertSnapshot(snapshots); }); + + it('captures mutations on adopted stylesheets', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + await ctx.page.evaluate(() => { + const sheet = new CSSStyleSheet(); + // Add stylesheet to a document. + document.adoptedStyleSheets = [sheet]; + }); + await frame.evaluate(() => { + const sheet = new CSSStyleSheet(); + // Add stylesheet to a document. + document.adoptedStyleSheets = [sheet]; + }); + await waitForRAF(ctx.page); + await ctx.page.evaluate(() => { + document.adoptedStyleSheets![0].replace!('div { color: yellow; }'); + }); + await frame.evaluate(() => { + document.adoptedStyleSheets![0].replace!('h1 { color: blue; }'); + }); + await waitForRAF(ctx.page); + await ctx.page.evaluate(() => { + document.adoptedStyleSheets![0].replaceSync!( + 'div { display: inline ; }', + ); + }); + await frame.evaluate(() => { + document.adoptedStyleSheets![0].replaceSync!( + 'h1 { font-size: large; }', + ); + }); + await waitForRAF(ctx.page); + await ctx.page.evaluate(() => { + (document.adoptedStyleSheets![0] + .cssRules[0] as CSSStyleRule).style.setProperty('color', 'green'); + (document.adoptedStyleSheets![0] + .cssRules[0] as CSSStyleRule).style.removeProperty('display'); + }); + await frame.evaluate(() => { + (document.adoptedStyleSheets![0] + .cssRules[0] as CSSStyleRule).style.setProperty( + 'font-size', + 'medium', + 'important', + ); + document.adoptedStyleSheets![0].insertRule('h2 { color: red; }'); + }); + await waitForRAF(ctx.page); + await ctx.page.evaluate(() => { + document.adoptedStyleSheets![0].insertRule( + 'body { border: 2px solid blue; }', + 1, + ); + }); + await frame.evaluate(() => { + document.adoptedStyleSheets![0].deleteRule(0); + }); + await waitForRAF(ctx.page); + + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); }); describe('audio.html', function (this: ISuite) { From fc2ad8cc68a092fdf237092bb107557145dcced0 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 27 Oct 2022 14:34:05 +0200 Subject: [PATCH 33/61] Map traditional stylesheet mutations on cross origin iframes --- packages/rrweb/src/record/iframe-manager.ts | 1 + .../cross-origin-iframes.test.ts.snap | 397 ++++++++++++++++++ .../test/record/cross-origin-iframes.test.ts | 54 +++ 3 files changed, 452 insertions(+) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 4369221a15..c5bf73d6ff 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -175,6 +175,7 @@ export class IframeManager { } case IncrementalSource.StyleSheetRule: case IncrementalSource.StyleDeclaration: { + this.replaceIds(e.data, iframeEl, ['id']); this.replaceStyleIds(e.data, iframeEl, ['styleId']); break; } diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index 13ec85f993..f1c697d34c 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -2210,6 +2210,403 @@ exports[`cross origin iframes move-node.html captures mutations on adopted style ]" `; +exports[`cross origin iframes move-node.html captures mutations on stylesheets 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\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"rr_src\\": \\"http://localhost:3030/html/move-node.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 16 + } + ], + \\"id\\": 15 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 29 + } + ], + \\"id\\": 28 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + } + ], + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 32 + } + ], + \\"id\\": 17 + } + ], + \\"id\\": 13 + } + ], + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 4, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 33 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 14, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 34 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 33, + \\"adds\\": [ + { + \\"rule\\": \\"div { color: yellow; }\\" + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 34, + \\"adds\\": [ + { + \\"rule\\": \\"h1 { color: blue; }\\" + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"id\\": 33, + \\"set\\": { + \\"property\\": \\"color\\", + \\"value\\": \\"green\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"id\\": 33, + \\"remove\\": { + \\"property\\": \\"display\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"id\\": 34, + \\"set\\": { + \\"property\\": \\"font-size\\", + \\"value\\": \\"medium\\", + \\"priority\\": \\"important\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 34, + \\"adds\\": [ + { + \\"rule\\": \\"h2 { color: red; }\\" + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 33, + \\"adds\\": [ + { + \\"rule\\": \\"body { border: 2px solid blue; }\\", + \\"index\\": 1 + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 34, + \\"removes\\": [ + { + \\"index\\": 0 + } + ] + } + } +]" +`; + exports[`cross origin iframes move-node.html should record DOM attribute changes 1`] = ` "[ { diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index f8a829a78b..f93f36f019 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -400,6 +400,60 @@ describe('cross origin iframes', function (this: ISuite) { )) as eventWithTime[]; assertSnapshot(snapshots); }); + + it('captures mutations on stylesheets', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + await ctx.page.evaluate(() => { + // Add stylesheet to a document. + const style = document.createElement('style'); + document.head.appendChild(style); + }); + await frame.evaluate(() => { + // Add stylesheet to a document. + const style = document.createElement('style'); + document.head.appendChild(style); + }); + await waitForRAF(ctx.page); + await ctx.page.evaluate(() => { + document.styleSheets[0].insertRule('div { color: yellow; }'); + }); + await frame.evaluate(() => { + document.styleSheets[0].insertRule('h1 { color: blue; }'); + }); + await waitForRAF(ctx.page); + await ctx.page.evaluate(() => { + (document.styleSheets[0].cssRules[0] as CSSStyleRule).style.setProperty( + 'color', + 'green', + ); + (document.styleSheets[0] + .cssRules[0] as CSSStyleRule).style.removeProperty('display'); + }); + await frame.evaluate(() => { + (document.styleSheets[0].cssRules[0] as CSSStyleRule).style.setProperty( + 'font-size', + 'medium', + 'important', + ); + document.styleSheets[0].insertRule('h2 { color: red; }'); + }); + await waitForRAF(ctx.page); + await ctx.page.evaluate(() => { + document.styleSheets[0].insertRule( + 'body { border: 2px solid blue; }', + 1, + ); + }); + await frame.evaluate(() => { + document.styleSheets[0].deleteRule(0); + }); + await waitForRAF(ctx.page); + + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); }); describe('audio.html', function (this: ISuite) { From a348d33ad45e8ea8572385061e5df5499b24e78b Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 27 Oct 2022 18:35:22 +0200 Subject: [PATCH 34/61] Add todo --- packages/rrweb/src/plugins/canvas-webrtc/record/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts b/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts index e4000d3e82..df67966cee 100644 --- a/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts +++ b/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts @@ -83,6 +83,7 @@ export class RRWebPluginCanvasWebRTCRecord { if (stream) return stream; const el = this.mirror.getNode(id) as HTMLCanvasElement | null; + // TODO: no node found, might be in a child iframe if (!el || !('captureStream' in el)) return false; stream = el.captureStream(); From bbdea9ba3c6c0c80b57076113b12a844d6b4377e Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 28 Oct 2022 08:24:03 +0200 Subject: [PATCH 35/61] Add iframe mirror --- packages/rrweb/src/record/iframe-mirror.ts | 73 ++++++++++++++++++++++ packages/rrweb/src/types.ts | 13 ++++ 2 files changed, 86 insertions(+) create mode 100644 packages/rrweb/src/record/iframe-mirror.ts diff --git a/packages/rrweb/src/record/iframe-mirror.ts b/packages/rrweb/src/record/iframe-mirror.ts new file mode 100644 index 0000000000..9f774d7ae8 --- /dev/null +++ b/packages/rrweb/src/record/iframe-mirror.ts @@ -0,0 +1,73 @@ +export default class IframeMirror { + private iframeParentIdToLocalIdMap: WeakMap< + HTMLIFrameElement, + Map + > = new WeakMap(); + private iframeLocalIdToParentIdMap: WeakMap< + HTMLIFrameElement, + Map + > = new WeakMap(); + + constructor(private generateIdFn: () => number) {} + + getParentId(iframe: HTMLIFrameElement, localId: number): number { + const parentIdToLocalIdMap = this.getParentIdToLocalIdMap(iframe); + const localIdToParentIdMap = this.getLocalIdToParentIdMap(iframe); + + let parentId = parentIdToLocalIdMap.get(localId); + if (!parentId) { + parentId = this.generateIdFn(); + parentIdToLocalIdMap.set(localId, parentId); + localIdToParentIdMap.set(parentId, localId); + } + return parentId; + } + + getParentIds(iframe: HTMLIFrameElement, localId: number[]): number[] { + return localId.map((id) => this.getParentId(iframe, id)); + } + + getLocalId( + iframe: HTMLIFrameElement, + parentId: T, + ): T { + const localIdToParentIdMap = this.getLocalIdToParentIdMap(iframe); + if (Array.isArray(parentId)) { + return parentId.map((id) => this.getLocalId(iframe, id)) as T; + } + + if (typeof parentId !== 'number') return parentId; + + const localId = localIdToParentIdMap.get(parentId); + if (!localId) return -1 as T; + return localId as T; + } + + reset(iframe?: HTMLIFrameElement) { + if (!iframe) { + this.iframeParentIdToLocalIdMap = new WeakMap(); + this.iframeLocalIdToParentIdMap = new WeakMap(); + return; + } + this.iframeParentIdToLocalIdMap.delete(iframe); + this.iframeLocalIdToParentIdMap.delete(iframe); + } + + private getParentIdToLocalIdMap(iframe: HTMLIFrameElement) { + let parentToLocalMap = this.iframeParentIdToLocalIdMap.get(iframe); + if (!parentToLocalMap) { + parentToLocalMap = new Map(); + this.iframeParentIdToLocalIdMap.set(iframe, parentToLocalMap); + } + return parentToLocalMap; + } + + private getLocalIdToParentIdMap(iframe: HTMLIFrameElement) { + let localIdToParentIdMap = this.iframeLocalIdToParentIdMap.get(iframe); + if (!localIdToParentIdMap) { + localIdToParentIdMap = new Map(); + this.iframeLocalIdToParentIdMap.set(iframe, localIdToParentIdMap); + } + return localIdToParentIdMap; + } +} diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 68e549bee8..6f42ef90fc 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -798,6 +798,19 @@ export type IWindow = Window & typeof globalThis; export type Optional = Pick, K> & Omit; +export type GetTypedKeys = TakeTypeHelper< + Obj, + ValueType +>[keyof TakeTypeHelper]; +export type TakeTypeHelper = { + [K in keyof Obj]: Obj[K] extends ValueType ? K : never; +}; + +export type TakeTypedKeyValues = Pick< + Obj, + TakeTypeHelper[keyof TakeTypeHelper] +>; + export type CrossOriginIframeMessageEventContent = { type: 'rrweb'; event: T; From 4bca6bb98fc6d31285e0f6d4fafd0fdb840ecf11 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 31 Oct 2022 14:35:16 +0100 Subject: [PATCH 36/61] Get iframe manager to use iframe mirrors internally --- packages/rrweb/src/record/iframe-manager.ts | 61 +++++-------- packages/rrweb/src/record/iframe-mirror.ts | 94 +++++++++++++-------- 2 files changed, 78 insertions(+), 77 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index c5bf73d6ff..61ac63c393 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -7,6 +7,7 @@ import { IncrementalSource, mutationCallBack, } from '../types'; +import IframeMirror from './iframe-mirror'; import type { StylesheetManager } from './stylesheet-manager'; export class IframeManager { @@ -15,14 +16,8 @@ export class IframeManager { MessageEventSource, HTMLIFrameElement > = new WeakMap(); - private iframeIdMap: WeakMap< - HTMLIFrameElement, - Map - > = new WeakMap(); - private iframeStyleIdMap: WeakMap< - HTMLIFrameElement, - Map - > = new WeakMap(); + private crossOriginIframeIdMap = new IframeMirror(genId); + private crossOriginIframeStyleIdMap: IframeMirror; private mirror: Mirror; private mutationCb: mutationCallBack; private wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void; @@ -41,6 +36,11 @@ export class IframeManager { this.wrappedEmit = options.wrappedEmit; this.stylesheetManager = options.stylesheetManager; this.recordCrossOriginIframes = options.recordCrossOriginIframes; + this.crossOriginIframeStyleIdMap = new IframeMirror( + this.stylesheetManager.styleMirror.generateId.bind( + this.stylesheetManager.styleMirror, + ), + ); this.mirror = options.mirror; if (this.recordCrossOriginIframes) { window.addEventListener('message', this.handleMessage.bind(this)); @@ -112,7 +112,7 @@ export class IframeManager { e: eventWithTime, ): eventWithTime | void { if (e.type === EventType.FullSnapshot) { - this.iframeIdMap.set(iframeEl, new Map()); + this.crossOriginIframeIdMap.reset(iframeEl); /** * Replaces the original id of the iframe with a new set of unique ids */ @@ -228,36 +228,23 @@ export class IframeManager { } private replace>( - map: WeakMap>, - generateIdFn: () => number, + iframeMirror: IframeMirror, obj: T, iframeEl: HTMLIFrameElement, keys: Array, ): T { - let idMap = map.get(iframeEl); - if (!idMap) { - idMap = new Map(); - map.set(iframeEl, idMap); - } - for (const key of keys) { if (!Array.isArray(obj[key]) && typeof obj[key] !== 'number') continue; if (Array.isArray(obj[key])) { - obj[key] = (obj[key] as unknown[]).map((id) => { - if (typeof id !== 'number') return id; - if (idMap!.has(id)) return idMap!.get(id); - const newId = generateIdFn(); - idMap!.set(id, newId); - return newId; - }) as T[keyof T]; + obj[key] = iframeMirror.getParentIds( + iframeEl, + obj[key] as number[], + ) as T[keyof T]; } else { - const originalId = obj[key] as number; - let newId = idMap.get(originalId); - if (!newId) { - newId = generateIdFn(); - idMap.set(originalId, newId); - } - (obj[key] as number) = newId; + (obj[key] as number) = iframeMirror.getParentId( + iframeEl, + obj[key] as number, + ); } } @@ -269,7 +256,7 @@ export class IframeManager { iframeEl: HTMLIFrameElement, keys: Array, ): T { - return this.replace(this.iframeIdMap, genId, obj, iframeEl, keys); + return this.replace(this.crossOriginIframeIdMap, obj, iframeEl, keys); } private replaceStyleIds>( @@ -277,15 +264,7 @@ export class IframeManager { iframeEl: HTMLIFrameElement, keys: Array, ): T { - return this.replace( - this.iframeStyleIdMap, - this.stylesheetManager.styleMirror.generateId.bind( - this.stylesheetManager.styleMirror, - ), - obj, - iframeEl, - keys, - ); + return this.replace(this.crossOriginIframeStyleIdMap, obj, iframeEl, keys); } private replaceIdOnNode( diff --git a/packages/rrweb/src/record/iframe-mirror.ts b/packages/rrweb/src/record/iframe-mirror.ts index 9f774d7ae8..61e24a96e2 100644 --- a/packages/rrweb/src/record/iframe-mirror.ts +++ b/packages/rrweb/src/record/iframe-mirror.ts @@ -1,73 +1,95 @@ export default class IframeMirror { - private iframeParentIdToLocalIdMap: WeakMap< + private iframeParentIdToRemoteIdMap: WeakMap< HTMLIFrameElement, Map > = new WeakMap(); - private iframeLocalIdToParentIdMap: WeakMap< + private iframeRemoteIdToParentIdMap: WeakMap< HTMLIFrameElement, Map > = new WeakMap(); constructor(private generateIdFn: () => number) {} - getParentId(iframe: HTMLIFrameElement, localId: number): number { - const parentIdToLocalIdMap = this.getParentIdToLocalIdMap(iframe); - const localIdToParentIdMap = this.getLocalIdToParentIdMap(iframe); + getParentId( + iframe: HTMLIFrameElement, + remoteId: number, + parentToRemoteMap?: Map, + remoteToParentMap?: Map, + ): number { + const parentIdToRemoteIdMap = + parentToRemoteMap || this.getParentIdToRemoteIdMap(iframe); + const remoteIdToParentIdMap = + remoteToParentMap || this.getRemoteIdToParentIdMap(iframe); - let parentId = parentIdToLocalIdMap.get(localId); + let parentId = parentIdToRemoteIdMap.get(remoteId); if (!parentId) { parentId = this.generateIdFn(); - parentIdToLocalIdMap.set(localId, parentId); - localIdToParentIdMap.set(parentId, localId); + parentIdToRemoteIdMap.set(remoteId, parentId); + remoteIdToParentIdMap.set(parentId, remoteId); } return parentId; } - getParentIds(iframe: HTMLIFrameElement, localId: number[]): number[] { - return localId.map((id) => this.getParentId(iframe, id)); + getParentIds(iframe: HTMLIFrameElement, remoteId: number[]): number[] { + const parentIdToRemoteIdMap = this.getParentIdToRemoteIdMap(iframe); + const remoteIdToParentIdMap = this.getRemoteIdToParentIdMap(iframe); + return remoteId.map((id) => + this.getParentId( + iframe, + id, + parentIdToRemoteIdMap, + remoteIdToParentIdMap, + ), + ); } - getLocalId( + getRemoteId( iframe: HTMLIFrameElement, - parentId: T, - ): T { - const localIdToParentIdMap = this.getLocalIdToParentIdMap(iframe); - if (Array.isArray(parentId)) { - return parentId.map((id) => this.getLocalId(iframe, id)) as T; - } + parentId: number, + map?: Map, + ): number { + const remoteIdToParentIdMap = map || this.getRemoteIdToParentIdMap(iframe); if (typeof parentId !== 'number') return parentId; - const localId = localIdToParentIdMap.get(parentId); - if (!localId) return -1 as T; - return localId as T; + const remoteId = remoteIdToParentIdMap.get(parentId); + if (!remoteId) return -1; + return remoteId; + } + + getRemoteIds(iframe: HTMLIFrameElement, parentId: number[]): number[] { + const remoteIdToParentIdMap = this.getRemoteIdToParentIdMap(iframe); + + return parentId.map((id) => + this.getRemoteId(iframe, id, remoteIdToParentIdMap), + ); } reset(iframe?: HTMLIFrameElement) { if (!iframe) { - this.iframeParentIdToLocalIdMap = new WeakMap(); - this.iframeLocalIdToParentIdMap = new WeakMap(); + this.iframeParentIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToParentIdMap = new WeakMap(); return; } - this.iframeParentIdToLocalIdMap.delete(iframe); - this.iframeLocalIdToParentIdMap.delete(iframe); + this.iframeParentIdToRemoteIdMap.delete(iframe); + this.iframeRemoteIdToParentIdMap.delete(iframe); } - private getParentIdToLocalIdMap(iframe: HTMLIFrameElement) { - let parentToLocalMap = this.iframeParentIdToLocalIdMap.get(iframe); - if (!parentToLocalMap) { - parentToLocalMap = new Map(); - this.iframeParentIdToLocalIdMap.set(iframe, parentToLocalMap); + private getParentIdToRemoteIdMap(iframe: HTMLIFrameElement) { + let parentToRemoteMap = this.iframeParentIdToRemoteIdMap.get(iframe); + if (!parentToRemoteMap) { + parentToRemoteMap = new Map(); + this.iframeParentIdToRemoteIdMap.set(iframe, parentToRemoteMap); } - return parentToLocalMap; + return parentToRemoteMap; } - private getLocalIdToParentIdMap(iframe: HTMLIFrameElement) { - let localIdToParentIdMap = this.iframeLocalIdToParentIdMap.get(iframe); - if (!localIdToParentIdMap) { - localIdToParentIdMap = new Map(); - this.iframeLocalIdToParentIdMap.set(iframe, localIdToParentIdMap); + private getRemoteIdToParentIdMap(iframe: HTMLIFrameElement) { + let remoteIdToParentIdMap = this.iframeRemoteIdToParentIdMap.get(iframe); + if (!remoteIdToParentIdMap) { + remoteIdToParentIdMap = new Map(); + this.iframeRemoteIdToParentIdMap.set(iframe, remoteIdToParentIdMap); } - return localIdToParentIdMap; + return remoteIdToParentIdMap; } } From 8147d7f6260a2f3e83e48b522ff3aab0fdef5aff Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 31 Oct 2022 14:37:01 +0100 Subject: [PATCH 37/61] Rename `IframeMirror` to `CrossOriginIframeMirror` --- ...e-mirror.ts => cross-origin-iframe-mirror.ts} | 2 +- packages/rrweb/src/record/iframe-manager.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) rename packages/rrweb/src/record/{iframe-mirror.ts => cross-origin-iframe-mirror.ts} (98%) diff --git a/packages/rrweb/src/record/iframe-mirror.ts b/packages/rrweb/src/record/cross-origin-iframe-mirror.ts similarity index 98% rename from packages/rrweb/src/record/iframe-mirror.ts rename to packages/rrweb/src/record/cross-origin-iframe-mirror.ts index 61e24a96e2..7aefa762b3 100644 --- a/packages/rrweb/src/record/iframe-mirror.ts +++ b/packages/rrweb/src/record/cross-origin-iframe-mirror.ts @@ -1,4 +1,4 @@ -export default class IframeMirror { +export default class CrossOriginIframeMirror { private iframeParentIdToRemoteIdMap: WeakMap< HTMLIFrameElement, Map diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 61ac63c393..e7c66201fb 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -7,7 +7,7 @@ import { IncrementalSource, mutationCallBack, } from '../types'; -import IframeMirror from './iframe-mirror'; +import CrossOriginIframeMirror from './cross-origin-iframe-mirror'; import type { StylesheetManager } from './stylesheet-manager'; export class IframeManager { @@ -16,8 +16,8 @@ export class IframeManager { MessageEventSource, HTMLIFrameElement > = new WeakMap(); - private crossOriginIframeIdMap = new IframeMirror(genId); - private crossOriginIframeStyleIdMap: IframeMirror; + private crossOriginIframeMirror = new CrossOriginIframeMirror(genId); + private crossOriginIframeStyleMirror: CrossOriginIframeMirror; private mirror: Mirror; private mutationCb: mutationCallBack; private wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void; @@ -36,7 +36,7 @@ export class IframeManager { this.wrappedEmit = options.wrappedEmit; this.stylesheetManager = options.stylesheetManager; this.recordCrossOriginIframes = options.recordCrossOriginIframes; - this.crossOriginIframeStyleIdMap = new IframeMirror( + this.crossOriginIframeStyleMirror = new CrossOriginIframeMirror( this.stylesheetManager.styleMirror.generateId.bind( this.stylesheetManager.styleMirror, ), @@ -112,7 +112,7 @@ export class IframeManager { e: eventWithTime, ): eventWithTime | void { if (e.type === EventType.FullSnapshot) { - this.crossOriginIframeIdMap.reset(iframeEl); + this.crossOriginIframeMirror.reset(iframeEl); /** * Replaces the original id of the iframe with a new set of unique ids */ @@ -228,7 +228,7 @@ export class IframeManager { } private replace>( - iframeMirror: IframeMirror, + iframeMirror: CrossOriginIframeMirror, obj: T, iframeEl: HTMLIFrameElement, keys: Array, @@ -256,7 +256,7 @@ export class IframeManager { iframeEl: HTMLIFrameElement, keys: Array, ): T { - return this.replace(this.crossOriginIframeIdMap, obj, iframeEl, keys); + return this.replace(this.crossOriginIframeMirror, obj, iframeEl, keys); } private replaceStyleIds>( @@ -264,7 +264,7 @@ export class IframeManager { iframeEl: HTMLIFrameElement, keys: Array, ): T { - return this.replace(this.crossOriginIframeStyleIdMap, obj, iframeEl, keys); + return this.replace(this.crossOriginIframeStyleMirror, obj, iframeEl, keys); } private replaceIdOnNode( From 7f23d349be3008e2c03aadb46d8aa7c87c7c2ec3 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 31 Oct 2022 17:57:52 +0100 Subject: [PATCH 38/61] Setup basic cross origin canvas webrtc streaming --- .../src/plugins/canvas-webrtc/record/index.ts | 118 ++++++++++++++++-- .../src/plugins/canvas-webrtc/replay/index.ts | 4 +- packages/rrweb/src/record/iframe-manager.ts | 4 +- packages/rrweb/src/record/index.ts | 20 +-- packages/rrweb/src/replay/index.ts | 2 +- packages/rrweb/src/types.ts | 9 +- 6 files changed, 134 insertions(+), 23 deletions(-) diff --git a/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts b/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts index df67966cee..c4484dd12a 100644 --- a/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts +++ b/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts @@ -1,14 +1,31 @@ import type { Mirror } from 'rrweb-snapshot'; import SimplePeer from 'simple-peer-light'; +import type CrossOriginIframeMirror from '../../../record/cross-origin-iframe-mirror'; import type { RecordPlugin } from '../../../types'; import type { WebRTCDataChannel } from '../types'; export const PLUGIN_NAME = 'rrweb/canvas-webrtc@1'; +export type CrossOriginIframeMessageEventContent = { + type: 'rrweb-canvas-webrtc'; + data: + | { + type: 'setup-stream'; + id: number; + rootId: number; + } + | { + type: 'signal'; + signal: RTCSessionDescriptionInit; + }; +}; + export class RRWebPluginCanvasWebRTCRecord { private peer: SimplePeer.Instance | null = null; private mirror: Mirror; + private crossOriginIframeMirror: CrossOriginIframeMirror; private streamMap: Map = new Map(); + private crossOriginStreamMap: Map = new Map(); private signalSendCallback: (msg: RTCSessionDescriptionInit) => void; constructor({ @@ -19,22 +36,43 @@ export class RRWebPluginCanvasWebRTCRecord { peer?: SimplePeer.Instance; }) { this.signalSendCallback = signalSendCallback; + window.addEventListener( + 'message', + this.windowPostMessageHandler.bind(this), + ); if (peer) this.peer = peer; } public initPlugin(): RecordPlugin { return { name: PLUGIN_NAME, - getMirror: (mirror) => { - this.mirror = mirror; + getMirror: ({ nodeMirror, crossOriginIframeMirror }) => { + this.mirror = nodeMirror; + this.crossOriginIframeMirror = crossOriginIframeMirror; }, options: {}, }; } public signalReceive(signal: RTCSessionDescriptionInit) { - if (!this.peer) this.setupPeer(); - this.peer?.signal(signal); + if (this.streamMap.size) { + if (!this.peer) this.setupPeer(); + this.peer?.signal(signal); + } + if (this.crossOriginStreamMap.size) { + [...this.crossOriginStreamMap.values()].forEach((iframe) => { + iframe.contentWindow?.postMessage( + { + type: 'rrweb-canvas-webrtc', + data: { + type: 'signal', + signal: signal, + }, + } as CrossOriginIframeMessageEventContent, + '*', + ); + }); + } } private startStream(id: number, stream: MediaStream) { @@ -77,19 +115,81 @@ export class RRWebPluginCanvasWebRTCRecord { } } - public setupStream(id: number): false | MediaStream { + public setupStream( + id: number, + rootId?: number, + ): false | MediaStream | number { if (id === -1) return false; - let stream: MediaStream | undefined = this.streamMap.get(id); + let stream: MediaStream | undefined = this.streamMap.get(rootId || id); if (stream) return stream; const el = this.mirror.getNode(id) as HTMLCanvasElement | null; - // TODO: no node found, might be in a child iframe - if (!el || !('captureStream' in el)) return false; + + if (!el || !('captureStream' in el)) + return this.setupStreamInCrossOriginIframe(id, rootId || id); stream = el.captureStream(); - this.streamMap.set(id, stream); + this.streamMap.set(rootId || id, stream); this.setupPeer(); return stream; } + + public setupStreamInCrossOriginIframe( + id: number, + rootId: number, + ): false | MediaStream | number { + let stream: MediaStream | undefined | false | number = this.streamMap.get( + id, + ); + // TODO: no need to loop through everything if we set a master id/iframe map + document.querySelectorAll('iframe').forEach((iframe) => { + const remoteId = this.crossOriginIframeMirror.getRemoteId(iframe, id); + if (remoteId === -1) return; + this.crossOriginStreamMap.set(id, iframe); + iframe.contentWindow?.postMessage( + { + type: 'rrweb-canvas-webrtc', + data: { + type: 'setup-stream', + id: remoteId, + rootId, + }, + } as CrossOriginIframeMessageEventContent, + '*', + ); + stream = remoteId; + }); + + return stream || false; + } + + private isCrossOriginIframeMessageEventContent( + event: MessageEvent, + ): event is MessageEvent { + return Boolean( + 'type' in event.data && + 'data' in event.data && + (event.data as CrossOriginIframeMessageEventContent).type === + 'rrweb-canvas-webrtc' && + (event.data as CrossOriginIframeMessageEventContent).data, + ); + } + + private windowPostMessageHandler(event: MessageEvent) { + if (!this.isCrossOriginIframeMessageEventContent(event)) return; + + const { type } = event.data.data; + if (type === 'setup-stream') { + const { id, rootId } = event.data.data; + const stream = this.setupStream(id, rootId); + if (stream === false) return; + } else if (type === 'signal') { + const { signal } = event.data.data; + + this.signalReceive(signal); + } else { + console.warn('MESSAGE', event.data); + } + } } diff --git a/packages/rrweb/src/plugins/canvas-webrtc/replay/index.ts b/packages/rrweb/src/plugins/canvas-webrtc/replay/index.ts index a97bc1fa60..45a1297043 100644 --- a/packages/rrweb/src/plugins/canvas-webrtc/replay/index.ts +++ b/packages/rrweb/src/plugins/canvas-webrtc/replay/index.ts @@ -36,8 +36,8 @@ export class RRWebPluginCanvasWebRTCReplay { this.canvasFoundCallback(node, context); } }, - getMirror: (mirror: Mirror) => { - this.mirror = mirror; + getMirror: (options) => { + this.mirror = options.nodeMirror; }, }; } diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index e7c66201fb..6f77e0af7f 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -16,8 +16,8 @@ export class IframeManager { MessageEventSource, HTMLIFrameElement > = new WeakMap(); - private crossOriginIframeMirror = new CrossOriginIframeMirror(genId); - private crossOriginIframeStyleMirror: CrossOriginIframeMirror; + public crossOriginIframeMirror = new CrossOriginIframeMirror(genId); + public crossOriginIframeStyleMirror: CrossOriginIframeMirror; private mirror: Mirror; private mutationCb: mutationCallBack; private wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void; diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 1592fb0c6e..475b11fbea 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -153,13 +153,6 @@ function record( let lastFullSnapshotEvent: eventWithTime; let incrementalSnapshotCount = 0; - /** - * Exposes mirror to the plugins - */ - for (const plugin of plugins || []) { - if (plugin.getMirror) plugin.getMirror(mirror); - } - const eventProcessor = (e: eventWithTime): T => { for (const plugin of plugins || []) { if (plugin.eventProcessor) { @@ -276,6 +269,19 @@ function record( wrappedEmit, }); + /** + * Exposes mirror to the plugins + */ + for (const plugin of plugins || []) { + if (plugin.getMirror) + plugin.getMirror({ + nodeMirror: mirror, + crossOriginIframeMirror: iframeManager.crossOriginIframeMirror, + crossOriginIframeStyleMirror: + iframeManager.crossOriginIframeStyleMirror, + }); + } + canvasManager = new CanvasManager({ recordCanvas, mutationCb: wrappedCanvasMutationEmit, diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 6226211ed4..3d59ea2211 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -199,7 +199,7 @@ export class Replayer { * Exposes mirror to the plugins */ for (const plugin of this.config.plugins || []) { - if (plugin.getMirror) plugin.getMirror(this.mirror); + if (plugin.getMirror) plugin.getMirror({ nodeMirror: this.mirror }); } this.emitter.on(ReplayerEvents.Flush, () => { diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 6f42ef90fc..f16780c9fd 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -15,6 +15,7 @@ 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'; +import type CrossOriginIframeMirror from './record/cross-origin-iframe-mirror'; export enum EventType { DomContentLoaded, @@ -235,7 +236,11 @@ export type RecordPlugin = { options: TOptions, ) => listenerHandler; eventProcessor?: (event: eventWithTime) => eventWithTime & TExtend; - getMirror?: (mirror: Mirror) => void; + getMirror?: (mirrors: { + nodeMirror: Mirror; + crossOriginIframeMirror: CrossOriginIframeMirror; + crossOriginIframeStyleMirror: CrossOriginIframeMirror; + }) => void; options: TOptions; }; @@ -706,7 +711,7 @@ export type ReplayPlugin = { node: Node | RRNode, context: { id: number; replayer: Replayer }, ) => void; - getMirror?: (mirror: Mirror) => void; + getMirror?: (mirrors: { nodeMirror: Mirror }) => void; }; export type playerConfig = { speed: number; From da4c53be38139faed0ae7952818f5993508f46a9 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 31 Oct 2022 18:03:56 +0100 Subject: [PATCH 39/61] Clean up removed canvas elements --- packages/rrweb/src/plugins/canvas-webrtc/record/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts b/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts index c4484dd12a..00e73511f5 100644 --- a/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts +++ b/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts @@ -60,7 +60,12 @@ export class RRWebPluginCanvasWebRTCRecord { this.peer?.signal(signal); } if (this.crossOriginStreamMap.size) { - [...this.crossOriginStreamMap.values()].forEach((iframe) => { + [...this.crossOriginStreamMap.entries()].forEach(([id, iframe]) => { + if (this.crossOriginIframeMirror.getRemoteId(iframe, id) === -1) { + this.crossOriginStreamMap.delete(id); + return; + } + iframe.contentWindow?.postMessage( { type: 'rrweb-canvas-webrtc', From e0e1a9b9f82e099e0495b21da6eef5cf1c853217 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 1 Nov 2022 11:16:53 +0100 Subject: [PATCH 40/61] reset style mirror on new full snapshot --- packages/rrweb/src/record/iframe-manager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 6f77e0af7f..a202e9dbf5 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -113,6 +113,7 @@ export class IframeManager { ): eventWithTime | void { if (e.type === EventType.FullSnapshot) { this.crossOriginIframeMirror.reset(iframeEl); + this.crossOriginIframeStyleMirror.reset(iframeEl); /** * Replaces the original id of the iframe with a new set of unique ids */ From 2e64e5e54f01ff52a6594213a19c9460862185bd Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 2 Nov 2022 13:00:29 +0100 Subject: [PATCH 41/61] Fix cross origin canvas webrtc streaming --- .../src/plugins/canvas-webrtc/record/index.ts | 250 +++++++++++++----- 1 file changed, 178 insertions(+), 72 deletions(-) diff --git a/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts b/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts index 00e73511f5..00315c8a0a 100644 --- a/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts +++ b/packages/rrweb/src/plugins/canvas-webrtc/record/index.ts @@ -10,13 +10,17 @@ export type CrossOriginIframeMessageEventContent = { type: 'rrweb-canvas-webrtc'; data: | { - type: 'setup-stream'; - id: number; + type: 'signal'; + signal: RTCSessionDescriptionInit; + } + | { + type: 'who-has-canvas'; rootId: number; + id: number; } | { - type: 'signal'; - signal: RTCSessionDescriptionInit; + type: 'i-have-canvas'; + rootId: number; }; }; @@ -25,7 +29,12 @@ export class RRWebPluginCanvasWebRTCRecord { private mirror: Mirror; private crossOriginIframeMirror: CrossOriginIframeMirror; private streamMap: Map = new Map(); - private crossOriginStreamMap: Map = new Map(); + private incomingStreams = new Set(); + private outgoingStreams = new Set(); + private streamNodeMap = new Map(); + private canvasWindowMap = new Map(); + private windowPeerMap = new WeakMap(); + private peerWindowMap = new WeakMap(); private signalSendCallback: (msg: RTCSessionDescriptionInit) => void; constructor({ @@ -55,75 +64,136 @@ export class RRWebPluginCanvasWebRTCRecord { } public signalReceive(signal: RTCSessionDescriptionInit) { - if (this.streamMap.size) { - if (!this.peer) this.setupPeer(); - this.peer?.signal(signal); - } - if (this.crossOriginStreamMap.size) { - [...this.crossOriginStreamMap.entries()].forEach(([id, iframe]) => { - if (this.crossOriginIframeMirror.getRemoteId(iframe, id) === -1) { - this.crossOriginStreamMap.delete(id); - return; - } + if (!this.peer) this.setupPeer(); + this.peer?.signal(signal); + } - iframe.contentWindow?.postMessage( - { - type: 'rrweb-canvas-webrtc', - data: { - type: 'signal', - signal: signal, - }, - } as CrossOriginIframeMessageEventContent, - '*', - ); - }); - } + public signalReceiveFromCrossOriginIframe( + signal: RTCSessionDescriptionInit, + source: WindowProxy, + ) { + const peer = this.setupPeer(source); + peer.signal(signal); } private startStream(id: number, stream: MediaStream) { - if (!this.peer) return this.setupPeer(); + if (!this.peer) this.setupPeer(); const data: WebRTCDataChannel = { nodeId: id, streamId: stream.id, }; this.peer?.send(JSON.stringify(data)); - this.peer?.addStream(stream); + if (!this.outgoingStreams.has(stream)) this.peer?.addStream(stream); + this.outgoingStreams.add(stream); } - public setupPeer() { - if (!this.peer) { - this.peer = new SimplePeer({ + public setupPeer(source?: WindowProxy): SimplePeer.Instance { + let peer: SimplePeer.Instance; + + if (!source) { + if (this.peer) return this.peer; + + peer = this.peer = new SimplePeer({ initiator: true, // trickle: false, // only create one WebRTC offer per session }); + } else { + const peerFromMap = this.windowPeerMap.get(source); - this.peer.on('error', (err: Error) => { - this.peer = null; - console.log('error', err); - }); + if (peerFromMap) return peerFromMap; - this.peer.on('close', () => { - this.peer = null; - console.log('closing'); + peer = new SimplePeer({ + initiator: false, + // trickle: false, // only create one WebRTC offer per session }); + this.windowPeerMap.set(source, peer); + this.peerWindowMap.set(peer, source); + } - this.peer.on('signal', (data: RTCSessionDescriptionInit) => { - this.signalSendCallback(data); - }); + const resetPeer = (source?: WindowProxy) => { + if (!source) return (this.peer = null); + + this.windowPeerMap.delete(source); + this.peerWindowMap.delete(peer); + }; + + peer.on('error', (err: Error) => { + resetPeer(source); + console.log('error', err); + }); + + peer.on('close', () => { + resetPeer(source); + console.log('closing'); + }); - this.peer.on('connect', () => { - for (const [id, stream] of this.streamMap) { - this.startStream(id, stream); + peer.on('signal', (data: RTCSessionDescriptionInit) => { + if (this.inRootFrame()) { + if (peer === this.peer) { + // connected to replayer + this.signalSendCallback(data); + } else { + // connected to cross-origin iframe + this.peerWindowMap.get(peer)?.postMessage( + { + type: 'rrweb-canvas-webrtc', + data: { + type: 'signal', + signal: data, + }, + } as CrossOriginIframeMessageEventContent, + '*', + ); } - }); - } + } else { + // connected to root frame + window.top?.postMessage( + { + type: 'rrweb-canvas-webrtc', + data: { + type: 'signal', + signal: data, + }, + } as CrossOriginIframeMessageEventContent, + '*', + ); + } + }); + + peer.on('connect', () => { + // connected to cross-origin iframe, no need to do anything + if (this.inRootFrame() && peer !== this.peer) return; + + // cross origin frame connected to root frame + // or root frame connected to replayer + // send all streams to peer + for (const [id, stream] of this.streamMap) { + this.startStream(id, stream); + } + }); + + if (!this.inRootFrame()) return peer; + + peer.on('data', (data: SimplePeer.SimplePeerData) => { + try { + const json = JSON.parse(data as string) as WebRTCDataChannel; + this.streamNodeMap.set(json.streamId, json.nodeId); + } catch (error) { + console.error('Could not parse data', error); + } + this.flushStreams(); + }); + + peer.on('stream', (stream: MediaStream) => { + this.incomingStreams.add(stream); + this.flushStreams(); + }); + + return peer; } - public setupStream( - id: number, - rootId?: number, - ): false | MediaStream | number { + public setupStream(id: number, rootId?: number): boolean | MediaStream { if (id === -1) return false; let stream: MediaStream | undefined = this.streamMap.get(rootId || id); if (stream) return stream; @@ -131,8 +201,22 @@ export class RRWebPluginCanvasWebRTCRecord { const el = this.mirror.getNode(id) as HTMLCanvasElement | null; if (!el || !('captureStream' in el)) + // we don't have it, lets check our iframes return this.setupStreamInCrossOriginIframe(id, rootId || id); + if (!this.inRootFrame()) { + window.top?.postMessage( + { + type: 'rrweb-canvas-webrtc', + data: { + type: 'i-have-canvas', + rootId: rootId || id, + }, + } as CrossOriginIframeMessageEventContent, + '*', + ); + } + stream = el.captureStream(); this.streamMap.set(rootId || id, stream); this.setupPeer(); @@ -140,33 +224,42 @@ export class RRWebPluginCanvasWebRTCRecord { return stream; } - public setupStreamInCrossOriginIframe( - id: number, - rootId: number, - ): false | MediaStream | number { - let stream: MediaStream | undefined | false | number = this.streamMap.get( - id, - ); - // TODO: no need to loop through everything if we set a master id/iframe map + private flushStreams() { + this.incomingStreams.forEach((stream) => { + const nodeId = this.streamNodeMap.get(stream.id); + if (!nodeId) return; + // got remote video stream, now let's send it to the replayer + this.startStream(nodeId, stream); + }); + } + + private inRootFrame(): boolean { + return Boolean(window.top && window.top === window); + } + + public setupStreamInCrossOriginIframe(id: number, rootId: number): boolean { + let found = false; + document.querySelectorAll('iframe').forEach((iframe) => { + if (found) return; + const remoteId = this.crossOriginIframeMirror.getRemoteId(iframe, id); if (remoteId === -1) return; - this.crossOriginStreamMap.set(id, iframe); + + found = true; iframe.contentWindow?.postMessage( { type: 'rrweb-canvas-webrtc', data: { - type: 'setup-stream', + type: 'who-has-canvas', id: remoteId, rootId, }, } as CrossOriginIframeMessageEventContent, '*', ); - stream = remoteId; }); - - return stream || false; + return found; } private isCrossOriginIframeMessageEventContent( @@ -181,20 +274,33 @@ export class RRWebPluginCanvasWebRTCRecord { ); } - private windowPostMessageHandler(event: MessageEvent) { + /** + * All messages being sent to the (root or sub) frame are received through `windowPostMessageHandler`. + * @param event - The message event + */ + private windowPostMessageHandler( + event: MessageEvent | MessageEvent, + ) { if (!this.isCrossOriginIframeMessageEventContent(event)) return; const { type } = event.data.data; - if (type === 'setup-stream') { + if (type === 'who-has-canvas') { const { id, rootId } = event.data.data; - const stream = this.setupStream(id, rootId); - if (stream === false) return; + this.setupStream(id, rootId); } else if (type === 'signal') { const { signal } = event.data.data; - - this.signalReceive(signal); - } else { - console.warn('MESSAGE', event.data); + const { source } = event; + if (!source || !('self' in source)) return; + if (this.inRootFrame()) { + this.signalReceiveFromCrossOriginIframe(signal, source); + } else { + this.signalReceive(signal); + } + } else if (type === 'i-have-canvas') { + const { rootId } = event.data.data; + const { source } = event; + if (!source || !('self' in source)) return; + this.canvasWindowMap.set(rootId, source); } } } From 7d9bcb7be9bd14cc15c38c6ba742e6881edceead Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 2 Nov 2022 14:48:13 +0100 Subject: [PATCH 42/61] Make emit optional --- packages/rrweb/src/record/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 475b11fbea..2e0e824afd 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -179,7 +179,7 @@ function record( } if (inEmittingFrame) { - emit!(eventProcessor(e), isCheckout); + emit?.(eventProcessor(e), isCheckout); } else if (passEmitsToParent) { const message: CrossOriginIframeMessageEventContent = { type: 'rrweb', From 8bc678f4f43dbef4bf2fc47ebd8a04e8cee6e1ad Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 2 Nov 2022 15:00:48 +0100 Subject: [PATCH 43/61] Run tests on github actions --- .github/workflows/ci-cd.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/ci-cd.yml diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000000..9a23b66e87 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,33 @@ +name: Tests + +on: [push, pull_request_target] + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + name: Tests + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v2 + with: + # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits + fetch-depth: 0 + + - name: Setup Node.js lts/* + uses: actions/setup-node@v2 + with: + node-version: lts/* + + - name: Install Dependencies + run: yarn + + - name: Build Project + run: yarn build:all + + - name: Check types + run: yarn turbo run check-types + + - name: Run tests + run: xvfb-run --server-args="-screen 0 1920x1080x24" yarn test From 7c985d0da3af8a8ac3ad125074d2615603ec6e07 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 2 Nov 2022 15:12:47 +0100 Subject: [PATCH 44/61] Upload image artifacts from failed tests --- .github/workflows/ci-cd.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 9a23b66e87..f4cc7423db 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -31,3 +31,11 @@ jobs: - name: Run tests run: xvfb-run --server-args="-screen 0 1920x1080x24" yarn test + + - name: Upload diff images to GitHub + uses: actions/upload-artifact@v3 + if: failure() + with: + name: image-diff + path: packages/rrweb/test/e2e/__image_snapshots__/__diff_output__/*.png + if-no-files-found: ignore From 48b8c1ebca891f33b12f0046ad2803a56c4fc47a Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 2 Nov 2022 15:20:01 +0100 Subject: [PATCH 45/61] Use newer github actions --- .github/workflows/ci-cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index f4cc7423db..6f98b26d8a 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -10,13 +10,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - name: Setup Node.js lts/* - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: lts/* From dc021636180a73525d8fa679310206a46d8963e6 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 2 Nov 2022 15:22:08 +0100 Subject: [PATCH 46/61] Test: hopefully adding more wait will fix it --- packages/rrweb/test/e2e/webgl.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/rrweb/test/e2e/webgl.test.ts b/packages/rrweb/test/e2e/webgl.test.ts index e42cbc4fe6..1762ff5884 100644 --- a/packages/rrweb/test/e2e/webgl.test.ts +++ b/packages/rrweb/test/e2e/webgl.test.ts @@ -110,8 +110,9 @@ describe('e2e webgl', () => { await waitForRAF(page); const element = await page.$('iframe'); + await waitForRAF(page); const frameImage = await element!.screenshot(); - + await waitForRAF(page); expect(frameImage).toMatchImageSnapshot(); }); @@ -147,7 +148,9 @@ describe('e2e webgl', () => { await waitForRAF(page); const element = await page.$('iframe'); + await waitForRAF(page); const frameImage = await element!.screenshot(); + await waitForRAF(page); expect(frameImage).toMatchImageSnapshot(); }); From 1a9c164eac93c238659294c0e8678c1b633dfb84 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 2 Nov 2022 15:34:44 +0100 Subject: [PATCH 47/61] add extra wait --- packages/rrweb/test/e2e/webgl.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rrweb/test/e2e/webgl.test.ts b/packages/rrweb/test/e2e/webgl.test.ts index 1762ff5884..fa783a24eb 100644 --- a/packages/rrweb/test/e2e/webgl.test.ts +++ b/packages/rrweb/test/e2e/webgl.test.ts @@ -124,6 +124,7 @@ describe('e2e webgl', () => { getHtml.call(this, 'canvas-webgl-image.html', { recordCanvas: true }), ); + await waitForRAF(page); await page.waitForTimeout(100); const snapshots: eventWithTime[] = (await page.evaluate( 'window.snapshots', From c51e84fdda6098489035041a251f86ba13a9a4a5 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 2 Nov 2022 15:50:49 +0100 Subject: [PATCH 48/61] Fix image snapshot tests --- packages/rrweb/package.json | 4 ++-- ...record-and-replay-a-webgl-image-1-snap.png | Bin 10913 -> 10752 bytes ...ecord-and-replay-a-webgl-square-1-snap.png | Bin 10812 -> 10650 bytes packages/rrweb/test/e2e/webgl.test.ts | 14 +++++--------- yarn.lock | 16 ++++++++-------- 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 64bf1c3ed9..c83a908655 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -50,7 +50,7 @@ "@types/dom-mediacapture-transform": "^0.1.3", "@types/inquirer": "^8.2.1", "@types/jest": "^27.4.1", - "@types/jest-image-snapshot": "^4.3.1", + "@types/jest-image-snapshot": "^5.1.0", "@types/node": "^17.0.21", "@types/offscreencanvas": "^2019.6.4", "@types/prettier": "^2.3.2", @@ -63,7 +63,7 @@ "ignore-styles": "^5.0.1", "inquirer": "^9.0.0", "jest": "^27.5.1", - "jest-image-snapshot": "^4.5.1", + "jest-image-snapshot": "^5.2.0", "jest-snapshot": "^23.6.0", "prettier": "2.2.1", "puppeteer": "^11.0.0", diff --git a/packages/rrweb/test/e2e/__image_snapshots__/webgl-test-ts-e-2-e-webgl-will-record-and-replay-a-webgl-image-1-snap.png b/packages/rrweb/test/e2e/__image_snapshots__/webgl-test-ts-e-2-e-webgl-will-record-and-replay-a-webgl-image-1-snap.png index 731898701b69eeca46729aa8eef2af872597d9de..88ff052f68ad25e7f9f617f280d7c3040194a5e4 100644 GIT binary patch literal 10752 zcmeAS@N?(olHy`uVBq!ia0y~yU~gbxV6os}1B$%3e9#$4F%}28J29*~C-ahlL4m>3 z#WAE}&YRl@bE6z3+%Ecj(ON9{YXQTr1%gqt-z++!q#*Q%?f#t2vXXY2_x?IQr?`$8 zXao>EIQ@HH6@04gZRWB_3~v@q~MSOTNUKmkE|fNWeV_hF6Uu}kmg z7wi44g9K)SeIX+i!u9qmM)_XDcl+YcU7tJc&bN&FbBdR%LTq5zUrZGn?!{Xi+wr^3 zGQK?OQ@PyR>M#G#TE<(lLo9iqE`9)N9R(={96*m>7JUC(v~SIs_URAuJ`91PG&ngHZy)QgFhjtRwepL%8)kzW-moPyhVY)f=LR z;oDOtqO{|;<=C9!b#fn98y>s+{q)aWmnYrsTYmWNck|zt5Z5;R@8BlUx!CNx5yt-` z<6PzU+MbO-E9BlngFWIXD;2VS&T4j>>~lM-m>>ajAi9VeiE*@00tM%2DGg3kqxBIu z7)Y;=M(c4S6*IQcM8P;ZYWq|t;lS{Z?}(P(7^4u;V}b+k}r7|jc#d117Z0x1Ya z^TKe*3--4*GfM(J(%|^-+V>w`${;@_IKf6ueK-<80zhd_*dT|2B5Y(yjRiWIa-an^ zo-{+?09ZK#gD`BgXw)zUhS4x%U>Hpr3=E^$iGg9X)EF%sM=KO?Eje0mjP+;(M zaSW-L^XB%(TqQ?_w!mjwuI5PQ9ho@0fKlXOk6Xq`)ibW*b9sf>p1r$Vt*Bh~zt{fk zj~_o~umcSOfuL`-^Y@4KLpTf%_I<0hfAhQU#=k!wzJ1$P#19oO|Mc@`rG*S1yLk&E zL}(F9B81VP;V1!NDR>EhTnPjnLZixn!9YoXY`e;wXL>B+*FVeWm%cNDLzN-sahm~z zbG_t>0NuXEpEt z%jeKQu$d)ITqqz6!V*|p*UA;FzWnh|yuCcc%?~b8CtSWZxL4NJ|GfAg7&LRIL8EKU zL+V6poXzt+6?VsVfI_6cRNH~lZ4&ZPY@hyjvr5__07`XgFYXL?JiqT3FwjyA( zMGeXgqb+K1nJ`*uLW;)GN)sFoBfipvq=wPLVYF}mdKI;Vst0Q@>>_5c6? diff --git a/packages/rrweb/test/e2e/__image_snapshots__/webgl-test-ts-e-2-e-webgl-will-record-and-replay-a-webgl-square-1-snap.png b/packages/rrweb/test/e2e/__image_snapshots__/webgl-test-ts-e-2-e-webgl-will-record-and-replay-a-webgl-square-1-snap.png index e22bc48831104120ac0c4a19cd3b9f41dc62b426..7c9c7951db31df46a146ec1f831cbdd8a77c5695 100644 GIT binary patch literal 10650 zcmeAS@N?(olHy`uVBq!ia0y~yU~gbxV6os}1B$%3e9#$4F%}28J29*~C-ahlL4m>3 z#WAE}&YL@iek_Ipu8z`gdv_>^Kk@PPIizyB=TC@e`3GTl|4;K~Mgnz$fXw^HFBu_B zhCS^JJP@XUA_Fso<-&0QDl5oj0AV?_U{SVb9pATm4mpFML~7^3ch6~p?i*M9wa zE?WMxu8LuQjh*cSU#<5LgBu?IWRilYQ*dGcc?SqO1R9`jQDR8|i!d~BFiJpJqsqYH zFd86`sK=;azi;h`x-`bH^XQ-Hd?R) ztLM?04pb_PR$-8`akL5phr>WuVWUk=P&ACj0yr^_7SZ5f7%if~!N4$DM1#X&v~U1Gc<5ALdQA{6k&soYAnzZ&I2v55y=?> z2OzP|Aj||EhfHvSjg9(nBtRrb4P#&!4KoIY(WJq^Fq)kh7)DDC28PkXk%3{fLSY!K zH-U|g(YD5D^8naVq*wDGx4Xrjfq~Nlv=(5rLIyR_M=Rvf3K^6RMl0me3K^ITMl0me o{yL~QpjU-FS_y+fd#F~zEXw85S>n%6HiJCj>FVdQ&MBb@0L-pQcmMzZ literal 10812 zcmeAS@N?(olHy`uVBq!ia0y~yU~geyV6ov~1Bz6wRMrPljKx9jP7LeL$-HD>P+;(M zaSW-L^XAUR+`|S8u8vwCOxA3k^!vd>vQoAdb7--lIu=j~*Nh&If%k?S`+Ch<=I$!Ol9Sr{Z><@wFZ*LC!v-SLD_78`D z-()Te+9?l_PN^`GBF1PUT`;N)9GE1>+-SIsh8zC8jX#Cx?YFA6&S3q2`}j>}NOoTE z@~Og}YgR?jVur!`bBYN>#$k#A4}>M4$^ljF!g&ZH#1PnIFsh7!VKhL%sbDmPfP-N) z%RtJ6(ZT^74x@#`XyGtgQ9_D@(Y!F47e@2KXkK6#Z6iSv!Dt%^91f%P!bq(bK3ttA zJB5LVg^?%5>*|{N`1FNpT7C00}Tl=sjP@{zdC_#)C4&ZDtTJ3^^VYIam z4hBkw0$$CXdGoI2^E-Laa^`{i`FH2u-FpWynBhUXylc0-o#vIc*B}v3S3j3^P6 { const hideMouseAnimation = async (p: puppeteer.Page) => { await p.addStyleTag({ - content: '.replayer-mouse-tail{display: none !important;}', + content: `.replayer-mouse-tail{display: none !important;} + html, body { margin: 0; padding: 0; } + iframe { border: none; }`, }); }; @@ -109,9 +111,7 @@ describe('e2e webgl', () => { `); await waitForRAF(page); - const element = await page.$('iframe'); - await waitForRAF(page); - const frameImage = await element!.screenshot(); + const frameImage = await page!.screenshot(); await waitForRAF(page); expect(frameImage).toMatchImageSnapshot(); }); @@ -148,11 +148,7 @@ describe('e2e webgl', () => { await page.evaluate(`replayer.play(500);`); await waitForRAF(page); - const element = await page.$('iframe'); - await waitForRAF(page); - const frameImage = await element!.screenshot(); - await waitForRAF(page); - + const frameImage = await page!.screenshot(); expect(frameImage).toMatchImageSnapshot(); }); }); diff --git a/yarn.lock b/yarn.lock index e5e5dc816d..79e4e6f5d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2089,10 +2089,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest-image-snapshot@^4.3.1": - version "4.3.1" - resolved "https://registry.npmjs.org/@types/jest-image-snapshot/-/jest-image-snapshot-4.3.1.tgz" - integrity sha512-WDdUruGF14C53axe/mNDgQP2YIhtcwXrwmmVP8eOGyfNTVD+FbxWjWR7RTU+lzEy4K6V6+z7nkVDm/auI/r3xQ== +"@types/jest-image-snapshot@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/jest-image-snapshot/-/jest-image-snapshot-5.1.0.tgz#aa355ec40625fcb338fd31c935791bc8fde72bcf" + integrity sha512-pfCz6dclA8mDxwXN/x/PuYBCPwzGuYcTfOVZvDAikC1GLXg/CwECF9UgtWse0tR42c6OaL7LPEnN7i/Dm86KkQ== dependencies: "@types/jest" "*" "@types/pixelmatch" "*" @@ -6805,10 +6805,10 @@ jest-haste-map@^27.5.1: optionalDependencies: fsevents "^2.3.2" -jest-image-snapshot@^4.5.1: - version "4.5.1" - resolved "https://registry.npmjs.org/jest-image-snapshot/-/jest-image-snapshot-4.5.1.tgz" - integrity sha512-0YkgupgkkCx0wIZkxvqs/oNiUT0X0d2WTpUhaAp+Dy6CpqBUZMRTIZo4KR1f+dqmx6WXrLCvecjnHLIsLkI+gQ== +jest-image-snapshot@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/jest-image-snapshot/-/jest-image-snapshot-5.2.0.tgz#4af046935b465f0460aa73e890717bbc25d431e9" + integrity sha512-msKQqsxr4ZS8S3FQ6ot1SPlDKc4pCfyKY3SxU9LEoASj1zoEfglDYjmxNX53pxpNf7Fp7CJZvwP4xkNXVQgEXA== dependencies: chalk "^1.1.3" get-stdin "^5.0.1" From 6eb1d2ed096baca9105c14970dea9cc0598924a5 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 2 Nov 2022 18:03:41 +0100 Subject: [PATCH 49/61] Make tests run with new puppeteer version --- .../test/__snapshots__/record.test.ts.snap | 950 +++++++++++------- packages/rrweb/test/html/assets/style.css | 3 + packages/rrweb/test/html/hello-world.html | 12 + packages/rrweb/test/record.test.ts | 137 +-- 4 files changed, 688 insertions(+), 414 deletions(-) create mode 100644 packages/rrweb/test/html/assets/style.css create mode 100644 packages/rrweb/test/html/hello-world.html diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index 7133805635..06964f5eff 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -1905,7 +1905,7 @@ exports[`record captures stylesheet rules 1`] = ` ]" `; -exports[`record captures stylesheets in iframes that are still loading 1`] = ` +exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = ` "[ { \\"type\\": 4, @@ -2012,8 +2012,7 @@ exports[`record captures stylesheets in iframes that are still loading 1`] = ` \\"type\\": 2, \\"tagName\\": \\"link\\", \\"attributes\\": { - \\"rel\\": \\"stylesheet\\", - \\"href\\": \\"blob:null\\" + \\"_cssText\\": \\"body { color: pink; }\\" }, \\"childNodes\\": [], \\"rootId\\": 10, @@ -2046,28 +2045,11 @@ exports[`record captures stylesheets in iframes that are still loading 1`] = ` \\"attributes\\": [], \\"isAttachIframe\\": true } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"adds\\": [], - \\"removes\\": [], - \\"texts\\": [], - \\"attributes\\": [ - { - \\"id\\": 13, - \\"attributes\\": { - \\"_cssText\\": \\"body { color: pink; }\\" - } - } - ] - } } ]" `; -exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = ` +exports[`record captures stylesheets with \`blob:\` url 1`] = ` "[ { \\"type\\": 4, @@ -2099,7 +2081,17 @@ exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = ` \\"type\\": 2, \\"tagName\\": \\"head\\", \\"attributes\\": {}, - \\"childNodes\\": [], + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"link\\", + \\"attributes\\": { + \\"_cssText\\": \\"body { color: pink; }\\" + }, + \\"childNodes\\": [], + \\"id\\": 5 + } + ], \\"id\\": 4 }, { @@ -2110,7 +2102,7 @@ exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = ` { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 6 + \\"id\\": 7 }, { \\"type\\": 2, @@ -2120,19 +2112,90 @@ exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = ` \\"size\\": \\"40\\" }, \\"childNodes\\": [], - \\"id\\": 7 + \\"id\\": 8 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", - \\"id\\": 8 + \\"id\\": 9 + } + ], + \\"id\\": 6 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + } +]" +`; + +exports[`record iframes captures stylesheet mutations in iframes 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\\": \\"iframe\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 9 + \\"attributes\\": { + \\"srcdoc\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 8 + } + ], + \\"id\\": 7 } ], \\"id\\": 5 @@ -2155,7 +2218,7 @@ exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = ` \\"source\\": 0, \\"adds\\": [ { - \\"parentId\\": 9, + \\"parentId\\": 7, \\"nextId\\": null, \\"node\\": { \\"type\\": 0, @@ -2169,36 +2232,40 @@ exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = ` \\"type\\": 2, \\"tagName\\": \\"head\\", \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 9, + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, \\"childNodes\\": [ { \\"type\\": 2, - \\"tagName\\": \\"link\\", - \\"attributes\\": { - \\"_cssText\\": \\"body { color: pink; }\\" - }, - \\"childNodes\\": [], - \\"rootId\\": 10, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Mysterious Button\\", + \\"rootId\\": 9, + \\"id\\": 14 + } + ], + \\"rootId\\": 9, \\"id\\": 13 } ], - \\"rootId\\": 10, + \\"rootId\\": 9, \\"id\\": 12 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"body\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"rootId\\": 10, - \\"id\\": 14 } ], - \\"rootId\\": 10, - \\"id\\": 11 + \\"rootId\\": 9, + \\"id\\": 10 } ], - \\"compatMode\\": \\"BackCompat\\", - \\"id\\": 10 + \\"id\\": 9 } } ], @@ -2207,11 +2274,105 @@ exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = ` \\"attributes\\": [], \\"isAttachIframe\\": true } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 11, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": { + \\"_cssText\\": \\"body { background: rgb(0, 0, 0); }@media {\\\\n body { background: rgb(0, 0, 0); }\\\\n}\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 9, + \\"id\\": 15 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 15, + \\"adds\\": [ + { + \\"rule\\": \\"body { color: #fff; }\\" + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 15, + \\"adds\\": [ + { + \\"rule\\": \\"body { color: #ccc; }\\", + \\"index\\": [ + 2, + 0 + ] + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 15, + \\"removes\\": [ + { + \\"index\\": 0 + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"id\\": 15, + \\"set\\": { + \\"property\\": \\"color\\", + \\"value\\": \\"green\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 15, + \\"removes\\": [ + { + \\"index\\": [ + 1, + 0 + ] + } + ] + } } ]" `; -exports[`record captures stylesheets that are still loading 1`] = ` +exports[`record is safe to checkout during async callbacks 1`] = ` "[ { \\"type\\": 4, @@ -2292,21 +2453,34 @@ exports[`record captures stylesheets that are still loading 1`] = ` \\"source\\": 0, \\"texts\\": [], \\"attributes\\": [], - \\"removes\\": [], - \\"adds\\": [ + \\"removes\\": [ { - \\"parentId\\": 4, + \\"parentId\\": 5, + \\"id\\": 7 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 5, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, - \\"tagName\\": \\"link\\", - \\"attributes\\": { - \\"rel\\": \\"stylesheet\\", - \\"href\\": \\"blob:null\\" - }, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, \\"childNodes\\": [], \\"id\\": 9 } + }, + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10 + } } ] } @@ -2315,24 +2489,22 @@ exports[`record captures stylesheets that are still loading 1`] = ` \\"type\\": 3, \\"data\\": { \\"source\\": 0, - \\"adds\\": [], - \\"removes\\": [], \\"texts\\": [], - \\"attributes\\": [ + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ { - \\"id\\": 9, - \\"attributes\\": { - \\"_cssText\\": \\"body { color: pink; }\\" + \\"parentId\\": 10, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"test\\", + \\"id\\": 11 } } ] } - } -]" -`; - -exports[`record captures stylesheets with \`blob:\` url 1`] = ` -"[ + }, { \\"type\\": 4, \\"data\\": { @@ -2363,17 +2535,7 @@ exports[`record captures stylesheets with \`blob:\` url 1`] = ` \\"type\\": 2, \\"tagName\\": \\"head\\", \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"link\\", - \\"attributes\\": { - \\"_cssText\\": \\"body { color: pink; }\\" - }, - \\"childNodes\\": [], - \\"id\\": 5 - } - ], + \\"childNodes\\": [], \\"id\\": 4 }, { @@ -2384,25 +2546,36 @@ exports[`record captures stylesheets with \`blob:\` url 1`] = ` { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"id\\": 7 + \\"id\\": 6 }, { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": { - \\"type\\": \\"text\\", - \\"size\\": \\"40\\" - }, - \\"childNodes\\": [], + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", \\"id\\": 8 }, { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"test\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + } + ], \\"id\\": 9 } ], - \\"id\\": 6 + \\"id\\": 5 } ], \\"id\\": 3 @@ -2415,11 +2588,47 @@ exports[`record captures stylesheets with \`blob:\` url 1`] = ` \\"top\\": 0 } } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 9, + \\"id\\": 10 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 5, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10 + } + }, + { + \\"parentId\\": 10, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"test\\", + \\"id\\": 11 + } + } + ] + } } ]" `; -exports[`record iframes captures stylesheet mutations in iframes 1`] = ` +exports[`record loading stylesheets captures stylesheets in iframes that are still loading 1`] = ` "[ { \\"type\\": 4, @@ -2445,42 +2654,109 @@ exports[`record iframes captures stylesheet mutations in iframes 1`] = ` { \\"type\\": 2, \\"tagName\\": \\"html\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, \\"childNodes\\": [ { \\"type\\": 2, \\"tagName\\": \\"head\\", \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 4 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"body\\", - \\"attributes\\": {}, \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], \\"id\\": 6 }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, { \\"type\\": 2, - \\"tagName\\": \\"iframe\\", + \\"tagName\\": \\"meta\\", \\"attributes\\": { - \\"srcdoc\\": \\"\\" + \\"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\\": \\"\\\\n \\\\n \\\\n \\", - \\"id\\": 8 + \\"textContent\\": \\"Hello World!\\", + \\"id\\": 13 } ], - \\"id\\": 7 + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 } ], - \\"id\\": 5 + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 18 + } + ], + \\"id\\": 16 } ], \\"id\\": 3 @@ -2500,54 +2776,139 @@ exports[`record iframes captures stylesheet mutations in iframes 1`] = ` \\"source\\": 0, \\"adds\\": [ { - \\"parentId\\": 7, + \\"parentId\\": 18, \\"nextId\\": null, \\"node\\": { \\"type\\": 0, \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"rootId\\": 19, + \\"id\\": 20 + }, { \\"type\\": 2, \\"tagName\\": \\"html\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, \\"childNodes\\": [ { \\"type\\": 2, \\"tagName\\": \\"head\\", \\"attributes\\": {}, - \\"childNodes\\": [], - \\"rootId\\": 9, - \\"id\\": 11 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"body\\", - \\"attributes\\": {}, \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 19, + \\"id\\": 23 + }, { \\"type\\": 2, - \\"tagName\\": \\"button\\", + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 19, + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 19, + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 19, + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 19, + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 19, + \\"id\\": 28 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 19, + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", \\"attributes\\": {}, \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"Mysterious Button\\", - \\"rootId\\": 9, - \\"id\\": 14 + \\"textContent\\": \\"Hello World!\\", + \\"rootId\\": 19, + \\"id\\": 31 } ], - \\"rootId\\": 9, - \\"id\\": 13 - } + \\"rootId\\": 19, + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 19, + \\"id\\": 32 + } ], - \\"rootId\\": 9, - \\"id\\": 12 + \\"rootId\\": 19, + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 19, + \\"id\\": 33 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\", + \\"rootId\\": 19, + \\"id\\": 35 + } + ], + \\"rootId\\": 19, + \\"id\\": 34 } ], - \\"rootId\\": 9, - \\"id\\": 10 + \\"rootId\\": 19, + \\"id\\": 21 } ], - \\"id\\": 9 + \\"id\\": 19 } } ], @@ -2566,17 +2927,18 @@ exports[`record iframes captures stylesheet mutations in iframes 1`] = ` \\"removes\\": [], \\"adds\\": [ { - \\"parentId\\": 11, + \\"parentId\\": 22, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, - \\"tagName\\": \\"style\\", + \\"tagName\\": \\"link\\", \\"attributes\\": { - \\"_cssText\\": \\"body { background: rgb(0, 0, 0); }@media {\\\\n body { background: rgb(0, 0, 0); }\\\\n}\\" + \\"rel\\": \\"stylesheet\\", + \\"href\\": \\"http://localhost:3030/html/assets/style.css\\" }, \\"childNodes\\": [], - \\"rootId\\": 9, - \\"id\\": 15 + \\"rootId\\": 19, + \\"id\\": 36 } } ] @@ -2585,68 +2947,16 @@ exports[`record iframes captures stylesheet mutations in iframes 1`] = ` { \\"type\\": 3, \\"data\\": { - \\"source\\": 8, - \\"id\\": 15, - \\"adds\\": [ - { - \\"rule\\": \\"body { color: #fff; }\\" - } - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"id\\": 15, - \\"adds\\": [ - { - \\"rule\\": \\"body { color: #ccc; }\\", - \\"index\\": [ - 2, - 0 - ] - } - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"id\\": 15, - \\"removes\\": [ - { - \\"index\\": 0 - } - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 13, - \\"id\\": 15, - \\"set\\": { - \\"property\\": \\"color\\", - \\"value\\": \\"green\\" - }, - \\"index\\": [ - 0 - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 8, - \\"id\\": 15, - \\"removes\\": [ + \\"source\\": 0, + \\"adds\\": [], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [ { - \\"index\\": [ - 1, - 0 - ] + \\"id\\": 36, + \\"attributes\\": { + \\"_cssText\\": \\"body { color: pink; }\\" + } } ] } @@ -2654,7 +2964,7 @@ exports[`record iframes captures stylesheet mutations in iframes 1`] = ` ]" `; -exports[`record is safe to checkout during async callbacks 1`] = ` +exports[`record loading stylesheets captures stylesheets that are still loading 1`] = ` "[ { \\"type\\": 4, @@ -2680,184 +2990,102 @@ exports[`record is safe to checkout during async callbacks 1`] = ` { \\"type\\": 2, \\"tagName\\": \\"html\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, \\"childNodes\\": [ { \\"type\\": 2, \\"tagName\\": \\"head\\", \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 4 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"body\\", - \\"attributes\\": {}, \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 6 + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 }, { \\"type\\": 2, - \\"tagName\\": \\"input\\", + \\"tagName\\": \\"meta\\", \\"attributes\\": { - \\"type\\": \\"text\\", - \\"size\\": \\"40\\" + \\"charset\\": \\"UTF-8\\" }, \\"childNodes\\": [], - \\"id\\": 7 + \\"id\\": 6 }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], \\"id\\": 8 - } - ], - \\"id\\": 5 - } - ], - \\"id\\": 3 - } - ], - \\"id\\": 1 - }, - \\"initialOffset\\": { - \\"left\\": 0, - \\"top\\": 0 - } - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [], - \\"removes\\": [ - { - \\"parentId\\": 5, - \\"id\\": 7 - } - ], - \\"adds\\": [ - { - \\"parentId\\": 5, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"p\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 9 - } - }, - { - \\"parentId\\": 9, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"span\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10 - } - } - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [], - \\"removes\\": [], - \\"adds\\": [ - { - \\"parentId\\": 10, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\"test\\", - \\"id\\": 11 - } - } - ] - } - }, - { - \\"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 + \\"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 \\\\n \\\\n \\", - \\"id\\": 8 + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 }, { \\"type\\": 2, - \\"tagName\\": \\"p\\", + \\"tagName\\": \\"title\\", \\"attributes\\": {}, \\"childNodes\\": [ { - \\"type\\": 2, - \\"tagName\\": \\"span\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"test\\", - \\"id\\": 11 - } - ], - \\"id\\": 10 + \\"type\\": 3, + \\"textContent\\": \\"Hello World!\\", + \\"id\\": 13 } ], - \\"id\\": 9 + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 } ], - \\"id\\": 5 + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\", + \\"id\\": 17 + } + ], + \\"id\\": 16 } ], \\"id\\": 3 @@ -2877,31 +3105,37 @@ exports[`record is safe to checkout during async callbacks 1`] = ` \\"source\\": 0, \\"texts\\": [], \\"attributes\\": [], - \\"removes\\": [ - { - \\"parentId\\": 9, - \\"id\\": 10 - } - ], + \\"removes\\": [], \\"adds\\": [ { - \\"parentId\\": 5, + \\"parentId\\": 4, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, - \\"tagName\\": \\"span\\", - \\"attributes\\": {}, + \\"tagName\\": \\"link\\", + \\"attributes\\": { + \\"rel\\": \\"stylesheet\\", + \\"href\\": \\"http://localhost:3030/html/assets/style.css\\" + }, \\"childNodes\\": [], - \\"id\\": 10 + \\"id\\": 18 } - }, + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [ { - \\"parentId\\": 10, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\"test\\", - \\"id\\": 11 + \\"id\\": 18, + \\"attributes\\": { + \\"_cssText\\": \\"body { color: pink; }\\" } } ] diff --git a/packages/rrweb/test/html/assets/style.css b/packages/rrweb/test/html/assets/style.css new file mode 100644 index 0000000000..3859bf3bbd --- /dev/null +++ b/packages/rrweb/test/html/assets/style.css @@ -0,0 +1,3 @@ +body { + color: pink; +} diff --git a/packages/rrweb/test/html/hello-world.html b/packages/rrweb/test/html/hello-world.html new file mode 100644 index 0000000000..04c1907d6a --- /dev/null +++ b/packages/rrweb/test/html/hello-world.html @@ -0,0 +1,12 @@ + + + + + + + Hello World! + + + Hello world! + + diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 8347825935..d52f4fe990 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -11,7 +11,14 @@ import { styleSheetRuleData, selectionData, } from '../src/types'; -import { assertSnapshot, launchPuppeteer, waitForRAF } from './utils'; +import { + assertSnapshot, + getServerURL, + launchPuppeteer, + startServer, + waitForRAF, +} from './utils'; +import type { Server } from 'http'; interface ISuite { code: string; @@ -611,73 +618,91 @@ describe('record', function (this: ISuite) { assertSnapshot(ctx.events); }); - it('captures stylesheets that are still loading', async () => { - await ctx.page.evaluate(() => { - const { record } = ((window as unknown) as IWindow).rrweb; + describe('loading stylesheets', () => { + let server: Server; + let serverURL: string; - record({ - inlineStylesheet: true, - emit: ((window as unknown) as IWindow).emit, + beforeAll(async () => { + server = await startServer(); + serverURL = getServerURL(server); + }); + + beforeEach(async () => { + ctx.page = await ctx.browser.newPage(); + await ctx.page.goto(`${serverURL}/html/hello-world.html`); + await ctx.page.evaluate(ctx.code); + ctx.events = []; + await ctx.page.exposeFunction('emit', (e: eventWithTime) => { + if ( + e.type === EventType.DomContentLoaded || + e.type === EventType.Load + ) { + return; + } + ctx.events.push(e); }); - 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); + ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); }); - // `blob:` URLs are not available immediately, so we need to wait for the browser to load them - await waitForRAF(ctx.page); - // 'blob' URL is different in every execution so we need to remove it from the snapshot. - const filteredEvents = JSON.parse( - JSON.stringify(ctx.events).replace(/blob\:[\w\d-/]+"/, 'blob:null"'), - ); - assertSnapshot(filteredEvents); - }); + afterAll(async () => { + await server.close(); + }); - it('captures stylesheets in iframes that are still loading', async () => { - await ctx.page.evaluate(() => { - const iframe = document.createElement('iframe'); - document.body.appendChild(iframe); - const iframeDoc = iframe.contentDocument!; + it('captures stylesheets that are still loading', async () => { + ctx.page.evaluate((serverURL) => { + const { record } = ((window as unknown) as IWindow).rrweb; - const { record } = ((window as unknown) as IWindow).rrweb; + record({ + inlineStylesheet: true, + emit: ((window as unknown) as IWindow).emit, + }); - record({ - inlineStylesheet: true, - emit: ((window as unknown) as IWindow).emit, - }); + const link1 = document.createElement('link'); + link1.setAttribute('rel', 'stylesheet'); + link1.setAttribute('href', `${serverURL}/html/assets/style.css`); + document.head.appendChild(link1); + }, serverURL); - 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); + await ctx.page.waitForResponse(`${serverURL}/html/assets/style.css`); + await waitForRAF(ctx.page); + + assertSnapshot(ctx.events); }); - // `blob:` URLs are not available immediately, so we need to wait for the browser to load them - // the following is a bit overkill but this test is super flaky on CI - await waitForRAF(ctx.page); - await ctx.page.waitForTimeout(50); - await waitForRAF(ctx.page); + it('captures stylesheets in iframes that are still loading', async () => { + ctx.page.evaluate(() => { + const iframe = document.createElement('iframe'); + iframe.setAttribute('src', `/html/hello-world.html?2`); + document.body.appendChild(iframe); - const filteredEvents = JSON.parse( - JSON.stringify(ctx.events).replace(/blob\:[\w\d-/]+"/, 'blob:null"'), - ); - assertSnapshot(filteredEvents); + const { record } = ((window as unknown) as IWindow).rrweb; + + record({ + inlineStylesheet: true, + emit: ((window as unknown) as IWindow).emit, + }); + }); + + await ctx.page.waitForResponse(`${serverURL}/html/hello-world.html?2`); + + await waitForRAF(ctx.page); + + ctx.page.evaluate(() => { + const iframe = document.querySelector('iframe')!; + const iframeDoc = iframe.contentDocument!; + const linkEl = document.createElement('link'); + linkEl.setAttribute('rel', 'stylesheet'); + linkEl.setAttribute('href', `/html/assets/style.css`); + iframeDoc.head.appendChild(linkEl); + }); + + await ctx.page.waitForResponse(`${serverURL}/html/assets/style.css`); + + await waitForRAF(ctx.page); + + assertSnapshot(ctx.events); + }); }); it('captures CORS stylesheets that are still loading', async () => { From 728e7796392ea9652be2a361cb7190932b92c27e Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 2 Nov 2022 18:04:07 +0100 Subject: [PATCH 50/61] upgrade eslint-plugin-jest --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 66ae51cc19..7192130275 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "concurrently": "^7.1.0", "eslint": "^8.19.0", "eslint-plugin-compat": "^4.0.2", - "eslint-plugin-jest": "^26.5.3", + "eslint-plugin-jest": "^27.1.3", "eslint-plugin-tsdoc": "^0.2.16", "lerna": "^4.0.0", "markdownlint": "^0.25.1", diff --git a/yarn.lock b/yarn.lock index 79e4e6f5d8..8589a5168f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4665,10 +4665,10 @@ eslint-plugin-compat@^4.0.2: lodash.memoize "4.1.2" semver "7.3.5" -eslint-plugin-jest@^26.5.3: - version "26.5.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-26.5.3.tgz#a3ceeaf4a757878342b8b00eca92379b246e5505" - integrity sha512-sICclUqJQnR1bFRZGLN2jnSVsYOsmPYYnroGCIMVSvTS3y8XR3yjzy1EcTQmk6typ5pRgyIWzbjqxK6cZHEZuQ== +eslint-plugin-jest@^27.1.3: + version "27.1.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.1.3.tgz#9f359eeac0c720a825f658e7e261a9eef869dc8d" + integrity sha512-7DrIfYRQPa7JQd1Le8G/BJsfYHVUKQdJQ/6vULSp/4NjKZmSMJ/605G2hhScEra++SiH68zPEjLnrO74nHrMLg== dependencies: "@typescript-eslint/utils" "^5.10.0" From 7b5c3cf582fca30dc0f86aebaece64d047fb3415 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 2 Nov 2022 18:17:29 +0100 Subject: [PATCH 51/61] Chore: Remove travis ci as ci's running on github actions --- .travis.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b122c43a8f..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: node_js - -os: linux - -dist: focal - -node_js: - - lts/* - -install: - - yarn - -script: - - yarn build:all - - yarn turbo run check-types - - xvfb-run --server-args="-screen 0 1920x1080x24" yarn test From 99ab6d79c542f81a8b2ab517cea4540137b482fa Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 3 Nov 2022 15:41:51 +0100 Subject: [PATCH 52/61] Chore: Support recording cross origin iframe in repl --- packages/rrweb/scripts/repl.js | 78 +++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/packages/rrweb/scripts/repl.js b/packages/rrweb/scripts/repl.js index 4edb07a3c3..0bf4a7de62 100644 --- a/packages/rrweb/scripts/repl.js +++ b/packages/rrweb/scripts/repl.js @@ -21,6 +21,43 @@ void (async () => { const code = getCode(); let events = []; + async function injectRecording(frame) { + await frame.evaluate((rrwebCode) => { + const win = window; + if (win.__IS_RECORDING__) return; + win.__IS_RECORDING__ = true; + + (async () => { + function loadScript(code) { + const s = document.createElement('script'); + let r = false; + s.type = 'text/javascript'; + s.innerHTML = code; + if (document.head) { + document.head.append(s); + } else { + requestAnimationFrame(() => { + document.head.append(s); + }); + } + } + loadScript(rrwebCode); + + win.events = []; + rrweb.record({ + emit: (event) => { + win.events.push(event); + win._replLog(event); + }, + plugins: [], + recordCanvas: true, + recordCrossOriginIframes: true, + collectFonts: true, + }); + })(); + }, code); + } + await start('https://react-redux.realworld.io'); const fakeGoto = async (page, url) => { @@ -44,8 +81,7 @@ void (async () => { { type: 'input', name: 'url', - message: - `Enter the url you want to record, e.g [${defaultURL}]: `, + message: `Enter the url you want to record, e.g [${defaultURL}]: `, }, ]); @@ -116,34 +152,18 @@ void (async () => { ], }); const page = await browser.newPage(); - await page.goto(url, { - waitUntil: 'domcontentloaded', - timeout: 300000, - }); await page.exposeFunction('_replLog', (event) => { events.push(event); }); - await page.evaluate(`;${code} - window.__IS_RECORDING__ = true - rrweb.record({ - emit: event => window._replLog(event), - recordCanvas: true, - collectFonts: true - }); - `); - page.on('framenavigated', async () => { - const isRecording = await page.evaluate('window.__IS_RECORDING__'); - if (!isRecording) { - await page.evaluate(`;${code} - window.__IS_RECORDING__ = true - rrweb.record({ - emit: event => window._replLog(event), - recordCanvas: true, - collectFonts: true - }); - `); - } + + page.on('framenavigated', async (frame) => { + await injectRecording(frame); + }); + + await page.goto(url, { + waitUntil: 'domcontentloaded', + timeout: 300000, }); emitter.once('done', async (shouldReplay) => { @@ -211,9 +231,9 @@ void (async () => {