Impl record iframe (#481)

* Impl record iframe

* iframe observe

* temp: add bundle file to git

* update bundle

* update with pick

* update bundle

* fix fragment map remove

* feat: add an option to determine whether to pause CSS animation when playback is paused (#428)

set pauseAnimation to true by default

* fix: elements would lose some states like scroll position because of "virtual parent" optimization (#427)

* fix: elements would lose some state like scroll position because of "virtual parent" optimization

* refactor: the bugfix code

bug: elements would lose some state like scroll position because of "virtual parent" optimization

* fix: an error occured at applyMutation(remove nodes part)

error message:
Uncaught (in promise) DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node

* pick fixes

* revert ignore file

* re-impl iframe record

* re-impl iframe replay

* code housekeeping

* move multi layer dimension calculation to replay side

* update test cases

* teardown test server

* upgrade rrweb-snapshot with iframe load timeout

Co-authored-by: Lucky Feng <yun.feng@smartx.com>
This commit is contained in:
yz-yu
2026-04-01 12:00:00 +08:00
committed by GitHub
parent b99e843e2a
commit 33f0ac5cfe
24 changed files with 1414 additions and 246 deletions

View File

@@ -1,5 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import * as http from 'http';
import * as url from 'url';
import * as puppeteer from 'puppeteer';
import { assertSnapshot, launchPuppeteer } from './utils';
import { Suite } from 'mocha';
@@ -8,10 +10,48 @@ import { recordOptions, eventWithTime, EventType } from '../src/types';
import { visitSnapshot, NodeType } from 'rrweb-snapshot';
interface ISuite extends Suite {
server: http.Server;
code: string;
browser: puppeteer.Browser;
}
interface IMimeType {
[key: string]: string;
}
const server = () =>
new Promise<http.Server>((resolve) => {
const mimeType: IMimeType = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
};
const s = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url!);
const sanitizePath = path
.normalize(parsedUrl.pathname!)
.replace(/^(\.\.[\/\\])+/, '');
let pathname = path.join(__dirname, sanitizePath);
try {
const data = fs.readFileSync(pathname);
const ext = path.parse(pathname).ext;
res.setHeader('Content-type', mimeType[ext] || 'text/plain');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET');
res.setHeader('Access-Control-Allow-Headers', 'Content-type');
setTimeout(() => {
res.end(data);
// mock delay
}, 100);
} catch (error) {
res.end();
}
});
s.listen(3030).on('listening', () => {
resolve(s);
});
});
describe('record integration tests', function (this: ISuite) {
this.timeout(10_000);
@@ -44,6 +84,7 @@ describe('record integration tests', function (this: ISuite) {
};
before(async () => {
this.server = await server();
this.browser = await launchPuppeteer();
const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js');
@@ -52,6 +93,7 @@ describe('record integration tests', function (this: ISuite) {
after(async () => {
await this.browser.close();
this.server.close();
});
it('can record form interactions', async () => {
@@ -367,4 +409,14 @@ describe('record integration tests', function (this: ISuite) {
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'log');
});
it('should nest record iframe', async () => {
const page: puppeteer.Page = await this.browser.newPage();
await page.goto(`http://localhost:3030/html`);
await page.setContent(getHtml.call(this, 'main.html'));
await page.waitFor(500);
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'iframe');
});
});