Files
rrweb/packages/rrweb/test/record/dialog.test.ts
Justin Halsall 335639af9b Support top-layer <dialog> recording & replay (#1503)
* chore: its important to run `yarn build:all` before running `yarn dev`

* feat: trigger showModal from rrdom and rrweb

* feat: Add support for replaying modal and non modal dialog elements

* chore: Update dev script to remove CLEAR_DIST_DIR flag

* Get modal recording and replay working

* DRY up dialog test and dedupe snapshot images

* feat: Refactor dialog test to use updated attribute name

* feat: Update dialog test to include rr_open attribute

* chore: Add npm dependency happy-dom@14.12.0

* Add more test cases for dialog

* Clean up naming

* Refactor dialog open code

* Revert changed code that doesn't do anything

* Add documentation for unimplemented type

* chore: Remove unnecessary comments in dialog.test.ts

* rename rr_open to rr_openMode

* Replace todo with a skipped test

* Add better logging for CI

* Rename rr_openMode to rr_open_mode

rrdom downcases all attribute names which made `rr_openMode` tricky to deal with

* Remove unused images

* Move after iframe append based on @YunFeng0817's comment
https://github.com/rrweb-io/rrweb/pull/1503#discussion_r1666363931

* Remove redundant dialog handling from rrdom.

rrdom already handles dialog element creation it's self

* Rename variables for dialog handling in rrweb replay module

* Update packages/rrdom/src/document.ts

---------

Co-authored-by: Eoghan Murray <eoghan@getthere.ie>
2024-08-02 09:53:05 +02:00

230 lines
6.0 KiB
TypeScript

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';
interface IWindow extends Window {
rrweb: {
record: (
options: recordOptions<eventWithTime>,
) => listenerHandler | undefined;
addCustomEvent<T>(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_mode: '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({ 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();
});
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);
});
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);
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
});
});