From 78849f3997e27690f2b6a3a026911329cf9e35ed Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 10 Jun 2024 10:49:14 +0200 Subject: [PATCH 01/29] WIP dialog recording --- packages/rrweb-snapshot/src/snapshot.ts | 11 + packages/rrweb-snapshot/src/types.ts | 9 + .../__snapshots__/integration.test.ts.snap | 122 +++++++++ packages/rrweb-snapshot/test/html/dialog.html | 5 + .../rrweb-snapshot/test/integration.test.ts | 75 +++++- packages/rrweb-snapshot/test/rebuild.test.ts | 28 +- packages/rrweb-snapshot/test/snapshot.test.ts | 76 ++---- packages/rrweb/test/events/dialog-playback.ts | 131 +++++++++ packages/rrweb/test/replay/dialog.test.ts | 251 ++++++++++++++++++ 9 files changed, 648 insertions(+), 60 deletions(-) create mode 100644 packages/rrweb-snapshot/test/html/dialog.html create mode 100644 packages/rrweb/test/events/dialog-playback.ts create mode 100644 packages/rrweb/test/replay/dialog.test.ts diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 40c932e9b1..5c88c75d79 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -6,6 +6,7 @@ import { MaskInputOptions, SlimDOMOptions, DataURLOptions, + DialogAttributes, MaskTextFn, MaskInputFn, KeepIframeSrcFn, @@ -704,6 +705,16 @@ function serializeElementNode( delete attributes.selected; } } + + if ( + tagName === 'dialog' && + (n as HTMLDialogElement).open && + n.matches('dialog:modal') + ) { + (attributes as DialogAttributes).rr_open = 'modal'; + delete attributes.open; // prevent default `show()` behavior which blocks `showModal()` from working + } + // canvas image data if (tagName === 'canvas' && recordCanvas) { if ((n as ICanvas).__context === '2d') { diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts index 1abfe4d6c0..3613b4cd8b 100644 --- a/packages/rrweb-snapshot/src/types.ts +++ b/packages/rrweb-snapshot/src/types.ts @@ -103,6 +103,15 @@ export type mediaAttributes = { rr_mediaVolume?: number; }; +export type DialogAttributes = + // | { + // open: ''; + // } + { + rr_open: 'modal'; + // rr_open_index?: number; + }; + // @deprecated export interface INode extends Node { __sn: serializedNodeWithId; diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index f43a3724b3..0c16d5898c 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -1,5 +1,121 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`dialog integration tests > should capture open attribute for modal dialogs 1`] = ` +"{ + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": { + \\"rr_open\\": \\"modal\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"I'm a dialog\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 8 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 +}" +`; + +exports[`dialog integration tests > should capture open attribute for non modal dialogs 1`] = ` +"{ + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": { + \\"open\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"I'm a dialog\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 8 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 +}" +`; + exports[`iframe integration tests > snapshot async iframes 1`] = ` "{ \\"type\\": 0, @@ -214,6 +330,12 @@ exports[`integration tests > [html file]: cors-style-sheet.html 1`] = ` " `; +exports[`integration tests > [html file]: dialog.html 1`] = ` +" + I'm a dialog + " +`; + exports[`integration tests > [html file]: dynamic-stylesheet.html 1`] = ` " diff --git a/packages/rrweb-snapshot/test/html/dialog.html b/packages/rrweb-snapshot/test/html/dialog.html new file mode 100644 index 0000000000..2380b8fade --- /dev/null +++ b/packages/rrweb-snapshot/test/html/dialog.html @@ -0,0 +1,5 @@ + + + I'm a dialog + + diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index 8f4476ecb9..ac8b9b1f66 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -1,10 +1,20 @@ import * as fs from 'fs'; -import * as path from 'path'; import * as http from 'http'; -import * as url from 'url'; +import * as path from 'path'; import * as puppeteer from 'puppeteer'; -import { vi, assert, describe, it, beforeAll, afterAll, expect } from 'vitest'; -import { waitForRAF, getServerURL } from './utils'; +import * as url from 'url'; +import { + afterAll, + assert, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; + +import { getServerURL, waitForRAF } from './utils'; const htmlFolder = path.join(__dirname, 'html'); const htmls = fs.readdirSync(htmlFolder).map((filePath) => { @@ -60,6 +70,15 @@ function sanitizeSnapshot(snapshot: string): string { return snapshot.replace(/localhost:[0-9]+/g, 'localhost:3030'); } +async function snapshot(page: puppeteer.Page, code: string): Promise { + await waitForRAF(page); + const result = (await page.evaluate(`${code} + const snapshot = rrwebSnapshot.snapshot(document); + JSON.stringify(snapshot, null, 2); + `)) as string; + return result; +} + function assertSnapshot(snapshot: string): void { expect(sanitizeSnapshot(snapshot)).toMatchSnapshot(); } @@ -68,6 +87,7 @@ interface ISuite { server: http.Server; serverURL: string; browser: puppeteer.Browser; + page: puppeteer.Page; code: string; } @@ -431,6 +451,53 @@ describe('iframe integration tests', function (this: ISuite) { }); }); +describe('dialog integration tests', function (this: ISuite) { + vi.setConfig({ testTimeout: 30_000 }); + let server: ISuite['server']; + let serverURL: ISuite['serverURL']; + let browser: ISuite['browser']; + let code: ISuite['code']; + let page: ISuite['page']; + + beforeAll(async () => { + server = await startServer(); + serverURL = getServerURL(server); + browser = await puppeteer.launch({ + // headless: false, + }); + + code = fs.readFileSync( + path.resolve(__dirname, '../dist/rrweb-snapshot.umd.cjs'), + 'utf-8', + ); + }); + + beforeEach(async () => { + page = await browser.newPage(); + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`${serverURL}/html/dialog.html`, { + waitUntil: 'load', + }); + }); + + afterAll(async () => { + await browser.close(); + await server.close(); + }); + + it('should capture open attribute for non modal dialogs', async () => { + page.evaluate('document.querySelector("dialog").show()'); + const snapshotResult = await snapshot(page, code); + assertSnapshot(snapshotResult); + }); + + it('should capture open attribute for modal dialogs', async () => { + await page.evaluate('document.querySelector("dialog").showModal()'); + const snapshotResult = await snapshot(page, code); + assertSnapshot(snapshotResult); + }); +}); + describe('shadow DOM integration tests', function (this: ISuite) { vi.setConfig({ testTimeout: 30_000 }); let server: ISuite['server']; diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index c71e1e8510..69dc7bf9fc 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -3,7 +3,8 @@ */ import * as fs from 'fs'; import * as path from 'path'; -import { describe, it, beforeEach, expect } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; + import { adaptCssForReplay, buildNodeWithSN, @@ -52,6 +53,31 @@ describe('rebuild', function () { }); }); + // this doesn't really test anything, maybe remove it? + // contemplate if rrweb-snapshot should trigger .showModal() on dialog elements on rebuild... + describe('rr_open', function () { + it('should call `show` on non-modal dialog', function () { + const node = buildNodeWithSN( + { + id: 1, + tagName: 'dialog', + type: NodeType.Element, + attributes: { + open: '', + }, + childNodes: [], + }, + { + doc: document, + mirror, + hackCss: false, + cache, + }, + ) as HTMLDialogElement; + expect(node?.open).toBe(true); + }); + }); + describe('shadowDom', function () { it('rebuild shadowRoot without siblings', function () { const node = buildNodeWithSN( diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index de1d79eb6d..8493791329 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -2,16 +2,32 @@ * @vitest-environment jsdom */ import { JSDOM } from 'jsdom'; -import { describe, it, expect } from 'vitest'; -import { +import { describe, expect, it } from 'vitest'; + +import snapshot, { + _isBlockedElement, absoluteToStylesheet, serializeNodeWithId, - _isBlockedElement, } from '../src/snapshot'; -import snapshot from '../src/snapshot'; -import { serializedNodeWithId, elementNode } from '../src/types'; +import { elementNode, serializedNodeWithId } from '../src/types'; import { Mirror } from '../src/utils'; +const serializeNode = (node: Node): serializedNodeWithId | null => { + return serializeNodeWithId(node, { + doc: document, + mirror: new Mirror(), + blockClass: 'blockblock', + blockSelector: null, + maskTextClass: 'maskmask', + maskTextSelector: null, + skipChild: false, + inlineStylesheet: true, + maskTextFn: undefined, + maskInputFn: undefined, + slimDOMOptions: {}, + }); +}; + describe('absolute url to stylesheet', () => { const href = 'http://localhost/css/style.css'; @@ -139,22 +155,6 @@ describe('isBlockedElement()', () => { }); describe('style elements', () => { - const serializeNode = (node: Node): serializedNodeWithId | null => { - return serializeNodeWithId(node, { - doc: document, - mirror: new Mirror(), - blockClass: 'blockblock', - blockSelector: null, - maskTextClass: 'maskmask', - maskTextSelector: null, - skipChild: false, - inlineStylesheet: true, - maskTextFn: undefined, - maskInputFn: undefined, - slimDOMOptions: {}, - }); - }; - const render = (html: string): HTMLStyleElement => { document.write(html); return document.querySelector('style')!; @@ -184,23 +184,6 @@ describe('style elements', () => { }); describe('scrollTop/scrollLeft', () => { - const serializeNode = (node: Node): serializedNodeWithId | null => { - return serializeNodeWithId(node, { - doc: document, - mirror: new Mirror(), - blockClass: 'blockblock', - blockSelector: null, - maskTextClass: 'maskmask', - maskTextSelector: null, - skipChild: false, - inlineStylesheet: true, - maskTextFn: undefined, - maskInputFn: undefined, - slimDOMOptions: {}, - newlyAddedElement: false, - }); - }; - const render = (html: string): HTMLDivElement => { document.write(html); return document.querySelector('div')!; @@ -222,23 +205,6 @@ describe('scrollTop/scrollLeft', () => { }); describe('form', () => { - const serializeNode = (node: Node): serializedNodeWithId | null => { - return serializeNodeWithId(node, { - doc: document, - mirror: new Mirror(), - blockClass: 'blockblock', - blockSelector: null, - maskTextClass: 'maskmask', - maskTextSelector: null, - skipChild: false, - inlineStylesheet: true, - maskTextFn: undefined, - maskInputFn: undefined, - slimDOMOptions: {}, - newlyAddedElement: false, - }); - }; - const render = (html: string): HTMLTextAreaElement => { document.write(html); return document.querySelector('textarea')!; diff --git a/packages/rrweb/test/events/dialog-playback.ts b/packages/rrweb/test/events/dialog-playback.ts new file mode 100644 index 0000000000..0d2bb53768 --- /dev/null +++ b/packages/rrweb/test/events/dialog-playback.ts @@ -0,0 +1,131 @@ +import { eventWithTime, IncrementalSource } from '@rrweb/types'; + +const events: eventWithTime[] = [ + { type: 0, data: {}, timestamp: 1900000001 }, + { type: 1, data: {}, timestamp: 1900000132 }, + { + type: 4, + data: { + href: 'http://127.0.0.1:5500/test/html/dialog.html', + width: 1600, + height: 900, + }, + timestamp: 1900000132, + }, + { + type: 2, + data: { + node: { + type: 0, + childNodes: [ + { type: 1, name: 'html', publicId: '', systemId: '', id: 2 }, + { + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 5 }, + { + type: 2, + tagName: 'meta', + attributes: { charset: 'UTF-8' }, + childNodes: [], + id: 6, + }, + { type: 3, textContent: '\n ', id: 7 }, + { + type: 2, + tagName: 'meta', + attributes: { + '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: '', id: 13 }], + id: 12, + }, + ], + id: 4, + }, + { type: 3, textContent: '\n ', id: 21 }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 23 }, + { + type: 2, + tagName: 'dialog', + attributes: { + rr_open: 'modal', + style: 'outline: blue solid 1px;', + }, + childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }], + id: 24, + }, + { type: 3, textContent: '\n ', id: 26 }, + { + type: 2, + tagName: 'dialog', + attributes: { + style: 'outline: red solid 1px;', + }, + childNodes: [{ type: 3, textContent: 'Dialog 2', id: 28 }], + id: 27, + }, + { type: 3, textContent: '\n ', id: 31 }, + ], + id: 22, + }, + ], + id: 3, + }, + ], + id: 1, + }, + initialOffset: { left: 0, top: 0 }, + }, + timestamp: 1900000136, + }, + { + type: 3, + data: { + source: IncrementalSource.Mutation, + adds: [], + removes: [], + attributes: [ + { + id: 27, + attributes: { rr_open: 'modal' }, + }, + ], + }, + timestamp: 1900001500, + }, +]; + +export default events; diff --git a/packages/rrweb/test/replay/dialog.test.ts b/packages/rrweb/test/replay/dialog.test.ts new file mode 100644 index 0000000000..acc2e972aa --- /dev/null +++ b/packages/rrweb/test/replay/dialog.test.ts @@ -0,0 +1,251 @@ +import * as fs from 'fs'; +import { toMatchImageSnapshot } from 'jest-image-snapshot'; +import * as path from 'path'; +import { vi } from 'vitest'; + +import dialogPlaybackEvents from '../events/dialog-playback'; +import { + fakeGoto, + getServerURL, + hideMouseAnimation, + ISuite, + launchPuppeteer, + startServer, + waitForRAF, +} from '../utils'; + +// expect.extend({ toMatchImageSnapshot }); + +// TODO: test the following: +// == on record == +// - dialog open +// - dialog close +// - dialog open and close (switching from modal to non modal and vise versa) +// - multiple dialogs open, recording order +// == on playback == +// - dialog open +// - dialog close +// - dialog open and close (switching from modal to non modal and vise versa) +// - multiple dialogs open, playback order +// == on rrdom == +// - that the modal modes are recorded... + +describe('dialog', () => { + vi.setConfig({ testTimeout: 100_000 }); + let code: ISuite['code']; + let page: ISuite['page']; + let browser: ISuite['browser']; + let server: ISuite['server']; + let serverURL: ISuite['serverURL']; + + beforeAll(async () => { + server = await startServer(); + serverURL = getServerURL(server); + browser = await launchPuppeteer(); + + const bundlePath = path.resolve(__dirname, '../../dist/rrweb.umd.cjs'); + code = fs.readFileSync(bundlePath, 'utf8'); + }); + + afterEach(async () => { + await page.close(); + }); + + afterAll(async () => { + await server.close(); + await browser.close(); + }); + + beforeEach(async () => { + page = await browser.newPage(); + + await fakeGoto(page, `${serverURL}/html/dialog.html`); + await page.evaluate(code); + await waitForRAF(page); + await hideMouseAnimation(page); + }); + + it('will seek to the correct moment', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events); + `); + await waitForRAF(page); + + // await page.waitForTimeout(50_000); + + // const frameImage = await page!.screenshot(); + // expect(frameImage).toMatchImageSnapshot({ + // failureThreshold: 0.05, + // failureThresholdType: 'percent', + // }); + }); + + // it('will seek to the correct moment without media interaction events', async () => { + // await page.evaluate(` + // let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}; + // const { Replayer } = rrweb; + // window.replayer = new Replayer(events); + // window.replayer.pause(6500); + // `); + + // await page.waitForNetworkIdle(); + // await waitForRAF(page); + + // const frameImage = await page!.screenshot(); + // await waitForRAF(page); + // expect(frameImage).toMatchImageSnapshot({ + // failureThreshold: 0.05, + // failureThresholdType: 'percent', + // }); + // }); + + // it("will be paused when the player wasn't started yet", async () => { + // await page.evaluate(` + // let events = ${JSON.stringify(videoPlaybackEvents)}; + // const { Replayer } = rrweb; + // window.replayer = new Replayer(events); + // `); + // await page.waitForNetworkIdle(); + // await waitForRAF(page); + + // const frameImage = await page!.screenshot(); + + // await waitForRAF(page); + // expect(frameImage).toMatchImageSnapshot({ + // failureThreshold: 0.05, + // failureThresholdType: 'percent', + // }); + // }); + + // it('will play from the correct moment', async () => { + // await page.evaluate(`let events = ${JSON.stringify(videoPlaybackEvents)}`); + // await page.evaluate(` + // const { Replayer } = rrweb; + // window.replayer = new Replayer(events, { + // UNSAFE_replayCanvas: true, + // }); + // `); + // await waitForRAF(page); + // await page.evaluate(` + // window.replayer.play(6500); + // `); + // await page.waitForNetworkIdle(); + // await waitForRAF(page); + + // const frameImage = await page!.screenshot(); + // await waitForRAF(page); + // expect(frameImage).toMatchImageSnapshot({ + // failureThreshold: 0.05, + // failureThresholdType: 'percent', + // }); + + // // TODO: check to see if video is same as basic replay + // }); + + // it('should play from the start', async () => { + // await page.evaluate(`let events = ${JSON.stringify(videoPlaybackEvents)}`); + // await page.evaluate(` + // const { Replayer } = rrweb; + // window.replayer = new Replayer(events); + // window.replayer.play(); + // `); + // await waitForRAF(page); + // await page.waitForNetworkIdle(); + // await waitForRAF(page); + + // const isPlaying = await page.evaluate(` + // !document.querySelector('iframe').contentDocument.querySelector('video').paused && + // document.querySelector('iframe').contentDocument.querySelector('video').currentTime !== 0 && + // !document.querySelector('iframe').contentDocument.querySelector('video').ended; + // `); + // expect(isPlaying).toBe(true); + // }); + + // it('should play from the start without media events', async () => { + // await page.evaluate( + // `let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}`, + // ); + // await page.evaluate(` + // const { Replayer } = rrweb; + // window.replayer = new Replayer(events); + // window.replayer.play(); + // `); + // await waitForRAF(page); + // await page.waitForNetworkIdle(); + // await waitForRAF(page); + + // const isPlaying = await page.evaluate(` + // !document.querySelector('iframe').contentDocument.querySelector('video').paused && + // document.querySelector('iframe').contentDocument.querySelector('video').currentTime !== 0 && + // !document.querySelector('iframe').contentDocument.querySelector('video').ended; + // `); + // expect(isPlaying).toBe(true); + // }); + + // it('should report the correct time for looping videos that have passed their total time', async () => { + // await page.evaluate( + // `let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}`, + // ); + // await page.evaluate(` + // const { Replayer } = rrweb; + // window.replayer = new Replayer(events); + // `); + // await waitForRAF(page); + // await page.waitForNetworkIdle(); + // await waitForRAF(page); + // await page.evaluate(` + // window.replayer.pause(25000); // 5 seconds after the video started a new loop + // `); + // await waitForRAF(page); + + // const time = await page.evaluate(` + // document.querySelector('iframe').contentDocument.querySelector('video').currentTime; + // `); + // expect(time).toBeCloseTo(5, 0); + // }); + + // it('should set the correct time on loading videos', async () => { + // await page.evaluate( + // `let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}`, + // ); + // await page.evaluate(` + // const { Replayer } = rrweb; + // window.replayer = new Replayer(events); + // window.replayer.pause(25000); // 5 seconds after the video started a new loop + // `); + // await waitForRAF(page); + // await page.waitForNetworkIdle(); + // await waitForRAF(page); + + // const time = await page.evaluate(` + // document.querySelector('iframe').contentDocument.querySelector('video').currentTime; + // `); + // expect(time).toBeCloseTo(5, 0); + // }); + + // it('should set the correct playbackRate on faster playback', async () => { + // page.on('console', (msg) => { + // console.log(msg.text()); + // }); + // await page.evaluate( + // `let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}`, + // ); + // await page.evaluate(` + // const { Replayer } = rrweb; + // window.replayer = new Replayer(events, { + // speed: 8, + // }); + // window.replayer.play(); + // `); + // await waitForRAF(page); + // await page.waitForNetworkIdle(); + // await waitForRAF(page); + + // const time = await page.evaluate(` + // document.querySelector('iframe').contentDocument.querySelector('video').playbackRate; + // `); + // expect(time).toBe(8); + // }); +}); From c1d850fd43fd88adab07a267ac49b2c2e9b50f81 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 10 Jun 2024 13:37:38 +0200 Subject: [PATCH 02/29] chore: its important to run `yarn build:all` before running `yarn dev` --- README.md | 2 +- turbo.json | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 17e6b5591a..e73c2a8768 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Since we want the record and replay sides to share a strongly typed data structu 1. Fork this repository. 2. Run `yarn install` in the root to install required dependencies for all sub-packages (note: `npm install` is _not_ recommended). -3. Run `yarn dev` in the root to get auto-building for all the sub-packages whenever you modify anything. +3. Run `yarn build:all` to build all packages and get a stable base, then `yarn dev` in the root to get auto-building for all the sub-packages whenever you modify anything. 4. Navigate to one of the sub-packages (in the `packages` folder) where you'd like to make a change. 5. Patch the code and run `yarn test` to run the tests, make sure they pass before you commit anything. Add test cases in order to avoid future regression. 6. If tests are failing, but the change in output is desirable, run `yarn test:update` and carefully commit the changes in test output. diff --git a/turbo.json b/turbo.json index 55987d162f..f1e05b29ce 100644 --- a/turbo.json +++ b/turbo.json @@ -8,6 +8,7 @@ "vite.config.defaults.ts", "tsconfig.json" ], + "globalPassThroughEnv": ["CLEAR_DIST_DIR", "PUPPETEER_HEADLESS"], "tasks": { "prepublish": { "dependsOn": ["^prepublish"], @@ -21,22 +22,18 @@ ] }, "test": { - "dependsOn": ["^prepublish"], - "passThroughEnv": ["PUPPETEER_HEADLESS"] + "dependsOn": ["^prepublish"] }, "test:watch": { - "persistent": true, - "passThroughEnv": ["PUPPETEER_HEADLESS"] + "persistent": true }, "test:update": { - "dependsOn": ["^prepublish"], - "passThroughEnv": ["PUPPETEER_HEADLESS"] + "dependsOn": ["^prepublish"] }, "dev": { - // "dependsOn": ["^prepublish"], + // "dependsOn": ["prepublish", "^prepublish"], "persistent": true, - "cache": false, - "passThroughEnv": ["CLEAR_DIST_DIR"] + "cache": false }, "lint": {}, "check-types": {} From b657233eac3dbb0e161a9fe98de911ac34e9d052 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 10 Jun 2024 17:51:18 +0200 Subject: [PATCH 03/29] feat: trigger showModal from rrdom and rrweb --- packages/rrdom/src/diff.ts | 38 ++- packages/rrdom/src/document.ts | 22 ++ packages/rrdom/src/index.ts | 6 + packages/rrdom/test/diff/dialog.test.ts | 112 ++++++++ packages/rrweb/src/replay/dialog/index.ts | 11 + packages/rrweb/src/replay/index.ts | 8 + packages/rrweb/test/events/dialog-playback.ts | 38 ++- ...log-closed-dialogs-show-nothing-1-snap.png | Bin 0 -> 10592 bytes ...hen-open-attribute-gets-removed-1-snap.png | Bin 0 -> 10592 bytes ...uld-open-dialog-with-show-modal-1-snap.png | Bin 0 -> 12690 bytes ...-show-modal-without-virtual-dom-1-snap.png | Bin 0 -> 12690 bytes ...-when-open-attribute-gets-added-1-snap.png | Bin 0 -> 12445 bytes packages/rrweb/test/replay/dialog.test.ts | 260 ++++++------------ 13 files changed, 315 insertions(+), 180 deletions(-) create mode 100644 packages/rrdom/test/diff/dialog.test.ts create mode 100644 packages/rrweb/src/replay/dialog/index.ts create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-closed-dialogs-show-nothing-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-without-virtual-dom-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-show-the-dialog-when-open-attribute-gets-added-1-snap.png diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index d5093b38fb..b510bb7f4b 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -21,6 +21,7 @@ import type { } from './document'; import type { RRCanvasElement, + RRDialogElement, RRElement, RRIFrameElement, RRMediaElement, @@ -285,6 +286,24 @@ function diffAfterUpdatingChildren( ); break; } + case 'DIALOG': { + const dialog = oldElement as HTMLDialogElement; + const rrDialog = newRRElement as unknown as RRDialogElement; + const wasOpen = dialog.open; + const wasModal = dialog.matches('dialog:modal'); + const isOpen = rrDialog.open; + const { isModal } = rrDialog; + + const modeChanged = (wasModal && !isModal) || (!wasModal && isModal); + + if (wasOpen && modeChanged) dialog.close(); + if ((wasOpen && modeChanged) || (isOpen && !wasOpen)) { + if (isModal) dialog.showModal(); + else dialog.show(); + } + + break; + } } break; } @@ -330,12 +349,25 @@ function diffProps( } }; } else if (newTree.tagName === 'IFRAME' && name === 'srcdoc') continue; - else oldTree.setAttribute(name, newValue); + else if ( + newTree.tagName === 'DIALOG' && + (name === 'rr_open' || name === 'open') + ) { + const rrDialog = newTree as RRDialogElement; + const isModal = newAttributes.rr_open === 'modal'; + const isOpen = isModal || newAttributes.open === ''; + if (isModal) rrDialog.showModal(); + else if (isOpen) rrDialog.show(); + else rrDialog.close(); + continue; + } else oldTree.setAttribute(name, newValue); } - for (const { name } of Array.from(oldAttributes)) + for (const { name } of Array.from(oldAttributes)) { + if (newTree.tagName === 'DIALOG' && (name === 'rr_open' || name === 'open')) + continue; // attributes are handled in diffAfterUpdatingChildren for Dialog elements if (!(name in newAttributes)) oldTree.removeAttribute(name); - + } newTree.scrollLeft && (oldTree.scrollLeft = newTree.scrollLeft); newTree.scrollTop && (oldTree.scrollTop = newTree.scrollTop); } diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index 70450123f1..9200a36567 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -547,6 +547,28 @@ export class BaseRRMediaElement extends BaseRRElement { } } +export class BaseRRDialogElement extends BaseRRElement { + public readonly tagName = 'DIALOG' as const; + public readonly nodeName = 'DIALOG' as const; + public open: boolean = false; + private _isModal: boolean = false; + get isModal() { + return this._isModal; + } + public close() { + this.open = false; + this._isModal = false; + } + public show() { + this.open = true; + this._isModal = false; + } + public showModal() { + this.open = true; + this._isModal = true; + } +} + export class BaseRRText extends BaseRRNode implements IRRText { public readonly nodeType: number = NodeType.TEXT_NODE; public readonly nodeName = '#text' as const; diff --git a/packages/rrdom/src/index.ts b/packages/rrdom/src/index.ts index 65ae197de8..577811766b 100644 --- a/packages/rrdom/src/index.ts +++ b/packages/rrdom/src/index.ts @@ -31,6 +31,7 @@ import { type IRRDocumentType, type IRRText, type IRRComment, + BaseRRDialogElement, } from './document'; export class RRDocument extends BaseRRDocument { @@ -104,6 +105,9 @@ export class RRDocument extends BaseRRDocument { case 'STYLE': element = new RRStyleElement(upperTagName); break; + case 'DIALOG': + element = new RRDialogElement(upperTagName); + break; default: element = new RRElement(upperTagName); break; @@ -151,6 +155,8 @@ export class RRElement extends BaseRRElement { export class RRMediaElement extends BaseRRMediaElement {} +export class RRDialogElement extends BaseRRDialogElement {} + export class RRCanvasElement extends RRElement implements IRRElement { public rr_dataURL: string | null = null; public canvasMutations: { diff --git a/packages/rrdom/test/diff/dialog.test.ts b/packages/rrdom/test/diff/dialog.test.ts new file mode 100644 index 0000000000..cf2df3c910 --- /dev/null +++ b/packages/rrdom/test/diff/dialog.test.ts @@ -0,0 +1,112 @@ +/** + * @vitest-environment happy-dom + */ +import { vi, MockInstance } from 'vitest'; +import { + NodeType as RRNodeType, + createMirror, + Mirror as NodeMirror, + serializedNodeWithId, +} from 'rrweb-snapshot'; +import { RRDocument } from '../../src'; +import { diff, ReplayerHandler } from '../../src/diff'; + +describe('diff algorithm for rrdom', () => { + let mirror: NodeMirror; + let replayer: ReplayerHandler; + let warn: MockInstance; + let elementSn: serializedNodeWithId; + let elementSn2: serializedNodeWithId; + + beforeEach(() => { + mirror = createMirror(); + replayer = { + mirror, + applyCanvas: () => {}, + applyInput: () => {}, + applyScroll: () => {}, + applyStyleSheetMutation: () => {}, + afterAppend: () => {}, + }; + document.write(''); + // Mock the original console.warn function to make the test fail once console.warn is called. + warn = vi.spyOn(console, 'warn'); + + elementSn = { + type: RRNodeType.Element, + tagName: 'DIALOG', + attributes: {}, + childNodes: [], + id: 1, + }; + + elementSn2 = { + ...elementSn, + attributes: {}, + }; + }); + + afterEach(() => { + // Check that warn was not called (fail on warning) + expect(warn).not.toBeCalled(); + vi.resetAllMocks(); + }); + describe('diff dialog elements', () => { + vi.setConfig({ testTimeout: 60_000 }); + + it('should trigger `showModal` on rr_open:modal attributes', () => { + const tagName = 'DIALOG'; + const node = document.createElement(tagName) as HTMLDialogElement; + vi.spyOn(node, 'matches').mockReturnValue(false); // matches is used to check if the dialog was opened with showModal + const showModalFn = vi.spyOn(node, 'showModal'); + + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.attributes = { rr_open: 'modal' }; + + mirror.add(node, elementSn); + rrDocument.mirror.add(rrNode, elementSn); + diff(node, rrNode, replayer); + + expect(showModalFn).toBeCalled(); + }); + + it('should trigger `close` on rr_open removed', () => { + const tagName = 'DIALOG'; + const node = document.createElement(tagName) as HTMLDialogElement; + node.showModal(); + console.log('node', { x: node.getAttribute('open') }); + vi.spyOn(node, 'matches').mockReturnValue(true); // matches is used to check if the dialog was opened with showModal + const closeFn = vi.spyOn(node, 'close'); + + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.attributes = {}; + + mirror.add(node, elementSn); + rrDocument.mirror.add(rrNode, elementSn); + diff(node, rrNode, replayer); + + expect(closeFn).toBeCalled(); + }); + + it('should not trigger `close` on rr_open is kept', () => { + const tagName = 'DIALOG'; + const node = document.createElement(tagName) as HTMLDialogElement; + node.setAttribute('rr_open', 'modal'); + node.setAttribute('open', ''); + const closeFn = vi.spyOn(node, 'close'); + + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.attributes = { rr_open: 'modal' }; + + mirror.add(node, elementSn); + rrDocument.mirror.add(rrNode, elementSn); + diff(node, rrNode, replayer); + + expect(closeFn).not.toBeCalled(); + expect(node.open).toBe(true); + }); + }); +}); diff --git a/packages/rrweb/src/replay/dialog/index.ts b/packages/rrweb/src/replay/dialog/index.ts new file mode 100644 index 0000000000..8a78e39511 --- /dev/null +++ b/packages/rrweb/src/replay/dialog/index.ts @@ -0,0 +1,11 @@ +import { RRNode } from 'rrdom'; + +export function triggerShowModalForModals( + node: HTMLDialogElement | Node | RRNode, +) { + if (node.nodeName !== 'DIALOG' || node instanceof RRNode) return; + const dialog = node as HTMLDialogElement; + console.log('dialog', dialog.getAttribute('rr_open')); + if (dialog.getAttribute('rr_open') !== 'modal') return; + dialog.showModal(); +} diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index a86f4aac81..2b508eccaf 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1791,6 +1791,14 @@ export class Replayer { value, ); } + + if ( + attributeName === 'rr_open' && + target.nodeName === 'DIALOG' && + value === 'modal' + ) { + triggerShowModalForModals(target); + } } catch (error) { this.warn( 'An error occurred may due to the checkout feature.', diff --git a/packages/rrweb/test/events/dialog-playback.ts b/packages/rrweb/test/events/dialog-playback.ts index 0d2bb53768..a27153113b 100644 --- a/packages/rrweb/test/events/dialog-playback.ts +++ b/packages/rrweb/test/events/dialog-playback.ts @@ -111,21 +111,57 @@ const events: eventWithTime[] = [ }, timestamp: 1900000136, }, + // open dialog with .show() { type: 3, data: { source: IncrementalSource.Mutation, adds: [], removes: [], + texts: [], attributes: [ { id: 27, - attributes: { rr_open: 'modal' }, + attributes: { open: '' }, }, ], }, timestamp: 1900001500, }, + // close dialog with .close() + { + type: 3, + data: { + source: IncrementalSource.Mutation, + adds: [], + removes: [], + texts: [], + attributes: [ + { + id: 27, + attributes: { open: null }, + }, + ], + }, + timestamp: 1900002000, + }, + // open dialog with .showModal() + { + type: 3, + data: { + source: IncrementalSource.Mutation, + adds: [], + removes: [], + texts: [], + attributes: [ + { + id: 27, + attributes: { rr_open: 'modal' }, + }, + ], + }, + timestamp: 1900002500, + }, ]; export default events; diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-closed-dialogs-show-nothing-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-closed-dialogs-show-nothing-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..9fb34401f4292e123559cdb06cd35f8842b1c146 GIT binary patch literal 10592 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 WNin}nDi?M_AL92B_U7*)ef=|oQDxTv literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/replay/dialog.test.ts b/packages/rrweb/test/replay/dialog.test.ts index acc2e972aa..5f38a36fe1 100644 --- a/packages/rrweb/test/replay/dialog.test.ts +++ b/packages/rrweb/test/replay/dialog.test.ts @@ -14,12 +14,18 @@ import { waitForRAF, } from '../utils'; -// expect.extend({ toMatchImageSnapshot }); +expect.extend({ toMatchImageSnapshot }); // TODO: test the following: // == on record == -// - dialog open -// - dialog close +// - dialog open (standard) full snapshot +// - dialog open (standard) incremental (virtual dom) +// - dialog open (standard) incremental (non virtual dom) +// - dialog open (showModal) full snapshot +// √ dialog open (showModal) incremental (virtual dom) +// √ dialog open (showModal) incremental (non virtual dom) +// √ dialog close (rrdom) +// - dialog close (non virtual dom) // - dialog open and close (switching from modal to non modal and vise versa) // - multiple dialogs open, recording order // == on playback == @@ -58,6 +64,11 @@ describe('dialog', () => { beforeEach(async () => { page = await browser.newPage(); + page.on('console', (msg) => { + for (let i = 0; i < msg.args().length; ++i) { + console.log(`${i}: ${msg.args()[i]}`); + } + }); await fakeGoto(page, `${serverURL}/html/dialog.html`); await page.evaluate(code); @@ -65,187 +76,84 @@ describe('dialog', () => { await hideMouseAnimation(page); }); - it('will seek to the correct moment', async () => { + it('closed dialogs show nothing', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('show show the dialog when open attribute gets added', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events); + window.replayer.pause(1500); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should close dialog again when open attribute gets removed', async () => { await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); await page.evaluate(` const { Replayer } = rrweb; window.replayer = new Replayer(events); + window.replayer.pause(2000); `); await waitForRAF(page); - // await page.waitForTimeout(50_000); + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should open dialog with showModal', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events); + window.replayer.pause(2500); + `); + await waitForRAF(page); - // const frameImage = await page!.screenshot(); - // expect(frameImage).toMatchImageSnapshot({ - // failureThreshold: 0.05, - // failureThresholdType: 'percent', - // }); + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); }); - // it('will seek to the correct moment without media interaction events', async () => { - // await page.evaluate(` - // let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}; - // const { Replayer } = rrweb; - // window.replayer = new Replayer(events); - // window.replayer.pause(6500); - // `); - - // await page.waitForNetworkIdle(); - // await waitForRAF(page); - - // const frameImage = await page!.screenshot(); - // await waitForRAF(page); - // expect(frameImage).toMatchImageSnapshot({ - // failureThreshold: 0.05, - // failureThresholdType: 'percent', - // }); - // }); - - // it("will be paused when the player wasn't started yet", async () => { - // await page.evaluate(` - // let events = ${JSON.stringify(videoPlaybackEvents)}; - // const { Replayer } = rrweb; - // window.replayer = new Replayer(events); - // `); - // await page.waitForNetworkIdle(); - // await waitForRAF(page); - - // const frameImage = await page!.screenshot(); - - // await waitForRAF(page); - // expect(frameImage).toMatchImageSnapshot({ - // failureThreshold: 0.05, - // failureThresholdType: 'percent', - // }); - // }); - - // it('will play from the correct moment', async () => { - // await page.evaluate(`let events = ${JSON.stringify(videoPlaybackEvents)}`); - // await page.evaluate(` - // const { Replayer } = rrweb; - // window.replayer = new Replayer(events, { - // UNSAFE_replayCanvas: true, - // }); - // `); - // await waitForRAF(page); - // await page.evaluate(` - // window.replayer.play(6500); - // `); - // await page.waitForNetworkIdle(); - // await waitForRAF(page); - - // const frameImage = await page!.screenshot(); - // await waitForRAF(page); - // expect(frameImage).toMatchImageSnapshot({ - // failureThreshold: 0.05, - // failureThresholdType: 'percent', - // }); - - // // TODO: check to see if video is same as basic replay - // }); - - // it('should play from the start', async () => { - // await page.evaluate(`let events = ${JSON.stringify(videoPlaybackEvents)}`); - // await page.evaluate(` - // const { Replayer } = rrweb; - // window.replayer = new Replayer(events); - // window.replayer.play(); - // `); - // await waitForRAF(page); - // await page.waitForNetworkIdle(); - // await waitForRAF(page); - - // const isPlaying = await page.evaluate(` - // !document.querySelector('iframe').contentDocument.querySelector('video').paused && - // document.querySelector('iframe').contentDocument.querySelector('video').currentTime !== 0 && - // !document.querySelector('iframe').contentDocument.querySelector('video').ended; - // `); - // expect(isPlaying).toBe(true); - // }); - - // it('should play from the start without media events', async () => { - // await page.evaluate( - // `let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}`, - // ); - // await page.evaluate(` - // const { Replayer } = rrweb; - // window.replayer = new Replayer(events); - // window.replayer.play(); - // `); - // await waitForRAF(page); - // await page.waitForNetworkIdle(); - // await waitForRAF(page); - - // const isPlaying = await page.evaluate(` - // !document.querySelector('iframe').contentDocument.querySelector('video').paused && - // document.querySelector('iframe').contentDocument.querySelector('video').currentTime !== 0 && - // !document.querySelector('iframe').contentDocument.querySelector('video').ended; - // `); - // expect(isPlaying).toBe(true); - // }); - - // it('should report the correct time for looping videos that have passed their total time', async () => { - // await page.evaluate( - // `let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}`, - // ); - // await page.evaluate(` - // const { Replayer } = rrweb; - // window.replayer = new Replayer(events); - // `); - // await waitForRAF(page); - // await page.waitForNetworkIdle(); - // await waitForRAF(page); - // await page.evaluate(` - // window.replayer.pause(25000); // 5 seconds after the video started a new loop - // `); - // await waitForRAF(page); - - // const time = await page.evaluate(` - // document.querySelector('iframe').contentDocument.querySelector('video').currentTime; - // `); - // expect(time).toBeCloseTo(5, 0); - // }); - - // it('should set the correct time on loading videos', async () => { - // await page.evaluate( - // `let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}`, - // ); - // await page.evaluate(` - // const { Replayer } = rrweb; - // window.replayer = new Replayer(events); - // window.replayer.pause(25000); // 5 seconds after the video started a new loop - // `); - // await waitForRAF(page); - // await page.waitForNetworkIdle(); - // await waitForRAF(page); - - // const time = await page.evaluate(` - // document.querySelector('iframe').contentDocument.querySelector('video').currentTime; - // `); - // expect(time).toBeCloseTo(5, 0); - // }); - - // it('should set the correct playbackRate on faster playback', async () => { - // page.on('console', (msg) => { - // console.log(msg.text()); - // }); - // await page.evaluate( - // `let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}`, - // ); - // await page.evaluate(` - // const { Replayer } = rrweb; - // window.replayer = new Replayer(events, { - // speed: 8, - // }); - // window.replayer.play(); - // `); - // await waitForRAF(page); - // await page.waitForNetworkIdle(); - // await waitForRAF(page); - - // const time = await page.evaluate(` - // document.querySelector('iframe').contentDocument.querySelector('video').playbackRate; - // `); - // expect(time).toBe(8); - // }); + it('should open dialog with showModal (without virtual dom)', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events, { useVirtualDom: false }); + window.replayer.pause(2500); + `); + await waitForRAF(page); + + // await page.waitForTimeout(30000); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); }); From 337b4c41981db0df666f6e1c2f0b06f7c96269b3 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 11 Jun 2024 14:56:25 +0200 Subject: [PATCH 04/29] feat: Add support for replaying modal and non modal dialog elements --- packages/rrweb-snapshot/src/rebuild.ts | 2 + packages/rrweb/src/replay/dialog/index.ts | 30 +- packages/rrweb/src/replay/index.ts | 17 +- packages/rrweb/test/events/dialog-playback.ts | 303 +++++++++++++++++- ...w-modal-in-incremental-snapshot-1-snap.png | Bin 0 -> 12721 bytes ...al-snapshot-without-virtual-dom-1-snap.png | Bin 0 -> 12721 bytes ...ets-removed-without-virtual-dom-1-snap.png | Bin 0 -> 10592 bytes ...alog-with-show-in-full-snapshot-1-snap.png | Bin 0 -> 12445 bytes ...ll-snapshot-without-virtual-dom-1-snap.png | Bin 0 -> 12445 bytes ...ith-show-modal-in-full-snapshot-1-snap.png | Bin 0 -> 12690 bytes ...ll-snapshot-without-virtual-dom-1-snap.png | Bin 0 -> 12690 bytes ...tch-between-show-and-show-modal-1-snap.png | Bin 0 -> 12690 bytes ...-show-modal-without-virtual-dom-1-snap.png | Bin 0 -> 12690 bytes ...tch-between-show-modal-and-show-1-snap.png | Bin 0 -> 12445 bytes ...al-and-show-without-virtual-dom-1-snap.png | Bin 0 -> 12445 bytes ...ttribute-gets-added-non-virtual-1-snap.png | Bin 0 -> 12445 bytes packages/rrweb/test/replay/dialog.test.ts | 237 +++++++++++++- 17 files changed, 563 insertions(+), 26 deletions(-) create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-without-virtual-dom-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed-without-virtual-dom-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot-without-virtual-dom-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot-without-virtual-dom-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-and-show-modal-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-and-show-modal-without-virtual-dom-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show-without-virtual-dom-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-show-the-dialog-when-open-attribute-gets-added-non-virtual-1-snap.png diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index c3300a59f1..11346ff2c8 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -353,6 +353,8 @@ function buildNode( (node as HTMLMediaElement).loop = value; } else if (name === 'rr_mediaVolume' && typeof value === 'number') { (node as HTMLMediaElement).volume = value; + } else if (name === 'rr_open') { + (node as HTMLDialogElement).setAttribute('rr_open', value as string); // keep this attribute for rrweb to trigger showModal } } diff --git a/packages/rrweb/src/replay/dialog/index.ts b/packages/rrweb/src/replay/dialog/index.ts index 8a78e39511..6f47b991ea 100644 --- a/packages/rrweb/src/replay/dialog/index.ts +++ b/packages/rrweb/src/replay/dialog/index.ts @@ -1,3 +1,4 @@ +import type { attributeMutation } from '@rrweb/types'; import { RRNode } from 'rrdom'; export function triggerShowModalForModals( @@ -5,7 +6,34 @@ export function triggerShowModalForModals( ) { if (node.nodeName !== 'DIALOG' || node instanceof RRNode) return; const dialog = node as HTMLDialogElement; - console.log('dialog', dialog.getAttribute('rr_open')); if (dialog.getAttribute('rr_open') !== 'modal') return; + + // complain if dialog is not attached to the dom + if (!dialog.isConnected) { + console.warn('dialog is not attached to the dom', dialog); + return; + } + dialog.showModal(); } + +export function triggerCloseForModals( + node: HTMLDialogElement | Node | RRNode, + attributeMuation: attributeMutation, +) { + if (node.nodeName !== 'DIALOG' || node instanceof RRNode) return; + const dialog = node as HTMLDialogElement; + + // complain if dialog is not attached to the dom + if (!dialog.isConnected) { + console.warn('dialog is not attached to the dom', dialog); + return; + } + + if (attributeMuation.attributes.rr_open === null) { + dialog.close(); + } + if (attributeMuation.attributes.open === '') { + dialog.show(); + } +} diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 2b508eccaf..b285907898 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -86,6 +86,7 @@ import './styles/style.css'; import canvasMutation from './canvas'; import { deserializeArg } from './canvas/deserialize-args'; import { MediaManager } from './media'; +import { triggerShowModalForModals, triggerCloseForModals } from './dialog'; const SKIP_TIME_INTERVAL = 5 * 1000; @@ -245,6 +246,7 @@ export class Replayer { this.applyStyleDeclaration(data, styleSheet); }, afterAppend: (node: Node, id: number) => { + triggerShowModalForModals(node); for (const plugin of this.config.plugins || []) { if (plugin.onBuild) plugin.onBuild(node, { id, replayer: this }); } @@ -801,9 +803,12 @@ export class Replayer { ); } this.legacy_missingNodeRetryMap = {}; - const collected: AppendedIframe[] = []; + const collectedIframes: AppendedIframe[] = []; + const collectedDialogs = new Set(); const afterAppend = (builtNode: Node, id: number) => { - this.collectIframeAndAttachDocument(collected, builtNode); + if (builtNode.nodeName === 'DIALOG') + collectedDialogs.add(builtNode as HTMLDialogElement); + this.collectIframeAndAttachDocument(collectedIframes, builtNode); if (this.mediaManager.isSupportedMediaElement(builtNode)) { const { events } = this.service.state.context; this.mediaManager.addMediaElements( @@ -840,7 +845,7 @@ export class Replayer { }); afterAppend(this.iframe.contentDocument, event.data.node.id); - for (const { mutationInQueue, builtNode } of collected) { + for (const { mutationInQueue, builtNode } of collectedIframes) { this.attachDocumentToIframe(mutationInQueue, builtNode); this.newDocumentQueue = this.newDocumentQueue.filter( (m) => m !== mutationInQueue, @@ -848,6 +853,7 @@ export class Replayer { } const { documentElement, head } = this.iframe.contentDocument; this.insertStyleRules(documentElement, head); + collectedDialogs.forEach((d) => triggerShowModalForModals(d)); if (!this.service.state.matches('playing')) { this.iframe.contentDocument .getElementsByTagName('html')[0] @@ -912,6 +918,7 @@ export class Replayer { const collected: AppendedIframe[] = []; const afterAppend = (builtNode: Node, id: number) => { + triggerShowModalForModals(builtNode); this.collectIframeAndAttachDocument(collected, builtNode); const sn = (mirror as TMirror).getMeta(builtNode as unknown as TNode); if ( @@ -1532,6 +1539,7 @@ export class Replayer { const afterAppend = (node: Node | RRNode, id: number) => { // Skip the plugin onBuild callback for virtual dom if (this.usingVirtualDom) return; + triggerShowModalForModals(node); for (const plugin of this.config.plugins || []) { if (plugin.onBuild) plugin.onBuild(node, { id, replayer: this }); } @@ -1736,6 +1744,9 @@ export class Replayer { const value = mutation.attributes[attributeName]; if (value === null) { (target as Element | RRElement).removeAttribute(attributeName); + if (attributeName === 'rr_open') { + triggerCloseForModals(target, mutation); + } } else if (typeof value === 'string') { try { // When building snapshot, some link styles haven't loaded. Then they are loaded, they will be inlined as incremental mutation change of attribute. We need to replace the old elements whose styles aren't inlined. diff --git a/packages/rrweb/test/events/dialog-playback.ts b/packages/rrweb/test/events/dialog-playback.ts index a27153113b..f2be2e88a2 100644 --- a/packages/rrweb/test/events/dialog-playback.ts +++ b/packages/rrweb/test/events/dialog-playback.ts @@ -1,8 +1,19 @@ import { eventWithTime, IncrementalSource } from '@rrweb/types'; +const startTime = 1900000000; +export const closedFullSnapshotTime = 132; +export const showIncrementalAttributeTime = 1500; +export const closeIncrementalAttributeTime = 2000; +export const showModalIncrementalAttributeTime = 2500; +export const switchBetweenShowModalAndShowIncrementalAttributeTime = 2600; +export const switchBetweenShowAndShowModalIncrementalAttributeTime = 2700; +export const showFullSnapshotTime = 3000; +export const showModalFullSnapshotTime = 3500; +export const showModalIncrementalAddTime = 4000; + const events: eventWithTime[] = [ - { type: 0, data: {}, timestamp: 1900000001 }, - { type: 1, data: {}, timestamp: 1900000132 }, + { type: 0, data: {}, timestamp: startTime + 1 }, + { type: 1, data: {}, timestamp: startTime + closedFullSnapshotTime }, { type: 4, data: { @@ -10,7 +21,7 @@ const events: eventWithTime[] = [ width: 1600, height: 900, }, - timestamp: 1900000132, + timestamp: startTime + closedFullSnapshotTime, }, { type: 2, @@ -81,7 +92,6 @@ const events: eventWithTime[] = [ type: 2, tagName: 'dialog', attributes: { - rr_open: 'modal', style: 'outline: blue solid 1px;', }, childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }], @@ -109,7 +119,7 @@ const events: eventWithTime[] = [ }, initialOffset: { left: 0, top: 0 }, }, - timestamp: 1900000136, + timestamp: startTime + closedFullSnapshotTime, }, // open dialog with .show() { @@ -126,7 +136,7 @@ const events: eventWithTime[] = [ }, ], }, - timestamp: 1900001500, + timestamp: startTime + showIncrementalAttributeTime, }, // close dialog with .close() { @@ -143,7 +153,7 @@ const events: eventWithTime[] = [ }, ], }, - timestamp: 1900002000, + timestamp: startTime + closeIncrementalAttributeTime, }, // open dialog with .showModal() { @@ -160,7 +170,284 @@ const events: eventWithTime[] = [ }, ], }, - timestamp: 1900002500, + timestamp: startTime + showModalIncrementalAttributeTime, + }, + // switch between .showModal() and .show() + { + type: 3, + data: { + source: IncrementalSource.Mutation, + adds: [], + removes: [], + texts: [], + attributes: [ + { + id: 27, + attributes: { + rr_open: null, + open: '', + class: 'switched-from-show-modal-to-show', + }, + }, + ], + }, + timestamp: + startTime + switchBetweenShowModalAndShowIncrementalAttributeTime, + }, + // switch between .show() and .showModal() + { + type: 3, + data: { + source: IncrementalSource.Mutation, + adds: [], + removes: [], + texts: [], + attributes: [ + { + id: 27, + attributes: { + open: null, + rr_open: 'modal', + class: 'switched-from-show-to-show-modal', + }, + }, + ], + }, + timestamp: + startTime + switchBetweenShowAndShowModalIncrementalAttributeTime, + }, + // open dialog with .show() + { + type: 2, + data: { + node: { + type: 0, + childNodes: [ + { type: 1, name: 'html', publicId: '', systemId: '', id: 2 }, + { + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 5 }, + { + type: 2, + tagName: 'meta', + attributes: { charset: 'UTF-8' }, + childNodes: [], + id: 6, + }, + { type: 3, textContent: '\n ', id: 7 }, + { + type: 2, + tagName: 'meta', + attributes: { + '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: '', id: 13 }], + id: 12, + }, + ], + id: 4, + }, + { type: 3, textContent: '\n ', id: 21 }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 23 }, + { + type: 2, + tagName: 'dialog', + attributes: { + open: '', + style: 'outline: blue solid 1px;', + }, + childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }], + id: 24, + }, + { type: 3, textContent: '\n ', id: 26 }, + { + type: 2, + tagName: 'dialog', + attributes: { + style: 'outline: red solid 1px;', + }, + childNodes: [{ type: 3, textContent: 'Dialog 2', id: 28 }], + id: 27, + }, + { type: 3, textContent: '\n ', id: 31 }, + ], + id: 22, + }, + ], + id: 3, + }, + ], + id: 1, + }, + initialOffset: { left: 0, top: 0 }, + }, + timestamp: startTime + showFullSnapshotTime, + }, + { + type: 2, + data: { + node: { + type: 0, + childNodes: [ + { type: 1, name: 'html', publicId: '', systemId: '', id: 2 }, + { + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 5 }, + { + type: 2, + tagName: 'meta', + attributes: { charset: 'UTF-8' }, + childNodes: [], + id: 6, + }, + { type: 3, textContent: '\n ', id: 7 }, + { + type: 2, + tagName: 'meta', + attributes: { + '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: '', id: 13 }], + id: 12, + }, + ], + id: 4, + }, + { type: 3, textContent: '\n ', id: 21 }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 23 }, + { + type: 2, + tagName: 'dialog', + attributes: { + rr_open: 'modal', + style: 'outline: blue solid 1px;', + }, + childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }], + id: 24, + }, + { type: 3, textContent: '\n ', id: 26 }, + { + type: 2, + tagName: 'dialog', + attributes: { + style: 'outline: red solid 1px;', + }, + childNodes: [{ type: 3, textContent: 'Dialog 2', id: 28 }], + id: 27, + }, + { type: 3, textContent: '\n ', id: 31 }, + ], + id: 22, + }, + ], + id: 3, + }, + ], + id: 1, + }, + initialOffset: { left: 0, top: 0 }, + }, + timestamp: startTime + showModalFullSnapshotTime, + }, + // add open dialog with .showModal() + { + type: 3, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 22, + previousId: 27, + nextId: 31, + node: { + type: 2, + tagName: 'dialog', + attributes: { + rr_open: 'modal', + style: 'outline: orange solid 1px;', + }, + childNodes: [], + id: 32, + }, + }, + { + parentId: 32, + previousId: null, + nextId: null, + node: { type: 3, textContent: 'Dialog 3', id: 33 }, + }, + ], + removes: [], + texts: [], + attributes: [], + }, + timestamp: startTime + showModalIncrementalAddTime, }, ]; diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..f328c34b5b1b95a917d80f69c2fa391285891f97 GIT binary patch literal 12721 zcmeHNc~nzZ8h?t4wor?m7L?5;N?Vatfw0AeifI+3OjQ&JElZ{XB1D$R5?m^61sTIG zflO;FZAF4mKp>D%HLOWaU07rnLQrBNgph=gr1vH>p6UEM|9E=dJvrxn@7??E`_|w6 z?sxOeVLxBvH#WS1Ac!&kfX}B0Vwi~_E8JGE0#BwNw(SF#Wu#Ai_aaq1i)RS35yAWH z`FjdYG@Rb?I9c63+t*kl_-)V4D7oq8I*cog(-g$*IubKrt=`1BIL(<}Ykd5+-M3CB zYwC}=H92niI*fJ7(6OTsx8e4&!J7BpTUQco{-7{c(DiAhfGbn&Yswgyf3T}>mYL?M zG2tmg=~8duXa${5td8t1>;$}k_P3xrr^l;(5wmY^f4)}#2w`__LJahD{@a<$^wh_z z+5hg|x*|+ZWv*N9pr>}PS!SlE{&f*C(o=``B2aA5-bbfn>lA??sP0flL8C$+3@D_`Dtr|8?EUI?*y*+jCd1j++QunI zf=PX+g>Hw^ChnU0si$CXg*)6f?&q>Bu6ts!zY#yo&1~DPTnT#iclTEt!uJafon(V6 zd$kFySlnkKfQHsf)1w4G}#R5BkZ4UZ;`RO-U1q*ipf0-a9ruTmlM(fCqvZyMZ_$?OUg#zI8cuS33L`$y zqMRy~J}7G4d7*71_%YH1+iO#1pE6~);<$Bvp{0XyEOh`yI#&vffM727G6QPVWPN7}MHGalI zEuNapoUl~R=2B>br=8z28BVL@`%ep(gHO_ai84wB*RVU~ zv)M!pK8i=f%AV8$l?0QxyU25SbQ;i1)M#sZqh@g?55pjHqpvjaG-{P|I49d^m6j4U z$Es2uvvfc6et7spE&VY|{UWYW+%z-ygN@e*_?no3nx&UnxU^upN;0$z?qagnL3?C| zFZQ>47uNFJw-dCBeR&q%z2wKgd`9j*G2$fa>AiKLHbKx89Do1a_14z>McGV6h*XHv zl1CIA*QnX2_k*g#_IS&F_P2~rmVm+1nxYn421PyNOwlYcRl^cq1y21eX0%lu5WOS# z≷|;!zgP+$E-?sCE8F8hNV3W@5%B-Vm;y4##0eJysmmQ+)MJyE_{xio4bcqv`TU zAhdF!fmP;}x^U1-THnyJgGh_t-^(D-6K8q_Et2#&jYiJ0PHcHG*g*XK`;SVBi#uW) zh6|0&T_*QgFCN~uZQG6Vpt_Tv71|TZPsFwC`lQH?3(P050Lt=De;$~N72Y^hnlM5Z zMoOo+%Eye1f=>Kk5_HywdC=<)&S3duKk8%yOC4+1I7ziVRz3E$+`d^Z)27pZAn`xE2K`;Hh`zE9}95J_02@L#rAUmZyq4Et7|?LRZ66U0a?V z!c&z}p>U;rN}D6m!e~`cV?tyUzq}0CdMcWrdNQHl0L>uRArK)qWS0p`oAGnhBIe{z z5<=>WIxKKzXrG@`&*#<|C~R;_{8Uc~>0UG=Lv}L{Ggu$n{jhxDS4>v!H9u-C-`k^e z{Car>>DiA(QD1zqIe9qoC^ce&lhdvawdt~CgAcLaFL4w>xdZ#e!_6f5aI#Rzr(^(@ z(jO5|-5I}>_VgWk$Bu>}s-bn7L&H!)w_~5j>9O4z%5H+y>=UJsj+2czgZN--)e^ob zOL3nmPGod*a?G6_9j^+#<&8EwB??8#$jeKvdH{2aT?0PM4zTo0agdztYo=m=cep)w z_DB}@4326RDhVual90#cm3dTzklN0#YDlVS?&of~-=; z&Uq@vE-0scac6&hi#K=p{(o+JcvDVi#cgZ~MA#t^62Q)nAR#bD1&AMULT%DZ)`;Dx?$$5uuzDG|qC&Nm{jB zm`7iV5Z{@&!jz;-x|!5)W%*8geE8nzb^t#l1P{MHj#m(ir<(nnp_Z zr)KBm2xuf;=etLPj0BX;(1DYY(%cG z5bsuCTpu&=G{F{#vxzl0UjSB6?Dne0nf%6?B!0OUcBZnHiqR_CaZUifvU4m|?C*T7 z$e85O^knG&M6a2kKt>#h{O50t4L2tf0Yz4fXGzbkP1{)N5_`?vWpNazVxKd>szkN^ zi}DRdtCXB9`<}`mMu*fcRfs28BS^|qTYx$JTM(nKpm(z;7Wj%%3;+*KJe|@NSWcDl zaoPX|L+S{Fh-6l`lhW`4G-yJ2(HGOqU#>1u*)lyR6?X}=&Y}LEHc zpdq7E!(RzQMER#K0Z=5s{{ojB++>-2!)wN4G_-di6PXkkvLhyMkK(}T4J>p@TE9!;-Y z@$nWwZd(D5;y`0xL}2Y3wBonfx+{Xbvm722gBG?IoKWc6!PQ}UB6E%IP!ZfXZ2HeX f@X^WsAD#JJu83GgI51ua6^#GL&!=i{*!e#Itmd7) literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-without-virtual-dom-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-without-virtual-dom-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..f328c34b5b1b95a917d80f69c2fa391285891f97 GIT binary patch literal 12721 zcmeHNc~nzZ8h?t4wor?m7L?5;N?Vatfw0AeifI+3OjQ&JElZ{XB1D$R5?m^61sTIG zflO;FZAF4mKp>D%HLOWaU07rnLQrBNgph=gr1vH>p6UEM|9E=dJvrxn@7??E`_|w6 z?sxOeVLxBvH#WS1Ac!&kfX}B0Vwi~_E8JGE0#BwNw(SF#Wu#Ai_aaq1i)RS35yAWH z`FjdYG@Rb?I9c63+t*kl_-)V4D7oq8I*cog(-g$*IubKrt=`1BIL(<}Ykd5+-M3CB zYwC}=H92niI*fJ7(6OTsx8e4&!J7BpTUQco{-7{c(DiAhfGbn&Yswgyf3T}>mYL?M zG2tmg=~8duXa${5td8t1>;$}k_P3xrr^l;(5wmY^f4)}#2w`__LJahD{@a<$^wh_z z+5hg|x*|+ZWv*N9pr>}PS!SlE{&f*C(o=``B2aA5-bbfn>lA??sP0flL8C$+3@D_`Dtr|8?EUI?*y*+jCd1j++QunI zf=PX+g>Hw^ChnU0si$CXg*)6f?&q>Bu6ts!zY#yo&1~DPTnT#iclTEt!uJafon(V6 zd$kFySlnkKfQHsf)1w4G}#R5BkZ4UZ;`RO-U1q*ipf0-a9ruTmlM(fCqvZyMZ_$?OUg#zI8cuS33L`$y zqMRy~J}7G4d7*71_%YH1+iO#1pE6~);<$Bvp{0XyEOh`yI#&vffM727G6QPVWPN7}MHGalI zEuNapoUl~R=2B>br=8z28BVL@`%ep(gHO_ai84wB*RVU~ zv)M!pK8i=f%AV8$l?0QxyU25SbQ;i1)M#sZqh@g?55pjHqpvjaG-{P|I49d^m6j4U z$Es2uvvfc6et7spE&VY|{UWYW+%z-ygN@e*_?no3nx&UnxU^upN;0$z?qagnL3?C| zFZQ>47uNFJw-dCBeR&q%z2wKgd`9j*G2$fa>AiKLHbKx89Do1a_14z>McGV6h*XHv zl1CIA*QnX2_k*g#_IS&F_P2~rmVm+1nxYn421PyNOwlYcRl^cq1y21eX0%lu5WOS# z≷|;!zgP+$E-?sCE8F8hNV3W@5%B-Vm;y4##0eJysmmQ+)MJyE_{xio4bcqv`TU zAhdF!fmP;}x^U1-THnyJgGh_t-^(D-6K8q_Et2#&jYiJ0PHcHG*g*XK`;SVBi#uW) zh6|0&T_*QgFCN~uZQG6Vpt_Tv71|TZPsFwC`lQH?3(P050Lt=De;$~N72Y^hnlM5Z zMoOo+%Eye1f=>Kk5_HywdC=<)&S3duKk8%yOC4+1I7ziVRz3E$+`d^Z)27pZAn`xE2K`;Hh`zE9}95J_02@L#rAUmZyq4Et7|?LRZ66U0a?V z!c&z}p>U;rN}D6m!e~`cV?tyUzq}0CdMcWrdNQHl0L>uRArK)qWS0p`oAGnhBIe{z z5<=>WIxKKzXrG@`&*#<|C~R;_{8Uc~>0UG=Lv}L{Ggu$n{jhxDS4>v!H9u-C-`k^e z{Car>>DiA(QD1zqIe9qoC^ce&lhdvawdt~CgAcLaFL4w>xdZ#e!_6f5aI#Rzr(^(@ z(jO5|-5I}>_VgWk$Bu>}s-bn7L&H!)w_~5j>9O4z%5H+y>=UJsj+2czgZN--)e^ob zOL3nmPGod*a?G6_9j^+#<&8EwB??8#$jeKvdH{2aT?0PM4zTo0agdztYo=m=cep)w z_DB}@4326RDhVual90#cm3dTzklN0#YDlVS?&of~-=; z&Uq@vE-0scac6&hi#K=p{(o+JcvDVi#cgZ~MA#t^62Q)nAR#bD1&AMULT%DZ)`;Dx?$$5uuzDG|qC&Nm{jB zm`7iV5Z{@&!jz;-x|!5)W%*8geE8nzb^t#l1P{MHj#m(ir<(nnp_Z zr)KBm2xuf;=etLPj0BX;(1DYY(%cG z5bsuCTpu&=G{F{#vxzl0UjSB6?Dne0nf%6?B!0OUcBZnHiqR_CaZUifvU4m|?C*T7 z$e85O^knG&M6a2kKt>#h{O50t4L2tf0Yz4fXGzbkP1{)N5_`?vWpNazVxKd>szkN^ zi}DRdtCXB9`<}`mMu*fcRfs28BS^|qTYx$JTM(nKpm(z;7Wj%%3;+*KJe|@NSWcDl zaoPX|L+S{Fh-6l`lhW`4G-yJ2(HGOqU#>1u*)lyR6?X}=&Y}LEHc zpdq7E!(RzQMER#K0Z=5s{{ojB++>-2!)wN4G_-di6PXkkvLhyMkK(}T4J>p@TE9!;-Y z@$nWwZd(D5;y`0xL}2Y3wBonfx+{Xbvm722gBG?IoKWc6!PQ}UB6E%IP!ZfXZ2HeX f@X^WsAD#JJu83GgI51ua6^#GL&!=i{*!e#Itmd7) literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed-without-virtual-dom-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed-without-virtual-dom-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..9fb34401f4292e123559cdb06cd35f8842b1c146 GIT binary patch literal 10592 zcmeHNJxjw-6g{b-wxyAXOB986aqi$y@T|9VC|KJ$zW#XbJF4QUo5$~?m8*syeA$D&`@0i;T-MTOZ_ZRu z)A^by8!fABDt8xSrV2b|3QY2tAVEMfK9C_943ta;1Dk_^fz5%osrx2B4}1iBog23c$HZdmv+h_F#1FLD;RX z0>ugW7hrU6$(zw$2?DfN8UxxZ+AGll?G^2n@PPK}zwJRT?3MRl>hVC}jW(J=$4}0F E0Mb(pg;wYkT6xtIG|vl zLLfY>F42NeLEevmRUo{AA_j~Ql<+8z1PBm9Leg`fGi%nG>5mzwGyb^e&po;4-m~}F z``h2%=iFR7KycAq`O!)QK{WB7?>&ehsxb(%++Ixu+!^kua|Rcsu!Ampko-3NpAcjd zg5SIQ@I}U0PgG;d%tFH?XRA1k|JVJw#m)r3&d;9=5<1i9EW&()T8Nv@zF4YN`@W=X zZwJTxA2RN)&BOVr#qGCvq++!$z)a_N>#k*->TpWlo&NDgr?G@#`GRprTd6p=LAgfZ z!yA>B_j1HiwkBIzk^krFD6v2jr~;c&@BOz1OI;*J>l;L4sefP(0(Ay$%VtD*sh7Ma zMro<{2esOz4q~#LgdjSn*Kc!EQK`ZLI6l^Ij1G$ymVuu}*P(Qyu1+r^2Hez!(qeAbZ_WPKQo$QY z%sMCqwq5E9+?<&*V#MgFa6rR2e* zym`;T7J68Fu_1Y)&Mj@{C%n0OyQ%3852{YT8D3D&>Y0GqbL)4j4cqNU8{|IT|%P#=8GURe4q?Q$AB5;qv6SJfcSq;b}$*PF4Y# zk^TmhxwkD02NA|l4@e{34P60HFj10UG4j2B$kVmnC1<^9cEO`$mPrcV1vme^9UI8W zZ@s$VSZbtb@Z?z6Bkw{#mJ)|N{oF;=Bc#|RnR&E5y`~-3;cj=5lkY3d6StnDSPwD6 z2&QhSiE-O1wv@gJb$wMpRNN{KW-iR$o=Z0-E{u33858GUr1WwkA)4b4Lo7~|sHm!j zjXcO11mYTt*x2+8$B02os>HSA91r`L?LnRI&=2oA4m7~8w>-vhwXEJ*P&L&=;T&aB zqD|e?k|m;{fSHlb^6?UG=(FE@`N?GxXQ@vp%Y<$d=qX?-kC4wb1<7tNSiC8>M zUE7pD*PnnJ&f^fN(Uh57;=n56+>{FIrX&QkBjf9sAU$v=D<=_@R zi8Z8L+{TnmT^6hY^S7TlU#1GKt9>n*vd~QO6pK6=M{EHmsN_Z8+UA>fb=3y{fu?V} z3s1Uw=KDxWM#o(Qw=qU>Gv zWQ1t2mF^f33_>nHUf0_H6j#K?WfcI%5X!DV*(=Ld>_iZ9OVFFL9r(f%4L6Kg0659O z;bs5@Z9NgZj1{Za`_;$mI!n*>RM>D!f?R25c{Lb(D2P=5o{9_JRTo2O{0BLiP50c> zr+5l^L{>4c3M0yh=yiD9n`RSe1)?r~qob{3_*kQusaPOOxx6-YGe)9&x~*nwDGuHT zYYTm%ZsrF}E14;vW&{>g+gkg#Gy+?8Yw@K}QysjI(%*SaCJ`l7O=|S08j`nL*35lL zz|6daGz9@^4I#+N#6%U6>b}nNt+d4#o_hYB#TP$p0+iPT)S_$Z+4AQ{3)m2SfH(ao z-#v5QJ(t3ioxI^Nn92yL*Y&RW>NH@*q#$m|zzm1T&iAdHlJXQ-yw#bP_w~bCcF^t4 z^cd&o&SlW|zKr0~fUi-NAhd@KjE1N_L}LnUv3uBusY!xrp$pIY@Rf z;_+5MEU_EIyLO8ed@~&V5WwqJE(r4sZ4aj%!0oMFk4_|2t&KBtPy4DpKnc>$CYYXM z6SHJ|Gl_&v&4gCFn?z^|V`Eahh|xAoX^+tA()93K11<1$U!vLK^YP7=OegDrwicXx zG9lgo^BJJP-l6r+p*H%dFf-{NtiP_864GC!R9$Sb*){s)*qWC{X^7g*bEo3gZuAPlUPx zSm49ho}#WmU4dpYQ7s|9K(?T4e^)1S@00t=2q6a3B4$yNUv^;<~c+c`6`=5^}(c{SfL3Hh{ WNin}nDi?M_AL92B_U7*)ef=|oQDxTv literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot-without-virtual-dom-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot-without-virtual-dom-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..a3faffd85acf2672b5411a7b05ffea0471ffe11e GIT binary patch literal 12445 zcmeHNYgAKL7QTq`P!N?Fg@T~vWebXeyb4KFRGI;TqYe}W6Dxv>pg;wYkT6xtIG|vl zLLfY>F42NeLEevmRUo{AA_j~Ql<+8z1PBm9Leg`fGi%nG>5mzwGyb^e&po;4-m~}F z``h2%=iFR7KycAq`O!)QK{WB7?>&ehsxb(%++Ixu+!^kua|Rcsu!Ampko-3NpAcjd zg5SIQ@I}U0PgG;d%tFH?XRA1k|JVJw#m)r3&d;9=5<1i9EW&()T8Nv@zF4YN`@W=X zZwJTxA2RN)&BOVr#qGCvq++!$z)a_N>#k*->TpWlo&NDgr?G@#`GRprTd6p=LAgfZ z!yA>B_j1HiwkBIzk^krFD6v2jr~;c&@BOz1OI;*J>l;L4sefP(0(Ay$%VtD*sh7Ma zMro<{2esOz4q~#LgdjSn*Kc!EQK`ZLI6l^Ij1G$ymVuu}*P(Qyu1+r^2Hez!(qeAbZ_WPKQo$QY z%sMCqwq5E9+?<&*V#MgFa6rR2e* zym`;T7J68Fu_1Y)&Mj@{C%n0OyQ%3852{YT8D3D&>Y0GqbL)4j4cqNU8{|IT|%P#=8GURe4q?Q$AB5;qv6SJfcSq;b}$*PF4Y# zk^TmhxwkD02NA|l4@e{34P60HFj10UG4j2B$kVmnC1<^9cEO`$mPrcV1vme^9UI8W zZ@s$VSZbtb@Z?z6Bkw{#mJ)|N{oF;=Bc#|RnR&E5y`~-3;cj=5lkY3d6StnDSPwD6 z2&QhSiE-O1wv@gJb$wMpRNN{KW-iR$o=Z0-E{u33858GUr1WwkA)4b4Lo7~|sHm!j zjXcO11mYTt*x2+8$B02os>HSA91r`L?LnRI&=2oA4m7~8w>-vhwXEJ*P&L&=;T&aB zqD|e?k|m;{fSHlb^6?UG=(FE@`N?GxXQ@vp%Y<$d=qX?-kC4wb1<7tNSiC8>M zUE7pD*PnnJ&f^fN(Uh57;=n56+>{FIrX&QkBjf9sAU$v=D<=_@R zi8Z8L+{TnmT^6hY^S7TlU#1GKt9>n*vd~QO6pK6=M{EHmsN_Z8+UA>fb=3y{fu?V} z3s1Uw=KDxWM#o(Qw=qU>Gv zWQ1t2mF^f33_>nHUf0_H6j#K?WfcI%5X!DV*(=Ld>_iZ9OVFFL9r(f%4L6Kg0659O z;bs5@Z9NgZj1{Za`_;$mI!n*>RM>D!f?R25c{Lb(D2P=5o{9_JRTo2O{0BLiP50c> zr+5l^L{>4c3M0yh=yiD9n`RSe1)?r~qob{3_*kQusaPOOxx6-YGe)9&x~*nwDGuHT zYYTm%ZsrF}E14;vW&{>g+gkg#Gy+?8Yw@K}QysjI(%*SaCJ`l7O=|S08j`nL*35lL zz|6daGz9@^4I#+N#6%U6>b}nNt+d4#o_hYB#TP$p0+iPT)S_$Z+4AQ{3)m2SfH(ao z-#v5QJ(t3ioxI^Nn92yL*Y&RW>NH@*q#$m|zzm1T&iAdHlJXQ-yw#bP_w~bCcF^t4 z^cd&o&SlW|zKr0~fUi-NAhd@KjE1N_L}LnUv3uBusY!xrp$pIY@Rf z;_+5MEU_EIyLO8ed@~&V5WwqJE(r4sZ4aj%!0oMFk4_|2t&KBtPy4DpKnc>$CYYXM z6SHJ|Gl_&v&4gCFn?z^|V`Eahh|xAoX^+tA()93K11<1$U!vLK^YP7=OegDrwicXx zG9lgo^BJJP-l6r+p*H%dFf-{NtiP_864GC!R9$Sb*){s)*qWC{X^7g*bEo3gZuAPlUPx zSm49ho}#WmU4dpYQ7s|9K(?T4e^)1S@00t=2q6a3B4$yNUv^;<~c+c`6`=5^}(c{SfL3Hh{ WNin}nDi?M_AL92B_U7*)ef=|oQDxTv literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..5fe6c04ee504091f90b95de22a0827aa7511188c GIT binary patch literal 12690 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 WNin}nDi?M_AL92B_U7*)ef=|oQDxTv literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show-without-virtual-dom-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show-without-virtual-dom-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..a3faffd85acf2672b5411a7b05ffea0471ffe11e GIT binary patch literal 12445 zcmeHNYgAKL7QTq`P!N?Fg@T~vWebXeyb4KFRGI;TqYe}W6Dxv>pg;wYkT6xtIG|vl zLLfY>F42NeLEevmRUo{AA_j~Ql<+8z1PBm9Leg`fGi%nG>5mzwGyb^e&po;4-m~}F z``h2%=iFR7KycAq`O!)QK{WB7?>&ehsxb(%++Ixu+!^kua|Rcsu!Ampko-3NpAcjd zg5SIQ@I}U0PgG;d%tFH?XRA1k|JVJw#m)r3&d;9=5<1i9EW&()T8Nv@zF4YN`@W=X zZwJTxA2RN)&BOVr#qGCvq++!$z)a_N>#k*->TpWlo&NDgr?G@#`GRprTd6p=LAgfZ z!yA>B_j1HiwkBIzk^krFD6v2jr~;c&@BOz1OI;*J>l;L4sefP(0(Ay$%VtD*sh7Ma zMro<{2esOz4q~#LgdjSn*Kc!EQK`ZLI6l^Ij1G$ymVuu}*P(Qyu1+r^2Hez!(qeAbZ_WPKQo$QY z%sMCqwq5E9+?<&*V#MgFa6rR2e* zym`;T7J68Fu_1Y)&Mj@{C%n0OyQ%3852{YT8D3D&>Y0GqbL)4j4cqNU8{|IT|%P#=8GURe4q?Q$AB5;qv6SJfcSq;b}$*PF4Y# zk^TmhxwkD02NA|l4@e{34P60HFj10UG4j2B$kVmnC1<^9cEO`$mPrcV1vme^9UI8W zZ@s$VSZbtb@Z?z6Bkw{#mJ)|N{oF;=Bc#|RnR&E5y`~-3;cj=5lkY3d6StnDSPwD6 z2&QhSiE-O1wv@gJb$wMpRNN{KW-iR$o=Z0-E{u33858GUr1WwkA)4b4Lo7~|sHm!j zjXcO11mYTt*x2+8$B02os>HSA91r`L?LnRI&=2oA4m7~8w>-vhwXEJ*P&L&=;T&aB zqD|e?k|m;{fSHlb^6?UG=(FE@`N?GxXQ@vp%Y<$d=qX?-kC4wb1<7tNSiC8>M zUE7pD*PnnJ&f^fN(Uh57;=n56+>{FIrX&QkBjf9sAU$v=D<=_@R zi8Z8L+{TnmT^6hY^S7TlU#1GKt9>n*vd~QO6pK6=M{EHmsN_Z8+UA>fb=3y{fu?V} z3s1Uw=KDxWM#o(Qw=qU>Gv zWQ1t2mF^f33_>nHUf0_H6j#K?WfcI%5X!DV*(=Ld>_iZ9OVFFL9r(f%4L6Kg0659O z;bs5@Z9NgZj1{Za`_;$mI!n*>RM>D!f?R25c{Lb(D2P=5o{9_JRTo2O{0BLiP50c> zr+5l^L{>4c3M0yh=yiD9n`RSe1)?r~qob{3_*kQusaPOOxx6-YGe)9&x~*nwDGuHT zYYTm%ZsrF}E14;vW&{>g+gkg#Gy+?8Yw@K}QysjI(%*SaCJ`l7O=|S08j`nL*35lL zz|6daGz9@^4I#+N#6%U6>b}nNt+d4#o_hYB#TP$p0+iPT)S_$Z+4AQ{3)m2SfH(ao z-#v5QJ(t3ioxI^Nn92yL*Y&RW>NH@*q#$m|zzm1T&iAdHlJXQ-yw#bP_w~bCcF^t4 z^cd&o&SlW|zKr0~fUi-NAhd@KjE1N_L}LnUv3uBusY!xrp$pIY@Rf z;_+5MEU_EIyLO8ed@~&V5WwqJE(r4sZ4aj%!0oMFk4_|2t&KBtPy4DpKnc>$CYYXM z6SHJ|Gl_&v&4gCFn?z^|V`Eahh|xAoX^+tA()93K11<1$U!vLK^YP7=OegDrwicXx zG9lgo^BJJP-l6r+p*H%dFf-{NtiP_864GC!R9$Sb*){s)*qWC{X^7g*bEo3gZuAPlUPx zSm49ho}#WmU4dpYQ7s|9K(?T4e^)1S@00t=2q6a3B4$yNUv^;<~c+c`6`=5^}(c{SfL3Hh{ WNin}nDi?M_AL92B_U7*)ef=|oQDxTv literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-show-the-dialog-when-open-attribute-gets-added-non-virtual-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-show-the-dialog-when-open-attribute-gets-added-non-virtual-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..a3faffd85acf2672b5411a7b05ffea0471ffe11e GIT binary patch literal 12445 zcmeHNYgAKL7QTq`P!N?Fg@T~vWebXeyb4KFRGI;TqYe}W6Dxv>pg;wYkT6xtIG|vl zLLfY>F42NeLEevmRUo{AA_j~Ql<+8z1PBm9Leg`fGi%nG>5mzwGyb^e&po;4-m~}F z``h2%=iFR7KycAq`O!)QK{WB7?>&ehsxb(%++Ixu+!^kua|Rcsu!Ampko-3NpAcjd zg5SIQ@I}U0PgG;d%tFH?XRA1k|JVJw#m)r3&d;9=5<1i9EW&()T8Nv@zF4YN`@W=X zZwJTxA2RN)&BOVr#qGCvq++!$z)a_N>#k*->TpWlo&NDgr?G@#`GRprTd6p=LAgfZ z!yA>B_j1HiwkBIzk^krFD6v2jr~;c&@BOz1OI;*J>l;L4sefP(0(Ay$%VtD*sh7Ma zMro<{2esOz4q~#LgdjSn*Kc!EQK`ZLI6l^Ij1G$ymVuu}*P(Qyu1+r^2Hez!(qeAbZ_WPKQo$QY z%sMCqwq5E9+?<&*V#MgFa6rR2e* zym`;T7J68Fu_1Y)&Mj@{C%n0OyQ%3852{YT8D3D&>Y0GqbL)4j4cqNU8{|IT|%P#=8GURe4q?Q$AB5;qv6SJfcSq;b}$*PF4Y# zk^TmhxwkD02NA|l4@e{34P60HFj10UG4j2B$kVmnC1<^9cEO`$mPrcV1vme^9UI8W zZ@s$VSZbtb@Z?z6Bkw{#mJ)|N{oF;=Bc#|RnR&E5y`~-3;cj=5lkY3d6StnDSPwD6 z2&QhSiE-O1wv@gJb$wMpRNN{KW-iR$o=Z0-E{u33858GUr1WwkA)4b4Lo7~|sHm!j zjXcO11mYTt*x2+8$B02os>HSA91r`L?LnRI&=2oA4m7~8w>-vhwXEJ*P&L&=;T&aB zqD|e?k|m;{fSHlb^6?UG=(FE@`N?GxXQ@vp%Y<$d=qX?-kC4wb1<7tNSiC8>M zUE7pD*PnnJ&f^fN(Uh57;=n56+>{FIrX&QkBjf9sAU$v=D<=_@R zi8Z8L+{TnmT^6hY^S7TlU#1GKt9>n*vd~QO6pK6=M{EHmsN_Z8+UA>fb=3y{fu?V} z3s1Uw=KDxWM#o(Qw=qU>Gv zWQ1t2mF^f33_>nHUf0_H6j#K?WfcI%5X!DV*(=Ld>_iZ9OVFFL9r(f%4L6Kg0659O z;bs5@Z9NgZj1{Za`_;$mI!n*>RM>D!f?R25c{Lb(D2P=5o{9_JRTo2O{0BLiP50c> zr+5l^L{>4c3M0yh=yiD9n`RSe1)?r~qob{3_*kQusaPOOxx6-YGe)9&x~*nwDGuHT zYYTm%ZsrF}E14;vW&{>g+gkg#Gy+?8Yw@K}QysjI(%*SaCJ`l7O=|S08j`nL*35lL zz|6daGz9@^4I#+N#6%U6>b}nNt+d4#o_hYB#TP$p0+iPT)S_$Z+4AQ{3)m2SfH(ao z-#v5QJ(t3ioxI^Nn92yL*Y&RW>NH@*q#$m|zzm1T&iAdHlJXQ-yw#bP_w~bCcF^t4 z^cd&o&SlW|zKr0~fUi-NAhd@KjE1N_L}LnUv3uBusY!xrp$pIY@Rf z;_+5MEU_EIyLO8ed@~&V5WwqJE(r4sZ4aj%!0oMFk4_|2t&KBtPy4DpKnc>$CYYXM z6SHJ|Gl_&v&4gCFn?z^|V`Eahh|xAoX^+tA()93K11<1$U!vLK^YP7=OegDrwicXx zG9lgo^BJJP-l6r+p*H%dFf-{NtiP_864GC!R9$Sb*){s)*qWC{X^7g*bEo3gZuAPlUPx zSm49ho}#WmU4dpYQ7s|9K(?T4e^)1S@00t=2q6a3B4$yNUv^;<~c+c`6`=5^}(c{SfL3Hh{ WNin}nDi?M_AL92B_U7*)ef=|oQDxTv literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/replay/dialog.test.ts b/packages/rrweb/test/replay/dialog.test.ts index 5f38a36fe1..c9921da859 100644 --- a/packages/rrweb/test/replay/dialog.test.ts +++ b/packages/rrweb/test/replay/dialog.test.ts @@ -3,7 +3,17 @@ import { toMatchImageSnapshot } from 'jest-image-snapshot'; import * as path from 'path'; import { vi } from 'vitest'; -import dialogPlaybackEvents from '../events/dialog-playback'; +import dialogPlaybackEvents, { + closedFullSnapshotTime, + showIncrementalAttributeTime, + closeIncrementalAttributeTime, + showModalIncrementalAttributeTime, + showFullSnapshotTime, + showModalFullSnapshotTime, + showModalIncrementalAddTime, + switchBetweenShowModalAndShowIncrementalAttributeTime, + switchBetweenShowAndShowModalIncrementalAttributeTime, +} from '../events/dialog-playback'; import { fakeGoto, getServerURL, @@ -19,14 +29,17 @@ expect.extend({ toMatchImageSnapshot }); // TODO: test the following: // == on record == // - dialog open (standard) full snapshot -// - dialog open (standard) incremental (virtual dom) -// - dialog open (standard) incremental (non virtual dom) -// - dialog open (showModal) full snapshot +// √ dialog open (standard) incremental (virtual dom) +// √ dialog open (standard) incremental (non virtual dom) +// √ dialog open (showModal) full snapshot // √ dialog open (showModal) incremental (virtual dom) // √ dialog open (showModal) incremental (non virtual dom) +// √ append dialog open (showModal) incremental (virtual dom) +// √ append dialog open (showModal) incremental (non virtual dom) // √ dialog close (rrdom) -// - dialog close (non virtual dom) -// - dialog open and close (switching from modal to non modal and vise versa) +// √ dialog close (non virtual dom) +// √ dialog open and close (switching from modal to non modal) +// √ dialog open and close (switching from non modal to modal) // - multiple dialogs open, recording order // == on playback == // - dialog open @@ -65,9 +78,7 @@ describe('dialog', () => { beforeEach(async () => { page = await browser.newPage(); page.on('console', (msg) => { - for (let i = 0; i < msg.args().length; ++i) { - console.log(`${i}: ${msg.args()[i]}`); - } + console.log(msg.text()); }); await fakeGoto(page, `${serverURL}/html/dialog.html`); @@ -96,7 +107,23 @@ describe('dialog', () => { await page.evaluate(` const { Replayer } = rrweb; window.replayer = new Replayer(events); - window.replayer.pause(1500); + window.replayer.pause(${showIncrementalAttributeTime}); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('show show the dialog when open attribute gets added (non virtual)', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events, { useVirtualDom: false }); + window.replayer.pause(${showIncrementalAttributeTime}); `); await waitForRAF(page); @@ -112,7 +139,23 @@ describe('dialog', () => { await page.evaluate(` const { Replayer } = rrweb; window.replayer = new Replayer(events); - window.replayer.pause(2000); + window.replayer.pause(${closeIncrementalAttributeTime}); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should close dialog again when open attribute gets removed (without virtual dom)', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events, { useVirtualDom: false }); + window.replayer.pause(${closeIncrementalAttributeTime}); `); await waitForRAF(page); @@ -128,7 +171,7 @@ describe('dialog', () => { await page.evaluate(` const { Replayer } = rrweb; window.replayer = new Replayer(events); - window.replayer.pause(2500); + window.replayer.pause(${showModalIncrementalAttributeTime}); `); await waitForRAF(page); @@ -144,11 +187,177 @@ describe('dialog', () => { await page.evaluate(` const { Replayer } = rrweb; window.replayer = new Replayer(events, { useVirtualDom: false }); - window.replayer.pause(2500); + window.replayer.pause(${showModalIncrementalAttributeTime}); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should switch between showModal and show', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events); + window.replayer.pause(${switchBetweenShowModalAndShowIncrementalAttributeTime}); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should switch between showModal and show (without virtual dom)', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events, { useVirtualDom: false }); + window.replayer.pause(${switchBetweenShowModalAndShowIncrementalAttributeTime}); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should switch between show and showModal', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events); + window.replayer.pause(${switchBetweenShowAndShowModalIncrementalAttributeTime}); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should switch between show and showModal (without virtual dom)', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events, { useVirtualDom: false }); + window.replayer.pause(${switchBetweenShowAndShowModalIncrementalAttributeTime}); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should open dialog with show in full snapshot', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events); + window.replayer.pause(${showFullSnapshotTime}); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should open dialog with show in full snapshot (without virtual dom)', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events, { useVirtualDom: false }); + window.replayer.pause(${showFullSnapshotTime}); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should open dialog with showModal in full snapshot', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events); + window.replayer.pause(${showModalFullSnapshotTime}); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should open dialog with showModal in full snapshot (without virtual dom)', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + try { + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events, { useVirtualDom: false }); + window.replayer.pause(${showModalFullSnapshotTime}); + `); + } catch (e) { + console.log('error'); + } + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should add an opened dialog with showModal in incremental snapshot', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events); + window.replayer.pause(${showModalIncrementalAddTime}); `); await waitForRAF(page); - // await page.waitForTimeout(30000); + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + it('should add an opened dialog with showModal in incremental snapshot (without virtual dom)', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + try { + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events, { useVirtualDom: false }); + window.replayer.pause(${showModalIncrementalAddTime}); + `); + } catch (e) { + console.log('error'); + } + await waitForRAF(page); const frameImage = await page!.screenshot(); expect(frameImage).toMatchImageSnapshot({ From 29d4827566eed10dc912b723a2c5656f794d6404 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 11 Jun 2024 15:02:23 +0200 Subject: [PATCH 05/29] chore: Update dev script to remove CLEAR_DIST_DIR flag --- package.json | 2 +- packages/web-extension/vite.config.ts | 4 +++- turbo.json | 2 +- vite.config.default.ts | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 573ec620c0..9b9d67297a 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "check-types": "yarn turbo run check-types --continue", "format": "yarn prettier --write '**/*.{ts,md}'", "format:head": "git diff --name-only HEAD^ |grep '\\.ts$\\|\\.md$' |xargs yarn prettier --write", - "dev": "CLEAR_DIST_DIR=false yarn turbo run dev --concurrency=17", + "dev": "yarn turbo run dev --concurrency=17", "repl": "cd packages/rrweb && npm run repl", "live-stream": "cd packages/rrweb && yarn live-stream", "lint": "yarn run concurrently --success=all -r -m=1 'yarn run markdownlint docs' 'yarn eslint packages/*/src --ext .ts,.tsx,.js,.jsx,.svelte'", diff --git a/packages/web-extension/vite.config.ts b/packages/web-extension/vite.config.ts index 76d2c631a3..5ccecccff0 100644 --- a/packages/web-extension/vite.config.ts +++ b/packages/web-extension/vite.config.ts @@ -5,6 +5,8 @@ import * as path from 'path'; import type { PackageJson } from 'type-fest'; import react from '@vitejs/plugin-react'; +const emptyOutDir = !process.argv.includes('--watch'); + function useSpecialFormat( entriesToUse: string[], format: LibraryFormats, @@ -46,7 +48,7 @@ export default defineConfig({ 'dist', process.env.TARGET_BROWSER as string, ), - emptyOutDir: true, + emptyOutDir, }, // Add the webExtension plugin plugins: [ diff --git a/turbo.json b/turbo.json index f1e05b29ce..0dcf34ece1 100644 --- a/turbo.json +++ b/turbo.json @@ -8,7 +8,7 @@ "vite.config.defaults.ts", "tsconfig.json" ], - "globalPassThroughEnv": ["CLEAR_DIST_DIR", "PUPPETEER_HEADLESS"], + "globalPassThroughEnv": ["PUPPETEER_HEADLESS"], "tasks": { "prepublish": { "dependsOn": ["^prepublish"], diff --git a/vite.config.default.ts b/vite.config.default.ts index 9bfb2fd017..082ca27396 100644 --- a/vite.config.default.ts +++ b/vite.config.default.ts @@ -7,7 +7,7 @@ import { build, Format } from 'esbuild'; import { resolve } from 'path'; import { umdWrapper } from 'esbuild-plugin-umd-wrapper'; -const emptyOutDir = process.env.CLEAR_DIST_DIR !== 'false'; +const emptyOutDir = !process.argv.includes('--watch'); function minifyAndUMDPlugin({ name, From dcc867ca02f4d8b4ac34c1e297519d1abd8584ce Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 13:43:11 +0200 Subject: [PATCH 06/29] Get modal recording and replay working --- packages/rrdom/src/diff.ts | 34 ++-- packages/rrdom/src/document.ts | 2 +- packages/rrdom/test/diff/dialog.test.ts | 6 +- packages/rrweb-snapshot/src/snapshot.ts | 14 +- packages/rrweb-snapshot/src/types.ts | 19 +- .../__snapshots__/integration.test.ts.snap | 4 +- packages/rrweb/src/record/mutation.ts | 6 + packages/rrweb/src/replay/dialog/index.ts | 30 ++- packages/rrweb/src/replay/index.ts | 12 +- packages/rrweb/test/events/dialog-playback.ts | 20 +- packages/rrweb/test/html/dialog.html | 5 + .../record/__snapshots__/dialog.test.ts.snap | 127 ++++++++++++ packages/rrweb/test/record/dialog.test.ts | 192 ++++++++++++++++++ packages/rrweb/test/replay/dialog.test.ts | 7 +- 14 files changed, 407 insertions(+), 71 deletions(-) create mode 100644 packages/rrweb/test/html/dialog.html create mode 100644 packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap create mode 100644 packages/rrweb/test/record/dialog.test.ts diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index b510bb7f4b..a1caf2a4e9 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -291,15 +291,20 @@ function diffAfterUpdatingChildren( const rrDialog = newRRElement as unknown as RRDialogElement; const wasOpen = dialog.open; const wasModal = dialog.matches('dialog:modal'); - const isOpen = rrDialog.open; - const { isModal } = rrDialog; + const isOpen = typeof rrDialog.getAttribute('open') === 'string'; + const isModal = rrDialog.getAttribute('rr_open') === 'modal'; - const modeChanged = (wasModal && !isModal) || (!wasModal && isModal); + const modeChanged = wasModal !== isModal; + const openChanged = wasOpen !== isOpen; - if (wasOpen && modeChanged) dialog.close(); - if ((wasOpen && modeChanged) || (isOpen && !wasOpen)) { - if (isModal) dialog.showModal(); - else dialog.show(); + if (modeChanged || wasOpen !== isOpen) dialog.close(); + if (isOpen && (openChanged || modeChanged)) { + try { + if (isModal) dialog.showModal(); + else dialog.show(); + } catch (e) { + console.warn(e); + } } break; @@ -349,23 +354,10 @@ function diffProps( } }; } else if (newTree.tagName === 'IFRAME' && name === 'srcdoc') continue; - else if ( - newTree.tagName === 'DIALOG' && - (name === 'rr_open' || name === 'open') - ) { - const rrDialog = newTree as RRDialogElement; - const isModal = newAttributes.rr_open === 'modal'; - const isOpen = isModal || newAttributes.open === ''; - if (isModal) rrDialog.showModal(); - else if (isOpen) rrDialog.show(); - else rrDialog.close(); - continue; - } else oldTree.setAttribute(name, newValue); + else oldTree.setAttribute(name, newValue); } for (const { name } of Array.from(oldAttributes)) { - if (newTree.tagName === 'DIALOG' && (name === 'rr_open' || name === 'open')) - continue; // attributes are handled in diffAfterUpdatingChildren for Dialog elements if (!(name in newAttributes)) oldTree.removeAttribute(name); } newTree.scrollLeft && (oldTree.scrollLeft = newTree.scrollLeft); diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index 9200a36567..51757693bd 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -474,7 +474,7 @@ export class BaseRRElement extends BaseRRNode implements IRRElement { } public getAttribute(name: string): string | null { - return this.attributes[name] || null; + return this.attributes[name] ?? null; } public setAttribute(name: string, attribute: string) { diff --git a/packages/rrdom/test/diff/dialog.test.ts b/packages/rrdom/test/diff/dialog.test.ts index cf2df3c910..8721adc196 100644 --- a/packages/rrdom/test/diff/dialog.test.ts +++ b/packages/rrdom/test/diff/dialog.test.ts @@ -62,7 +62,7 @@ describe('diff algorithm for rrdom', () => { const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); - rrNode.attributes = { rr_open: 'modal' }; + rrNode.attributes = { rr_open: 'modal', open: '' }; mirror.add(node, elementSn); rrDocument.mirror.add(rrNode, elementSn); @@ -75,7 +75,6 @@ describe('diff algorithm for rrdom', () => { const tagName = 'DIALOG'; const node = document.createElement(tagName) as HTMLDialogElement; node.showModal(); - console.log('node', { x: node.getAttribute('open') }); vi.spyOn(node, 'matches').mockReturnValue(true); // matches is used to check if the dialog was opened with showModal const closeFn = vi.spyOn(node, 'close'); @@ -93,13 +92,14 @@ describe('diff algorithm for rrdom', () => { it('should not trigger `close` on rr_open is kept', () => { const tagName = 'DIALOG'; const node = document.createElement(tagName) as HTMLDialogElement; + vi.spyOn(node, 'matches').mockReturnValue(true); // matches is used to check if the dialog was opened with showModal node.setAttribute('rr_open', 'modal'); node.setAttribute('open', ''); const closeFn = vi.spyOn(node, 'close'); const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); - rrNode.attributes = { rr_open: 'modal' }; + rrNode.attributes = { rr_open: 'modal', open: '' }; mirror.add(node, elementSn); rrDocument.mirror.add(rrNode, elementSn); diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 5c88c75d79..e81df7be51 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -706,13 +706,13 @@ function serializeElementNode( } } - if ( - tagName === 'dialog' && - (n as HTMLDialogElement).open && - n.matches('dialog:modal') - ) { - (attributes as DialogAttributes).rr_open = 'modal'; - delete attributes.open; // prevent default `show()` behavior which blocks `showModal()` from working + if (tagName === 'dialog' && (n as HTMLDialogElement).open) { + // register what type of dialog is this + // `modal` or `non-modal` + // this is used to trigger `showModal()` or `show()` on replay (outside of rrweb-snapshot, in rrweb) + (attributes as DialogAttributes).rr_open = n.matches('dialog:modal') + ? 'modal' + : 'non-modal'; } // canvas image data diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts index 3613b4cd8b..e6efc58c10 100644 --- a/packages/rrweb-snapshot/src/types.ts +++ b/packages/rrweb-snapshot/src/types.ts @@ -103,14 +103,17 @@ export type mediaAttributes = { rr_mediaVolume?: number; }; -export type DialogAttributes = - // | { - // open: ''; - // } - { - rr_open: 'modal'; - // rr_open_index?: number; - }; +export type DialogAttributes = { + open: string; + /** + * Represents the dialog's open mode. + * `modal` means the dialog is opened with `showModal()`. + * `non-modal` means the dialog is opened with `show()` or + * by adding an `open` attribute. + */ + rr_open: 'modal' | 'non-modal'; + // rr_open_index?: number; +}; // @deprecated export interface INode extends Node { diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index 0c16d5898c..60844bdf10 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -30,6 +30,7 @@ exports[`dialog integration tests > should capture open attribute for modal dial \\"type\\": 2, \\"tagName\\": \\"dialog\\", \\"attributes\\": { + \\"open\\": \\"\\", \\"rr_open\\": \\"modal\\" }, \\"childNodes\\": [ @@ -88,7 +89,8 @@ exports[`dialog integration tests > should capture open attribute for non modal \\"type\\": 2, \\"tagName\\": \\"dialog\\", \\"attributes\\": { - \\"open\\": \\"\\" + \\"open\\": \\"\\", + \\"rr_open\\": \\"non-modal\\" }, \\"childNodes\\": [ { diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index d96fab315d..bc666cb47f 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -663,6 +663,12 @@ export default class MutationBuffer { item.styleDiff[pname] = false; // delete } } + } else if (attributeName === 'open' && target.tagName === 'DIALOG') { + if (target.matches('dialog:modal')) { + item.attributes['rr_open'] = 'modal'; + } else { + item.attributes['rr_open'] = 'non-modal'; + } } } break; diff --git a/packages/rrweb/src/replay/dialog/index.ts b/packages/rrweb/src/replay/dialog/index.ts index 6f47b991ea..290a2ccbac 100644 --- a/packages/rrweb/src/replay/dialog/index.ts +++ b/packages/rrweb/src/replay/dialog/index.ts @@ -3,23 +3,39 @@ import { RRNode } from 'rrdom'; export function triggerShowModalForModals( node: HTMLDialogElement | Node | RRNode, + attributeMutation?: attributeMutation, ) { if (node.nodeName !== 'DIALOG' || node instanceof RRNode) return; const dialog = node as HTMLDialogElement; - if (dialog.getAttribute('rr_open') !== 'modal') return; + const isOpen = dialog.open; + const isModal = isOpen && dialog.matches('dialog:modal'); + const shouldBeOpen = + typeof attributeMutation?.attributes.open === 'string' || + typeof dialog.getAttribute('open') === 'string'; + const shouldBeModal = dialog.getAttribute('rr_open') === 'modal'; + const shouldBeNonModal = dialog.getAttribute('rr_open') === 'non-modal'; + const modeChanged = + (isModal && shouldBeNonModal) || (!isModal && shouldBeModal); + + if (isOpen && !modeChanged) return; // complain if dialog is not attached to the dom if (!dialog.isConnected) { console.warn('dialog is not attached to the dom', dialog); return; } - dialog.showModal(); + if (isOpen) dialog.close(); + + if (!shouldBeOpen) return; + + if (shouldBeModal) dialog.showModal(); + else dialog.show(); } export function triggerCloseForModals( node: HTMLDialogElement | Node | RRNode, - attributeMuation: attributeMutation, + attributeMutation: attributeMutation, ) { if (node.nodeName !== 'DIALOG' || node instanceof RRNode) return; const dialog = node as HTMLDialogElement; @@ -30,10 +46,8 @@ export function triggerCloseForModals( return; } - if (attributeMuation.attributes.rr_open === null) { - dialog.close(); - } - if (attributeMuation.attributes.open === '') { - dialog.show(); + if (attributeMutation.attributes.open === null) { + dialog.removeAttribute('open'); + dialog.removeAttribute('rr_open'); } } diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index b285907898..c4d98627bc 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1743,10 +1743,10 @@ export class Replayer { if (typeof attributeName === 'string') { const value = mutation.attributes[attributeName]; if (value === null) { - (target as Element | RRElement).removeAttribute(attributeName); - if (attributeName === 'rr_open') { + if (attributeName === 'open') { triggerCloseForModals(target, mutation); } + (target as Element | RRElement).removeAttribute(attributeName); } else if (typeof value === 'string') { try { // When building snapshot, some link styles haven't loaded. Then they are loaded, they will be inlined as incremental mutation change of attribute. We need to replace the old elements whose styles aren't inlined. @@ -1803,12 +1803,8 @@ export class Replayer { ); } - if ( - attributeName === 'rr_open' && - target.nodeName === 'DIALOG' && - value === 'modal' - ) { - triggerShowModalForModals(target); + if (attributeName === 'rr_open' && target.nodeName === 'DIALOG') { + triggerShowModalForModals(target, mutation); } } catch (error) { this.warn( diff --git a/packages/rrweb/test/events/dialog-playback.ts b/packages/rrweb/test/events/dialog-playback.ts index f2be2e88a2..e5c2bc36b7 100644 --- a/packages/rrweb/test/events/dialog-playback.ts +++ b/packages/rrweb/test/events/dialog-playback.ts @@ -132,7 +132,7 @@ const events: eventWithTime[] = [ attributes: [ { id: 27, - attributes: { open: '' }, + attributes: { open: '', rr_open: 'non-modal', class: 'show' }, }, ], }, @@ -149,7 +149,7 @@ const events: eventWithTime[] = [ attributes: [ { id: 27, - attributes: { open: null }, + attributes: { open: null, class: 'closed' }, }, ], }, @@ -166,7 +166,7 @@ const events: eventWithTime[] = [ attributes: [ { id: 27, - attributes: { rr_open: 'modal' }, + attributes: { rr_open: 'modal', open: '', class: 'showModal' }, }, ], }, @@ -184,8 +184,7 @@ const events: eventWithTime[] = [ { id: 27, attributes: { - rr_open: null, - open: '', + rr_open: 'non-modal', class: 'switched-from-show-modal-to-show', }, }, @@ -206,7 +205,6 @@ const events: eventWithTime[] = [ { id: 27, attributes: { - open: null, rr_open: 'modal', class: 'switched-from-show-to-show-modal', }, @@ -287,6 +285,7 @@ const events: eventWithTime[] = [ tagName: 'dialog', attributes: { open: '', + rr_open: 'non-modal', style: 'outline: blue solid 1px;', }, childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }], @@ -386,7 +385,9 @@ const events: eventWithTime[] = [ tagName: 'dialog', attributes: { rr_open: 'modal', + open: '', style: 'outline: blue solid 1px;', + class: 'existing-1', }, childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }], id: 24, @@ -397,6 +398,7 @@ const events: eventWithTime[] = [ tagName: 'dialog', attributes: { style: 'outline: red solid 1px;', + class: 'existing-2', }, childNodes: [{ type: 3, textContent: 'Dialog 2', id: 28 }], id: 27, @@ -423,14 +425,16 @@ const events: eventWithTime[] = [ adds: [ { parentId: 22, - previousId: 27, - nextId: 31, + previousId: 23, + nextId: 24, node: { type: 2, tagName: 'dialog', attributes: { rr_open: 'modal', + open: '', style: 'outline: orange solid 1px;', + class: 'new-dialog', }, childNodes: [], id: 32, diff --git a/packages/rrweb/test/html/dialog.html b/packages/rrweb/test/html/dialog.html new file mode 100644 index 0000000000..2380b8fade --- /dev/null +++ b/packages/rrweb/test/html/dialog.html @@ -0,0 +1,5 @@ + + + I'm a dialog + + diff --git a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap new file mode 100644 index 0000000000..6e4cdff78a --- /dev/null +++ b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap @@ -0,0 +1,127 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`dialog > switch to showModal dialog 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"text/javascript\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 5 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"I'm a dialog\\", + \\"id\\": 9 + } + ], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 10 + } + ], + \\"id\\": 6 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 8, + \\"attributes\\": { + \\"open\\": \\"\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 8, + \\"attributes\\": { + \\"open\\": null, + \\"rr_open\\": \\"modal\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; diff --git a/packages/rrweb/test/record/dialog.test.ts b/packages/rrweb/test/record/dialog.test.ts new file mode 100644 index 0000000000..b8eceb0d9e --- /dev/null +++ b/packages/rrweb/test/record/dialog.test.ts @@ -0,0 +1,192 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { vi } from 'vitest'; + +import { + assertSnapshot, + getServerURL, + ISuite, + launchPuppeteer, + startServer, + waitForRAF, +} from '../utils'; +import { + attributeMutation, + EventType, + eventWithTime, + listenerHandler, +} from '@rrweb/types'; +import { recordOptions } from '../../src/types'; + +// TODO: test the following: +// == on record == +// √ dialog show +// √ dialog showModal +// √ dialog close +// - dialog open and close +// - dialog showModal and switch to show +// √ dialog show and switch to showModal +// - dialog add and showModal +// - multiple dialogs open, playback order + +interface IWindow extends Window { + rrweb: { + record: ( + options: recordOptions, + ) => listenerHandler | undefined; + addCustomEvent(tag: string, payload: T): void; + }; + emit: (e: eventWithTime) => undefined; +} + +const attributeMutationFactory = ( + mutation: attributeMutation['attributes'], +) => { + return { + data: { + attributes: [ + { + attributes: mutation, + }, + ], + }, + }; +}; + +describe('dialog', () => { + vi.setConfig({ testTimeout: 100_000 }); + let code: ISuite['code']; + let page: ISuite['page']; + let browser: ISuite['browser']; + let server: ISuite['server']; + let serverURL: ISuite['serverURL']; + let events: ISuite['events']; + + beforeAll(async () => { + server = await startServer(); + serverURL = getServerURL(server); + browser = await launchPuppeteer(); + + const bundlePath = path.resolve(__dirname, '../../dist/rrweb.umd.cjs'); + code = fs.readFileSync(bundlePath, 'utf8'); + }); + + afterEach(async () => { + await page.close(); + }); + + afterAll(async () => { + await server.close(); + await browser.close(); + }); + + beforeEach(async () => { + page = await browser.newPage(); + page.on('console', (msg) => { + console.log(msg.text()); + }); + + await page.goto(`${serverURL}/html/dialog.html`); + await page.addScriptTag({ + path: path.resolve(__dirname, '../../dist/rrweb.umd.cjs'), + }); + await waitForRAF(page); + events = []; + + await page.exposeFunction('emit', (e: eventWithTime) => { + if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) { + return; + } + events.push(e); + }); + + page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); + + await page.evaluate(() => { + const { record } = (window as unknown as IWindow).rrweb; + record({ + emit: (window as unknown as IWindow).emit, + }); + }); + + await waitForRAF(page); + }); + + it('show dialog', async () => { + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.show(); + }); + + const lastEvent = events[events.length - 1]; + + expect(lastEvent).toMatchObject(attributeMutationFactory({ open: '' })); + // assertSnapshot(events); + }); + + it('showModal dialog', async () => { + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.showModal(); + }); + + const lastEvent = events[events.length - 1]; + + expect(lastEvent).toMatchObject( + attributeMutationFactory({ rr_open: 'modal' }), + ); + }); + + it('showModal & close dialog', async () => { + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.showModal(); + }); + await waitForRAF(page); + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.close(); + }); + + const lastEvent = events[events.length - 1]; + + expect(lastEvent).toMatchObject( + attributeMutationFactory({ rr_open: null }), + ); + }); + + it('show & close dialog', async () => { + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.show(); + }); + await waitForRAF(page); + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.close(); + }); + + const lastEvent = events[events.length - 1]; + + expect(lastEvent).toMatchObject(attributeMutationFactory({ open: null })); + }); + + it('switch to showModal dialog', async () => { + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.show(); + }); + await waitForRAF(page); + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.close(); + dialog.showModal(); + }); + + assertSnapshot(events); + + // const lastEvent = events[events.length - 1]; + + // expect(lastEvent).toMatchObject(attributeMutationFactory({ open: null })); + }); +}); diff --git a/packages/rrweb/test/replay/dialog.test.ts b/packages/rrweb/test/replay/dialog.test.ts index c9921da859..59b2a9aed0 100644 --- a/packages/rrweb/test/replay/dialog.test.ts +++ b/packages/rrweb/test/replay/dialog.test.ts @@ -27,7 +27,7 @@ import { expect.extend({ toMatchImageSnapshot }); // TODO: test the following: -// == on record == +// == on playback == // - dialog open (standard) full snapshot // √ dialog open (standard) incremental (virtual dom) // √ dialog open (standard) incremental (non virtual dom) @@ -41,11 +41,6 @@ expect.extend({ toMatchImageSnapshot }); // √ dialog open and close (switching from modal to non modal) // √ dialog open and close (switching from non modal to modal) // - multiple dialogs open, recording order -// == on playback == -// - dialog open -// - dialog close -// - dialog open and close (switching from modal to non modal and vise versa) -// - multiple dialogs open, playback order // == on rrdom == // - that the modal modes are recorded... From 1144c9d01f783d5ee5684b6719a54ab8dac77744 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 14:58:00 +0200 Subject: [PATCH 07/29] DRY up dialog test and dedupe snapshot images --- ...ith-show-modal-in-incremental-snapshot.png | Bin 0 -> 12721 bytes ...ets-removed-without-virtual-dom-1-snap.png | Bin 10592 -> 0 bytes ...gain-when-open-attribute-gets-removed.png} | Bin ...open-dialog-with-show-in-full-snapshot.png | Bin 0 -> 12261 bytes ...ll-snapshot-without-virtual-dom-1-snap.png | Bin 12690 -> 0 bytes ...ialog-with-show-modal-in-full-snapshot.png | Bin 0 -> 12505 bytes ...-show-modal-without-virtual-dom-1-snap.png | Bin 12690 -> 0 bytes ...og-should-open-dialog-with-show-modal.png} | Bin ...tch-between-show-and-show-modal-1-snap.png | Bin 12690 -> 0 bytes ...-show-modal-without-virtual-dom-1-snap.png | Bin 12690 -> 0 bytes ...ld-switch-between-show-and-show-modal.png} | Bin ...tch-between-show-modal-and-show-1-snap.png | Bin 12445 -> 0 bytes ...al-and-show-without-virtual-dom-1-snap.png | Bin 12445 -> 0 bytes ...ld-switch-between-show-modal-and-show.png} | Bin ...-when-open-attribute-gets-added-1-snap.png | Bin 12445 -> 0 bytes ...ttribute-gets-added-non-virtual-1-snap.png | Bin 12445 -> 0 bytes ...dialog-when-open-attribute-gets-added.png} | Bin packages/rrweb/test/replay/dialog.test.ts | 324 ++++-------------- 18 files changed, 62 insertions(+), 262 deletions(-) create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot.png delete mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed-without-virtual-dom-1-snap.png rename packages/rrweb/test/replay/__image_snapshots__/{dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed-1-snap.png => dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed.png} (100%) create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot.png delete mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot-without-virtual-dom-1-snap.png create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot.png delete mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-without-virtual-dom-1-snap.png rename packages/rrweb/test/replay/__image_snapshots__/{dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-1-snap.png => dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal.png} (100%) delete mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-and-show-modal-1-snap.png delete mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-and-show-modal-without-virtual-dom-1-snap.png rename packages/rrweb/test/replay/__image_snapshots__/{dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot-1-snap.png => dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-and-show-modal.png} (100%) delete mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show-1-snap.png delete mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show-without-virtual-dom-1-snap.png rename packages/rrweb/test/replay/__image_snapshots__/{dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot-1-snap.png => dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show.png} (100%) delete mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-show-the-dialog-when-open-attribute-gets-added-1-snap.png delete mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-show-the-dialog-when-open-attribute-gets-added-non-virtual-1-snap.png rename packages/rrweb/test/replay/__image_snapshots__/{dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot-without-virtual-dom-1-snap.png => dialog-test-ts-test-replay-dialog-test-ts-dialog-show-the-dialog-when-open-attribute-gets-added.png} (100%) diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot.png new file mode 100644 index 0000000000000000000000000000000000000000..f328c34b5b1b95a917d80f69c2fa391285891f97 GIT binary patch literal 12721 zcmeHNc~nzZ8h?t4wor?m7L?5;N?Vatfw0AeifI+3OjQ&JElZ{XB1D$R5?m^61sTIG zflO;FZAF4mKp>D%HLOWaU07rnLQrBNgph=gr1vH>p6UEM|9E=dJvrxn@7??E`_|w6 z?sxOeVLxBvH#WS1Ac!&kfX}B0Vwi~_E8JGE0#BwNw(SF#Wu#Ai_aaq1i)RS35yAWH z`FjdYG@Rb?I9c63+t*kl_-)V4D7oq8I*cog(-g$*IubKrt=`1BIL(<}Ykd5+-M3CB zYwC}=H92niI*fJ7(6OTsx8e4&!J7BpTUQco{-7{c(DiAhfGbn&Yswgyf3T}>mYL?M zG2tmg=~8duXa${5td8t1>;$}k_P3xrr^l;(5wmY^f4)}#2w`__LJahD{@a<$^wh_z z+5hg|x*|+ZWv*N9pr>}PS!SlE{&f*C(o=``B2aA5-bbfn>lA??sP0flL8C$+3@D_`Dtr|8?EUI?*y*+jCd1j++QunI zf=PX+g>Hw^ChnU0si$CXg*)6f?&q>Bu6ts!zY#yo&1~DPTnT#iclTEt!uJafon(V6 zd$kFySlnkKfQHsf)1w4G}#R5BkZ4UZ;`RO-U1q*ipf0-a9ruTmlM(fCqvZyMZ_$?OUg#zI8cuS33L`$y zqMRy~J}7G4d7*71_%YH1+iO#1pE6~);<$Bvp{0XyEOh`yI#&vffM727G6QPVWPN7}MHGalI zEuNapoUl~R=2B>br=8z28BVL@`%ep(gHO_ai84wB*RVU~ zv)M!pK8i=f%AV8$l?0QxyU25SbQ;i1)M#sZqh@g?55pjHqpvjaG-{P|I49d^m6j4U z$Es2uvvfc6et7spE&VY|{UWYW+%z-ygN@e*_?no3nx&UnxU^upN;0$z?qagnL3?C| zFZQ>47uNFJw-dCBeR&q%z2wKgd`9j*G2$fa>AiKLHbKx89Do1a_14z>McGV6h*XHv zl1CIA*QnX2_k*g#_IS&F_P2~rmVm+1nxYn421PyNOwlYcRl^cq1y21eX0%lu5WOS# z≷|;!zgP+$E-?sCE8F8hNV3W@5%B-Vm;y4##0eJysmmQ+)MJyE_{xio4bcqv`TU zAhdF!fmP;}x^U1-THnyJgGh_t-^(D-6K8q_Et2#&jYiJ0PHcHG*g*XK`;SVBi#uW) zh6|0&T_*QgFCN~uZQG6Vpt_Tv71|TZPsFwC`lQH?3(P050Lt=De;$~N72Y^hnlM5Z zMoOo+%Eye1f=>Kk5_HywdC=<)&S3duKk8%yOC4+1I7ziVRz3E$+`d^Z)27pZAn`xE2K`;Hh`zE9}95J_02@L#rAUmZyq4Et7|?LRZ66U0a?V z!c&z}p>U;rN}D6m!e~`cV?tyUzq}0CdMcWrdNQHl0L>uRArK)qWS0p`oAGnhBIe{z z5<=>WIxKKzXrG@`&*#<|C~R;_{8Uc~>0UG=Lv}L{Ggu$n{jhxDS4>v!H9u-C-`k^e z{Car>>DiA(QD1zqIe9qoC^ce&lhdvawdt~CgAcLaFL4w>xdZ#e!_6f5aI#Rzr(^(@ z(jO5|-5I}>_VgWk$Bu>}s-bn7L&H!)w_~5j>9O4z%5H+y>=UJsj+2czgZN--)e^ob zOL3nmPGod*a?G6_9j^+#<&8EwB??8#$jeKvdH{2aT?0PM4zTo0agdztYo=m=cep)w z_DB}@4326RDhVual90#cm3dTzklN0#YDlVS?&of~-=; z&Uq@vE-0scac6&hi#K=p{(o+JcvDVi#cgZ~MA#t^62Q)nAR#bD1&AMULT%DZ)`;Dx?$$5uuzDG|qC&Nm{jB zm`7iV5Z{@&!jz;-x|!5)W%*8geE8nzb^t#l1P{MHj#m(ir<(nnp_Z zr)KBm2xuf;=etLPj0BX;(1DYY(%cG z5bsuCTpu&=G{F{#vxzl0UjSB6?Dne0nf%6?B!0OUcBZnHiqR_CaZUifvU4m|?C*T7 z$e85O^knG&M6a2kKt>#h{O50t4L2tf0Yz4fXGzbkP1{)N5_`?vWpNazVxKd>szkN^ zi}DRdtCXB9`<}`mMu*fcRfs28BS^|qTYx$JTM(nKpm(z;7Wj%%3;+*KJe|@NSWcDl zaoPX|L+S{Fh-6l`lhW`4G-yJ2(HGOqU#>1u*)lyR6?X}=&Y}LEHc zpdq7E!(RzQMER#K0Z=5s{{ojB++>-2!)wN4G_-di6PXkkvLhyMkK(}T4J>p@TE9!;-Y z@$nWwZd(D5;y`0xL}2Y3wBonfx+{Xbvm722gBG?IoKWc6!PQ}UB6E%IP!ZfXZ2HeX f@X^WsAD#JJu83GgI51ua6^#GL&!=i{*!e#Itmd7) literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed-without-virtual-dom-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed-without-virtual-dom-1-snap.png deleted file mode 100644 index 9fb34401f4292e123559cdb06cd35f8842b1c146..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10592 zcmeHNJxjw-6g{b-wxyAXOB986aqi$y@T|9VC|KJ$zW#XbJF4QUo5$~?m8*syeA$D&`@0i;T-MTOZ_ZRu z)A^by8!fABDt8xSrV2b|3QY2tAVEMfK9C_943ta;1Dk_^fz5%osrx2B4}1iBog23c$HZdmv+h_F#1FLD;RX z0>ugW7hrU6$(zw$2?DfN8UxxZ+AGll?G^2n@PPK}zwJRT?3MRl>hVC}jW(J=$4}0F E0Mb(|e&6qW_ne##CIu}1!2SaSK^E`av2_oESY{!}yiHaMK#T18eSc6`r0xmuL#jJg z$`RyaWari`dyifk?c0}&(j40cC99kZKmX)&VsLhD#G3Nu9|j&QJiRxq`{YLpzc>(P ze_+2qCC>T0L$F6~-w~&!YjV$p+x@Pna_PKN^U8~MJidN|lD}hkf4_A`PFPHBp@$o} z%yP0_{AP+H5vRIoifYrYv4DUvK#`3X)9JHS1nF30@zHGk;}giD*_y2%0*4L6YBe%< zwsyic%VM_XXVrorU2RW`LIep%FFz4o+U#Z8X9@1hRMdnpW1WDvS93kK5Wy+|@z4?E12NYoeYx}Qi@}b^ zHCuo=t3Kt*%4?W>&^f}m3`6XaP!L9rMo}M=HY9I_d-eAGl^G9=Hl7;aJO^SOhJD)b z`dbV$jI}uiaYc<8TTMuQbr16}>xaLOZ4*8?X|0f!WmdEI)eE^aMWU?hD)7xf zwoNE1&S?&zH-7&bCoa>#rk3<5fZnV^z`N>)Er z(!{7e_~blyQcR_GUD>iWy5_*&zAbi(C$S@Iu{5O^WreameqsjXudGxzzcHVPb=rPb z##Dc^uDT+=j8WI0`NqW6^&g`?{N_VwR8drFdlByw&_@;p27St&fPz)*wPnZeVA$_n zV~#%~I5;>==!XhK00z)5g6{6G#9lys#FGoI9N+Upu-G%>unW1VF|NHB zweivh>kNIobWy3-F$}|MYHBdd+oXLi29XcZfniMjV9tn4R$(&g%5LkO2KJv^6>EBN{N^KbC#g-@$tG0FeaoUOa^*HF1LLI}2g=wXtpa^a(*2wU0}icK^YwgdZnkC}r*Y3*%OzDpp)m5!ft`UR zpRp%LdqZ+;9IKrI@_UY{`C%v;7#K)M8E63^XIPj@D&AP>ZPpJJ#a>(|}FOrflHaapeN_2#rRY(ApwM3)wxMBp4B}s1qZ{SoPDJCB3~7a~CWE zFZ-DF*NRI@Se5b~o0T3%WWh@8#DB8?4nU9!G+QozewCS6N z3fBfz?{7VY?%K5rFvwoPShy!3Ysq{#WQ`qM&U`szvTFsx@lGEyF>SPBMe@C?b%!J| zIZGU{bP1#0pi@hp55bPKbO_5X>eGuGt~rw~g_9W*GpzXN?DEtHPUY~COyT8Vo1r`5nMa`XMc)ip^n&(dNnJ#Mr7YLnh1+RdZ`S0iS-m`=On(Al9$4B?x!~yOK z+!c_$fy{j1%+&r5{EOob#~qHlnP|fo#{Xmo1}7+Bf%k1g2s#CaF%IKj*fqst7LQr5 zDfh3p*Kwe{M?ldyPLBG*{Tj--<_WK+XJF66dEVO-iIx-Z($|Z&Ei+z2kY#h;o~Tg# z-toY0E6VK6tc4bD4_Vf)es`dPE5mID(1Ak(LGW+_EQ7NK@*5$?BbHw}B8W{P{4c<7 zWXkw75d=>a5CqSN@GKIh#CWodCw>41c;bgAevl6E^Z-u}AP(^K08bAf4)FBg-%Ag! i{PPwi{*CcJk%-0ca}%kQH~$s{;m&QOt<`>%um1t{%+vn> literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot-without-virtual-dom-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot-without-virtual-dom-1-snap.png deleted file mode 100644 index 5fe6c04ee504091f90b95de22a0827aa7511188c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12690 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 WNin}nDi?M_AL92B_U7*)ef=|oQDxTv diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show-without-virtual-dom-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show-without-virtual-dom-1-snap.png deleted file mode 100644 index a3faffd85acf2672b5411a7b05ffea0471ffe11e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12445 zcmeHNYgAKL7QTq`P!N?Fg@T~vWebXeyb4KFRGI;TqYe}W6Dxv>pg;wYkT6xtIG|vl zLLfY>F42NeLEevmRUo{AA_j~Ql<+8z1PBm9Leg`fGi%nG>5mzwGyb^e&po;4-m~}F z``h2%=iFR7KycAq`O!)QK{WB7?>&ehsxb(%++Ixu+!^kua|Rcsu!Ampko-3NpAcjd zg5SIQ@I}U0PgG;d%tFH?XRA1k|JVJw#m)r3&d;9=5<1i9EW&()T8Nv@zF4YN`@W=X zZwJTxA2RN)&BOVr#qGCvq++!$z)a_N>#k*->TpWlo&NDgr?G@#`GRprTd6p=LAgfZ z!yA>B_j1HiwkBIzk^krFD6v2jr~;c&@BOz1OI;*J>l;L4sefP(0(Ay$%VtD*sh7Ma zMro<{2esOz4q~#LgdjSn*Kc!EQK`ZLI6l^Ij1G$ymVuu}*P(Qyu1+r^2Hez!(qeAbZ_WPKQo$QY z%sMCqwq5E9+?<&*V#MgFa6rR2e* zym`;T7J68Fu_1Y)&Mj@{C%n0OyQ%3852{YT8D3D&>Y0GqbL)4j4cqNU8{|IT|%P#=8GURe4q?Q$AB5;qv6SJfcSq;b}$*PF4Y# zk^TmhxwkD02NA|l4@e{34P60HFj10UG4j2B$kVmnC1<^9cEO`$mPrcV1vme^9UI8W zZ@s$VSZbtb@Z?z6Bkw{#mJ)|N{oF;=Bc#|RnR&E5y`~-3;cj=5lkY3d6StnDSPwD6 z2&QhSiE-O1wv@gJb$wMpRNN{KW-iR$o=Z0-E{u33858GUr1WwkA)4b4Lo7~|sHm!j zjXcO11mYTt*x2+8$B02os>HSA91r`L?LnRI&=2oA4m7~8w>-vhwXEJ*P&L&=;T&aB zqD|e?k|m;{fSHlb^6?UG=(FE@`N?GxXQ@vp%Y<$d=qX?-kC4wb1<7tNSiC8>M zUE7pD*PnnJ&f^fN(Uh57;=n56+>{FIrX&QkBjf9sAU$v=D<=_@R zi8Z8L+{TnmT^6hY^S7TlU#1GKt9>n*vd~QO6pK6=M{EHmsN_Z8+UA>fb=3y{fu?V} z3s1Uw=KDxWM#o(Qw=qU>Gv zWQ1t2mF^f33_>nHUf0_H6j#K?WfcI%5X!DV*(=Ld>_iZ9OVFFL9r(f%4L6Kg0659O z;bs5@Z9NgZj1{Za`_;$mI!n*>RM>D!f?R25c{Lb(D2P=5o{9_JRTo2O{0BLiP50c> zr+5l^L{>4c3M0yh=yiD9n`RSe1)?r~qob{3_*kQusaPOOxx6-YGe)9&x~*nwDGuHT zYYTm%ZsrF}E14;vW&{>g+gkg#Gy+?8Yw@K}QysjI(%*SaCJ`l7O=|S08j`nL*35lL zz|6daGz9@^4I#+N#6%U6>b}nNt+d4#o_hYB#TP$p0+iPT)S_$Z+4AQ{3)m2SfH(ao z-#v5QJ(t3ioxI^Nn92yL*Y&RW>NH@*q#$m|zzm1T&iAdHlJXQ-yw#bP_w~bCcF^t4 z^cd&o&SlW|zKr0~fUi-NAhd@KjE1N_L}LnUv3uBusY!xrp$pIY@Rf z;_+5MEU_EIyLO8ed@~&V5WwqJE(r4sZ4aj%!0oMFk4_|2t&KBtPy4DpKnc>$CYYXM z6SHJ|Gl_&v&4gCFn?z^|V`Eahh|xAoX^+tA()93K11<1$U!vLK^YP7=OegDrwicXx zG9lgo^BJJP-l6r+p*H%dFf-{NtiP_864GC!R9$Sb*){s)*qWC{X^7g*bEo3gZuAPlUPx zSm49ho}#WmU4dpYQ7s|9K(?T4e^)1S@00t=2q6a3B4$yNUv^;<~c+c`6`=5^}(c{SfL3Hh{ WNin}nDi?M_AL92B_U7*)ef=|oQDxTv diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show.png similarity index 100% rename from packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot-1-snap.png rename to packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show.png diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-show-the-dialog-when-open-attribute-gets-added-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-show-the-dialog-when-open-attribute-gets-added-1-snap.png deleted file mode 100644 index a3faffd85acf2672b5411a7b05ffea0471ffe11e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12445 zcmeHNYgAKL7QTq`P!N?Fg@T~vWebXeyb4KFRGI;TqYe}W6Dxv>pg;wYkT6xtIG|vl zLLfY>F42NeLEevmRUo{AA_j~Ql<+8z1PBm9Leg`fGi%nG>5mzwGyb^e&po;4-m~}F z``h2%=iFR7KycAq`O!)QK{WB7?>&ehsxb(%++Ixu+!^kua|Rcsu!Ampko-3NpAcjd zg5SIQ@I}U0PgG;d%tFH?XRA1k|JVJw#m)r3&d;9=5<1i9EW&()T8Nv@zF4YN`@W=X zZwJTxA2RN)&BOVr#qGCvq++!$z)a_N>#k*->TpWlo&NDgr?G@#`GRprTd6p=LAgfZ z!yA>B_j1HiwkBIzk^krFD6v2jr~;c&@BOz1OI;*J>l;L4sefP(0(Ay$%VtD*sh7Ma zMro<{2esOz4q~#LgdjSn*Kc!EQK`ZLI6l^Ij1G$ymVuu}*P(Qyu1+r^2Hez!(qeAbZ_WPKQo$QY z%sMCqwq5E9+?<&*V#MgFa6rR2e* zym`;T7J68Fu_1Y)&Mj@{C%n0OyQ%3852{YT8D3D&>Y0GqbL)4j4cqNU8{|IT|%P#=8GURe4q?Q$AB5;qv6SJfcSq;b}$*PF4Y# zk^TmhxwkD02NA|l4@e{34P60HFj10UG4j2B$kVmnC1<^9cEO`$mPrcV1vme^9UI8W zZ@s$VSZbtb@Z?z6Bkw{#mJ)|N{oF;=Bc#|RnR&E5y`~-3;cj=5lkY3d6StnDSPwD6 z2&QhSiE-O1wv@gJb$wMpRNN{KW-iR$o=Z0-E{u33858GUr1WwkA)4b4Lo7~|sHm!j zjXcO11mYTt*x2+8$B02os>HSA91r`L?LnRI&=2oA4m7~8w>-vhwXEJ*P&L&=;T&aB zqD|e?k|m;{fSHlb^6?UG=(FE@`N?GxXQ@vp%Y<$d=qX?-kC4wb1<7tNSiC8>M zUE7pD*PnnJ&f^fN(Uh57;=n56+>{FIrX&QkBjf9sAU$v=D<=_@R zi8Z8L+{TnmT^6hY^S7TlU#1GKt9>n*vd~QO6pK6=M{EHmsN_Z8+UA>fb=3y{fu?V} z3s1Uw=KDxWM#o(Qw=qU>Gv zWQ1t2mF^f33_>nHUf0_H6j#K?WfcI%5X!DV*(=Ld>_iZ9OVFFL9r(f%4L6Kg0659O z;bs5@Z9NgZj1{Za`_;$mI!n*>RM>D!f?R25c{Lb(D2P=5o{9_JRTo2O{0BLiP50c> zr+5l^L{>4c3M0yh=yiD9n`RSe1)?r~qob{3_*kQusaPOOxx6-YGe)9&x~*nwDGuHT zYYTm%ZsrF}E14;vW&{>g+gkg#Gy+?8Yw@K}QysjI(%*SaCJ`l7O=|S08j`nL*35lL zz|6daGz9@^4I#+N#6%U6>b}nNt+d4#o_hYB#TP$p0+iPT)S_$Z+4AQ{3)m2SfH(ao z-#v5QJ(t3ioxI^Nn92yL*Y&RW>NH@*q#$m|zzm1T&iAdHlJXQ-yw#bP_w~bCcF^t4 z^cd&o&SlW|zKr0~fUi-NAhd@KjE1N_L}LnUv3uBusY!xrp$pIY@Rf z;_+5MEU_EIyLO8ed@~&V5WwqJE(r4sZ4aj%!0oMFk4_|2t&KBtPy4DpKnc>$CYYXM z6SHJ|Gl_&v&4gCFn?z^|V`Eahh|xAoX^+tA()93K11<1$U!vLK^YP7=OegDrwicXx zG9lgo^BJJP-l6r+p*H%dFf-{NtiP_864GC!R9$Sb*){s)*qWC{X^7g*bEo3gZuAPlUPx zSm49ho}#WmU4dpYQ7s|9K(?T4e^)1S@00t=2q6a3B4$yNUv^;<~c+c`6`=5^}(c{SfL3Hh{ WNin}nDi?M_AL92B_U7*)ef=|oQDxTv diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-show-the-dialog-when-open-attribute-gets-added-non-virtual-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-show-the-dialog-when-open-attribute-gets-added-non-virtual-1-snap.png deleted file mode 100644 index a3faffd85acf2672b5411a7b05ffea0471ffe11e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12445 zcmeHNYgAKL7QTq`P!N?Fg@T~vWebXeyb4KFRGI;TqYe}W6Dxv>pg;wYkT6xtIG|vl zLLfY>F42NeLEevmRUo{AA_j~Ql<+8z1PBm9Leg`fGi%nG>5mzwGyb^e&po;4-m~}F z``h2%=iFR7KycAq`O!)QK{WB7?>&ehsxb(%++Ixu+!^kua|Rcsu!Ampko-3NpAcjd zg5SIQ@I}U0PgG;d%tFH?XRA1k|JVJw#m)r3&d;9=5<1i9EW&()T8Nv@zF4YN`@W=X zZwJTxA2RN)&BOVr#qGCvq++!$z)a_N>#k*->TpWlo&NDgr?G@#`GRprTd6p=LAgfZ z!yA>B_j1HiwkBIzk^krFD6v2jr~;c&@BOz1OI;*J>l;L4sefP(0(Ay$%VtD*sh7Ma zMro<{2esOz4q~#LgdjSn*Kc!EQK`ZLI6l^Ij1G$ymVuu}*P(Qyu1+r^2Hez!(qeAbZ_WPKQo$QY z%sMCqwq5E9+?<&*V#MgFa6rR2e* zym`;T7J68Fu_1Y)&Mj@{C%n0OyQ%3852{YT8D3D&>Y0GqbL)4j4cqNU8{|IT|%P#=8GURe4q?Q$AB5;qv6SJfcSq;b}$*PF4Y# zk^TmhxwkD02NA|l4@e{34P60HFj10UG4j2B$kVmnC1<^9cEO`$mPrcV1vme^9UI8W zZ@s$VSZbtb@Z?z6Bkw{#mJ)|N{oF;=Bc#|RnR&E5y`~-3;cj=5lkY3d6StnDSPwD6 z2&QhSiE-O1wv@gJb$wMpRNN{KW-iR$o=Z0-E{u33858GUr1WwkA)4b4Lo7~|sHm!j zjXcO11mYTt*x2+8$B02os>HSA91r`L?LnRI&=2oA4m7~8w>-vhwXEJ*P&L&=;T&aB zqD|e?k|m;{fSHlb^6?UG=(FE@`N?GxXQ@vp%Y<$d=qX?-kC4wb1<7tNSiC8>M zUE7pD*PnnJ&f^fN(Uh57;=n56+>{FIrX&QkBjf9sAU$v=D<=_@R zi8Z8L+{TnmT^6hY^S7TlU#1GKt9>n*vd~QO6pK6=M{EHmsN_Z8+UA>fb=3y{fu?V} z3s1Uw=KDxWM#o(Qw=qU>Gv zWQ1t2mF^f33_>nHUf0_H6j#K?WfcI%5X!DV*(=Ld>_iZ9OVFFL9r(f%4L6Kg0659O z;bs5@Z9NgZj1{Za`_;$mI!n*>RM>D!f?R25c{Lb(D2P=5o{9_JRTo2O{0BLiP50c> zr+5l^L{>4c3M0yh=yiD9n`RSe1)?r~qob{3_*kQusaPOOxx6-YGe)9&x~*nwDGuHT zYYTm%ZsrF}E14;vW&{>g+gkg#Gy+?8Yw@K}QysjI(%*SaCJ`l7O=|S08j`nL*35lL zz|6daGz9@^4I#+N#6%U6>b}nNt+d4#o_hYB#TP$p0+iPT)S_$Z+4AQ{3)m2SfH(ao z-#v5QJ(t3ioxI^Nn92yL*Y&RW>NH@*q#$m|zzm1T&iAdHlJXQ-yw#bP_w~bCcF^t4 z^cd&o&SlW|zKr0~fUi-NAhd@KjE1N_L}LnUv3uBusY!xrp$pIY@Rf z;_+5MEU_EIyLO8ed@~&V5WwqJE(r4sZ4aj%!0oMFk4_|2t&KBtPy4DpKnc>$CYYXM z6SHJ|Gl_&v&4gCFn?z^|V`Eahh|xAoX^+tA()93K11<1$U!vLK^YP7=OegDrwicXx zG9lgo^BJJP-l6r+p*H%dFf-{NtiP_864GC!R9$Sb*){s)*qWC{X^7g*bEo3gZuAPlUPx zSm49ho}#WmU4dpYQ7s|9K(?T4e^)1S@00t=2q6a3B4$yNUv^;<~c+c`6`=5^}(c{SfL3Hh{ WNin}nDi?M_AL92B_U7*)ef=|oQDxTv diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot-without-virtual-dom-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-the-dialog-when-open-attribute-gets-added.png similarity index 100% rename from packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot-without-virtual-dom-1-snap.png rename to packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-the-dialog-when-open-attribute-gets-added.png diff --git a/packages/rrweb/test/replay/dialog.test.ts b/packages/rrweb/test/replay/dialog.test.ts index 59b2a9aed0..980ed8e6cf 100644 --- a/packages/rrweb/test/replay/dialog.test.ts +++ b/packages/rrweb/test/replay/dialog.test.ts @@ -82,255 +82,75 @@ describe('dialog', () => { await hideMouseAnimation(page); }); - it('closed dialogs show nothing', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('show show the dialog when open attribute gets added', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events); - window.replayer.pause(${showIncrementalAttributeTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('show show the dialog when open attribute gets added (non virtual)', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events, { useVirtualDom: false }); - window.replayer.pause(${showIncrementalAttributeTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('should close dialog again when open attribute gets removed', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events); - window.replayer.pause(${closeIncrementalAttributeTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', + [ + { + name: 'show the dialog when open attribute gets added', + time: showIncrementalAttributeTime, + }, + { + name: 'should close dialog again when open attribute gets removed', + time: closeIncrementalAttributeTime, + }, + { + name: 'should open dialog with showModal', + time: showModalIncrementalAttributeTime, + }, + { + name: 'should switch between showModal and show', + time: switchBetweenShowModalAndShowIncrementalAttributeTime, + }, + { + name: 'should switch between show and showModal', + time: switchBetweenShowAndShowModalIncrementalAttributeTime, + }, + { + name: 'should open dialog with show in full snapshot', + time: showFullSnapshotTime, + }, + { + name: 'should open dialog with showModal in full snapshot', + time: showModalFullSnapshotTime, + }, + { + name: 'should add an opened dialog with showModal in incremental snapshot', + time: showModalIncrementalAddTime, + }, + ].forEach(({ name, time }) => { + [true, false].forEach((useVirtualDom) => { + it(`${name} (virtual dom: ${useVirtualDom})`, async () => { + await page.evaluate( + `let events = ${JSON.stringify(dialogPlaybackEvents)}`, + ); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events, { useVirtualDom: ${useVirtualDom} }); + window.replayer.pause(${time}); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot({ + fullPage: false, + }); + const defaultImageFilePrefix = + 'dialog-test-ts-test-replay-dialog-test-ts-dialog'; + const kebabCaseName = name + .replace(/ /g, '-') + .replace(/showModal/g, 'show-modal'); + const imageFileName = `${defaultImageFilePrefix}-${kebabCaseName}`; + expect(frameImage).toMatchImageSnapshot({ + customSnapshotIdentifier: imageFileName, + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); }); }); - it('should close dialog again when open attribute gets removed (without virtual dom)', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events, { useVirtualDom: false }); - window.replayer.pause(${closeIncrementalAttributeTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('should open dialog with showModal', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events); - window.replayer.pause(${showModalIncrementalAttributeTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('should open dialog with showModal (without virtual dom)', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events, { useVirtualDom: false }); - window.replayer.pause(${showModalIncrementalAttributeTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('should switch between showModal and show', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events); - window.replayer.pause(${switchBetweenShowModalAndShowIncrementalAttributeTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('should switch between showModal and show (without virtual dom)', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events, { useVirtualDom: false }); - window.replayer.pause(${switchBetweenShowModalAndShowIncrementalAttributeTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('should switch between show and showModal', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events); - window.replayer.pause(${switchBetweenShowAndShowModalIncrementalAttributeTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('should switch between show and showModal (without virtual dom)', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events, { useVirtualDom: false }); - window.replayer.pause(${switchBetweenShowAndShowModalIncrementalAttributeTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('should open dialog with show in full snapshot', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events); - window.replayer.pause(${showFullSnapshotTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('should open dialog with show in full snapshot (without virtual dom)', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events, { useVirtualDom: false }); - window.replayer.pause(${showFullSnapshotTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('should open dialog with showModal in full snapshot', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events); - window.replayer.pause(${showModalFullSnapshotTime}); - `); - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('should open dialog with showModal in full snapshot (without virtual dom)', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - try { - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events, { useVirtualDom: false }); - window.replayer.pause(${showModalFullSnapshotTime}); - `); - } catch (e) { - console.log('error'); - } - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); - - it('should add an opened dialog with showModal in incremental snapshot', async () => { + it('closed dialogs show nothing', async () => { await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); await page.evaluate(` const { Replayer } = rrweb; window.replayer = new Replayer(events); - window.replayer.pause(${showModalIncrementalAddTime}); `); await waitForRAF(page); @@ -340,24 +160,4 @@ describe('dialog', () => { failureThresholdType: 'percent', }); }); - - it('should add an opened dialog with showModal in incremental snapshot (without virtual dom)', async () => { - await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); - try { - await page.evaluate(` - const { Replayer } = rrweb; - window.replayer = new Replayer(events, { useVirtualDom: false }); - window.replayer.pause(${showModalIncrementalAddTime}); - `); - } catch (e) { - console.log('error'); - } - await waitForRAF(page); - - const frameImage = await page!.screenshot(); - expect(frameImage).toMatchImageSnapshot({ - failureThreshold: 0.05, - failureThresholdType: 'percent', - }); - }); }); From 7507f4d57bdfb7739d6e66f5dc78cf620ac23c90 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 15:02:10 +0200 Subject: [PATCH 08/29] Fix eslint error --- packages/rrdom/src/document.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index 51757693bd..aa79ae8ef0 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -550,8 +550,8 @@ export class BaseRRMediaElement extends BaseRRElement { export class BaseRRDialogElement extends BaseRRElement { public readonly tagName = 'DIALOG' as const; public readonly nodeName = 'DIALOG' as const; - public open: boolean = false; - private _isModal: boolean = false; + public open = false; + private _isModal = false; get isModal() { return this._isModal; } From b8679a0de198effff9fe954ebda9eae6246ec5f4 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 15:05:27 +0200 Subject: [PATCH 09/29] feat: Refactor dialog test to use updated attribute name --- packages/rrweb/test/record/dialog.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/rrweb/test/record/dialog.test.ts b/packages/rrweb/test/record/dialog.test.ts index b8eceb0d9e..4fabe29594 100644 --- a/packages/rrweb/test/record/dialog.test.ts +++ b/packages/rrweb/test/record/dialog.test.ts @@ -150,9 +150,7 @@ describe('dialog', () => { const lastEvent = events[events.length - 1]; - expect(lastEvent).toMatchObject( - attributeMutationFactory({ rr_open: null }), - ); + expect(lastEvent).toMatchObject(attributeMutationFactory({ open: null })); }); it('show & close dialog', async () => { From 6ec4727c7e26d8a0e8d8a9337f0f9184b278a38f Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 15:08:23 +0200 Subject: [PATCH 10/29] feat: Update dialog test to include rr_open attribute --- packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap | 3 ++- packages/rrweb/test/record/dialog.test.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap index 6e4cdff78a..6d2cdb868b 100644 --- a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap @@ -97,7 +97,8 @@ exports[`dialog > switch to showModal dialog 1`] = ` { \\"id\\": 8, \\"attributes\\": { - \\"open\\": \\"\\" + \\"open\\": \\"\\", + \\"rr_open\\": \\"non-modal\\" } } ], diff --git a/packages/rrweb/test/record/dialog.test.ts b/packages/rrweb/test/record/dialog.test.ts index 4fabe29594..f16156fc0f 100644 --- a/packages/rrweb/test/record/dialog.test.ts +++ b/packages/rrweb/test/record/dialog.test.ts @@ -181,7 +181,7 @@ describe('dialog', () => { dialog.showModal(); }); - assertSnapshot(events); + await assertSnapshot(events); // const lastEvent = events[events.length - 1]; From f7db71cad34502a4ecca38d2c2b0c3e013bb13e0 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 15:16:59 +0200 Subject: [PATCH 11/29] chore: Add npm dependency happy-dom@14.12.0 --- packages/rrdom/package.json | 1 + yarn.lock | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json index 9e226602ff..bb88e93e53 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -46,6 +46,7 @@ "@typescript-eslint/eslint-plugin": "^5.23.0", "@typescript-eslint/parser": "^5.23.0", "eslint": "^8.15.0", + "happy-dom": "^14.12.0", "puppeteer": "^17.1.3", "typescript": "^4.9.0", "vite": "^5.2.8", diff --git a/yarn.lock b/yarn.lock index b3077360f7..127a7c7d9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6533,6 +6533,15 @@ growly@^1.3.0: resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw== +happy-dom@^14.12.0: + version "14.12.0" + resolved "https://registry.yarnpkg.com/happy-dom/-/happy-dom-14.12.0.tgz#40c748578c6ebfb707e6ae69179d6c541d8f63b3" + integrity sha512-dHcnlGFY2o2CdxfuYpqwSrBrpj/Kuzv4u4f3TU5yHW1GL24dKij4pv1BRjXnXc3uWo8qsCbToF9weaDsm/He8A== + dependencies: + entities "^4.5.0" + webidl-conversions "^7.0.0" + whatwg-mimetype "^3.0.0" + hard-rejection@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz" @@ -11274,6 +11283,11 @@ webidl-conversions@^6.1.0: resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz" @@ -11286,6 +11300,11 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" From b3006743b55491cf2338d16890f38f6b7330bf55 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 15:22:53 +0200 Subject: [PATCH 12/29] Fix dialog test --- packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap index 6d2cdb868b..73afc3a16d 100644 --- a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap @@ -115,7 +115,7 @@ exports[`dialog > switch to showModal dialog 1`] = ` { \\"id\\": 8, \\"attributes\\": { - \\"open\\": null, + \\"open\\": \\"\\", \\"rr_open\\": \\"modal\\" } } From 178ec1f18c1cdf93b244676f51b87058cb788c39 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 16:01:29 +0200 Subject: [PATCH 13/29] Add more test cases for dialog --- packages/rrweb-snapshot/test/rebuild.test.ts | 25 -- .../record/__snapshots__/dialog.test.ts.snap | 359 ++++++++++++++++++ packages/rrweb/test/record/dialog.test.ts | 43 ++- 3 files changed, 397 insertions(+), 30 deletions(-) diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index 69dc7bf9fc..32016815e1 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -53,31 +53,6 @@ describe('rebuild', function () { }); }); - // this doesn't really test anything, maybe remove it? - // contemplate if rrweb-snapshot should trigger .showModal() on dialog elements on rebuild... - describe('rr_open', function () { - it('should call `show` on non-modal dialog', function () { - const node = buildNodeWithSN( - { - id: 1, - tagName: 'dialog', - type: NodeType.Element, - attributes: { - open: '', - }, - childNodes: [], - }, - { - doc: document, - mirror, - hackCss: false, - cache, - }, - ) as HTMLDialogElement; - expect(node?.open).toBe(true); - }); - }); - describe('shadowDom', function () { it('rebuild shadowRoot without siblings', function () { const node = buildNodeWithSN( diff --git a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap index 73afc3a16d..233b348636 100644 --- a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap @@ -1,5 +1,364 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`dialog > add dialog and show 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"text/javascript\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 5 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"I'm a dialog\\", + \\"id\\": 9 + } + ], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 10 + } + ], + \\"id\\": 6 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 6, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": { + \\"open\\": \\"\\", + \\"rr_open\\": \\"non-modal\\" + }, + \\"childNodes\\": [], + \\"id\\": 11 + } + } + ] + } + } +]" +`; + +exports[`dialog > add dialog and showModal 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"text/javascript\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 5 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"I'm a dialog\\", + \\"id\\": 9 + } + ], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 10 + } + ], + \\"id\\": 6 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 6, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": { + \\"open\\": \\"\\", + \\"rr_open\\": \\"modal\\" + }, + \\"childNodes\\": [], + \\"id\\": 11 + } + } + ] + } + } +]" +`; + +exports[`dialog > switch to show dialog 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"text/javascript\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 5 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"I'm a dialog\\", + \\"id\\": 9 + } + ], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 10 + } + ], + \\"id\\": 6 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 8, + \\"attributes\\": { + \\"open\\": \\"\\", + \\"rr_open\\": \\"modal\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 8, + \\"attributes\\": { + \\"open\\": \\"\\", + \\"rr_open\\": \\"non-modal\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + exports[`dialog > switch to showModal dialog 1`] = ` "[ { diff --git a/packages/rrweb/test/record/dialog.test.ts b/packages/rrweb/test/record/dialog.test.ts index f16156fc0f..8cac8ea5cd 100644 --- a/packages/rrweb/test/record/dialog.test.ts +++ b/packages/rrweb/test/record/dialog.test.ts @@ -23,10 +23,10 @@ import { recordOptions } from '../../src/types'; // √ dialog show // √ dialog showModal // √ dialog close -// - dialog open and close -// - dialog showModal and switch to show +// √ dialog open and close +// √ dialog showModal and switch to show // √ dialog show and switch to showModal -// - dialog add and showModal +// √ dialog add and showModal // - multiple dialogs open, playback order interface IWindow extends Window { @@ -182,9 +182,42 @@ describe('dialog', () => { }); await assertSnapshot(events); + }); + + it('switch to show dialog', async () => { + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.showModal(); + }); + await waitForRAF(page); + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.close(); + dialog.show(); + }); + + await assertSnapshot(events); + }); + + it('add dialog and showModal', async () => { + await page.evaluate(() => { + const dialog = document.createElement('dialog') as HTMLDialogElement; + document.body.appendChild(dialog); + dialog.showModal(); + }); + await waitForRAF(page); + + await assertSnapshot(events); + }); - // const lastEvent = events[events.length - 1]; + it('add dialog and show', async () => { + await page.evaluate(() => { + const dialog = document.createElement('dialog') as HTMLDialogElement; + document.body.appendChild(dialog); + dialog.show(); + }); + await waitForRAF(page); - // expect(lastEvent).toMatchObject(attributeMutationFactory({ open: null })); + await assertSnapshot(events); }); }); From 4229391267346c01da692958e65f2fa295346da5 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 16:15:57 +0200 Subject: [PATCH 14/29] Add changesets --- .changeset/happy-carrots-hide.md | 5 +++++ .changeset/silly-knives-chew.md | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/happy-carrots-hide.md create mode 100644 .changeset/silly-knives-chew.md diff --git a/.changeset/happy-carrots-hide.md b/.changeset/happy-carrots-hide.md new file mode 100644 index 0000000000..c5825b4024 --- /dev/null +++ b/.changeset/happy-carrots-hide.md @@ -0,0 +1,5 @@ +--- +"rrweb-snapshot": minor +--- + +Record dialog's modal status for replay in rrweb. (Currently triggering `dialog.showModal()` is not supported in rrweb-snapshot's rebuild) diff --git a/.changeset/silly-knives-chew.md b/.changeset/silly-knives-chew.md new file mode 100644 index 0000000000..98acbbebf8 --- /dev/null +++ b/.changeset/silly-knives-chew.md @@ -0,0 +1,7 @@ +--- +"rrdom": minor +"rrweb": minor +"@rrweb/types": minor +--- + +Support top-layer components. Fixes #1381. From b0065cecbdde30fdb345a41781a9fb20cb4fefbf Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 16:39:15 +0200 Subject: [PATCH 15/29] Clean up naming --- packages/rrdom/src/document.ts | 20 +++++++++-------- packages/rrweb/src/replay/dialog/index.ts | 27 +++++++++++++++++------ packages/rrweb/src/replay/index.ts | 25 +++++++++++---------- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index aa79ae8ef0..ab308551ce 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -550,22 +550,24 @@ export class BaseRRMediaElement extends BaseRRElement { export class BaseRRDialogElement extends BaseRRElement { public readonly tagName = 'DIALOG' as const; public readonly nodeName = 'DIALOG' as const; - public open = false; - private _isModal = false; + get isModal() { - return this._isModal; + return this.getAttribute('rr_open') === 'modal'; + } + get open() { + return this.getAttribute('open') !== null; } public close() { - this.open = false; - this._isModal = false; + this.removeAttribute('open'); + this.removeAttribute('rr_open'); } public show() { - this.open = true; - this._isModal = false; + this.setAttribute('open', ''); + this.setAttribute('rr_open', 'non-modal'); } public showModal() { - this.open = true; - this._isModal = true; + this.setAttribute('open', ''); + this.setAttribute('rr_open', 'modal'); } } diff --git a/packages/rrweb/src/replay/dialog/index.ts b/packages/rrweb/src/replay/dialog/index.ts index 290a2ccbac..edd024072f 100644 --- a/packages/rrweb/src/replay/dialog/index.ts +++ b/packages/rrweb/src/replay/dialog/index.ts @@ -1,20 +1,28 @@ import type { attributeMutation } from '@rrweb/types'; import { RRNode } from 'rrdom'; -export function triggerShowModalForModals( +/** + * Checks if the dialog is a top level dialog and applies the dialog to the top level + * @param node - potential dialog element to apply top level `showModal()` to, or other node (which will be ignored) + * @param attributeMutation - the attribute mutation used to change the dialog (optional) + * @returns void + */ +export function applyDialogToTopLevel( node: HTMLDialogElement | Node | RRNode, attributeMutation?: attributeMutation, -) { +): void { if (node.nodeName !== 'DIALOG' || node instanceof RRNode) return; const dialog = node as HTMLDialogElement; const isOpen = dialog.open; const isModal = isOpen && dialog.matches('dialog:modal'); + const rrOpen = dialog.getAttribute('rr_open'); const shouldBeOpen = typeof attributeMutation?.attributes.open === 'string' || typeof dialog.getAttribute('open') === 'string'; - const shouldBeModal = dialog.getAttribute('rr_open') === 'modal'; - const shouldBeNonModal = dialog.getAttribute('rr_open') === 'non-modal'; + const shouldBeModal = rrOpen === 'modal'; + const shouldBeNonModal = rrOpen === 'non-modal'; + const modeChanged = (isModal && shouldBeNonModal) || (!isModal && shouldBeModal); @@ -26,17 +34,22 @@ export function triggerShowModalForModals( } if (isOpen) dialog.close(); - if (!shouldBeOpen) return; if (shouldBeModal) dialog.showModal(); else dialog.show(); } -export function triggerCloseForModals( +/** + * Check if the dialog is a top level dialog and removes the dialog from the top level if necessary + * @param node - potential dialog element to remove from top level, or other node (which will be ignored) + * @param attributeMutation - the attribute mutation used to change the dialog + * @returns void + */ +export function removeDialogFromTopLevel( node: HTMLDialogElement | Node | RRNode, attributeMutation: attributeMutation, -) { +): void { if (node.nodeName !== 'DIALOG' || node instanceof RRNode) return; const dialog = node as HTMLDialogElement; diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index c4d98627bc..a3a318b88c 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -86,7 +86,7 @@ import './styles/style.css'; import canvasMutation from './canvas'; import { deserializeArg } from './canvas/deserialize-args'; import { MediaManager } from './media'; -import { triggerShowModalForModals, triggerCloseForModals } from './dialog'; +import { applyDialogToTopLevel, removeDialogFromTopLevel } from './dialog'; const SKIP_TIME_INTERVAL = 5 * 1000; @@ -246,7 +246,7 @@ export class Replayer { this.applyStyleDeclaration(data, styleSheet); }, afterAppend: (node: Node, id: number) => { - triggerShowModalForModals(node); + applyDialogToTopLevel(node); for (const plugin of this.config.plugins || []) { if (plugin.onBuild) plugin.onBuild(node, { id, replayer: this }); } @@ -853,7 +853,7 @@ export class Replayer { } const { documentElement, head } = this.iframe.contentDocument; this.insertStyleRules(documentElement, head); - collectedDialogs.forEach((d) => triggerShowModalForModals(d)); + collectedDialogs.forEach((d) => applyDialogToTopLevel(d)); if (!this.service.state.matches('playing')) { this.iframe.contentDocument .getElementsByTagName('html')[0] @@ -916,10 +916,11 @@ export class Replayer { type TNode = typeof mirror extends Mirror ? Node : RRNode; type TMirror = typeof mirror extends Mirror ? Mirror : RRDOMMirror; - const collected: AppendedIframe[] = []; + const collectedIframes: AppendedIframe[] = []; + const collectedDialogs = new Set(); const afterAppend = (builtNode: Node, id: number) => { - triggerShowModalForModals(builtNode); - this.collectIframeAndAttachDocument(collected, builtNode); + if (builtNode.nodeName === 'DIALOG') collectedDialogs.add(builtNode as HTMLDialogElement); + this.collectIframeAndAttachDocument(collectedIframes, builtNode); const sn = (mirror as TMirror).getMeta(builtNode as unknown as TNode); if ( sn?.type === NodeType.Element && @@ -953,7 +954,8 @@ export class Replayer { }); afterAppend(iframeEl.contentDocument! as Document, mutation.node.id); - for (const { mutationInQueue, builtNode } of collected) { + collectedDialogs.forEach((d) => applyDialogToTopLevel(d)); + for (const { mutationInQueue, builtNode } of collectedIframes) { this.attachDocumentToIframe(mutationInQueue, builtNode); this.newDocumentQueue = this.newDocumentQueue.filter( (m) => m !== mutationInQueue, @@ -1539,7 +1541,7 @@ export class Replayer { const afterAppend = (node: Node | RRNode, id: number) => { // Skip the plugin onBuild callback for virtual dom if (this.usingVirtualDom) return; - triggerShowModalForModals(node); + applyDialogToTopLevel(node); for (const plugin of this.config.plugins || []) { if (plugin.onBuild) plugin.onBuild(node, { id, replayer: this }); } @@ -1743,10 +1745,9 @@ export class Replayer { if (typeof attributeName === 'string') { const value = mutation.attributes[attributeName]; if (value === null) { - if (attributeName === 'open') { - triggerCloseForModals(target, mutation); - } (target as Element | RRElement).removeAttribute(attributeName); + if (attributeName === 'open') + removeDialogFromTopLevel(target, mutation); } else if (typeof value === 'string') { try { // When building snapshot, some link styles haven't loaded. Then they are loaded, they will be inlined as incremental mutation change of attribute. We need to replace the old elements whose styles aren't inlined. @@ -1804,7 +1805,7 @@ export class Replayer { } if (attributeName === 'rr_open' && target.nodeName === 'DIALOG') { - triggerShowModalForModals(target, mutation); + applyDialogToTopLevel(target, mutation); } } catch (error) { this.warn( From 25678f4861c905fc6b7ead460c4a866d1b7f49e0 Mon Sep 17 00:00:00 2001 From: Juice10 Date: Wed, 12 Jun 2024 14:40:22 +0000 Subject: [PATCH 16/29] Apply formatting changes --- packages/rrweb/src/replay/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index a3a318b88c..8605cfb5c2 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -919,7 +919,8 @@ export class Replayer { const collectedIframes: AppendedIframe[] = []; const collectedDialogs = new Set(); const afterAppend = (builtNode: Node, id: number) => { - if (builtNode.nodeName === 'DIALOG') collectedDialogs.add(builtNode as HTMLDialogElement); + if (builtNode.nodeName === 'DIALOG') + collectedDialogs.add(builtNode as HTMLDialogElement); this.collectIframeAndAttachDocument(collectedIframes, builtNode); const sn = (mirror as TMirror).getMeta(builtNode as unknown as TNode); if ( From b71c8b70a59bd52ff25a4a3c976f6a8e82a4ac3e Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 16:49:19 +0200 Subject: [PATCH 17/29] Refactor dialog open code --- packages/rrdom/src/diff.ts | 14 +++++++------- packages/rrweb/src/replay/dialog/index.ts | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index a1caf2a4e9..5745d6b272 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -291,16 +291,16 @@ function diffAfterUpdatingChildren( const rrDialog = newRRElement as unknown as RRDialogElement; const wasOpen = dialog.open; const wasModal = dialog.matches('dialog:modal'); - const isOpen = typeof rrDialog.getAttribute('open') === 'string'; - const isModal = rrDialog.getAttribute('rr_open') === 'modal'; + const shouldBeOpen = rrDialog.open; + const shouldBeModal = rrDialog.isModal; - const modeChanged = wasModal !== isModal; - const openChanged = wasOpen !== isOpen; + const modalChanged = wasModal !== shouldBeModal; + const openChanged = wasOpen !== shouldBeOpen; - if (modeChanged || wasOpen !== isOpen) dialog.close(); - if (isOpen && (openChanged || modeChanged)) { + if (modalChanged || (wasOpen && openChanged)) dialog.close(); + if (shouldBeOpen && (openChanged || modalChanged)) { try { - if (isModal) dialog.showModal(); + if (shouldBeModal) dialog.showModal(); else dialog.show(); } catch (e) { console.warn(e); diff --git a/packages/rrweb/src/replay/dialog/index.ts b/packages/rrweb/src/replay/dialog/index.ts index edd024072f..ea9dd6434a 100644 --- a/packages/rrweb/src/replay/dialog/index.ts +++ b/packages/rrweb/src/replay/dialog/index.ts @@ -23,10 +23,10 @@ export function applyDialogToTopLevel( const shouldBeModal = rrOpen === 'modal'; const shouldBeNonModal = rrOpen === 'non-modal'; - const modeChanged = + const modalChanged = (isModal && shouldBeNonModal) || (!isModal && shouldBeModal); - if (isOpen && !modeChanged) return; + if (isOpen && !modalChanged) return; // complain if dialog is not attached to the dom if (!dialog.isConnected) { console.warn('dialog is not attached to the dom', dialog); From e7316360a74c8a3226917454eda679cc6dfb57d7 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 16:51:09 +0200 Subject: [PATCH 18/29] Revert changed code that doesn't do anything --- packages/rrdom/src/diff.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 5745d6b272..1c84ad2cc6 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -357,9 +357,8 @@ function diffProps( else oldTree.setAttribute(name, newValue); } - for (const { name } of Array.from(oldAttributes)) { + for (const { name } of Array.from(oldAttributes)) if (!(name in newAttributes)) oldTree.removeAttribute(name); - } newTree.scrollLeft && (oldTree.scrollLeft = newTree.scrollLeft); newTree.scrollTop && (oldTree.scrollTop = newTree.scrollTop); } From 184e4fa1aff40d98c982f8e79e03f3c233a81691 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 16:55:43 +0200 Subject: [PATCH 19/29] Add documentation for unimplemented type --- packages/rrweb-snapshot/src/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts index e6efc58c10..9566102ea8 100644 --- a/packages/rrweb-snapshot/src/types.ts +++ b/packages/rrweb-snapshot/src/types.ts @@ -112,6 +112,11 @@ export type DialogAttributes = { * by adding an `open` attribute. */ rr_open: 'modal' | 'non-modal'; + /** + * Currently unimplemented, but in future can be used to: + * Represents the order of which of the dialog was opened. + * This is useful for replaying the dialog `.showModal()` in the correct order. + */ // rr_open_index?: number; }; From 6a58edc026f93ce89eba6d7391a39158d409383c Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 12 Jun 2024 17:01:09 +0200 Subject: [PATCH 20/29] chore: Remove unnecessary comments in dialog.test.ts --- packages/rrweb/test/replay/dialog.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/rrweb/test/replay/dialog.test.ts b/packages/rrweb/test/replay/dialog.test.ts index 980ed8e6cf..6eb7b12ab9 100644 --- a/packages/rrweb/test/replay/dialog.test.ts +++ b/packages/rrweb/test/replay/dialog.test.ts @@ -41,8 +41,6 @@ expect.extend({ toMatchImageSnapshot }); // √ dialog open and close (switching from modal to non modal) // √ dialog open and close (switching from non modal to modal) // - multiple dialogs open, recording order -// == on rrdom == -// - that the modal modes are recorded... describe('dialog', () => { vi.setConfig({ testTimeout: 100_000 }); From 0df7994663eb661fa87f391328ee32e3b3572a9d Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Jun 2024 12:08:36 +0200 Subject: [PATCH 21/29] rename rr_open to rr_openMode --- packages/rrdom/src/document.ts | 8 ++++---- packages/rrdom/test/diff/dialog.test.ts | 12 ++++++------ packages/rrweb-snapshot/src/rebuild.ts | 7 +++++-- packages/rrweb-snapshot/src/snapshot.ts | 2 +- packages/rrweb-snapshot/src/types.ts | 4 ++-- .../test/__snapshots__/integration.test.ts.snap | 4 ++-- packages/rrweb/src/record/mutation.ts | 4 ++-- packages/rrweb/src/replay/dialog/index.ts | 4 ++-- packages/rrweb/src/replay/index.ts | 5 ++++- packages/rrweb/test/events/dialog-playback.ts | 14 +++++++------- .../test/record/__snapshots__/dialog.test.ts.snap | 12 ++++++------ packages/rrweb/test/record/dialog.test.ts | 2 +- 12 files changed, 42 insertions(+), 36 deletions(-) diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index ab308551ce..00318eb298 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -552,22 +552,22 @@ export class BaseRRDialogElement extends BaseRRElement { public readonly nodeName = 'DIALOG' as const; get isModal() { - return this.getAttribute('rr_open') === 'modal'; + return this.getAttribute('rr_openMode') === 'modal'; } get open() { return this.getAttribute('open') !== null; } public close() { this.removeAttribute('open'); - this.removeAttribute('rr_open'); + this.removeAttribute('rr_openMode'); } public show() { this.setAttribute('open', ''); - this.setAttribute('rr_open', 'non-modal'); + this.setAttribute('rr_openMode', 'non-modal'); } public showModal() { this.setAttribute('open', ''); - this.setAttribute('rr_open', 'modal'); + this.setAttribute('rr_openMode', 'modal'); } } diff --git a/packages/rrdom/test/diff/dialog.test.ts b/packages/rrdom/test/diff/dialog.test.ts index 8721adc196..6f1914c48e 100644 --- a/packages/rrdom/test/diff/dialog.test.ts +++ b/packages/rrdom/test/diff/dialog.test.ts @@ -54,7 +54,7 @@ describe('diff algorithm for rrdom', () => { describe('diff dialog elements', () => { vi.setConfig({ testTimeout: 60_000 }); - it('should trigger `showModal` on rr_open:modal attributes', () => { + it('should trigger `showModal` on rr_openMode:modal attributes', () => { const tagName = 'DIALOG'; const node = document.createElement(tagName) as HTMLDialogElement; vi.spyOn(node, 'matches').mockReturnValue(false); // matches is used to check if the dialog was opened with showModal @@ -62,7 +62,7 @@ describe('diff algorithm for rrdom', () => { const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); - rrNode.attributes = { rr_open: 'modal', open: '' }; + rrNode.attributes = { rr_openMode: 'modal', open: '' }; mirror.add(node, elementSn); rrDocument.mirror.add(rrNode, elementSn); @@ -71,7 +71,7 @@ describe('diff algorithm for rrdom', () => { expect(showModalFn).toBeCalled(); }); - it('should trigger `close` on rr_open removed', () => { + it('should trigger `close` on rr_openMode removed', () => { const tagName = 'DIALOG'; const node = document.createElement(tagName) as HTMLDialogElement; node.showModal(); @@ -89,17 +89,17 @@ describe('diff algorithm for rrdom', () => { expect(closeFn).toBeCalled(); }); - it('should not trigger `close` on rr_open is kept', () => { + it('should not trigger `close` on rr_openMode is kept', () => { const tagName = 'DIALOG'; const node = document.createElement(tagName) as HTMLDialogElement; vi.spyOn(node, 'matches').mockReturnValue(true); // matches is used to check if the dialog was opened with showModal - node.setAttribute('rr_open', 'modal'); + node.setAttribute('rr_openMode', 'modal'); node.setAttribute('open', ''); const closeFn = vi.spyOn(node, 'close'); const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); - rrNode.attributes = { rr_open: 'modal', open: '' }; + rrNode.attributes = { rr_openMode: 'modal', open: '' }; mirror.add(node, elementSn); rrDocument.mirror.add(rrNode, elementSn); diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 7f58a3bdd9..8a56bb3fea 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -353,8 +353,11 @@ function buildNode( (node as HTMLMediaElement).loop = value; } else if (name === 'rr_mediaVolume' && typeof value === 'number') { (node as HTMLMediaElement).volume = value; - } else if (name === 'rr_open') { - (node as HTMLDialogElement).setAttribute('rr_open', value as string); // keep this attribute for rrweb to trigger showModal + } else if (name === 'rr_openMode') { + (node as HTMLDialogElement).setAttribute( + 'rr_openMode', + value as string, + ); // keep this attribute for rrweb to trigger showModal } } diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index ad1db0ceaf..ff1d238dbb 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -719,7 +719,7 @@ function serializeElementNode( // register what type of dialog is this // `modal` or `non-modal` // this is used to trigger `showModal()` or `show()` on replay (outside of rrweb-snapshot, in rrweb) - (attributes as DialogAttributes).rr_open = n.matches('dialog:modal') + (attributes as DialogAttributes).rr_openMode = n.matches('dialog:modal') ? 'modal' : 'non-modal'; } diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts index 8a4c1a5a06..e3687cd6cc 100644 --- a/packages/rrweb-snapshot/src/types.ts +++ b/packages/rrweb-snapshot/src/types.ts @@ -111,13 +111,13 @@ export type DialogAttributes = { * `non-modal` means the dialog is opened with `show()` or * by adding an `open` attribute. */ - rr_open: 'modal' | 'non-modal'; + rr_openMode: 'modal' | 'non-modal'; /** * Currently unimplemented, but in future can be used to: * Represents the order of which of the dialog was opened. * This is useful for replaying the dialog `.showModal()` in the correct order. */ - // rr_open_index?: number; + // rr_openMode_index?: number; }; // @deprecated diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index 60844bdf10..793e167c6b 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -31,7 +31,7 @@ exports[`dialog integration tests > should capture open attribute for modal dial \\"tagName\\": \\"dialog\\", \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_open\\": \\"modal\\" + \\"rr_openMode\\": \\"modal\\" }, \\"childNodes\\": [ { @@ -90,7 +90,7 @@ exports[`dialog integration tests > should capture open attribute for non modal \\"tagName\\": \\"dialog\\", \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_open\\": \\"non-modal\\" + \\"rr_openMode\\": \\"non-modal\\" }, \\"childNodes\\": [ { diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 4d78da57df..5562b8be84 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -665,9 +665,9 @@ export default class MutationBuffer { } } else if (attributeName === 'open' && target.tagName === 'DIALOG') { if (target.matches('dialog:modal')) { - item.attributes['rr_open'] = 'modal'; + item.attributes['rr_openMode'] = 'modal'; } else { - item.attributes['rr_open'] = 'non-modal'; + item.attributes['rr_openMode'] = 'non-modal'; } } } diff --git a/packages/rrweb/src/replay/dialog/index.ts b/packages/rrweb/src/replay/dialog/index.ts index ea9dd6434a..49c3c7d45f 100644 --- a/packages/rrweb/src/replay/dialog/index.ts +++ b/packages/rrweb/src/replay/dialog/index.ts @@ -15,7 +15,7 @@ export function applyDialogToTopLevel( const dialog = node as HTMLDialogElement; const isOpen = dialog.open; const isModal = isOpen && dialog.matches('dialog:modal'); - const rrOpen = dialog.getAttribute('rr_open'); + const rrOpen = dialog.getAttribute('rr_openMode'); const shouldBeOpen = typeof attributeMutation?.attributes.open === 'string' || @@ -61,6 +61,6 @@ export function removeDialogFromTopLevel( if (attributeMutation.attributes.open === null) { dialog.removeAttribute('open'); - dialog.removeAttribute('rr_open'); + dialog.removeAttribute('rr_openMode'); } } diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 5b947fd3c8..83f922f3f6 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1807,7 +1807,10 @@ export class Replayer { ); } - if (attributeName === 'rr_open' && target.nodeName === 'DIALOG') { + if ( + attributeName === 'rr_openMode' && + target.nodeName === 'DIALOG' + ) { applyDialogToTopLevel(target, mutation); } } catch (error) { diff --git a/packages/rrweb/test/events/dialog-playback.ts b/packages/rrweb/test/events/dialog-playback.ts index e5c2bc36b7..9277af4d12 100644 --- a/packages/rrweb/test/events/dialog-playback.ts +++ b/packages/rrweb/test/events/dialog-playback.ts @@ -132,7 +132,7 @@ const events: eventWithTime[] = [ attributes: [ { id: 27, - attributes: { open: '', rr_open: 'non-modal', class: 'show' }, + attributes: { open: '', rr_openMode: 'non-modal', class: 'show' }, }, ], }, @@ -166,7 +166,7 @@ const events: eventWithTime[] = [ attributes: [ { id: 27, - attributes: { rr_open: 'modal', open: '', class: 'showModal' }, + attributes: { rr_openMode: 'modal', open: '', class: 'showModal' }, }, ], }, @@ -184,7 +184,7 @@ const events: eventWithTime[] = [ { id: 27, attributes: { - rr_open: 'non-modal', + rr_openMode: 'non-modal', class: 'switched-from-show-modal-to-show', }, }, @@ -205,7 +205,7 @@ const events: eventWithTime[] = [ { id: 27, attributes: { - rr_open: 'modal', + rr_openMode: 'modal', class: 'switched-from-show-to-show-modal', }, }, @@ -285,7 +285,7 @@ const events: eventWithTime[] = [ tagName: 'dialog', attributes: { open: '', - rr_open: 'non-modal', + rr_openMode: 'non-modal', style: 'outline: blue solid 1px;', }, childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }], @@ -384,7 +384,7 @@ const events: eventWithTime[] = [ type: 2, tagName: 'dialog', attributes: { - rr_open: 'modal', + rr_openMode: 'modal', open: '', style: 'outline: blue solid 1px;', class: 'existing-1', @@ -431,7 +431,7 @@ const events: eventWithTime[] = [ type: 2, tagName: 'dialog', attributes: { - rr_open: 'modal', + rr_openMode: 'modal', open: '', style: 'outline: orange solid 1px;', class: 'new-dialog', diff --git a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap index 233b348636..0d88a22916 100644 --- a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap @@ -104,7 +104,7 @@ exports[`dialog > add dialog and show 1`] = ` \\"tagName\\": \\"dialog\\", \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_open\\": \\"non-modal\\" + \\"rr_openMode\\": \\"non-modal\\" }, \\"childNodes\\": [], \\"id\\": 11 @@ -220,7 +220,7 @@ exports[`dialog > add dialog and showModal 1`] = ` \\"tagName\\": \\"dialog\\", \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_open\\": \\"modal\\" + \\"rr_openMode\\": \\"modal\\" }, \\"childNodes\\": [], \\"id\\": 11 @@ -330,7 +330,7 @@ exports[`dialog > switch to show dialog 1`] = ` \\"id\\": 8, \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_open\\": \\"modal\\" + \\"rr_openMode\\": \\"modal\\" } } ], @@ -348,7 +348,7 @@ exports[`dialog > switch to show dialog 1`] = ` \\"id\\": 8, \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_open\\": \\"non-modal\\" + \\"rr_openMode\\": \\"non-modal\\" } } ], @@ -457,7 +457,7 @@ exports[`dialog > switch to showModal dialog 1`] = ` \\"id\\": 8, \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_open\\": \\"non-modal\\" + \\"rr_openMode\\": \\"non-modal\\" } } ], @@ -475,7 +475,7 @@ exports[`dialog > switch to showModal dialog 1`] = ` \\"id\\": 8, \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_open\\": \\"modal\\" + \\"rr_openMode\\": \\"modal\\" } } ], diff --git a/packages/rrweb/test/record/dialog.test.ts b/packages/rrweb/test/record/dialog.test.ts index 8cac8ea5cd..4d128e1c5f 100644 --- a/packages/rrweb/test/record/dialog.test.ts +++ b/packages/rrweb/test/record/dialog.test.ts @@ -133,7 +133,7 @@ describe('dialog', () => { const lastEvent = events[events.length - 1]; expect(lastEvent).toMatchObject( - attributeMutationFactory({ rr_open: 'modal' }), + attributeMutationFactory({ rr_openMode: 'modal' }), ); }); From 1ce8f70a6f14839a224103bbaf25e7b58877cd07 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Jun 2024 12:11:25 +0200 Subject: [PATCH 22/29] Replace todo with a skipped test --- packages/rrweb/test/record/dialog.test.ts | 28 ++++++++++++++--------- packages/rrweb/test/replay/dialog.test.ts | 19 +++------------ 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/rrweb/test/record/dialog.test.ts b/packages/rrweb/test/record/dialog.test.ts index 4d128e1c5f..715a287111 100644 --- a/packages/rrweb/test/record/dialog.test.ts +++ b/packages/rrweb/test/record/dialog.test.ts @@ -18,17 +18,6 @@ import { } from '@rrweb/types'; import { recordOptions } from '../../src/types'; -// TODO: test the following: -// == on record == -// √ dialog show -// √ dialog showModal -// √ dialog close -// √ dialog open and close -// √ dialog showModal and switch to show -// √ dialog show and switch to showModal -// √ dialog add and showModal -// - multiple dialogs open, playback order - interface IWindow extends Window { rrweb: { record: ( @@ -220,4 +209,21 @@ describe('dialog', () => { await assertSnapshot(events); }); + + // TODO: implement me in the future + it.skip('should record playback order with multiple dialogs opening', async () => { + await page.evaluate(() => { + const dialog1 = document.createElement('dialog') as HTMLDialogElement; + dialog1.className = 'dialog1'; + document.body.appendChild(dialog1); + const dialog2 = document.createElement('dialog') as HTMLDialogElement; + dialog1.className = 'dialog2'; + document.body.appendChild(dialog2); + dialog2.showModal(); // <== Note that dialog TWO is being triggered first + dialog1.showModal(); + }); + + await waitForRAF(page); + await assertSnapshot(events); // <== This should trigger showModal() on dialog2 first, then dialog1 + }); }); diff --git a/packages/rrweb/test/replay/dialog.test.ts b/packages/rrweb/test/replay/dialog.test.ts index 6eb7b12ab9..6bd591f374 100644 --- a/packages/rrweb/test/replay/dialog.test.ts +++ b/packages/rrweb/test/replay/dialog.test.ts @@ -26,22 +26,6 @@ import { expect.extend({ toMatchImageSnapshot }); -// TODO: test the following: -// == on playback == -// - dialog open (standard) full snapshot -// √ dialog open (standard) incremental (virtual dom) -// √ dialog open (standard) incremental (non virtual dom) -// √ dialog open (showModal) full snapshot -// √ dialog open (showModal) incremental (virtual dom) -// √ dialog open (showModal) incremental (non virtual dom) -// √ append dialog open (showModal) incremental (virtual dom) -// √ append dialog open (showModal) incremental (non virtual dom) -// √ dialog close (rrdom) -// √ dialog close (non virtual dom) -// √ dialog open and close (switching from modal to non modal) -// √ dialog open and close (switching from non modal to modal) -// - multiple dialogs open, recording order - describe('dialog', () => { vi.setConfig({ testTimeout: 100_000 }); let code: ISuite['code']; @@ -158,4 +142,7 @@ describe('dialog', () => { failureThresholdType: 'percent', }); }); + + // TODO: implement me in the future + it.skip('should trigger showModal on multiple dialogs in a specific order'); }); From 8aeb47f9d1d1bbe683fc82ae5ae4ba28db710c5e Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Jun 2024 12:28:22 +0200 Subject: [PATCH 23/29] Add better logging for CI --- .github/workflows/ci-cd.yml | 2 +- packages/rrweb/test/replay/dialog.test.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 7fe6a4bd54..b65caf96cd 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -38,5 +38,5 @@ jobs: if: failure() with: name: image-diff - path: packages/rrweb/test/*/__image_snapshots__/__diff_output__/*.png + path: packages/**/__image_snapshots__/__diff_output__/*.png if-no-files-found: ignore diff --git a/packages/rrweb/test/replay/dialog.test.ts b/packages/rrweb/test/replay/dialog.test.ts index 6bd591f374..8bcb202159 100644 --- a/packages/rrweb/test/replay/dialog.test.ts +++ b/packages/rrweb/test/replay/dialog.test.ts @@ -123,6 +123,8 @@ describe('dialog', () => { customSnapshotIdentifier: imageFileName, failureThreshold: 0.05, failureThresholdType: 'percent', + dumpDiffToConsole: true, + storeReceivedOnFailure: true, }); }); }); From 985ac9f768391bea6f49c8f3a1f8f91390fb100f Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Jun 2024 14:21:07 +0200 Subject: [PATCH 24/29] Rename rr_openMode to rr_open_mode rrdom downcases all attribute names which made `rr_openMode` tricky to deal with --- packages/rrdom/src/document.ts | 8 ++++---- packages/rrdom/test/diff/dialog.test.ts | 12 ++++++------ packages/rrweb-snapshot/src/rebuild.ts | 4 ++-- packages/rrweb-snapshot/src/snapshot.ts | 2 +- packages/rrweb-snapshot/src/types.ts | 4 ++-- .../test/__snapshots__/integration.test.ts.snap | 4 ++-- packages/rrweb/src/record/mutation.ts | 4 ++-- packages/rrweb/src/replay/dialog/index.ts | 4 ++-- packages/rrweb/src/replay/index.ts | 2 +- packages/rrweb/test/events/dialog-playback.ts | 14 +++++++------- .../test/record/__snapshots__/dialog.test.ts.snap | 12 ++++++------ packages/rrweb/test/record/dialog.test.ts | 2 +- 12 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index 00318eb298..7df931860a 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -552,22 +552,22 @@ export class BaseRRDialogElement extends BaseRRElement { public readonly nodeName = 'DIALOG' as const; get isModal() { - return this.getAttribute('rr_openMode') === 'modal'; + return this.getAttribute('rr_open_mode') === 'modal'; } get open() { return this.getAttribute('open') !== null; } public close() { this.removeAttribute('open'); - this.removeAttribute('rr_openMode'); + this.removeAttribute('rr_open_mode'); } public show() { this.setAttribute('open', ''); - this.setAttribute('rr_openMode', 'non-modal'); + this.setAttribute('rr_open_mode', 'non-modal'); } public showModal() { this.setAttribute('open', ''); - this.setAttribute('rr_openMode', 'modal'); + this.setAttribute('rr_open_mode', 'modal'); } } diff --git a/packages/rrdom/test/diff/dialog.test.ts b/packages/rrdom/test/diff/dialog.test.ts index 6f1914c48e..11a80e6ec5 100644 --- a/packages/rrdom/test/diff/dialog.test.ts +++ b/packages/rrdom/test/diff/dialog.test.ts @@ -54,7 +54,7 @@ describe('diff algorithm for rrdom', () => { describe('diff dialog elements', () => { vi.setConfig({ testTimeout: 60_000 }); - it('should trigger `showModal` on rr_openMode:modal attributes', () => { + it('should trigger `showModal` on rr_open_mode:modal attributes', () => { const tagName = 'DIALOG'; const node = document.createElement(tagName) as HTMLDialogElement; vi.spyOn(node, 'matches').mockReturnValue(false); // matches is used to check if the dialog was opened with showModal @@ -62,7 +62,7 @@ describe('diff algorithm for rrdom', () => { const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); - rrNode.attributes = { rr_openMode: 'modal', open: '' }; + rrNode.attributes = { rr_open_mode: 'modal', open: '' }; mirror.add(node, elementSn); rrDocument.mirror.add(rrNode, elementSn); @@ -71,7 +71,7 @@ describe('diff algorithm for rrdom', () => { expect(showModalFn).toBeCalled(); }); - it('should trigger `close` on rr_openMode removed', () => { + it('should trigger `close` on rr_open_mode removed', () => { const tagName = 'DIALOG'; const node = document.createElement(tagName) as HTMLDialogElement; node.showModal(); @@ -89,17 +89,17 @@ describe('diff algorithm for rrdom', () => { expect(closeFn).toBeCalled(); }); - it('should not trigger `close` on rr_openMode is kept', () => { + it('should not trigger `close` on rr_open_mode is kept', () => { const tagName = 'DIALOG'; const node = document.createElement(tagName) as HTMLDialogElement; vi.spyOn(node, 'matches').mockReturnValue(true); // matches is used to check if the dialog was opened with showModal - node.setAttribute('rr_openMode', 'modal'); + node.setAttribute('rr_open_mode', 'modal'); node.setAttribute('open', ''); const closeFn = vi.spyOn(node, 'close'); const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); - rrNode.attributes = { rr_openMode: 'modal', open: '' }; + rrNode.attributes = { rr_open_mode: 'modal', open: '' }; mirror.add(node, elementSn); rrDocument.mirror.add(rrNode, elementSn); diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 8a56bb3fea..9ed6267013 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -353,9 +353,9 @@ function buildNode( (node as HTMLMediaElement).loop = value; } else if (name === 'rr_mediaVolume' && typeof value === 'number') { (node as HTMLMediaElement).volume = value; - } else if (name === 'rr_openMode') { + } else if (name === 'rr_open_mode') { (node as HTMLDialogElement).setAttribute( - 'rr_openMode', + 'rr_open_mode', value as string, ); // keep this attribute for rrweb to trigger showModal } diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index ff1d238dbb..bfb78170a7 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -719,7 +719,7 @@ function serializeElementNode( // register what type of dialog is this // `modal` or `non-modal` // this is used to trigger `showModal()` or `show()` on replay (outside of rrweb-snapshot, in rrweb) - (attributes as DialogAttributes).rr_openMode = n.matches('dialog:modal') + (attributes as DialogAttributes).rr_open_mode = n.matches('dialog:modal') ? 'modal' : 'non-modal'; } diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts index e3687cd6cc..8117afa544 100644 --- a/packages/rrweb-snapshot/src/types.ts +++ b/packages/rrweb-snapshot/src/types.ts @@ -111,13 +111,13 @@ export type DialogAttributes = { * `non-modal` means the dialog is opened with `show()` or * by adding an `open` attribute. */ - rr_openMode: 'modal' | 'non-modal'; + rr_open_mode: 'modal' | 'non-modal'; /** * Currently unimplemented, but in future can be used to: * Represents the order of which of the dialog was opened. * This is useful for replaying the dialog `.showModal()` in the correct order. */ - // rr_openMode_index?: number; + // rr_open_mode_index?: number; }; // @deprecated diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index 793e167c6b..b20f39887d 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -31,7 +31,7 @@ exports[`dialog integration tests > should capture open attribute for modal dial \\"tagName\\": \\"dialog\\", \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_openMode\\": \\"modal\\" + \\"rr_open_mode\\": \\"modal\\" }, \\"childNodes\\": [ { @@ -90,7 +90,7 @@ exports[`dialog integration tests > should capture open attribute for non modal \\"tagName\\": \\"dialog\\", \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_openMode\\": \\"non-modal\\" + \\"rr_open_mode\\": \\"non-modal\\" }, \\"childNodes\\": [ { diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 5562b8be84..b7b9c88cef 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -665,9 +665,9 @@ export default class MutationBuffer { } } else if (attributeName === 'open' && target.tagName === 'DIALOG') { if (target.matches('dialog:modal')) { - item.attributes['rr_openMode'] = 'modal'; + item.attributes['rr_open_mode'] = 'modal'; } else { - item.attributes['rr_openMode'] = 'non-modal'; + item.attributes['rr_open_mode'] = 'non-modal'; } } } diff --git a/packages/rrweb/src/replay/dialog/index.ts b/packages/rrweb/src/replay/dialog/index.ts index 49c3c7d45f..2ced72e97c 100644 --- a/packages/rrweb/src/replay/dialog/index.ts +++ b/packages/rrweb/src/replay/dialog/index.ts @@ -15,7 +15,7 @@ export function applyDialogToTopLevel( const dialog = node as HTMLDialogElement; const isOpen = dialog.open; const isModal = isOpen && dialog.matches('dialog:modal'); - const rrOpen = dialog.getAttribute('rr_openMode'); + const rrOpen = dialog.getAttribute('rr_open_mode'); const shouldBeOpen = typeof attributeMutation?.attributes.open === 'string' || @@ -61,6 +61,6 @@ export function removeDialogFromTopLevel( if (attributeMutation.attributes.open === null) { dialog.removeAttribute('open'); - dialog.removeAttribute('rr_openMode'); + dialog.removeAttribute('rr_open_mode'); } } diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 83f922f3f6..67c5513c1a 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1808,7 +1808,7 @@ export class Replayer { } if ( - attributeName === 'rr_openMode' && + attributeName === 'rr_open_mode' && target.nodeName === 'DIALOG' ) { applyDialogToTopLevel(target, mutation); diff --git a/packages/rrweb/test/events/dialog-playback.ts b/packages/rrweb/test/events/dialog-playback.ts index 9277af4d12..add86aca9d 100644 --- a/packages/rrweb/test/events/dialog-playback.ts +++ b/packages/rrweb/test/events/dialog-playback.ts @@ -132,7 +132,7 @@ const events: eventWithTime[] = [ attributes: [ { id: 27, - attributes: { open: '', rr_openMode: 'non-modal', class: 'show' }, + attributes: { open: '', rr_open_mode: 'non-modal', class: 'show' }, }, ], }, @@ -166,7 +166,7 @@ const events: eventWithTime[] = [ attributes: [ { id: 27, - attributes: { rr_openMode: 'modal', open: '', class: 'showModal' }, + attributes: { rr_open_mode: 'modal', open: '', class: 'showModal' }, }, ], }, @@ -184,7 +184,7 @@ const events: eventWithTime[] = [ { id: 27, attributes: { - rr_openMode: 'non-modal', + rr_open_mode: 'non-modal', class: 'switched-from-show-modal-to-show', }, }, @@ -205,7 +205,7 @@ const events: eventWithTime[] = [ { id: 27, attributes: { - rr_openMode: 'modal', + rr_open_mode: 'modal', class: 'switched-from-show-to-show-modal', }, }, @@ -285,7 +285,7 @@ const events: eventWithTime[] = [ tagName: 'dialog', attributes: { open: '', - rr_openMode: 'non-modal', + rr_open_mode: 'non-modal', style: 'outline: blue solid 1px;', }, childNodes: [{ type: 3, textContent: 'Dialog 1', id: 25 }], @@ -384,7 +384,7 @@ const events: eventWithTime[] = [ type: 2, tagName: 'dialog', attributes: { - rr_openMode: 'modal', + rr_open_mode: 'modal', open: '', style: 'outline: blue solid 1px;', class: 'existing-1', @@ -431,7 +431,7 @@ const events: eventWithTime[] = [ type: 2, tagName: 'dialog', attributes: { - rr_openMode: 'modal', + rr_open_mode: 'modal', open: '', style: 'outline: orange solid 1px;', class: 'new-dialog', diff --git a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap index 0d88a22916..03526f8c0f 100644 --- a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap @@ -104,7 +104,7 @@ exports[`dialog > add dialog and show 1`] = ` \\"tagName\\": \\"dialog\\", \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_openMode\\": \\"non-modal\\" + \\"rr_open_mode\\": \\"non-modal\\" }, \\"childNodes\\": [], \\"id\\": 11 @@ -220,7 +220,7 @@ exports[`dialog > add dialog and showModal 1`] = ` \\"tagName\\": \\"dialog\\", \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_openMode\\": \\"modal\\" + \\"rr_open_mode\\": \\"modal\\" }, \\"childNodes\\": [], \\"id\\": 11 @@ -330,7 +330,7 @@ exports[`dialog > switch to show dialog 1`] = ` \\"id\\": 8, \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_openMode\\": \\"modal\\" + \\"rr_open_mode\\": \\"modal\\" } } ], @@ -348,7 +348,7 @@ exports[`dialog > switch to show dialog 1`] = ` \\"id\\": 8, \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_openMode\\": \\"non-modal\\" + \\"rr_open_mode\\": \\"non-modal\\" } } ], @@ -457,7 +457,7 @@ exports[`dialog > switch to showModal dialog 1`] = ` \\"id\\": 8, \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_openMode\\": \\"non-modal\\" + \\"rr_open_mode\\": \\"non-modal\\" } } ], @@ -475,7 +475,7 @@ exports[`dialog > switch to showModal dialog 1`] = ` \\"id\\": 8, \\"attributes\\": { \\"open\\": \\"\\", - \\"rr_openMode\\": \\"modal\\" + \\"rr_open_mode\\": \\"modal\\" } } ], diff --git a/packages/rrweb/test/record/dialog.test.ts b/packages/rrweb/test/record/dialog.test.ts index 715a287111..ab6542b547 100644 --- a/packages/rrweb/test/record/dialog.test.ts +++ b/packages/rrweb/test/record/dialog.test.ts @@ -122,7 +122,7 @@ describe('dialog', () => { const lastEvent = events[events.length - 1]; expect(lastEvent).toMatchObject( - attributeMutationFactory({ rr_openMode: 'modal' }), + attributeMutationFactory({ rr_open_mode: 'modal' }), ); }); From d693da936fa9df1d9aa41113c57791528442e31d Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 26 Jun 2024 14:48:32 +0200 Subject: [PATCH 25/29] Remove unused images --- ...how-modal-in-incremental-snapshot-1-snap.png | Bin 12721 -> 0 bytes ...ntal-snapshot-without-virtual-dom-1-snap.png | Bin 12721 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-1-snap.png delete mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-without-virtual-dom-1-snap.png diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-1-snap.png deleted file mode 100644 index f328c34b5b1b95a917d80f69c2fa391285891f97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12721 zcmeHNc~nzZ8h?t4wor?m7L?5;N?Vatfw0AeifI+3OjQ&JElZ{XB1D$R5?m^61sTIG zflO;FZAF4mKp>D%HLOWaU07rnLQrBNgph=gr1vH>p6UEM|9E=dJvrxn@7??E`_|w6 z?sxOeVLxBvH#WS1Ac!&kfX}B0Vwi~_E8JGE0#BwNw(SF#Wu#Ai_aaq1i)RS35yAWH z`FjdYG@Rb?I9c63+t*kl_-)V4D7oq8I*cog(-g$*IubKrt=`1BIL(<}Ykd5+-M3CB zYwC}=H92niI*fJ7(6OTsx8e4&!J7BpTUQco{-7{c(DiAhfGbn&Yswgyf3T}>mYL?M zG2tmg=~8duXa${5td8t1>;$}k_P3xrr^l;(5wmY^f4)}#2w`__LJahD{@a<$^wh_z z+5hg|x*|+ZWv*N9pr>}PS!SlE{&f*C(o=``B2aA5-bbfn>lA??sP0flL8C$+3@D_`Dtr|8?EUI?*y*+jCd1j++QunI zf=PX+g>Hw^ChnU0si$CXg*)6f?&q>Bu6ts!zY#yo&1~DPTnT#iclTEt!uJafon(V6 zd$kFySlnkKfQHsf)1w4G}#R5BkZ4UZ;`RO-U1q*ipf0-a9ruTmlM(fCqvZyMZ_$?OUg#zI8cuS33L`$y zqMRy~J}7G4d7*71_%YH1+iO#1pE6~);<$Bvp{0XyEOh`yI#&vffM727G6QPVWPN7}MHGalI zEuNapoUl~R=2B>br=8z28BVL@`%ep(gHO_ai84wB*RVU~ zv)M!pK8i=f%AV8$l?0QxyU25SbQ;i1)M#sZqh@g?55pjHqpvjaG-{P|I49d^m6j4U z$Es2uvvfc6et7spE&VY|{UWYW+%z-ygN@e*_?no3nx&UnxU^upN;0$z?qagnL3?C| zFZQ>47uNFJw-dCBeR&q%z2wKgd`9j*G2$fa>AiKLHbKx89Do1a_14z>McGV6h*XHv zl1CIA*QnX2_k*g#_IS&F_P2~rmVm+1nxYn421PyNOwlYcRl^cq1y21eX0%lu5WOS# z≷|;!zgP+$E-?sCE8F8hNV3W@5%B-Vm;y4##0eJysmmQ+)MJyE_{xio4bcqv`TU zAhdF!fmP;}x^U1-THnyJgGh_t-^(D-6K8q_Et2#&jYiJ0PHcHG*g*XK`;SVBi#uW) zh6|0&T_*QgFCN~uZQG6Vpt_Tv71|TZPsFwC`lQH?3(P050Lt=De;$~N72Y^hnlM5Z zMoOo+%Eye1f=>Kk5_HywdC=<)&S3duKk8%yOC4+1I7ziVRz3E$+`d^Z)27pZAn`xE2K`;Hh`zE9}95J_02@L#rAUmZyq4Et7|?LRZ66U0a?V z!c&z}p>U;rN}D6m!e~`cV?tyUzq}0CdMcWrdNQHl0L>uRArK)qWS0p`oAGnhBIe{z z5<=>WIxKKzXrG@`&*#<|C~R;_{8Uc~>0UG=Lv}L{Ggu$n{jhxDS4>v!H9u-C-`k^e z{Car>>DiA(QD1zqIe9qoC^ce&lhdvawdt~CgAcLaFL4w>xdZ#e!_6f5aI#Rzr(^(@ z(jO5|-5I}>_VgWk$Bu>}s-bn7L&H!)w_~5j>9O4z%5H+y>=UJsj+2czgZN--)e^ob zOL3nmPGod*a?G6_9j^+#<&8EwB??8#$jeKvdH{2aT?0PM4zTo0agdztYo=m=cep)w z_DB}@4326RDhVual90#cm3dTzklN0#YDlVS?&of~-=; z&Uq@vE-0scac6&hi#K=p{(o+JcvDVi#cgZ~MA#t^62Q)nAR#bD1&AMULT%DZ)`;Dx?$$5uuzDG|qC&Nm{jB zm`7iV5Z{@&!jz;-x|!5)W%*8geE8nzb^t#l1P{MHj#m(ir<(nnp_Z zr)KBm2xuf;=etLPj0BX;(1DYY(%cG z5bsuCTpu&=G{F{#vxzl0UjSB6?Dne0nf%6?B!0OUcBZnHiqR_CaZUifvU4m|?C*T7 z$e85O^knG&M6a2kKt>#h{O50t4L2tf0Yz4fXGzbkP1{)N5_`?vWpNazVxKd>szkN^ zi}DRdtCXB9`<}`mMu*fcRfs28BS^|qTYx$JTM(nKpm(z;7Wj%%3;+*KJe|@NSWcDl zaoPX|L+S{Fh-6l`lhW`4G-yJ2(HGOqU#>1u*)lyR6?X}=&Y}LEHc zpdq7E!(RzQMER#K0Z=5s{{ojB++>-2!)wN4G_-di6PXkkvLhyMkK(}T4J>p@TE9!;-Y z@$nWwZd(D5;y`0xL}2Y3wBonfx+{Xbvm722gBG?IoKWc6!PQ}UB6E%IP!ZfXZ2HeX f@X^WsAD#JJu83GgI51ua6^#GL&!=i{*!e#Itmd7) diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-without-virtual-dom-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-without-virtual-dom-1-snap.png deleted file mode 100644 index f328c34b5b1b95a917d80f69c2fa391285891f97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12721 zcmeHNc~nzZ8h?t4wor?m7L?5;N?Vatfw0AeifI+3OjQ&JElZ{XB1D$R5?m^61sTIG zflO;FZAF4mKp>D%HLOWaU07rnLQrBNgph=gr1vH>p6UEM|9E=dJvrxn@7??E`_|w6 z?sxOeVLxBvH#WS1Ac!&kfX}B0Vwi~_E8JGE0#BwNw(SF#Wu#Ai_aaq1i)RS35yAWH z`FjdYG@Rb?I9c63+t*kl_-)V4D7oq8I*cog(-g$*IubKrt=`1BIL(<}Ykd5+-M3CB zYwC}=H92niI*fJ7(6OTsx8e4&!J7BpTUQco{-7{c(DiAhfGbn&Yswgyf3T}>mYL?M zG2tmg=~8duXa${5td8t1>;$}k_P3xrr^l;(5wmY^f4)}#2w`__LJahD{@a<$^wh_z z+5hg|x*|+ZWv*N9pr>}PS!SlE{&f*C(o=``B2aA5-bbfn>lA??sP0flL8C$+3@D_`Dtr|8?EUI?*y*+jCd1j++QunI zf=PX+g>Hw^ChnU0si$CXg*)6f?&q>Bu6ts!zY#yo&1~DPTnT#iclTEt!uJafon(V6 zd$kFySlnkKfQHsf)1w4G}#R5BkZ4UZ;`RO-U1q*ipf0-a9ruTmlM(fCqvZyMZ_$?OUg#zI8cuS33L`$y zqMRy~J}7G4d7*71_%YH1+iO#1pE6~);<$Bvp{0XyEOh`yI#&vffM727G6QPVWPN7}MHGalI zEuNapoUl~R=2B>br=8z28BVL@`%ep(gHO_ai84wB*RVU~ zv)M!pK8i=f%AV8$l?0QxyU25SbQ;i1)M#sZqh@g?55pjHqpvjaG-{P|I49d^m6j4U z$Es2uvvfc6et7spE&VY|{UWYW+%z-ygN@e*_?no3nx&UnxU^upN;0$z?qagnL3?C| zFZQ>47uNFJw-dCBeR&q%z2wKgd`9j*G2$fa>AiKLHbKx89Do1a_14z>McGV6h*XHv zl1CIA*QnX2_k*g#_IS&F_P2~rmVm+1nxYn421PyNOwlYcRl^cq1y21eX0%lu5WOS# z≷|;!zgP+$E-?sCE8F8hNV3W@5%B-Vm;y4##0eJysmmQ+)MJyE_{xio4bcqv`TU zAhdF!fmP;}x^U1-THnyJgGh_t-^(D-6K8q_Et2#&jYiJ0PHcHG*g*XK`;SVBi#uW) zh6|0&T_*QgFCN~uZQG6Vpt_Tv71|TZPsFwC`lQH?3(P050Lt=De;$~N72Y^hnlM5Z zMoOo+%Eye1f=>Kk5_HywdC=<)&S3duKk8%yOC4+1I7ziVRz3E$+`d^Z)27pZAn`xE2K`;Hh`zE9}95J_02@L#rAUmZyq4Et7|?LRZ66U0a?V z!c&z}p>U;rN}D6m!e~`cV?tyUzq}0CdMcWrdNQHl0L>uRArK)qWS0p`oAGnhBIe{z z5<=>WIxKKzXrG@`&*#<|C~R;_{8Uc~>0UG=Lv}L{Ggu$n{jhxDS4>v!H9u-C-`k^e z{Car>>DiA(QD1zqIe9qoC^ce&lhdvawdt~CgAcLaFL4w>xdZ#e!_6f5aI#Rzr(^(@ z(jO5|-5I}>_VgWk$Bu>}s-bn7L&H!)w_~5j>9O4z%5H+y>=UJsj+2czgZN--)e^ob zOL3nmPGod*a?G6_9j^+#<&8EwB??8#$jeKvdH{2aT?0PM4zTo0agdztYo=m=cep)w z_DB}@4326RDhVual90#cm3dTzklN0#YDlVS?&of~-=; z&Uq@vE-0scac6&hi#K=p{(o+JcvDVi#cgZ~MA#t^62Q)nAR#bD1&AMULT%DZ)`;Dx?$$5uuzDG|qC&Nm{jB zm`7iV5Z{@&!jz;-x|!5)W%*8geE8nzb^t#l1P{MHj#m(ir<(nnp_Z zr)KBm2xuf;=etLPj0BX;(1DYY(%cG z5bsuCTpu&=G{F{#vxzl0UjSB6?Dne0nf%6?B!0OUcBZnHiqR_CaZUifvU4m|?C*T7 z$e85O^knG&M6a2kKt>#h{O50t4L2tf0Yz4fXGzbkP1{)N5_`?vWpNazVxKd>szkN^ zi}DRdtCXB9`<}`mMu*fcRfs28BS^|qTYx$JTM(nKpm(z;7Wj%%3;+*KJe|@NSWcDl zaoPX|L+S{Fh-6l`lhW`4G-yJ2(HGOqU#>1u*)lyR6?X}=&Y}LEHc zpdq7E!(RzQMER#K0Z=5s{{ojB++>-2!)wN4G_-di6PXkkvLhyMkK(}T4J>p@TE9!;-Y z@$nWwZd(D5;y`0xL}2Y3wBonfx+{Xbvm722gBG?IoKWc6!PQ}UB6E%IP!ZfXZ2HeX f@X^WsAD#JJu83GgI51ua6^#GL&!=i{*!e#Itmd7) From 760908f71e10feac097300a0ac4df345bb1fc68b Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 9 Jul 2024 13:39:45 +0200 Subject: [PATCH 26/29] Move after iframe append based on @YunFeng0817's comment https://github.com/rrweb-io/rrweb/pull/1503#discussion_r1666363931 --- packages/rrweb/src/replay/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 67c5513c1a..ac172ca3c5 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -957,13 +957,14 @@ export class Replayer { }); afterAppend(iframeEl.contentDocument! as Document, mutation.node.id); - collectedDialogs.forEach((d) => applyDialogToTopLevel(d)); for (const { mutationInQueue, builtNode } of collectedIframes) { this.attachDocumentToIframe(mutationInQueue, builtNode); this.newDocumentQueue = this.newDocumentQueue.filter( (m) => m !== mutationInQueue, ); } + + collectedDialogs.forEach((d) => applyDialogToTopLevel(d)); } private collectIframeAndAttachDocument( From 2dfeb01690de041c65fb1cf3447e43ad45ee64da Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 9 Jul 2024 14:15:55 +0200 Subject: [PATCH 27/29] Remove redundant dialog handling from rrdom. rrdom already handles dialog element creation it's self --- packages/rrweb/src/replay/index.ts | 1 - ...odal-in-incremental-snapshot-alternative.png | Bin 0 -> 12721 bytes packages/rrweb/test/replay/dialog.test.ts | 13 +++++++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-alternative.png diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index ac172ca3c5..615d2618e6 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -248,7 +248,6 @@ export class Replayer { this.applyStyleDeclaration(data, styleSheet); }, afterAppend: (node: Node, id: number) => { - applyDialogToTopLevel(node); for (const plugin of this.config.plugins || []) { if (plugin.onBuild) plugin.onBuild(node, { id, replayer: this }); } diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-alternative.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-alternative.png new file mode 100644 index 0000000000000000000000000000000000000000..f328c34b5b1b95a917d80f69c2fa391285891f97 GIT binary patch literal 12721 zcmeHNc~nzZ8h?t4wor?m7L?5;N?Vatfw0AeifI+3OjQ&JElZ{XB1D$R5?m^61sTIG zflO;FZAF4mKp>D%HLOWaU07rnLQrBNgph=gr1vH>p6UEM|9E=dJvrxn@7??E`_|w6 z?sxOeVLxBvH#WS1Ac!&kfX}B0Vwi~_E8JGE0#BwNw(SF#Wu#Ai_aaq1i)RS35yAWH z`FjdYG@Rb?I9c63+t*kl_-)V4D7oq8I*cog(-g$*IubKrt=`1BIL(<}Ykd5+-M3CB zYwC}=H92niI*fJ7(6OTsx8e4&!J7BpTUQco{-7{c(DiAhfGbn&Yswgyf3T}>mYL?M zG2tmg=~8duXa${5td8t1>;$}k_P3xrr^l;(5wmY^f4)}#2w`__LJahD{@a<$^wh_z z+5hg|x*|+ZWv*N9pr>}PS!SlE{&f*C(o=``B2aA5-bbfn>lA??sP0flL8C$+3@D_`Dtr|8?EUI?*y*+jCd1j++QunI zf=PX+g>Hw^ChnU0si$CXg*)6f?&q>Bu6ts!zY#yo&1~DPTnT#iclTEt!uJafon(V6 zd$kFySlnkKfQHsf)1w4G}#R5BkZ4UZ;`RO-U1q*ipf0-a9ruTmlM(fCqvZyMZ_$?OUg#zI8cuS33L`$y zqMRy~J}7G4d7*71_%YH1+iO#1pE6~);<$Bvp{0XyEOh`yI#&vffM727G6QPVWPN7}MHGalI zEuNapoUl~R=2B>br=8z28BVL@`%ep(gHO_ai84wB*RVU~ zv)M!pK8i=f%AV8$l?0QxyU25SbQ;i1)M#sZqh@g?55pjHqpvjaG-{P|I49d^m6j4U z$Es2uvvfc6et7spE&VY|{UWYW+%z-ygN@e*_?no3nx&UnxU^upN;0$z?qagnL3?C| zFZQ>47uNFJw-dCBeR&q%z2wKgd`9j*G2$fa>AiKLHbKx89Do1a_14z>McGV6h*XHv zl1CIA*QnX2_k*g#_IS&F_P2~rmVm+1nxYn421PyNOwlYcRl^cq1y21eX0%lu5WOS# z≷|;!zgP+$E-?sCE8F8hNV3W@5%B-Vm;y4##0eJysmmQ+)MJyE_{xio4bcqv`TU zAhdF!fmP;}x^U1-THnyJgGh_t-^(D-6K8q_Et2#&jYiJ0PHcHG*g*XK`;SVBi#uW) zh6|0&T_*QgFCN~uZQG6Vpt_Tv71|TZPsFwC`lQH?3(P050Lt=De;$~N72Y^hnlM5Z zMoOo+%Eye1f=>Kk5_HywdC=<)&S3duKk8%yOC4+1I7ziVRz3E$+`d^Z)27pZAn`xE2K`;Hh`zE9}95J_02@L#rAUmZyq4Et7|?LRZ66U0a?V z!c&z}p>U;rN}D6m!e~`cV?tyUzq}0CdMcWrdNQHl0L>uRArK)qWS0p`oAGnhBIe{z z5<=>WIxKKzXrG@`&*#<|C~R;_{8Uc~>0UG=Lv}L{Ggu$n{jhxDS4>v!H9u-C-`k^e z{Car>>DiA(QD1zqIe9qoC^ce&lhdvawdt~CgAcLaFL4w>xdZ#e!_6f5aI#Rzr(^(@ z(jO5|-5I}>_VgWk$Bu>}s-bn7L&H!)w_~5j>9O4z%5H+y>=UJsj+2czgZN--)e^ob zOL3nmPGod*a?G6_9j^+#<&8EwB??8#$jeKvdH{2aT?0PM4zTo0agdztYo=m=cep)w z_DB}@4326RDhVual90#cm3dTzklN0#YDlVS?&of~-=; z&Uq@vE-0scac6&hi#K=p{(o+JcvDVi#cgZ~MA#t^62Q)nAR#bD1&AMULT%DZ)`;Dx?$$5uuzDG|qC&Nm{jB zm`7iV5Z{@&!jz;-x|!5)W%*8geE8nzb^t#l1P{MHj#m(ir<(nnp_Z zr)KBm2xuf;=etLPj0BX;(1DYY(%cG z5bsuCTpu&=G{F{#vxzl0UjSB6?Dne0nf%6?B!0OUcBZnHiqR_CaZUifvU4m|?C*T7 z$e85O^knG&M6a2kKt>#h{O50t4L2tf0Yz4fXGzbkP1{)N5_`?vWpNazVxKd>szkN^ zi}DRdtCXB9`<}`mMu*fcRfs28BS^|qTYx$JTM(nKpm(z;7Wj%%3;+*KJe|@NSWcDl zaoPX|L+S{Fh-6l`lhW`4G-yJ2(HGOqU#>1u*)lyR6?X}=&Y}LEHc zpdq7E!(RzQMER#K0Z=5s{{ojB++>-2!)wN4G_-di6PXkkvLhyMkK(}T4J>p@TE9!;-Y z@$nWwZd(D5;y`0xL}2Y3wBonfx+{Xbvm722gBG?IoKWc6!PQ}UB6E%IP!ZfXZ2HeX f@X^WsAD#JJu83GgI51ua6^#GL&!=i{*!e#Itmd7) literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/replay/dialog.test.ts b/packages/rrweb/test/replay/dialog.test.ts index 8bcb202159..7fc1ab99f2 100644 --- a/packages/rrweb/test/replay/dialog.test.ts +++ b/packages/rrweb/test/replay/dialog.test.ts @@ -97,6 +97,10 @@ describe('dialog', () => { name: 'should add an opened dialog with showModal in incremental snapshot', time: showModalIncrementalAddTime, }, + { + name: 'should add an opened dialog with showModal in incremental snapshot alternative', + time: [showModalFullSnapshotTime, showModalIncrementalAddTime], + }, ].forEach(({ name, time }) => { [true, false].forEach((useVirtualDom) => { it(`${name} (virtual dom: ${useVirtualDom})`, async () => { @@ -106,9 +110,14 @@ describe('dialog', () => { await page.evaluate(` const { Replayer } = rrweb; window.replayer = new Replayer(events, { useVirtualDom: ${useVirtualDom} }); - window.replayer.pause(${time}); `); - await waitForRAF(page); + const timeArray = Array.isArray(time) ? time : [time]; + for (let i = 0; i < timeArray.length; i++) { + await page.evaluate(` + window.replayer.pause(${timeArray[i]}); + `); + await waitForRAF(page); + } const frameImage = await page!.screenshot({ fullPage: false, From 21c4826e4bc861a1efe80297f3320a6b05cfb66a Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 9 Jul 2024 15:26:49 +0200 Subject: [PATCH 28/29] Rename variables for dialog handling in rrweb replay module --- packages/rrweb/src/replay/dialog/index.ts | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/rrweb/src/replay/dialog/index.ts b/packages/rrweb/src/replay/dialog/index.ts index 2ced72e97c..9fc57d4523 100644 --- a/packages/rrweb/src/replay/dialog/index.ts +++ b/packages/rrweb/src/replay/dialog/index.ts @@ -13,30 +13,31 @@ export function applyDialogToTopLevel( ): void { if (node.nodeName !== 'DIALOG' || node instanceof RRNode) return; const dialog = node as HTMLDialogElement; - const isOpen = dialog.open; - const isModal = isOpen && dialog.matches('dialog:modal'); - const rrOpen = dialog.getAttribute('rr_open_mode'); + const oldIsOpen = dialog.open; + const oldIsModalState = oldIsOpen && dialog.matches('dialog:modal'); + const rrOpenMode = dialog.getAttribute('rr_open_mode'); - const shouldBeOpen = + const newIsOpen = typeof attributeMutation?.attributes.open === 'string' || typeof dialog.getAttribute('open') === 'string'; - const shouldBeModal = rrOpen === 'modal'; - const shouldBeNonModal = rrOpen === 'non-modal'; + const newIsModalState = rrOpenMode === 'modal'; + const newIsNonModalState = rrOpenMode === 'non-modal'; - const modalChanged = - (isModal && shouldBeNonModal) || (!isModal && shouldBeModal); + const modalStateChanged = + (oldIsModalState && newIsNonModalState) || + (!oldIsModalState && newIsModalState); - if (isOpen && !modalChanged) return; + if (oldIsOpen && !modalStateChanged) return; // complain if dialog is not attached to the dom if (!dialog.isConnected) { console.warn('dialog is not attached to the dom', dialog); return; } - if (isOpen) dialog.close(); - if (!shouldBeOpen) return; + if (oldIsOpen) dialog.close(); + if (!newIsOpen) return; - if (shouldBeModal) dialog.showModal(); + if (newIsModalState) dialog.showModal(); else dialog.show(); } From 9f2a27cc84ad2e054e610effea5e92b5c02128fd Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 2 Aug 2024 09:23:49 +0200 Subject: [PATCH 29/29] Update packages/rrdom/src/document.ts --- packages/rrdom/src/document.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index 7df931860a..f3f55aec1a 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -474,7 +474,8 @@ export class BaseRRElement extends BaseRRNode implements IRRElement { } public getAttribute(name: string): string | null { - return this.attributes[name] ?? null; + if (this.attributes[name] === undefined) return null; + return this.attributes[name]; } public setAttribute(name: string, attribute: string) {