Files
rrweb/packages/rrweb/test/e2e/webgl.test.ts
Justin Halsall 2887c8c7e5 #853 Second try: fast-forward implementation v2: virtual dom optimization (#895)
* rrdom: add a diff function for properties

* implement diffChildren function and unit tests

* finish basic functions of diff algorithm

* fix several bugs in the diff algorithm

* replace the virtual parent optimization in applyMutation()

* fix: moveAndHover after the diff algorithm is executed

* replace virtual style map with rrdom

cssom version has to be above 0.5.0 to pass virtual style tests

* fix: failed virtual style tests in replayer.test.ts

* fix: failed polyfill tests caused by nodejs compatibility of different versions

* fix: svg viewBox attribute doesn't work

Cause the attribute viewBox is case sensitive, set value for viewbox doesn't work

* feat: replace treeIndex optimization with rrdom

* fix bug of diffProps and disable smooth scrolling animation in fast-forward mode

* feat: add iframe support

* fix: @rollup/plugin-typescript build errors in rrweb-player

Error: @rollup/plugin-typescript TS1371: This import is never used as a value and must use 'import type' because the 'importsNotUsedAsValues' is set to 'error'

* fix: bug when fast-forward input events and add test for it

* add test for fast-forward scroll events

* fix: custom style rules don't get inserted into some iframe elements

* code style tweak

* fix: enable to diff iframe elements

* fix  the jest error "Unexpected token 'export'"

* try to fix build error of rrweb-player

* correct the attributes definition in rrdom

* fix: custom style rules are not inserted in some iframes

* add support for shadow dom

* add support for MediaInteraction

* add canvas support

* fix unit test error in rrdom

* add support for Text, Comment

* try to refactor RRDom

* refactor RRDom to reduce duplicate code

* rename document-browser to virtual-dom

* increase the test coverage for document.ts and add ownerDocument for it

* Merge branch 'master' into virtual-dom

* add more test for virtual-dom.ts

* use cssstyle in document-nodejs

* fix: bundle error

* improve document-nodejs

* enable to diff scroll positions of an element

* rename rrdom to virtualDom for more readability and make the tree public

* revert unknown change

* improve the css style parser for comments

* improve code style

* update typings

* add handling for the case where legacy_missingNodeRetryMap is not empty

* only import types from rrweb into rrdom

* Apply suggestions from code review

Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>

* Apply suggestions from code review

* fix building error in rrweb

* add a method setDefaultSN to set a default value for a RRNode's __sn

* fix rrweb test error and bump up other packages

* add support for custom property of css styles

* add a switch for virtual-dom optimization

* Apply suggestions from code review

1. add an enum type for NodeType
2. rename nodeType from rrweb-snapshot to RRNodeType
3. rename notSerializedId to unserializedId
4. add comments for some confusing variables

* adapt changes of #865 to virtual-dom and improve the test case for more coverage

* apply review suggestions

https://github.com/rrweb-io/rrweb/pull/853#pullrequestreview-922474953

* tweak the diff algorithm

* add description of the flag useVirtualDom and remove outdated logConfig

* Remove console.log

* Contain changes to document

* Upgrade rollup to 2.70.2

* Revert "Upgrade rollup to 2.70.2"

This reverts commit b1be81a2a76565935c9dc391f31beb7f64d25956.

* Fix type checking rrdom

* Fix typing error while bundling

* Fix tslib error on build

Rollup would output the following error:
`semantic error TS2343: This syntax requires an imported helper named '__spreadArray' which does not exist in 'tslib'. Consider upgrading your version of 'tslib'.`

* Increase memory limit for rollup

* Use esbuild for bundling

Speeds up bundling significantly

* Avoid circular dependencies and import un-bundled rrdom

* Fix imports

* Revert back to pre-esbuild

This reverts the following commits:
b7b3c8dbaa551a0129da1477136b1baaad28e6e1
72e23b8e27f9030d911358d3a17fe5ad1b3b5d4f
85d600a20c56cfa764cf1f858932ba14e67b1d23
61e1a5d323212ca8fbe0569e0b3062ddd53fc612

* Set node to lts (12 is no longer supported)

* Speed up bundling and use less memory

This fixes the out of memory errors happening while bundling

* remove __sn from rrdom

* fix typo

* test: add a test case for StyleSheet mutation exceptions while fast-forwarding

* rename Array.prototype.slice.call() to Array.from()

* improve test cases

* fix: PR #887 in 'virtual-dom' branch

* apply justin's suggestion on 'Array.from' refactor

related commit 0f6729d27a323260b36fbe79485a86715c0bc98a

* improve import code structure

Co-authored-by: Yun Feng <yun.feng@anu.edu.au>
2026-04-01 12:00:00 +08:00

171 lines
4.7 KiB
TypeScript

import type * as http from 'http';
import * as fs from 'fs';
import * as path from 'path';
import type * as puppeteer from 'puppeteer';
import {
startServer,
launchPuppeteer,
getServerURL,
replaceLast,
waitForRAF,
} from '../utils';
import type { recordOptions, eventWithTime } from '../../src/types';
import { toMatchImageSnapshot } from 'jest-image-snapshot';
expect.extend({ toMatchImageSnapshot });
interface ISuite {
code: string;
browser: puppeteer.Browser;
server: http.Server;
page: puppeteer.Page;
events: eventWithTime[];
serverURL: string;
}
describe('e2e webgl', () => {
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.min.js');
code = fs.readFileSync(bundlePath, 'utf8');
});
afterEach(async () => {
await page.close();
});
afterAll(async () => {
await server.close();
await browser.close();
});
const getHtml = (
fileName: string,
options: recordOptions<eventWithTime> = {},
): string => {
const filePath = path.resolve(__dirname, `../html/${fileName}`);
const html = fs.readFileSync(filePath, 'utf8');
return replaceLast(
html,
'</body>',
`
<script>
${code}
window.snapshots = [];
rrweb.record({
emit: event => {
window.snapshots.push(event);
},
maskTextSelector: ${JSON.stringify(options.maskTextSelector)},
maskAllInputs: ${options.maskAllInputs},
maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
userTriggeredOnInput: ${options.userTriggeredOnInput},
maskTextFn: ${options.maskTextFn},
recordCanvas: ${options.recordCanvas},
plugins: ${options.plugins}
});
</script>
</body>
`,
);
};
const fakeGoto = async (page: puppeteer.Page, url: string) => {
const intercept = async (request: puppeteer.HTTPRequest) => {
await request.respond({
status: 200,
contentType: 'text/html',
body: ' ', // non-empty string or page will load indefinitely
});
};
await page.setRequestInterception(true);
page.on('request', intercept);
await page.goto(url);
page.off('request', intercept);
await page.setRequestInterception(false);
};
const hideMouseAnimation = async (page: puppeteer.Page) => {
await page.addStyleTag({
content: '.replayer-mouse-tail{display: none !important;}',
});
};
it('will record and replay a webgl square', async () => {
page = await browser.newPage();
await fakeGoto(page, `${serverURL}/html/canvas-webgl-square.html`);
await page.setContent(
getHtml.call(this, 'canvas-webgl-square.html', { recordCanvas: true }),
);
await waitForRAF(page);
const snapshots: eventWithTime[] = await page.evaluate('window.snapshots');
page = await browser.newPage();
await page.goto('about:blank');
await page.evaluate(code);
await hideMouseAnimation(page);
await page.evaluate(`let events = ${JSON.stringify(snapshots)}`);
await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events, {
UNSAFE_replayCanvas: true,
});
replayer.play(500);
`);
await waitForRAF(page);
const element = await page.$('iframe');
const frameImage = await element!.screenshot();
expect(frameImage).toMatchImageSnapshot();
});
it('will record and replay a webgl image', async () => {
page = await browser.newPage();
await fakeGoto(page, `${serverURL}/html/canvas-webgl-image.html`);
await page.setContent(
getHtml.call(this, 'canvas-webgl-image.html', { recordCanvas: true }),
);
await page.waitForTimeout(100);
const snapshots: eventWithTime[] = await page.evaluate('window.snapshots');
page = await browser.newPage();
await page.goto('about:blank');
await page.evaluate(code);
await hideMouseAnimation(page);
await page.evaluate(`let events = ${JSON.stringify(snapshots)}`);
await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events, {
UNSAFE_replayCanvas: true,
});
`);
// wait for iframe to get added and `preloadAllImages` to ge called
await page.waitForSelector('iframe');
await page.evaluate(`replayer.play(500);`);
await waitForRAF(page);
const element = await page.$('iframe');
const frameImage = await element!.screenshot();
expect(frameImage).toMatchImageSnapshot();
});
});