Cross origin iframe support (#1035)
* Add `recordCrossOriginIframe` setting * Set up messaging between iframes * should emit full snapshot event from iframe as mutation event * this.mirror was dropped on attachIframe * should use unique id for child of iframe * Cross origin iframe recording in `yarn live-stream` * Root iframe check thats supported by firefox * Live stream: Inject script in all frames * Record same origin and cross origin iframes differently * Should map Input events correctly * Turn on other tests * Fix compatibility with newer puppeteer * puppeteer vs 12 seems stable without to many changes needed * normalize port numbers in snapshots * Handle scroll and ViewportResize events in cross origin iframe * Correctly map cross origin mutations * Map selection events for cross origin iframes * Map canvas mutations for cross origin iframes * Update snapshot to include canvas events * Skip all meta events * Support custom events as best we can in cross origin iframes * Use earliest version of puppeteer that works with cross origin live-stream * Map mouse/touch interaction events * Update snapshots for correctly mapped click events * Tweak tests for new puppeteer version * Map MediaInteraction correctly for cross origin iframes * Make tests consistent between high and low dpi devices * Make test less flaky * Make test less flaky * Make test less flaky * Make test less flaky * Add support for styles in cross origin iframes * Map traditional stylesheet mutations on cross origin iframes * Add todo * Add iframe mirror * Get iframe manager to use iframe mirrors internally * Rename `IframeMirror` to `CrossOriginIframeMirror` * Setup basic cross origin canvas webrtc streaming * Clean up removed canvas elements * reset style mirror on new full snapshot * Fix cross origin canvas webrtc streaming * Make emit optional * Run tests on github actions * Upload image artifacts from failed tests * Use newer github actions * Test: hopefully adding more wait will fix it * add extra wait * Fix image snapshot tests * Make tests run with new puppeteer version * upgrade eslint-plugin-jest * Chore: Remove travis ci as ci's running on github actions * Chore: Support recording cross origin iframe in repl * Force developers to update the cross origin iframe mapping when adding new events https://github.com/rrweb-io/rrweb/pull/1035#discussion_r1012516277 * Document cross origin iframe recording * Docs: cross origin iframes recording methods * Docs: AI translated, cross origin iframe recording * rename getParentId to getId * Migrate to @rrweb/types * Run on pull request * doc: improve Chinese doc * Rename `parentId` to `Id` Co-authored-by: Mark-Fenng <f18846188605@gmail.com>
This commit is contained in:
2
packages/rrweb-player/typings/index.d.ts
vendored
2
packages/rrweb-player/typings/index.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { playerConfig } from 'rrweb/typings/types';
|
||||
import type { eventWithTime } from '@rrweb/types';
|
||||
import { eventWithTime } from '@rrweb/types';
|
||||
import { Replayer, mirror } from 'rrweb';
|
||||
import { SvelteComponent } from 'svelte';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import snapshot, {
|
||||
needMaskingText,
|
||||
classMatchesRegex,
|
||||
IGNORED_NODE,
|
||||
genId,
|
||||
} from './snapshot';
|
||||
import rebuild, {
|
||||
buildNodeWithSN,
|
||||
@@ -28,4 +29,5 @@ export {
|
||||
needMaskingText,
|
||||
classMatchesRegex,
|
||||
IGNORED_NODE,
|
||||
genId,
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ const tagNameRegex = new RegExp('[^a-z0-9-_:]');
|
||||
|
||||
export const IGNORED_NODE = -2;
|
||||
|
||||
function genId(): number {
|
||||
export function genId(): number {
|
||||
return _id++;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"@types/dom-mediacapture-transform": "^0.1.3",
|
||||
"@types/inquirer": "^8.2.1",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/jest-image-snapshot": "^4.3.1",
|
||||
"@types/jest-image-snapshot": "^5.1.0",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/offscreencanvas": "^2019.6.4",
|
||||
"@types/prettier": "^2.3.2",
|
||||
@@ -63,10 +63,10 @@
|
||||
"ignore-styles": "^5.0.1",
|
||||
"inquirer": "^9.0.0",
|
||||
"jest": "^27.5.1",
|
||||
"jest-image-snapshot": "^4.5.1",
|
||||
"jest-image-snapshot": "^5.2.0",
|
||||
"jest-snapshot": "^23.6.0",
|
||||
"prettier": "2.2.1",
|
||||
"puppeteer": "^9.1.1",
|
||||
"puppeteer": "^11.0.0",
|
||||
"rollup": "^2.68.0",
|
||||
"rollup-plugin-esbuild": "^4.9.1",
|
||||
"rollup-plugin-postcss": "^3.1.1",
|
||||
|
||||
@@ -21,6 +21,43 @@ void (async () => {
|
||||
const code = getCode();
|
||||
let events = [];
|
||||
|
||||
async function injectRecording(frame) {
|
||||
await frame.evaluate((rrwebCode) => {
|
||||
const win = window;
|
||||
if (win.__IS_RECORDING__) return;
|
||||
win.__IS_RECORDING__ = true;
|
||||
|
||||
(async () => {
|
||||
function loadScript(code) {
|
||||
const s = document.createElement('script');
|
||||
let r = false;
|
||||
s.type = 'text/javascript';
|
||||
s.innerHTML = code;
|
||||
if (document.head) {
|
||||
document.head.append(s);
|
||||
} else {
|
||||
requestAnimationFrame(() => {
|
||||
document.head.append(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
loadScript(rrwebCode);
|
||||
|
||||
win.events = [];
|
||||
rrweb.record({
|
||||
emit: (event) => {
|
||||
win.events.push(event);
|
||||
win._replLog(event);
|
||||
},
|
||||
plugins: [],
|
||||
recordCanvas: true,
|
||||
recordCrossOriginIframes: true,
|
||||
collectFonts: true,
|
||||
});
|
||||
})();
|
||||
}, code);
|
||||
}
|
||||
|
||||
await start('https://react-redux.realworld.io');
|
||||
|
||||
const fakeGoto = async (page, url) => {
|
||||
@@ -44,8 +81,7 @@ void (async () => {
|
||||
{
|
||||
type: 'input',
|
||||
name: 'url',
|
||||
message:
|
||||
`Enter the url you want to record, e.g [${defaultURL}]: `,
|
||||
message: `Enter the url you want to record, e.g [${defaultURL}]: `,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -116,34 +152,18 @@ void (async () => {
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.goto(url, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 300000,
|
||||
});
|
||||
|
||||
await page.exposeFunction('_replLog', (event) => {
|
||||
events.push(event);
|
||||
});
|
||||
await page.evaluate(`;${code}
|
||||
window.__IS_RECORDING__ = true
|
||||
rrweb.record({
|
||||
emit: event => window._replLog(event),
|
||||
recordCanvas: true,
|
||||
collectFonts: true
|
||||
});
|
||||
`);
|
||||
page.on('framenavigated', async () => {
|
||||
const isRecording = await page.evaluate('window.__IS_RECORDING__');
|
||||
if (!isRecording) {
|
||||
await page.evaluate(`;${code}
|
||||
window.__IS_RECORDING__ = true
|
||||
rrweb.record({
|
||||
emit: event => window._replLog(event),
|
||||
recordCanvas: true,
|
||||
collectFonts: true
|
||||
});
|
||||
`);
|
||||
}
|
||||
|
||||
page.on('framenavigated', async (frame) => {
|
||||
await injectRecording(frame);
|
||||
});
|
||||
|
||||
await page.goto(url, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 300000,
|
||||
});
|
||||
|
||||
emitter.once('done', async (shouldReplay) => {
|
||||
@@ -211,9 +231,9 @@ void (async () => {
|
||||
<script>
|
||||
/*<!--*/
|
||||
const events = ${JSON.stringify(events).replace(
|
||||
/<\/script>/g,
|
||||
'<\\/script>',
|
||||
)};
|
||||
/<\/script>/g,
|
||||
'<\\/script>',
|
||||
)};
|
||||
/*-->*/
|
||||
const replayer = new rrweb.Replayer(events, {
|
||||
UNSAFE_replayCanvas: true
|
||||
|
||||
@@ -17,41 +17,62 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
const code = fs.readFileSync(path.join(__dirname, '../dist/rrweb.js'), 'utf8');
|
||||
const pluginCode = fs.readFileSync(
|
||||
path.join(__dirname, '../dist/plugins/canvas-webrtc-record.js'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
async function startRecording(page, serverURL) {
|
||||
try {
|
||||
await page.addScriptTag({ url: `${serverURL}/rrweb.js` });
|
||||
await page.addScriptTag({
|
||||
url: `${serverURL}/plugins/canvas-webrtc-record.js`,
|
||||
});
|
||||
await page.evaluate((serverURL) => {
|
||||
async function injectRecording(frame) {
|
||||
await frame.evaluate(
|
||||
(rrwebCode, pluginCode) => {
|
||||
const win = window;
|
||||
if (win.__IS_RECORDING__) return;
|
||||
win.__IS_RECORDING__ = true;
|
||||
win.events = [];
|
||||
window.record = win.rrweb.record;
|
||||
window.plugin = new rrwebCanvasWebRTCRecord.RRWebPluginCanvasWebRTCRecord(
|
||||
{
|
||||
signalSendCallback: (msg) => {
|
||||
// [record#callback] provides canvas id, stream, and webrtc sdpOffer signal & connect message
|
||||
_signal(msg);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
window.record({
|
||||
emit: (event) => {
|
||||
win.events.push(event);
|
||||
win._captureEvent(event);
|
||||
},
|
||||
plugins: [window.plugin.initPlugin()],
|
||||
recordCanvas: false,
|
||||
collectFonts: true,
|
||||
inlineImages: true,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
(async () => {
|
||||
function loadScript(code) {
|
||||
const s = document.createElement('script');
|
||||
s.type = 'text/javascript';
|
||||
s.innerHTML = code;
|
||||
if (document.head) {
|
||||
document.head.append(s);
|
||||
} else {
|
||||
requestAnimationFrame(() => {
|
||||
document.head.append(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
loadScript(rrwebCode);
|
||||
loadScript(pluginCode);
|
||||
|
||||
win.events = [];
|
||||
window.record = win.rrweb.record;
|
||||
window.plugin = new rrwebCanvasWebRTCRecord.RRWebPluginCanvasWebRTCRecord(
|
||||
{
|
||||
signalSendCallback: (msg) => {
|
||||
// [record#callback] provides canvas id, stream, and webrtc sdpOffer signal & connect message
|
||||
_signal(msg);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
window.record({
|
||||
emit: (event) => {
|
||||
win.events.push(event);
|
||||
win._captureEvent(event);
|
||||
},
|
||||
plugins: [window.plugin.initPlugin()],
|
||||
recordCanvas: false,
|
||||
recordCrossOriginIframes: true,
|
||||
collectFonts: true,
|
||||
inlineImages: true,
|
||||
});
|
||||
})();
|
||||
},
|
||||
code,
|
||||
pluginCode,
|
||||
);
|
||||
}
|
||||
|
||||
async function startReplay(page, serverURL, recordedPage) {
|
||||
@@ -209,6 +230,17 @@ void (async () => {
|
||||
resizeWindow(replayerPage, 0, 800, 800, 800),
|
||||
]);
|
||||
|
||||
await recordedPage.exposeFunction('_captureEvent', (event) => {
|
||||
replayerPage.evaluate((event) => {
|
||||
window.replayer.addEvent(event);
|
||||
}, event);
|
||||
});
|
||||
|
||||
recordedPage.on('framenavigated', async (frame) => {
|
||||
console.log('framenavigated');
|
||||
await injectRecording(frame, serverURL);
|
||||
});
|
||||
|
||||
await recordedPage.goto(url, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 300000,
|
||||
@@ -216,24 +248,6 @@ void (async () => {
|
||||
|
||||
if (!replayerPage) throw new Error('No replayer page found');
|
||||
|
||||
await recordedPage.exposeFunction('_captureEvent', (event) => {
|
||||
replayerPage.evaluate((event) => {
|
||||
window.replayer.addEvent(event);
|
||||
}, event);
|
||||
});
|
||||
await startRecording(recordedPage, serverURL);
|
||||
recordedPage.on('framenavigated', async () => {
|
||||
const isRecording = await recordedPage.evaluate(
|
||||
'window.__IS_RECORDING__',
|
||||
);
|
||||
if (!isRecording) {
|
||||
// When the page navigates, I notice this event is emitted twice so that there are two recording processes running in a single page.
|
||||
// Set recording flag True ASAP to prevent recording twice.
|
||||
await recordedPage.evaluate('window.__IS_RECORDING__ = true');
|
||||
await startRecording(recordedPage, serverURL);
|
||||
}
|
||||
});
|
||||
|
||||
emitter.once('done', async () => {
|
||||
const pages = [
|
||||
...(await recordingBrowser.pages()),
|
||||
|
||||
@@ -1,14 +1,39 @@
|
||||
import type { Mirror } from 'rrweb-snapshot';
|
||||
import SimplePeer from 'simple-peer-light';
|
||||
import type { RecordPlugin } from '@rrweb/types';
|
||||
import type { RecordPlugin, ICrossOriginIframeMirror } from '@rrweb/types';
|
||||
import type { WebRTCDataChannel } from '../types';
|
||||
|
||||
export const PLUGIN_NAME = 'rrweb/canvas-webrtc@1';
|
||||
|
||||
export type CrossOriginIframeMessageEventContent = {
|
||||
type: 'rrweb-canvas-webrtc';
|
||||
data:
|
||||
| {
|
||||
type: 'signal';
|
||||
signal: RTCSessionDescriptionInit;
|
||||
}
|
||||
| {
|
||||
type: 'who-has-canvas';
|
||||
rootId: number;
|
||||
id: number;
|
||||
}
|
||||
| {
|
||||
type: 'i-have-canvas';
|
||||
rootId: number;
|
||||
};
|
||||
};
|
||||
|
||||
export class RRWebPluginCanvasWebRTCRecord {
|
||||
private peer: SimplePeer.Instance | null = null;
|
||||
private mirror: Mirror;
|
||||
private crossOriginIframeMirror: ICrossOriginIframeMirror;
|
||||
private streamMap: Map<number, MediaStream> = new Map();
|
||||
private incomingStreams = new Set<MediaStream>();
|
||||
private outgoingStreams = new Set<MediaStream>();
|
||||
private streamNodeMap = new Map<string, number>();
|
||||
private canvasWindowMap = new Map<number, WindowProxy>();
|
||||
private windowPeerMap = new WeakMap<WindowProxy, SimplePeer.Instance>();
|
||||
private peerWindowMap = new WeakMap<SimplePeer.Instance, WindowProxy>();
|
||||
private signalSendCallback: (msg: RTCSessionDescriptionInit) => void;
|
||||
|
||||
constructor({
|
||||
@@ -19,14 +44,19 @@ export class RRWebPluginCanvasWebRTCRecord {
|
||||
peer?: SimplePeer.Instance;
|
||||
}) {
|
||||
this.signalSendCallback = signalSendCallback;
|
||||
window.addEventListener(
|
||||
'message',
|
||||
this.windowPostMessageHandler.bind(this),
|
||||
);
|
||||
if (peer) this.peer = peer;
|
||||
}
|
||||
|
||||
public initPlugin(): RecordPlugin {
|
||||
return {
|
||||
name: PLUGIN_NAME,
|
||||
getMirror: (mirror) => {
|
||||
this.mirror = mirror;
|
||||
getMirror: ({ nodeMirror, crossOriginIframeMirror }) => {
|
||||
this.mirror = nodeMirror;
|
||||
this.crossOriginIframeMirror = crossOriginIframeMirror;
|
||||
},
|
||||
options: {},
|
||||
};
|
||||
@@ -37,58 +67,239 @@ export class RRWebPluginCanvasWebRTCRecord {
|
||||
this.peer?.signal(signal);
|
||||
}
|
||||
|
||||
public signalReceiveFromCrossOriginIframe(
|
||||
signal: RTCSessionDescriptionInit,
|
||||
source: WindowProxy,
|
||||
) {
|
||||
const peer = this.setupPeer(source);
|
||||
peer.signal(signal);
|
||||
}
|
||||
|
||||
private startStream(id: number, stream: MediaStream) {
|
||||
if (!this.peer) return this.setupPeer();
|
||||
if (!this.peer) this.setupPeer();
|
||||
|
||||
const data: WebRTCDataChannel = {
|
||||
nodeId: id,
|
||||
streamId: stream.id,
|
||||
};
|
||||
this.peer?.send(JSON.stringify(data));
|
||||
this.peer?.addStream(stream);
|
||||
if (!this.outgoingStreams.has(stream)) this.peer?.addStream(stream);
|
||||
this.outgoingStreams.add(stream);
|
||||
}
|
||||
|
||||
public setupPeer() {
|
||||
if (!this.peer) {
|
||||
this.peer = new SimplePeer({
|
||||
public setupPeer(source?: WindowProxy): SimplePeer.Instance {
|
||||
let peer: SimplePeer.Instance;
|
||||
|
||||
if (!source) {
|
||||
if (this.peer) return this.peer;
|
||||
|
||||
peer = this.peer = new SimplePeer({
|
||||
initiator: true,
|
||||
// trickle: false, // only create one WebRTC offer per session
|
||||
});
|
||||
} else {
|
||||
const peerFromMap = this.windowPeerMap.get(source);
|
||||
|
||||
this.peer.on('error', (err: Error) => {
|
||||
this.peer = null;
|
||||
console.log('error', err);
|
||||
});
|
||||
if (peerFromMap) return peerFromMap;
|
||||
|
||||
this.peer.on('close', () => {
|
||||
this.peer = null;
|
||||
console.log('closing');
|
||||
});
|
||||
|
||||
this.peer.on('signal', (data: RTCSessionDescriptionInit) => {
|
||||
this.signalSendCallback(data);
|
||||
});
|
||||
|
||||
this.peer.on('connect', () => {
|
||||
for (const [id, stream] of this.streamMap) {
|
||||
this.startStream(id, stream);
|
||||
}
|
||||
peer = new SimplePeer({
|
||||
initiator: false,
|
||||
// trickle: false, // only create one WebRTC offer per session
|
||||
});
|
||||
this.windowPeerMap.set(source, peer);
|
||||
this.peerWindowMap.set(peer, source);
|
||||
}
|
||||
|
||||
const resetPeer = (source?: WindowProxy) => {
|
||||
if (!source) return (this.peer = null);
|
||||
|
||||
this.windowPeerMap.delete(source);
|
||||
this.peerWindowMap.delete(peer);
|
||||
};
|
||||
|
||||
peer.on('error', (err: Error) => {
|
||||
resetPeer(source);
|
||||
console.log('error', err);
|
||||
});
|
||||
|
||||
peer.on('close', () => {
|
||||
resetPeer(source);
|
||||
console.log('closing');
|
||||
});
|
||||
|
||||
peer.on('signal', (data: RTCSessionDescriptionInit) => {
|
||||
if (this.inRootFrame()) {
|
||||
if (peer === this.peer) {
|
||||
// connected to replayer
|
||||
this.signalSendCallback(data);
|
||||
} else {
|
||||
// connected to cross-origin iframe
|
||||
this.peerWindowMap.get(peer)?.postMessage(
|
||||
{
|
||||
type: 'rrweb-canvas-webrtc',
|
||||
data: {
|
||||
type: 'signal',
|
||||
signal: data,
|
||||
},
|
||||
} as CrossOriginIframeMessageEventContent,
|
||||
'*',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// connected to root frame
|
||||
window.top?.postMessage(
|
||||
{
|
||||
type: 'rrweb-canvas-webrtc',
|
||||
data: {
|
||||
type: 'signal',
|
||||
signal: data,
|
||||
},
|
||||
} as CrossOriginIframeMessageEventContent,
|
||||
'*',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
peer.on('connect', () => {
|
||||
// connected to cross-origin iframe, no need to do anything
|
||||
if (this.inRootFrame() && peer !== this.peer) return;
|
||||
|
||||
// cross origin frame connected to root frame
|
||||
// or root frame connected to replayer
|
||||
// send all streams to peer
|
||||
for (const [id, stream] of this.streamMap) {
|
||||
this.startStream(id, stream);
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.inRootFrame()) return peer;
|
||||
|
||||
peer.on('data', (data: SimplePeer.SimplePeerData) => {
|
||||
try {
|
||||
const json = JSON.parse(data as string) as WebRTCDataChannel;
|
||||
this.streamNodeMap.set(json.streamId, json.nodeId);
|
||||
} catch (error) {
|
||||
console.error('Could not parse data', error);
|
||||
}
|
||||
this.flushStreams();
|
||||
});
|
||||
|
||||
peer.on('stream', (stream: MediaStream) => {
|
||||
this.incomingStreams.add(stream);
|
||||
this.flushStreams();
|
||||
});
|
||||
|
||||
return peer;
|
||||
}
|
||||
|
||||
public setupStream(id: number): false | MediaStream {
|
||||
public setupStream(id: number, rootId?: number): boolean | MediaStream {
|
||||
if (id === -1) return false;
|
||||
let stream: MediaStream | undefined = this.streamMap.get(id);
|
||||
let stream: MediaStream | undefined = this.streamMap.get(rootId || id);
|
||||
if (stream) return stream;
|
||||
|
||||
const el = this.mirror.getNode(id) as HTMLCanvasElement | null;
|
||||
if (!el || !('captureStream' in el)) return false;
|
||||
|
||||
if (!el || !('captureStream' in el))
|
||||
// we don't have it, lets check our iframes
|
||||
return this.setupStreamInCrossOriginIframe(id, rootId || id);
|
||||
|
||||
if (!this.inRootFrame()) {
|
||||
window.top?.postMessage(
|
||||
{
|
||||
type: 'rrweb-canvas-webrtc',
|
||||
data: {
|
||||
type: 'i-have-canvas',
|
||||
rootId: rootId || id,
|
||||
},
|
||||
} as CrossOriginIframeMessageEventContent,
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
stream = el.captureStream();
|
||||
this.streamMap.set(id, stream);
|
||||
this.streamMap.set(rootId || id, stream);
|
||||
this.setupPeer();
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
private flushStreams() {
|
||||
this.incomingStreams.forEach((stream) => {
|
||||
const nodeId = this.streamNodeMap.get(stream.id);
|
||||
if (!nodeId) return;
|
||||
// got remote video stream, now let's send it to the replayer
|
||||
this.startStream(nodeId, stream);
|
||||
});
|
||||
}
|
||||
|
||||
private inRootFrame(): boolean {
|
||||
return Boolean(window.top && window.top === window);
|
||||
}
|
||||
|
||||
public setupStreamInCrossOriginIframe(id: number, rootId: number): boolean {
|
||||
let found = false;
|
||||
|
||||
document.querySelectorAll('iframe').forEach((iframe) => {
|
||||
if (found) return;
|
||||
|
||||
const remoteId = this.crossOriginIframeMirror.getRemoteId(iframe, id);
|
||||
if (remoteId === -1) return;
|
||||
|
||||
found = true;
|
||||
iframe.contentWindow?.postMessage(
|
||||
{
|
||||
type: 'rrweb-canvas-webrtc',
|
||||
data: {
|
||||
type: 'who-has-canvas',
|
||||
id: remoteId,
|
||||
rootId,
|
||||
},
|
||||
} as CrossOriginIframeMessageEventContent,
|
||||
'*',
|
||||
);
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
private isCrossOriginIframeMessageEventContent(
|
||||
event: MessageEvent,
|
||||
): event is MessageEvent<CrossOriginIframeMessageEventContent> {
|
||||
return Boolean(
|
||||
'type' in event.data &&
|
||||
'data' in event.data &&
|
||||
(event.data as CrossOriginIframeMessageEventContent).type ===
|
||||
'rrweb-canvas-webrtc' &&
|
||||
(event.data as CrossOriginIframeMessageEventContent).data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* All messages being sent to the (root or sub) frame are received through `windowPostMessageHandler`.
|
||||
* @param event - The message event
|
||||
*/
|
||||
private windowPostMessageHandler(
|
||||
event: MessageEvent<CrossOriginIframeMessageEventContent> | MessageEvent,
|
||||
) {
|
||||
if (!this.isCrossOriginIframeMessageEventContent(event)) return;
|
||||
|
||||
const { type } = event.data.data;
|
||||
if (type === 'who-has-canvas') {
|
||||
const { id, rootId } = event.data.data;
|
||||
this.setupStream(id, rootId);
|
||||
} else if (type === 'signal') {
|
||||
const { signal } = event.data.data;
|
||||
const { source } = event;
|
||||
if (!source || !('self' in source)) return;
|
||||
if (this.inRootFrame()) {
|
||||
this.signalReceiveFromCrossOriginIframe(signal, source);
|
||||
} else {
|
||||
this.signalReceive(signal);
|
||||
}
|
||||
} else if (type === 'i-have-canvas') {
|
||||
const { rootId } = event.data.data;
|
||||
const { source } = event;
|
||||
if (!source || !('self' in source)) return;
|
||||
this.canvasWindowMap.set(rootId, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ export class RRWebPluginCanvasWebRTCReplay {
|
||||
this.canvasFoundCallback(node, context);
|
||||
}
|
||||
},
|
||||
getMirror: (mirror: Mirror) => {
|
||||
this.mirror = mirror;
|
||||
getMirror: (options) => {
|
||||
this.mirror = options.nodeMirror;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
88
packages/rrweb/src/record/cross-origin-iframe-mirror.ts
Normal file
88
packages/rrweb/src/record/cross-origin-iframe-mirror.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { ICrossOriginIframeMirror } from '@rrweb/types';
|
||||
export default class CrossOriginIframeMirror
|
||||
implements ICrossOriginIframeMirror {
|
||||
private iframeIdToRemoteIdMap: WeakMap<
|
||||
HTMLIFrameElement,
|
||||
Map<number, number>
|
||||
> = new WeakMap();
|
||||
private iframeRemoteIdToIdMap: WeakMap<
|
||||
HTMLIFrameElement,
|
||||
Map<number, number>
|
||||
> = new WeakMap();
|
||||
|
||||
constructor(private generateIdFn: () => number) {}
|
||||
|
||||
getId(
|
||||
iframe: HTMLIFrameElement,
|
||||
remoteId: number,
|
||||
idToRemoteMap?: Map<number, number>,
|
||||
remoteToIdMap?: Map<number, number>,
|
||||
): number {
|
||||
const idToRemoteIdMap = idToRemoteMap || this.getIdToRemoteIdMap(iframe);
|
||||
const remoteIdToIdMap = remoteToIdMap || this.getRemoteIdToIdMap(iframe);
|
||||
|
||||
let id = idToRemoteIdMap.get(remoteId);
|
||||
if (!id) {
|
||||
id = this.generateIdFn();
|
||||
idToRemoteIdMap.set(remoteId, id);
|
||||
remoteIdToIdMap.set(id, remoteId);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
getIds(iframe: HTMLIFrameElement, remoteId: number[]): number[] {
|
||||
const idToRemoteIdMap = this.getIdToRemoteIdMap(iframe);
|
||||
const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe);
|
||||
return remoteId.map((id) =>
|
||||
this.getId(iframe, id, idToRemoteIdMap, remoteIdToIdMap),
|
||||
);
|
||||
}
|
||||
|
||||
getRemoteId(
|
||||
iframe: HTMLIFrameElement,
|
||||
id: number,
|
||||
map?: Map<number, number>,
|
||||
): number {
|
||||
const remoteIdToIdMap = map || this.getRemoteIdToIdMap(iframe);
|
||||
|
||||
if (typeof id !== 'number') return id;
|
||||
|
||||
const remoteId = remoteIdToIdMap.get(id);
|
||||
if (!remoteId) return -1;
|
||||
return remoteId;
|
||||
}
|
||||
|
||||
getRemoteIds(iframe: HTMLIFrameElement, ids: number[]): number[] {
|
||||
const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe);
|
||||
|
||||
return ids.map((id) => this.getRemoteId(iframe, id, remoteIdToIdMap));
|
||||
}
|
||||
|
||||
reset(iframe?: HTMLIFrameElement) {
|
||||
if (!iframe) {
|
||||
this.iframeIdToRemoteIdMap = new WeakMap();
|
||||
this.iframeRemoteIdToIdMap = new WeakMap();
|
||||
return;
|
||||
}
|
||||
this.iframeIdToRemoteIdMap.delete(iframe);
|
||||
this.iframeRemoteIdToIdMap.delete(iframe);
|
||||
}
|
||||
|
||||
private getIdToRemoteIdMap(iframe: HTMLIFrameElement) {
|
||||
let idToRemoteIdMap = this.iframeIdToRemoteIdMap.get(iframe);
|
||||
if (!idToRemoteIdMap) {
|
||||
idToRemoteIdMap = new Map();
|
||||
this.iframeIdToRemoteIdMap.set(iframe, idToRemoteIdMap);
|
||||
}
|
||||
return idToRemoteIdMap;
|
||||
}
|
||||
|
||||
private getRemoteIdToIdMap(iframe: HTMLIFrameElement) {
|
||||
let remoteIdToIdMap = this.iframeRemoteIdToIdMap.get(iframe);
|
||||
if (!remoteIdToIdMap) {
|
||||
remoteIdToIdMap = new Map();
|
||||
this.iframeRemoteIdToIdMap.set(iframe, remoteIdToIdMap);
|
||||
}
|
||||
return remoteIdToIdMap;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,52 @@
|
||||
import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
|
||||
import type { mutationCallBack } from '@rrweb/types';
|
||||
import { genId } from 'rrweb-snapshot';
|
||||
import type { CrossOriginIframeMessageEvent } from '../types';
|
||||
import CrossOriginIframeMirror from './cross-origin-iframe-mirror';
|
||||
import { EventType, IncrementalSource } from '@rrweb/types';
|
||||
import type { eventWithTime, mutationCallBack } from '@rrweb/types';
|
||||
import type { StylesheetManager } from './stylesheet-manager';
|
||||
|
||||
export class IframeManager {
|
||||
private iframes: WeakMap<HTMLIFrameElement, true> = new WeakMap();
|
||||
private crossOriginIframeMap: WeakMap<
|
||||
MessageEventSource,
|
||||
HTMLIFrameElement
|
||||
> = new WeakMap();
|
||||
public crossOriginIframeMirror = new CrossOriginIframeMirror(genId);
|
||||
public crossOriginIframeStyleMirror: CrossOriginIframeMirror;
|
||||
private mirror: Mirror;
|
||||
private mutationCb: mutationCallBack;
|
||||
private wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void;
|
||||
private loadListener?: (iframeEl: HTMLIFrameElement) => unknown;
|
||||
private stylesheetManager: StylesheetManager;
|
||||
private recordCrossOriginIframes: boolean;
|
||||
|
||||
constructor(options: {
|
||||
mirror: Mirror;
|
||||
mutationCb: mutationCallBack;
|
||||
stylesheetManager: StylesheetManager;
|
||||
recordCrossOriginIframes: boolean;
|
||||
wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void;
|
||||
}) {
|
||||
this.mutationCb = options.mutationCb;
|
||||
this.wrappedEmit = options.wrappedEmit;
|
||||
this.stylesheetManager = options.stylesheetManager;
|
||||
this.recordCrossOriginIframes = options.recordCrossOriginIframes;
|
||||
this.crossOriginIframeStyleMirror = new CrossOriginIframeMirror(
|
||||
this.stylesheetManager.styleMirror.generateId.bind(
|
||||
this.stylesheetManager.styleMirror,
|
||||
),
|
||||
);
|
||||
this.mirror = options.mirror;
|
||||
if (this.recordCrossOriginIframes) {
|
||||
window.addEventListener('message', this.handleMessage.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
public addIframe(iframeEl: HTMLIFrameElement) {
|
||||
this.iframes.set(iframeEl, true);
|
||||
if (iframeEl.contentWindow)
|
||||
this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl);
|
||||
}
|
||||
|
||||
public addLoadListener(cb: (iframeEl: HTMLIFrameElement) => unknown) {
|
||||
@@ -27,12 +56,11 @@ export class IframeManager {
|
||||
public attachIframe(
|
||||
iframeEl: HTMLIFrameElement,
|
||||
childSn: serializedNodeWithId,
|
||||
mirror: Mirror,
|
||||
) {
|
||||
this.mutationCb({
|
||||
adds: [
|
||||
{
|
||||
parentId: mirror.getId(iframeEl),
|
||||
parentId: this.mirror.getId(iframeEl),
|
||||
nextId: null,
|
||||
node: childSn,
|
||||
},
|
||||
@@ -51,7 +79,199 @@ export class IframeManager {
|
||||
)
|
||||
this.stylesheetManager.adoptStyleSheets(
|
||||
iframeEl.contentDocument.adoptedStyleSheets,
|
||||
mirror.getId(iframeEl.contentDocument),
|
||||
this.mirror.getId(iframeEl.contentDocument),
|
||||
);
|
||||
}
|
||||
private handleMessage(message: MessageEvent | CrossOriginIframeMessageEvent) {
|
||||
if ((message as CrossOriginIframeMessageEvent).data.type === 'rrweb') {
|
||||
const iframeSourceWindow = message.source;
|
||||
if (!iframeSourceWindow) return;
|
||||
|
||||
const iframeEl = this.crossOriginIframeMap.get(message.source);
|
||||
if (!iframeEl) return;
|
||||
|
||||
const transformedEvent = this.transformCrossOriginEvent(
|
||||
iframeEl,
|
||||
(message as CrossOriginIframeMessageEvent).data.event,
|
||||
);
|
||||
|
||||
if (transformedEvent)
|
||||
this.wrappedEmit(
|
||||
transformedEvent,
|
||||
(message as CrossOriginIframeMessageEvent).data.isCheckout,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private transformCrossOriginEvent(
|
||||
iframeEl: HTMLIFrameElement,
|
||||
e: eventWithTime,
|
||||
): eventWithTime | false {
|
||||
switch (e.type) {
|
||||
case EventType.FullSnapshot: {
|
||||
this.crossOriginIframeMirror.reset(iframeEl);
|
||||
this.crossOriginIframeStyleMirror.reset(iframeEl);
|
||||
/**
|
||||
* Replaces the original id of the iframe with a new set of unique ids
|
||||
*/
|
||||
this.replaceIdOnNode(e.data.node, iframeEl);
|
||||
return {
|
||||
timestamp: e.timestamp,
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
adds: [
|
||||
{
|
||||
parentId: this.mirror.getId(iframeEl),
|
||||
nextId: null,
|
||||
node: e.data.node,
|
||||
},
|
||||
],
|
||||
removes: [],
|
||||
texts: [],
|
||||
attributes: [],
|
||||
isAttachIframe: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
case EventType.Meta:
|
||||
case EventType.Load:
|
||||
case EventType.DomContentLoaded: {
|
||||
return false;
|
||||
}
|
||||
case EventType.Plugin: {
|
||||
return e;
|
||||
}
|
||||
case EventType.Custom: {
|
||||
this.replaceIds(
|
||||
e.data.payload as {
|
||||
id?: unknown;
|
||||
parentId?: unknown;
|
||||
previousId?: unknown;
|
||||
nextId?: unknown;
|
||||
},
|
||||
iframeEl,
|
||||
['id', 'parentId', 'previousId', 'nextId'],
|
||||
);
|
||||
return e;
|
||||
}
|
||||
case EventType.IncrementalSnapshot: {
|
||||
switch (e.data.source) {
|
||||
case IncrementalSource.Mutation: {
|
||||
e.data.adds.forEach((n) => {
|
||||
this.replaceIds(n, iframeEl, [
|
||||
'parentId',
|
||||
'nextId',
|
||||
'previousId',
|
||||
]);
|
||||
this.replaceIdOnNode(n.node, iframeEl);
|
||||
});
|
||||
e.data.removes.forEach((n) => {
|
||||
this.replaceIds(n, iframeEl, ['parentId', 'id']);
|
||||
});
|
||||
e.data.attributes.forEach((n) => {
|
||||
this.replaceIds(n, iframeEl, ['id']);
|
||||
});
|
||||
e.data.texts.forEach((n) => {
|
||||
this.replaceIds(n, iframeEl, ['id']);
|
||||
});
|
||||
return e;
|
||||
}
|
||||
case IncrementalSource.Drag:
|
||||
case IncrementalSource.TouchMove:
|
||||
case IncrementalSource.MouseMove: {
|
||||
e.data.positions.forEach((p) => {
|
||||
this.replaceIds(p, iframeEl, ['id']);
|
||||
});
|
||||
return e;
|
||||
}
|
||||
case IncrementalSource.ViewportResize: {
|
||||
// can safely ignore these events
|
||||
return false;
|
||||
}
|
||||
case IncrementalSource.MediaInteraction:
|
||||
case IncrementalSource.MouseInteraction:
|
||||
case IncrementalSource.Scroll:
|
||||
case IncrementalSource.CanvasMutation:
|
||||
case IncrementalSource.Input: {
|
||||
this.replaceIds(e.data, iframeEl, ['id']);
|
||||
return e;
|
||||
}
|
||||
case IncrementalSource.StyleSheetRule:
|
||||
case IncrementalSource.StyleDeclaration: {
|
||||
this.replaceIds(e.data, iframeEl, ['id']);
|
||||
this.replaceStyleIds(e.data, iframeEl, ['styleId']);
|
||||
return e;
|
||||
}
|
||||
case IncrementalSource.Font: {
|
||||
// fine as-is no modification needed
|
||||
return e;
|
||||
}
|
||||
case IncrementalSource.Selection: {
|
||||
e.data.ranges.forEach((range) => {
|
||||
this.replaceIds(range, iframeEl, ['start', 'end']);
|
||||
});
|
||||
return e;
|
||||
}
|
||||
case IncrementalSource.AdoptedStyleSheet: {
|
||||
this.replaceIds(e.data, iframeEl, ['id']);
|
||||
this.replaceStyleIds(e.data, iframeEl, ['styleIds']);
|
||||
e.data.styles?.forEach((style) => {
|
||||
this.replaceStyleIds(style, iframeEl, ['styleId']);
|
||||
});
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private replace<T extends Record<string, unknown>>(
|
||||
iframeMirror: CrossOriginIframeMirror,
|
||||
obj: T,
|
||||
iframeEl: HTMLIFrameElement,
|
||||
keys: Array<keyof T>,
|
||||
): T {
|
||||
for (const key of keys) {
|
||||
if (!Array.isArray(obj[key]) && typeof obj[key] !== 'number') continue;
|
||||
if (Array.isArray(obj[key])) {
|
||||
obj[key] = iframeMirror.getIds(
|
||||
iframeEl,
|
||||
obj[key] as number[],
|
||||
) as T[keyof T];
|
||||
} else {
|
||||
(obj[key] as number) = iframeMirror.getId(iframeEl, obj[key] as number);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private replaceIds<T extends Record<string, unknown>>(
|
||||
obj: T,
|
||||
iframeEl: HTMLIFrameElement,
|
||||
keys: Array<keyof T>,
|
||||
): T {
|
||||
return this.replace(this.crossOriginIframeMirror, obj, iframeEl, keys);
|
||||
}
|
||||
|
||||
private replaceStyleIds<T extends Record<string, unknown>>(
|
||||
obj: T,
|
||||
iframeEl: HTMLIFrameElement,
|
||||
keys: Array<keyof T>,
|
||||
): T {
|
||||
return this.replace(this.crossOriginIframeStyleMirror, obj, iframeEl, keys);
|
||||
}
|
||||
|
||||
private replaceIdOnNode(
|
||||
node: serializedNodeWithId,
|
||||
iframeEl: HTMLIFrameElement,
|
||||
) {
|
||||
this.replaceIds(node, iframeEl, ['id']);
|
||||
if ('childNodes' in node) {
|
||||
node.childNodes.forEach((child) => {
|
||||
this.replaceIdOnNode(child, iframeEl);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
canvasMutationParam,
|
||||
adoptedStyleSheetParam,
|
||||
} from '@rrweb/types';
|
||||
import type { CrossOriginIframeMessageEventContent } from '../types';
|
||||
import { IframeManager } from './iframe-manager';
|
||||
import { ShadowDomManager } from './shadow-dom-manager';
|
||||
import { CanvasManager } from './observers/canvas/canvas-manager';
|
||||
@@ -69,6 +70,7 @@ function record<T = eventWithTime>(
|
||||
dataURLOptions = {},
|
||||
mousemoveWait,
|
||||
recordCanvas = false,
|
||||
recordCrossOriginIframes = false,
|
||||
userTriggeredOnInput = false,
|
||||
collectFonts = false,
|
||||
inlineImages = false,
|
||||
@@ -77,8 +79,22 @@ function record<T = eventWithTime>(
|
||||
ignoreCSSAttributes = new Set([]),
|
||||
} = options;
|
||||
|
||||
const inEmittingFrame = recordCrossOriginIframes
|
||||
? window.parent === window
|
||||
: true;
|
||||
|
||||
let passEmitsToParent = false;
|
||||
if (!inEmittingFrame) {
|
||||
try {
|
||||
window.parent.document; // throws if parent is cross-origin
|
||||
passEmitsToParent = false; // if parent is same origin we collect iframe events from the parent
|
||||
} catch (e) {
|
||||
passEmitsToParent = true;
|
||||
}
|
||||
}
|
||||
|
||||
// runtime checks for user options
|
||||
if (!emit) {
|
||||
if (inEmittingFrame && !emit) {
|
||||
throw new Error('emit function is required');
|
||||
}
|
||||
// move departed options to new options
|
||||
@@ -138,13 +154,6 @@ function record<T = eventWithTime>(
|
||||
let lastFullSnapshotEvent: eventWithTime;
|
||||
let incrementalSnapshotCount = 0;
|
||||
|
||||
/**
|
||||
* Exposes mirror to the plugins
|
||||
*/
|
||||
for (const plugin of plugins || []) {
|
||||
if (plugin.getMirror) plugin.getMirror(mirror);
|
||||
}
|
||||
|
||||
const eventProcessor = (e: eventWithTime): T => {
|
||||
for (const plugin of plugins || []) {
|
||||
if (plugin.eventProcessor) {
|
||||
@@ -170,7 +179,17 @@ function record<T = eventWithTime>(
|
||||
mutationBuffers.forEach((buf) => buf.unfreeze());
|
||||
}
|
||||
|
||||
emit(eventProcessor(e), isCheckout);
|
||||
if (inEmittingFrame) {
|
||||
emit?.(eventProcessor(e), isCheckout);
|
||||
} else if (passEmitsToParent) {
|
||||
const message: CrossOriginIframeMessageEventContent<T> = {
|
||||
type: 'rrweb',
|
||||
event: eventProcessor(e),
|
||||
isCheckout,
|
||||
};
|
||||
window.parent.postMessage(message, '*');
|
||||
}
|
||||
|
||||
if (e.type === EventType.FullSnapshot) {
|
||||
lastFullSnapshotEvent = e;
|
||||
incrementalSnapshotCount = 0;
|
||||
@@ -244,10 +263,26 @@ function record<T = eventWithTime>(
|
||||
});
|
||||
|
||||
const iframeManager = new IframeManager({
|
||||
mirror,
|
||||
mutationCb: wrappedMutationEmit,
|
||||
stylesheetManager: stylesheetManager,
|
||||
recordCrossOriginIframes,
|
||||
wrappedEmit,
|
||||
});
|
||||
|
||||
/**
|
||||
* Exposes mirror to the plugins
|
||||
*/
|
||||
for (const plugin of plugins || []) {
|
||||
if (plugin.getMirror)
|
||||
plugin.getMirror({
|
||||
nodeMirror: mirror,
|
||||
crossOriginIframeMirror: iframeManager.crossOriginIframeMirror,
|
||||
crossOriginIframeStyleMirror:
|
||||
iframeManager.crossOriginIframeStyleMirror,
|
||||
});
|
||||
}
|
||||
|
||||
canvasManager = new CanvasManager({
|
||||
recordCanvas,
|
||||
mutationCb: wrappedCanvasMutationEmit,
|
||||
@@ -326,7 +361,7 @@ function record<T = eventWithTime>(
|
||||
}
|
||||
},
|
||||
onIframeLoad: (iframe, childSn) => {
|
||||
iframeManager.attachIframe(iframe, childSn, mirror);
|
||||
iframeManager.attachIframe(iframe, childSn);
|
||||
shadowDomManager.observeAttachShadow(iframe);
|
||||
},
|
||||
onStylesheetLoad: (linkEl, childSn) => {
|
||||
|
||||
@@ -324,7 +324,7 @@ export default class MutationBuffer {
|
||||
}
|
||||
},
|
||||
onIframeLoad: (iframe, childSn) => {
|
||||
this.iframeManager.attachIframe(iframe, childSn, this.mirror);
|
||||
this.iframeManager.attachIframe(iframe, childSn);
|
||||
this.shadowDomManager.observeAttachShadow(iframe);
|
||||
},
|
||||
onStylesheetLoad: (link, childSn) => {
|
||||
|
||||
@@ -198,7 +198,7 @@ export class Replayer {
|
||||
* Exposes mirror to the plugins
|
||||
*/
|
||||
for (const plugin of this.config.plugins || []) {
|
||||
if (plugin.getMirror) plugin.getMirror(this.mirror);
|
||||
if (plugin.getMirror) plugin.getMirror({ nodeMirror: this.mirror });
|
||||
}
|
||||
|
||||
this.emitter.on(ReplayerEvents.Flush, () => {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type {
|
||||
serializedNodeWithId,
|
||||
Mirror,
|
||||
INode,
|
||||
MaskInputOptions,
|
||||
SlimDOMOptions,
|
||||
MaskInputFn,
|
||||
@@ -61,6 +59,7 @@ export type recordOptions<T> = {
|
||||
sampling?: SamplingStrategy;
|
||||
dataURLOptions?: DataURLOptions;
|
||||
recordCanvas?: boolean;
|
||||
recordCrossOriginIframes?: boolean;
|
||||
userTriggeredOnInput?: boolean;
|
||||
collectFonts?: boolean;
|
||||
inlineImages?: boolean;
|
||||
@@ -152,7 +151,7 @@ export type ReplayPlugin = {
|
||||
node: Node | RRNode,
|
||||
context: { id: number; replayer: Replayer },
|
||||
) => void;
|
||||
getMirror?: (mirror: Mirror) => void;
|
||||
getMirror?: (mirrors: { nodeMirror: Mirror }) => void;
|
||||
};
|
||||
export type playerConfig = {
|
||||
speed: number;
|
||||
@@ -194,3 +193,10 @@ declare global {
|
||||
FontFace: typeof FontFace;
|
||||
}
|
||||
}
|
||||
|
||||
export type CrossOriginIframeMessageEventContent<T = eventWithTime> = {
|
||||
type: 'rrweb';
|
||||
event: T;
|
||||
isCheckout?: boolean;
|
||||
};
|
||||
export type CrossOriginIframeMessageEvent = MessageEvent<CrossOriginIframeMessageEventContent>;
|
||||
|
||||
@@ -491,4 +491,8 @@ export class StyleSheetMirror {
|
||||
this.idStyleMap = new Map();
|
||||
this.id = 1;
|
||||
}
|
||||
|
||||
generateId(): number {
|
||||
return this.id++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4014,8 +4014,27 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n\\\\n\\",
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 7
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"style\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"#b-class, #b-class-2 { height: 33px; width: 200px; }\\",
|
||||
\\"isStyle\\": true,
|
||||
\\"id\\": 9
|
||||
}
|
||||
],
|
||||
\\"id\\": 8
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n\\",
|
||||
\\"id\\": 10
|
||||
}
|
||||
],
|
||||
\\"id\\": 3
|
||||
@@ -4023,7 +4042,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n\\\\n\\",
|
||||
\\"id\\": 8
|
||||
\\"id\\": 11
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
@@ -4033,7 +4052,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 10
|
||||
\\"id\\": 13
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
@@ -4043,40 +4062,22 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||
\\"id\\": 12
|
||||
\\"id\\": 15
|
||||
}
|
||||
],
|
||||
\\"id\\": 11
|
||||
\\"id\\": 14
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 13
|
||||
\\"id\\": 16
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 14
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 15
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"h1\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n Verify that block class bugs are fixed\\\\n \\",
|
||||
\\"id\\": 17
|
||||
}
|
||||
],
|
||||
\\"id\\": 16
|
||||
\\"id\\": 17
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
@@ -4085,15 +4086,33 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"tagName\\": \\"h1\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"Verify that block class bugs are fixed\\",
|
||||
\\"id\\": 20
|
||||
}
|
||||
],
|
||||
\\"id\\": 19
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 20
|
||||
\\"id\\": 21
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 22
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 23
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
@@ -4105,7 +4124,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 22
|
||||
\\"id\\": 25
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
@@ -4117,7 +4136,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 24
|
||||
\\"id\\": 27
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
@@ -4127,91 +4146,91 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"VISIBLE\\",
|
||||
\\"id\\": 26
|
||||
\\"id\\": 29
|
||||
}
|
||||
],
|
||||
\\"id\\": 25
|
||||
\\"id\\": 28
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 27
|
||||
\\"id\\": 30
|
||||
}
|
||||
],
|
||||
\\"id\\": 23
|
||||
\\"id\\": 26
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 28
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 29
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 30
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 31
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 32
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 33
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 34
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 32
|
||||
\\"id\\": 35
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {
|
||||
\\"class\\": \\"rr-block\\",
|
||||
\\"rr_width\\": \\"1904px\\",
|
||||
\\"rr_height\\": \\"21px\\"
|
||||
\\"rr_width\\": \\"200px\\",
|
||||
\\"rr_height\\": \\"33px\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 33
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 34
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 35
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 36
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 37
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 37
|
||||
\\"id\\": 38
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 39
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 40
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 38
|
||||
\\"id\\": 41
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
@@ -4223,49 +4242,49 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"MUTATE\\",
|
||||
\\"id\\": 40
|
||||
\\"id\\": 43
|
||||
}
|
||||
],
|
||||
\\"id\\": 39
|
||||
\\"id\\": 42
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 41
|
||||
\\"id\\": 44
|
||||
}
|
||||
],
|
||||
\\"id\\": 21
|
||||
\\"id\\": 24
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 42
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 43
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 44
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 45
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 46
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 47
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 48
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 46
|
||||
\\"id\\": 49
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
@@ -4277,7 +4296,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 48
|
||||
\\"id\\": 51
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
@@ -4289,7 +4308,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 50
|
||||
\\"id\\": 53
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
@@ -4299,91 +4318,91 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"VISIBLE\\",
|
||||
\\"id\\": 52
|
||||
\\"id\\": 55
|
||||
}
|
||||
],
|
||||
\\"id\\": 51
|
||||
\\"id\\": 54
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 53
|
||||
\\"id\\": 56
|
||||
}
|
||||
],
|
||||
\\"id\\": 49
|
||||
\\"id\\": 52
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 54
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 55
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 56
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 57
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 58
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 59
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 60
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 58
|
||||
\\"id\\": 61
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {
|
||||
\\"class\\": \\"rr-block\\",
|
||||
\\"rr_width\\": \\"1904px\\",
|
||||
\\"rr_height\\": \\"21px\\"
|
||||
\\"rr_width\\": \\"200px\\",
|
||||
\\"rr_height\\": \\"33px\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 59
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 60
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 61
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 62
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 63
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 63
|
||||
\\"id\\": 64
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 65
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"br\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 66
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 64
|
||||
\\"id\\": 67
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
@@ -4395,23 +4414,23 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"MUTATE\\",
|
||||
\\"id\\": 66
|
||||
\\"id\\": 69
|
||||
}
|
||||
],
|
||||
\\"id\\": 65
|
||||
\\"id\\": 68
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 67
|
||||
\\"id\\": 70
|
||||
}
|
||||
],
|
||||
\\"id\\": 47
|
||||
\\"id\\": 50
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n\\\\n \\",
|
||||
\\"id\\": 68
|
||||
\\"id\\": 71
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
@@ -4421,18 +4440,18 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||
\\"id\\": 70
|
||||
\\"id\\": 73
|
||||
}
|
||||
],
|
||||
\\"id\\": 69
|
||||
\\"id\\": 72
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\",
|
||||
\\"id\\": 71
|
||||
\\"id\\": 74
|
||||
}
|
||||
],
|
||||
\\"id\\": 9
|
||||
\\"id\\": 12
|
||||
}
|
||||
],
|
||||
\\"id\\": 2
|
||||
@@ -4452,7 +4471,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 1,
|
||||
\\"id\\": 39
|
||||
\\"id\\": 42
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4460,7 +4479,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 5,
|
||||
\\"id\\": 39
|
||||
\\"id\\": 42
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4468,7 +4487,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 0,
|
||||
\\"id\\": 39
|
||||
\\"id\\": 42
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4476,7 +4495,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 2,
|
||||
\\"id\\": 39
|
||||
\\"id\\": 42
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4486,7 +4505,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [
|
||||
{
|
||||
\\"id\\": 33,
|
||||
\\"id\\": 36,
|
||||
\\"attributes\\": {
|
||||
\\"class\\": \\"notB\\"
|
||||
}
|
||||
@@ -4495,50 +4514,19 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 23,
|
||||
\\"parentId\\": 26,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 72
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 72,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 73
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 73,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"button\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 74
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 74,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"I1I2 VISIBLE\\",
|
||||
\\"id\\": 75
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 72,
|
||||
\\"nextId\\": 73,
|
||||
\\"parentId\\": 75,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
@@ -4563,71 +4551,13 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"I1I1 VISIBLE\\",
|
||||
\\"textContent\\": \\"I1I2 VISIBLE\\",
|
||||
\\"id\\": 78
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 1,
|
||||
\\"id\\": 65
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 6,
|
||||
\\"id\\": 39
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 5,
|
||||
\\"id\\": 65
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 0,
|
||||
\\"id\\": 65
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 2,
|
||||
\\"id\\": 65
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [
|
||||
},
|
||||
{
|
||||
\\"id\\": 59,
|
||||
\\"attributes\\": {
|
||||
\\"class\\": \\"notB\\"
|
||||
}
|
||||
}
|
||||
],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 49,
|
||||
\\"nextId\\": null,
|
||||
\\"parentId\\": 75,
|
||||
\\"nextId\\": 76,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
@@ -4641,7 +4571,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"tagName\\": \\"button\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 80
|
||||
@@ -4651,25 +4581,83 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
\\"parentId\\": 80,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"button\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"I1I1 VISIBLE\\",
|
||||
\\"id\\": 81
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 1,
|
||||
\\"id\\": 68
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 6,
|
||||
\\"id\\": 42
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 5,
|
||||
\\"id\\": 68
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 0,
|
||||
\\"id\\": 68
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 2,
|
||||
\\"id\\": 68
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [
|
||||
{
|
||||
\\"parentId\\": 81,
|
||||
\\"id\\": 62,
|
||||
\\"attributes\\": {
|
||||
\\"class\\": \\"notB\\"
|
||||
}
|
||||
}
|
||||
],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 52,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"I1I2 VISIBLE\\",
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 82
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 79,
|
||||
\\"nextId\\": 80,
|
||||
\\"parentId\\": 82,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
@@ -4694,9 +4682,40 @@ exports[`record integration tests mutations should work when blocked class is un
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"I1I1 VISIBLE\\",
|
||||
\\"textContent\\": \\"I1I2 VISIBLE\\",
|
||||
\\"id\\": 85
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 82,
|
||||
\\"nextId\\": 83,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 86
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 86,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"button\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 87
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 87,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"I1I1 VISIBLE\\",
|
||||
\\"id\\": 88
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1905,168 +1905,6 @@ exports[`record captures stylesheet rules 1`] = `
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record captures stylesheets in iframes that are still loading 1`] = `
|
||||
"[
|
||||
{
|
||||
\\"type\\": 4,
|
||||
\\"data\\": {
|
||||
\\"href\\": \\"about:blank\\",
|
||||
\\"width\\": 1920,
|
||||
\\"height\\": 1080
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"data\\": {
|
||||
\\"node\\": {
|
||||
\\"type\\": 0,
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 1,
|
||||
\\"name\\": \\"html\\",
|
||||
\\"publicId\\": \\"\\",
|
||||
\\"systemId\\": \\"\\",
|
||||
\\"id\\": 2
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 4
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 6
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"input\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"text\\",
|
||||
\\"size\\": \\"40\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 7
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
|
||||
\\"id\\": 8
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"iframe\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 9
|
||||
}
|
||||
],
|
||||
\\"id\\": 5
|
||||
}
|
||||
],
|
||||
\\"id\\": 3
|
||||
}
|
||||
],
|
||||
\\"id\\": 1
|
||||
},
|
||||
\\"initialOffset\\": {
|
||||
\\"left\\": 0,
|
||||
\\"top\\": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 9,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 0,
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"link\\",
|
||||
\\"attributes\\": {
|
||||
\\"rel\\": \\"stylesheet\\",
|
||||
\\"href\\": \\"blob:null\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 10,
|
||||
\\"id\\": 13
|
||||
}
|
||||
],
|
||||
\\"rootId\\": 10,
|
||||
\\"id\\": 12
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 10,
|
||||
\\"id\\": 14
|
||||
}
|
||||
],
|
||||
\\"rootId\\": 10,
|
||||
\\"id\\": 11
|
||||
}
|
||||
],
|
||||
\\"compatMode\\": \\"BackCompat\\",
|
||||
\\"id\\": 10
|
||||
}
|
||||
}
|
||||
],
|
||||
\\"removes\\": [],
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"isAttachIframe\\": true
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"adds\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [
|
||||
{
|
||||
\\"id\\": 13,
|
||||
\\"attributes\\": {
|
||||
\\"_cssText\\": \\"body { color: pink; }\\"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = `
|
||||
"[
|
||||
{
|
||||
@@ -2211,126 +2049,6 @@ exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = `
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record captures stylesheets that are still loading 1`] = `
|
||||
"[
|
||||
{
|
||||
\\"type\\": 4,
|
||||
\\"data\\": {
|
||||
\\"href\\": \\"about:blank\\",
|
||||
\\"width\\": 1920,
|
||||
\\"height\\": 1080
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"data\\": {
|
||||
\\"node\\": {
|
||||
\\"type\\": 0,
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 1,
|
||||
\\"name\\": \\"html\\",
|
||||
\\"publicId\\": \\"\\",
|
||||
\\"systemId\\": \\"\\",
|
||||
\\"id\\": 2
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 4
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 6
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"input\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"text\\",
|
||||
\\"size\\": \\"40\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 7
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
|
||||
\\"id\\": 8
|
||||
}
|
||||
],
|
||||
\\"id\\": 5
|
||||
}
|
||||
],
|
||||
\\"id\\": 3
|
||||
}
|
||||
],
|
||||
\\"id\\": 1
|
||||
},
|
||||
\\"initialOffset\\": {
|
||||
\\"left\\": 0,
|
||||
\\"top\\": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 4,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"link\\",
|
||||
\\"attributes\\": {
|
||||
\\"rel\\": \\"stylesheet\\",
|
||||
\\"href\\": \\"blob:null\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 9
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"adds\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [
|
||||
{
|
||||
\\"id\\": 9,
|
||||
\\"attributes\\": {
|
||||
\\"_cssText\\": \\"body { color: pink; }\\"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record captures stylesheets with \`blob:\` url 1`] = `
|
||||
"[
|
||||
{
|
||||
@@ -2910,6 +2628,522 @@ exports[`record is safe to checkout during async callbacks 1`] = `
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record loading stylesheets captures stylesheets in iframes that are still loading 1`] = `
|
||||
"[
|
||||
{
|
||||
\\"type\\": 4,
|
||||
\\"data\\": {
|
||||
\\"href\\": \\"about:blank\\",
|
||||
\\"width\\": 1920,
|
||||
\\"height\\": 1080
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"data\\": {
|
||||
\\"node\\": {
|
||||
\\"type\\": 0,
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 1,
|
||||
\\"name\\": \\"html\\",
|
||||
\\"publicId\\": \\"\\",
|
||||
\\"systemId\\": \\"\\",
|
||||
\\"id\\": 2
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {
|
||||
\\"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\\": \\"Hello World!\\",
|
||||
\\"id\\": 13
|
||||
}
|
||||
],
|
||||
\\"id\\": 12
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 14
|
||||
}
|
||||
],
|
||||
\\"id\\": 4
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 15
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\",
|
||||
\\"id\\": 17
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"iframe\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 18
|
||||
}
|
||||
],
|
||||
\\"id\\": 16
|
||||
}
|
||||
],
|
||||
\\"id\\": 3
|
||||
}
|
||||
],
|
||||
\\"id\\": 1
|
||||
},
|
||||
\\"initialOffset\\": {
|
||||
\\"left\\": 0,
|
||||
\\"top\\": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 18,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 0,
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 1,
|
||||
\\"name\\": \\"html\\",
|
||||
\\"publicId\\": \\"\\",
|
||||
\\"systemId\\": \\"\\",
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 20
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {
|
||||
\\"lang\\": \\"en\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 23
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"meta\\",
|
||||
\\"attributes\\": {
|
||||
\\"charset\\": \\"UTF-8\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 24
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 25
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"meta\\",
|
||||
\\"attributes\\": {
|
||||
\\"http-equiv\\": \\"X-UA-Compatible\\",
|
||||
\\"content\\": \\"IE=edge\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 26
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 27
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"meta\\",
|
||||
\\"attributes\\": {
|
||||
\\"name\\": \\"viewport\\",
|
||||
\\"content\\": \\"width=device-width, initial-scale=1.0\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 28
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 29
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"title\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"Hello World!\\",
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 31
|
||||
}
|
||||
],
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 30
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 32
|
||||
}
|
||||
],
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 22
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 33
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\",
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 35
|
||||
}
|
||||
],
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 34
|
||||
}
|
||||
],
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 21
|
||||
}
|
||||
],
|
||||
\\"id\\": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
\\"removes\\": [],
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"isAttachIframe\\": true
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 22,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"link\\",
|
||||
\\"attributes\\": {
|
||||
\\"rel\\": \\"stylesheet\\",
|
||||
\\"href\\": \\"http://localhost:3030/html/assets/style.css\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 19,
|
||||
\\"id\\": 36
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"adds\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [
|
||||
{
|
||||
\\"id\\": 36,
|
||||
\\"attributes\\": {
|
||||
\\"_cssText\\": \\"body { color: pink; }\\"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record loading stylesheets captures stylesheets that are still loading 1`] = `
|
||||
"[
|
||||
{
|
||||
\\"type\\": 4,
|
||||
\\"data\\": {
|
||||
\\"href\\": \\"about:blank\\",
|
||||
\\"width\\": 1920,
|
||||
\\"height\\": 1080
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"data\\": {
|
||||
\\"node\\": {
|
||||
\\"type\\": 0,
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 1,
|
||||
\\"name\\": \\"html\\",
|
||||
\\"publicId\\": \\"\\",
|
||||
\\"systemId\\": \\"\\",
|
||||
\\"id\\": 2
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {
|
||||
\\"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\\": \\"Hello World!\\",
|
||||
\\"id\\": 13
|
||||
}
|
||||
],
|
||||
\\"id\\": 12
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 14
|
||||
}
|
||||
],
|
||||
\\"id\\": 4
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 15
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\",
|
||||
\\"id\\": 17
|
||||
}
|
||||
],
|
||||
\\"id\\": 16
|
||||
}
|
||||
],
|
||||
\\"id\\": 3
|
||||
}
|
||||
],
|
||||
\\"id\\": 1
|
||||
},
|
||||
\\"initialOffset\\": {
|
||||
\\"left\\": 0,
|
||||
\\"top\\": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 4,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"link\\",
|
||||
\\"attributes\\": {
|
||||
\\"rel\\": \\"stylesheet\\",
|
||||
\\"href\\": \\"http://localhost:3030/html/assets/style.css\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 18
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"adds\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [
|
||||
{
|
||||
\\"id\\": 18,
|
||||
\\"attributes\\": {
|
||||
\\"_cssText\\": \\"body { color: pink; }\\"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record should record scroll position 1`] = `
|
||||
"[
|
||||
{
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
@@ -76,7 +76,9 @@ describe('e2e webgl', () => {
|
||||
|
||||
const hideMouseAnimation = async (p: puppeteer.Page) => {
|
||||
await p.addStyleTag({
|
||||
content: '.replayer-mouse-tail{display: none !important;}',
|
||||
content: `.replayer-mouse-tail{display: none !important;}
|
||||
html, body { margin: 0; padding: 0; }
|
||||
iframe { border: none; }`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -90,7 +92,9 @@ describe('e2e webgl', () => {
|
||||
|
||||
await waitForRAF(page);
|
||||
|
||||
const snapshots: eventWithTime[] = await page.evaluate('window.snapshots');
|
||||
const snapshots: eventWithTime[] = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
|
||||
page = await browser.newPage();
|
||||
|
||||
@@ -108,9 +112,8 @@ describe('e2e webgl', () => {
|
||||
`);
|
||||
await waitForRAF(page);
|
||||
|
||||
const element = await page.$('iframe');
|
||||
const frameImage = await element!.screenshot();
|
||||
|
||||
const frameImage = await page!.screenshot();
|
||||
await waitForRAF(page);
|
||||
expect(frameImage).toMatchImageSnapshot();
|
||||
});
|
||||
|
||||
@@ -122,8 +125,11 @@ describe('e2e webgl', () => {
|
||||
getHtml.call(this, 'canvas-webgl-image.html', { recordCanvas: true }),
|
||||
);
|
||||
|
||||
await waitForRAF(page);
|
||||
await page.waitForTimeout(100);
|
||||
const snapshots: eventWithTime[] = await page.evaluate('window.snapshots');
|
||||
const snapshots: eventWithTime[] = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
|
||||
page = await browser.newPage();
|
||||
|
||||
@@ -143,9 +149,7 @@ describe('e2e webgl', () => {
|
||||
await page.evaluate(`replayer.play(500);`);
|
||||
await waitForRAF(page);
|
||||
|
||||
const element = await page.$('iframe');
|
||||
const frameImage = await element!.screenshot();
|
||||
|
||||
const frameImage = await page!.screenshot();
|
||||
expect(frameImage).toMatchImageSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
BIN
packages/rrweb/test/html/assets/1-minute-of-silence.mp3
Normal file
BIN
packages/rrweb/test/html/assets/1-minute-of-silence.mp3
Normal file
Binary file not shown.
3
packages/rrweb/test/html/assets/style.css
Normal file
3
packages/rrweb/test/html/assets/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
color: pink;
|
||||
}
|
||||
16
packages/rrweb/test/html/audio.html
Normal file
16
packages/rrweb/test/html/audio.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Audio</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>1 minute of silence</h1>
|
||||
<audio controls>
|
||||
<source src="assets/1-minute-of-silence.mp3" type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,88 +1,92 @@
|
||||
<head>
|
||||
<title>Uber Application for Codegen Testing</title>
|
||||
|
||||
<style>
|
||||
#b-class,
|
||||
#b-class-2 {
|
||||
height: 33px;
|
||||
width: 200px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
function mutate1() {
|
||||
const bClassDiv = document.getElementById("b-class");
|
||||
bClassDiv.className = "notB";
|
||||
function mutate1() {
|
||||
const bClassDiv = document.getElementById('b-class');
|
||||
bClassDiv.className = 'notB';
|
||||
|
||||
const removeBlockedButton = document.getElementById("remove");
|
||||
removeBlockedButton.remove();
|
||||
const removeBlockedButton = document.getElementById('remove');
|
||||
removeBlockedButton.remove();
|
||||
|
||||
const visibleCollection = document.getElementsByClassName("visible");
|
||||
const i1Div = document.createElement("div");
|
||||
const i1i1Div = document.createElement("div");
|
||||
const i1i2Div = document.createElement("div");
|
||||
const visibleCollection = document.getElementsByClassName('visible');
|
||||
const i1Div = document.createElement('div');
|
||||
const i1i1Div = document.createElement('div');
|
||||
const i1i2Div = document.createElement('div');
|
||||
|
||||
const i1i1Button = document.createElement("button");
|
||||
i1i1Button.innerHTML = "I1I1 VISIBLE";
|
||||
i1i1Div.appendChild(i1i1Button);
|
||||
const i1i1Button = document.createElement('button');
|
||||
i1i1Button.innerHTML = 'I1I1 VISIBLE';
|
||||
i1i1Div.appendChild(i1i1Button);
|
||||
|
||||
const i1i2Button = document.createElement("button");
|
||||
i1i2Button.innerHTML = "I1I2 VISIBLE";
|
||||
i1i2Div.appendChild(i1i2Button);
|
||||
const i1i2Button = document.createElement('button');
|
||||
i1i2Button.innerHTML = 'I1I2 VISIBLE';
|
||||
i1i2Div.appendChild(i1i2Button);
|
||||
|
||||
i1Div.appendChild(i1i1Div);
|
||||
i1Div.appendChild(i1i2Div);
|
||||
visibleCollection[0].appendChild(i1Div);
|
||||
}
|
||||
function mutate2() {
|
||||
const bClassDiv = document.getElementById("b-class-2");
|
||||
bClassDiv.className = "notB";
|
||||
i1Div.appendChild(i1i1Div);
|
||||
i1Div.appendChild(i1i2Div);
|
||||
visibleCollection[0].appendChild(i1Div);
|
||||
}
|
||||
function mutate2() {
|
||||
const bClassDiv = document.getElementById('b-class-2');
|
||||
bClassDiv.className = 'notB';
|
||||
|
||||
const removeBlockedButton = document.getElementById("remove2");
|
||||
const innerButton = document.createElement("button");
|
||||
innerButton.innerHTML = "INNER BLOCKED";
|
||||
removeBlockedButton.appendChild(innerButton)
|
||||
removeBlockedButton.remove();
|
||||
const removeBlockedButton = document.getElementById('remove2');
|
||||
const innerButton = document.createElement('button');
|
||||
innerButton.innerHTML = 'INNER BLOCKED';
|
||||
removeBlockedButton.appendChild(innerButton);
|
||||
removeBlockedButton.remove();
|
||||
|
||||
const visibleCollection = document.getElementsByClassName("visible2");
|
||||
const i1Div = document.createElement("div");
|
||||
const i1i1Div = document.createElement("div");
|
||||
const i1i2Div = document.createElement("div");
|
||||
const visibleCollection = document.getElementsByClassName('visible2');
|
||||
const i1Div = document.createElement('div');
|
||||
const i1i1Div = document.createElement('div');
|
||||
const i1i2Div = document.createElement('div');
|
||||
|
||||
const i1i1Button = document.createElement("button");
|
||||
i1i1Button.innerHTML = "I1I1 VISIBLE";
|
||||
i1i1Div.appendChild(i1i1Button);
|
||||
const i1i1Button = document.createElement('button');
|
||||
i1i1Button.innerHTML = 'I1I1 VISIBLE';
|
||||
i1i1Div.appendChild(i1i1Button);
|
||||
|
||||
const i1i2Button = document.createElement("button");
|
||||
i1i2Button.innerHTML = "I1I2 VISIBLE";
|
||||
i1i2Div.appendChild(i1i2Button);
|
||||
const i1i2Button = document.createElement('button');
|
||||
i1i2Button.innerHTML = 'I1I2 VISIBLE';
|
||||
i1i2Div.appendChild(i1i2Button);
|
||||
|
||||
i1Div.appendChild(i1i1Div);
|
||||
i1Div.appendChild(i1i2Div);
|
||||
visibleCollection[0].appendChild(i1Div);
|
||||
}
|
||||
i1Div.appendChild(i1i1Div);
|
||||
i1Div.appendChild(i1i2Div);
|
||||
visibleCollection[0].appendChild(i1Div);
|
||||
}
|
||||
</script>
|
||||
<br/>
|
||||
<h1>
|
||||
Verify that block class bugs are fixed
|
||||
</h1>
|
||||
<br/>
|
||||
<br />
|
||||
<h1>Verify that block class bugs are fixed</h1>
|
||||
<br />
|
||||
<div class="first">
|
||||
<div class="visible">
|
||||
<button>VISIBLE</button>
|
||||
</div>
|
||||
<br/><br/><br/>
|
||||
<br /><br /><br />
|
||||
<div class="rr-block" id="b-class">
|
||||
<button id="remove">BLOCKED</button>
|
||||
</div>
|
||||
<br/><br/><br/>
|
||||
<br /><br /><br />
|
||||
<button onclick="mutate1()">MUTATE</button>
|
||||
</div>
|
||||
<br/><br/><br/>
|
||||
<br /><br /><br />
|
||||
<div class="second">
|
||||
<div class="visible2">
|
||||
<button>VISIBLE</button>
|
||||
</div>
|
||||
<br/><br/><br/>
|
||||
<br /><br /><br />
|
||||
<div class="rr-block" id="b-class-2">
|
||||
<button id="remove2">BLOCKED</button>
|
||||
</div>
|
||||
<br/><br/><br/>
|
||||
<br /><br /><br />
|
||||
<button onclick="mutate2()">MUTATE</button>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
12
packages/rrweb/test/html/hello-world.html
Normal file
12
packages/rrweb/test/html/hello-world.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hello World!</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello world!
|
||||
</body>
|
||||
</html>
|
||||
@@ -73,7 +73,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
await page.type('textarea', 'textarea test');
|
||||
await page.select('select', '1');
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -91,7 +93,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
p.appendChild(document.createElement('span'));
|
||||
});
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -111,7 +115,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
p.innerText = 'mutated';
|
||||
});
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -129,7 +135,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
document.body.setAttribute('test', 'true');
|
||||
});
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -146,7 +154,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
await page.evaluate(
|
||||
'document.getElementById("select2-drop").setAttribute("style", document.getElementById("select2-drop").style.cssText + "color:black !important")',
|
||||
);
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -180,7 +190,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
|
||||
await waitForRAF(page);
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -191,7 +203,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
|
||||
await page.type('.rr-ignore', 'secret');
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -209,7 +223,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
await page.type('textarea', 'textarea test');
|
||||
await page.select('select', '1');
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -233,7 +249,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
await page.type('input[type="password"]', 'password');
|
||||
await page.select('select', '1');
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -250,7 +268,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
|
||||
await page.type('input[type="password"]', 'secr3t');
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -268,7 +288,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
await page.type('textarea', 'textarea test');
|
||||
await page.select('select', '1');
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -281,7 +303,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
await page.evaluate(`document.getElementById('text').innerText = '1'`);
|
||||
await page.click('#text');
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -301,7 +325,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
nextElement.parentNode!.insertBefore(el, nextElement);
|
||||
});
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -310,13 +336,19 @@ describe('record integration tests', function (this: ISuite) {
|
||||
await page.goto('about: blank');
|
||||
await page.setContent(getHtml.call(this, 'blocked-unblocked.html'));
|
||||
|
||||
const elements1 = await page.$x('/html/body/div[1]/button');
|
||||
const elements1 = (await page.$x(
|
||||
'/html/body/div[1]/button',
|
||||
)) as puppeteer.ElementHandle<HTMLButtonElement>[];
|
||||
await elements1[0].click();
|
||||
|
||||
const elements2 = await page.$x('/html/body/div[2]/button');
|
||||
const elements2 = (await page.$x(
|
||||
'/html/body/div[2]/button',
|
||||
)) as puppeteer.ElementHandle<HTMLButtonElement>[];
|
||||
await elements2[0].click();
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -334,7 +366,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
p.removeChild(span);
|
||||
div.appendChild(span);
|
||||
});
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -349,7 +383,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
document.body.appendChild(div);
|
||||
div.appendChild(span);
|
||||
});
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -358,7 +394,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
await page.goto('about:blank');
|
||||
await page.setContent(getHtml.call(this, 'react-styled-components.html'));
|
||||
await page.click('.toggle');
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -371,7 +409,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
}),
|
||||
);
|
||||
await waitForRAF(page);
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
for (const event of snapshots) {
|
||||
if (event.type === EventType.FullSnapshot) {
|
||||
visitSnapshot(event.data.node, (n) => {
|
||||
@@ -393,7 +433,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
}),
|
||||
);
|
||||
await page.waitForTimeout(50);
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -406,7 +448,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
}),
|
||||
);
|
||||
await waitForRAF(page);
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -425,7 +469,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
}
|
||||
});
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -483,11 +529,15 @@ describe('record integration tests', function (this: ISuite) {
|
||||
document.body.appendChild(iframe);
|
||||
});
|
||||
|
||||
await waitForRAF(page);
|
||||
await page.frames()[1].evaluate(() => {
|
||||
console.log('from iframe');
|
||||
});
|
||||
await waitForRAF(page);
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -504,7 +554,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -519,7 +571,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
await page.waitForSelector('img'); // wait for image to get added
|
||||
await waitForRAF(page); // wait for image to be captured
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -534,7 +588,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
await page.waitForTimeout(50); // wait for image to get added
|
||||
await waitForRAF(page); // wait for image to be captured
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -555,7 +611,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
await page.waitForTimeout(50); // wait for image to get added
|
||||
await waitForRAF(page); // wait for image to be captured
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -603,7 +661,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
});
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -646,7 +706,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
});
|
||||
await waitForRAF(page); // wait till browser sent snapshots
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -674,7 +736,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
});
|
||||
|
||||
await waitForRAF(page); // wait for snapshot to be updated
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -706,7 +770,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
});
|
||||
await waitForRAF(page); // wait till browser sent snapshots
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -746,7 +812,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
});
|
||||
await waitForRAF(page); // wait till browser sent snapshots
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -759,7 +827,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
}),
|
||||
);
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -773,7 +843,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
}),
|
||||
);
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -794,7 +866,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
p.innerText = 'mutated';
|
||||
});
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,14 @@ import {
|
||||
styleSheetRuleData,
|
||||
selectionData,
|
||||
} from '@rrweb/types';
|
||||
import { assertSnapshot, launchPuppeteer, waitForRAF } from './utils';
|
||||
import {
|
||||
assertSnapshot,
|
||||
getServerURL,
|
||||
launchPuppeteer,
|
||||
startServer,
|
||||
waitForRAF,
|
||||
} from './utils';
|
||||
import type { Server } from 'http';
|
||||
|
||||
interface ISuite {
|
||||
code: string;
|
||||
@@ -465,54 +472,63 @@ describe('record', function (this: ISuite) {
|
||||
|
||||
it('captures mutations on adopted stylesheets', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
document.body.innerHTML = `
|
||||
return new Promise((resolve) => {
|
||||
document.body.innerHTML = `
|
||||
<div>div in outermost document</div>
|
||||
<iframe></iframe>
|
||||
`;
|
||||
|
||||
const sheet = new CSSStyleSheet();
|
||||
// Add stylesheet to a document.
|
||||
const sheet = new CSSStyleSheet();
|
||||
// Add stylesheet to a document.
|
||||
|
||||
document.adoptedStyleSheets = [sheet];
|
||||
document.adoptedStyleSheets = [sheet];
|
||||
|
||||
const iframe = document.querySelector('iframe');
|
||||
const sheet2 = new (iframe!.contentWindow! as Window &
|
||||
typeof globalThis).CSSStyleSheet();
|
||||
const iframe = document.querySelector('iframe');
|
||||
const sheet2 = new (iframe!.contentWindow! as Window &
|
||||
typeof globalThis).CSSStyleSheet();
|
||||
|
||||
// Add stylesheet to an IFrame document.
|
||||
iframe!.contentDocument!.adoptedStyleSheets = [sheet2];
|
||||
iframe!.contentDocument!.body.innerHTML = '<h1>h1 in iframe</h1>';
|
||||
// Add stylesheet to an IFrame document.
|
||||
iframe!.contentDocument!.adoptedStyleSheets = [sheet2];
|
||||
iframe!.contentDocument!.body.innerHTML = '<h1>h1 in iframe</h1>';
|
||||
|
||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||
record({
|
||||
emit: ((window as unknown) as IWindow).emit,
|
||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||
record({
|
||||
emit: ((window as unknown) as IWindow).emit,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
sheet.replace!('div { color: yellow; }');
|
||||
sheet2.replace!('h1 { color: blue; }');
|
||||
}, 0);
|
||||
|
||||
setTimeout(() => {
|
||||
sheet.replaceSync!('div { display: inline ; }');
|
||||
sheet2.replaceSync!('h1 { font-size: large; }');
|
||||
}, 5);
|
||||
|
||||
setTimeout(() => {
|
||||
(sheet.cssRules[0] as CSSStyleRule).style.setProperty(
|
||||
'color',
|
||||
'green',
|
||||
);
|
||||
(sheet.cssRules[0] as CSSStyleRule).style.removeProperty('display');
|
||||
(sheet2.cssRules[0] as CSSStyleRule).style.setProperty(
|
||||
'font-size',
|
||||
'medium',
|
||||
'important',
|
||||
);
|
||||
sheet2.insertRule('h2 { color: red; }');
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
sheet.insertRule('body { border: 2px solid blue; }', 1);
|
||||
sheet2.deleteRule(0);
|
||||
}, 15);
|
||||
|
||||
setTimeout(() => {
|
||||
resolve(undefined);
|
||||
}, 20);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
sheet.replace!('div { color: yellow; }');
|
||||
sheet2.replace!('h1 { color: blue; }');
|
||||
}, 0);
|
||||
|
||||
setTimeout(() => {
|
||||
sheet.replaceSync!('div { display: inline ; }');
|
||||
sheet2.replaceSync!('h1 { font-size: large; }');
|
||||
}, 5);
|
||||
|
||||
setTimeout(() => {
|
||||
(sheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green');
|
||||
(sheet.cssRules[0] as CSSStyleRule).style.removeProperty('display');
|
||||
(sheet2.cssRules[0] as CSSStyleRule).style.setProperty(
|
||||
'font-size',
|
||||
'medium',
|
||||
'important',
|
||||
);
|
||||
sheet2.insertRule('h2 { color: red; }');
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
sheet.insertRule('body { border: 2px solid blue; }', 1);
|
||||
sheet2.deleteRule(0);
|
||||
}, 15);
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
assertSnapshot(ctx.events);
|
||||
@@ -602,70 +618,91 @@ describe('record', function (this: ISuite) {
|
||||
assertSnapshot(ctx.events);
|
||||
});
|
||||
|
||||
it('captures stylesheets that are still loading', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||
describe('loading stylesheets', () => {
|
||||
let server: Server;
|
||||
let serverURL: string;
|
||||
|
||||
record({
|
||||
inlineStylesheet: true,
|
||||
emit: ((window as unknown) as IWindow).emit,
|
||||
});
|
||||
|
||||
const link1 = document.createElement('link');
|
||||
link1.setAttribute('rel', 'stylesheet');
|
||||
link1.setAttribute(
|
||||
'href',
|
||||
URL.createObjectURL(
|
||||
new Blob(['body { color: pink; }'], {
|
||||
type: 'text/css',
|
||||
}),
|
||||
),
|
||||
);
|
||||
document.head.appendChild(link1);
|
||||
beforeAll(async () => {
|
||||
server = await startServer();
|
||||
serverURL = getServerURL(server);
|
||||
});
|
||||
|
||||
// `blob:` URLs are not available immediately, so we need to wait for the browser to load them
|
||||
await waitForRAF(ctx.page);
|
||||
// 'blob' URL is different in every execution so we need to remove it from the snapshot.
|
||||
const filteredEvents = JSON.parse(
|
||||
JSON.stringify(ctx.events).replace(/blob\:[\w\d-/]+"/, 'blob:null"'),
|
||||
);
|
||||
assertSnapshot(filteredEvents);
|
||||
});
|
||||
|
||||
it('captures stylesheets in iframes that are still loading', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.setAttribute('src', 'about:blank');
|
||||
document.body.appendChild(iframe);
|
||||
const iframeDoc = iframe.contentDocument!;
|
||||
|
||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||
|
||||
record({
|
||||
inlineStylesheet: true,
|
||||
emit: ((window as unknown) as IWindow).emit,
|
||||
beforeEach(async () => {
|
||||
ctx.page = await ctx.browser.newPage();
|
||||
await ctx.page.goto(`${serverURL}/html/hello-world.html`);
|
||||
await ctx.page.evaluate(ctx.code);
|
||||
ctx.events = [];
|
||||
await ctx.page.exposeFunction('emit', (e: eventWithTime) => {
|
||||
if (
|
||||
e.type === EventType.DomContentLoaded ||
|
||||
e.type === EventType.Load
|
||||
) {
|
||||
return;
|
||||
}
|
||||
ctx.events.push(e);
|
||||
});
|
||||
|
||||
const linkEl = document.createElement('link');
|
||||
linkEl.setAttribute('rel', 'stylesheet');
|
||||
linkEl.setAttribute(
|
||||
'href',
|
||||
URL.createObjectURL(
|
||||
new Blob(['body { color: pink; }'], {
|
||||
type: 'text/css',
|
||||
}),
|
||||
),
|
||||
);
|
||||
iframeDoc.head.appendChild(linkEl);
|
||||
ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
|
||||
});
|
||||
|
||||
// `blob:` URLs are not available immediately, so we need to wait for the browser to load them
|
||||
await waitForRAF(ctx.page);
|
||||
const filteredEvents = JSON.parse(
|
||||
JSON.stringify(ctx.events).replace(/blob\:[\w\d-/]+"/, 'blob:null"'),
|
||||
);
|
||||
assertSnapshot(filteredEvents);
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('captures stylesheets that are still loading', async () => {
|
||||
ctx.page.evaluate((serverURL) => {
|
||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||
|
||||
record({
|
||||
inlineStylesheet: true,
|
||||
emit: ((window as unknown) as IWindow).emit,
|
||||
});
|
||||
|
||||
const link1 = document.createElement('link');
|
||||
link1.setAttribute('rel', 'stylesheet');
|
||||
link1.setAttribute('href', `${serverURL}/html/assets/style.css`);
|
||||
document.head.appendChild(link1);
|
||||
}, serverURL);
|
||||
|
||||
await ctx.page.waitForResponse(`${serverURL}/html/assets/style.css`);
|
||||
await waitForRAF(ctx.page);
|
||||
|
||||
assertSnapshot(ctx.events);
|
||||
});
|
||||
|
||||
it('captures stylesheets in iframes that are still loading', async () => {
|
||||
ctx.page.evaluate(() => {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.setAttribute('src', `/html/hello-world.html?2`);
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||
|
||||
record({
|
||||
inlineStylesheet: true,
|
||||
emit: ((window as unknown) as IWindow).emit,
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.page.waitForResponse(`${serverURL}/html/hello-world.html?2`);
|
||||
|
||||
await waitForRAF(ctx.page);
|
||||
|
||||
ctx.page.evaluate(() => {
|
||||
const iframe = document.querySelector('iframe')!;
|
||||
const iframeDoc = iframe.contentDocument!;
|
||||
const linkEl = document.createElement('link');
|
||||
linkEl.setAttribute('rel', 'stylesheet');
|
||||
linkEl.setAttribute('href', `/html/assets/style.css`);
|
||||
iframeDoc.head.appendChild(linkEl);
|
||||
});
|
||||
|
||||
await ctx.page.waitForResponse(`${serverURL}/html/assets/style.css`);
|
||||
|
||||
await waitForRAF(ctx.page);
|
||||
|
||||
assertSnapshot(ctx.events);
|
||||
});
|
||||
});
|
||||
|
||||
it('captures CORS stylesheets that are still loading', async () => {
|
||||
@@ -695,65 +732,71 @@ describe('record', function (this: ISuite) {
|
||||
|
||||
it('captures adopted stylesheets in shadow doms and iframe', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
document.body.innerHTML = `
|
||||
return new Promise((resolve) => {
|
||||
document.body.innerHTML = `
|
||||
<div>div in outermost document</div>
|
||||
<div id="shadow-host1"></div>
|
||||
<div id="shadow-host2"></div>
|
||||
<iframe></iframe>
|
||||
`;
|
||||
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync!(
|
||||
'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}',
|
||||
);
|
||||
// Add stylesheet to a document.
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync!(
|
||||
'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}',
|
||||
);
|
||||
// Add stylesheet to a document.
|
||||
|
||||
document.adoptedStyleSheets = [sheet];
|
||||
document.adoptedStyleSheets = [sheet];
|
||||
|
||||
// Add stylesheet to a shadow host.
|
||||
const host = document.querySelector('#shadow-host1');
|
||||
const shadow = host!.attachShadow({ mode: 'open' });
|
||||
shadow.innerHTML =
|
||||
'<div>div in shadow dom 1</div><span>span in shadow dom 1</span>';
|
||||
const sheet2 = new CSSStyleSheet();
|
||||
|
||||
sheet2.replaceSync!('span { color: red; }');
|
||||
|
||||
shadow.adoptedStyleSheets = [sheet, sheet2];
|
||||
|
||||
// Add stylesheet to an IFrame document.
|
||||
const iframe = document.querySelector('iframe');
|
||||
const sheet3 = new (iframe!.contentWindow! as IWindow &
|
||||
typeof globalThis).CSSStyleSheet();
|
||||
sheet3.replaceSync!('h1 { color: blue; }');
|
||||
|
||||
iframe!.contentDocument!.adoptedStyleSheets = [sheet3];
|
||||
|
||||
const ele = iframe!.contentDocument!.createElement('h1');
|
||||
ele.innerText = 'h1 in iframe';
|
||||
iframe!.contentDocument!.body.appendChild(ele);
|
||||
|
||||
((window as unknown) as IWindow).rrweb.record({
|
||||
emit: ((window.top as unknown) as IWindow).emit,
|
||||
});
|
||||
|
||||
// Make incremental changes to shadow dom.
|
||||
setTimeout(() => {
|
||||
const host = document.querySelector('#shadow-host2');
|
||||
// Add stylesheet to a shadow host.
|
||||
const host = document.querySelector('#shadow-host1');
|
||||
const shadow = host!.attachShadow({ mode: 'open' });
|
||||
shadow.innerHTML =
|
||||
'<div>div in shadow dom 2</div><span>span in shadow dom 2</span>';
|
||||
const sheet4 = new CSSStyleSheet();
|
||||
sheet4.replaceSync!('span { color: green; }');
|
||||
shadow.adoptedStyleSheets = [sheet, sheet4];
|
||||
'<div>div in shadow dom 1</div><span>span in shadow dom 1</span>';
|
||||
const sheet2 = new CSSStyleSheet();
|
||||
|
||||
document.adoptedStyleSheets = [sheet4, sheet, sheet2];
|
||||
sheet2.replaceSync!('span { color: red; }');
|
||||
|
||||
const sheet5 = new (iframe!.contentWindow! as IWindow &
|
||||
shadow.adoptedStyleSheets = [sheet, sheet2];
|
||||
|
||||
// Add stylesheet to an IFrame document.
|
||||
const iframe = document.querySelector('iframe');
|
||||
const sheet3 = new (iframe!.contentWindow! as IWindow &
|
||||
typeof globalThis).CSSStyleSheet();
|
||||
sheet5.replaceSync!('h2 { color: purple; }');
|
||||
iframe!.contentDocument!.adoptedStyleSheets = [sheet5, sheet3];
|
||||
}, 10);
|
||||
sheet3.replaceSync!('h1 { color: blue; }');
|
||||
|
||||
iframe!.contentDocument!.adoptedStyleSheets = [sheet3];
|
||||
|
||||
const ele = iframe!.contentDocument!.createElement('h1');
|
||||
ele.innerText = 'h1 in iframe';
|
||||
iframe!.contentDocument!.body.appendChild(ele);
|
||||
|
||||
((window as unknown) as IWindow).rrweb.record({
|
||||
emit: ((window.top as unknown) as IWindow).emit,
|
||||
});
|
||||
|
||||
// Make incremental changes to shadow dom.
|
||||
setTimeout(() => {
|
||||
const host = document.querySelector('#shadow-host2');
|
||||
const shadow = host!.attachShadow({ mode: 'open' });
|
||||
shadow.innerHTML =
|
||||
'<div>div in shadow dom 2</div><span>span in shadow dom 2</span>';
|
||||
const sheet4 = new CSSStyleSheet();
|
||||
sheet4.replaceSync!('span { color: green; }');
|
||||
shadow.adoptedStyleSheets = [sheet, sheet4];
|
||||
|
||||
document.adoptedStyleSheets = [sheet4, sheet, sheet2];
|
||||
|
||||
const sheet5 = new (iframe!.contentWindow! as IWindow &
|
||||
typeof globalThis).CSSStyleSheet();
|
||||
sheet5.replaceSync!('h2 { color: purple; }');
|
||||
iframe!.contentDocument!.adoptedStyleSheets = [sheet5, sheet3];
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
resolve(null);
|
||||
}, 20);
|
||||
});
|
||||
});
|
||||
await waitForRAF(ctx.page); // wait till events get sent
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
514
packages/rrweb/test/record/cross-origin-iframes.test.ts
Normal file
514
packages/rrweb/test/record/cross-origin-iframes.test.ts
Normal file
@@ -0,0 +1,514 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type * as puppeteer from 'puppeteer';
|
||||
import type { recordOptions } from '../../src/types';
|
||||
import type {
|
||||
listenerHandler,
|
||||
eventWithTime,
|
||||
mutationData,
|
||||
} from '@rrweb/types';
|
||||
import { EventType, IncrementalSource } from '@rrweb/types';
|
||||
import {
|
||||
assertSnapshot,
|
||||
getServerURL,
|
||||
launchPuppeteer,
|
||||
startServer,
|
||||
stripBase64,
|
||||
waitForRAF,
|
||||
} from '../utils';
|
||||
import type * as http from 'http';
|
||||
|
||||
interface ISuite {
|
||||
code: string;
|
||||
browser: puppeteer.Browser;
|
||||
page: puppeteer.Page;
|
||||
events: eventWithTime[];
|
||||
server: http.Server;
|
||||
serverURL: string;
|
||||
}
|
||||
|
||||
interface IWindow extends Window {
|
||||
rrweb: {
|
||||
record: (
|
||||
options: recordOptions<eventWithTime>,
|
||||
) => listenerHandler | undefined;
|
||||
addCustomEvent<T>(tag: string, payload: T): void;
|
||||
};
|
||||
emit: (e: eventWithTime) => undefined;
|
||||
snapshots: eventWithTime[];
|
||||
}
|
||||
|
||||
async function injectRecordScript(frame: puppeteer.Frame) {
|
||||
await frame.addScriptTag({
|
||||
path: path.resolve(__dirname, '../../dist/rrweb.js'),
|
||||
});
|
||||
await frame.evaluate(() => {
|
||||
((window as unknown) as IWindow).snapshots = [];
|
||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||
record({
|
||||
recordCrossOriginIframes: true,
|
||||
recordCanvas: true,
|
||||
emit(event) {
|
||||
((window as unknown) as IWindow).snapshots.push(event);
|
||||
((window as unknown) as IWindow).emit(event);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
for (const child of frame.childFrames()) {
|
||||
await injectRecordScript(child);
|
||||
}
|
||||
}
|
||||
|
||||
const setup = function (this: ISuite, content: string): ISuite {
|
||||
const ctx = {} as ISuite;
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx.browser = await launchPuppeteer();
|
||||
ctx.server = await startServer();
|
||||
ctx.serverURL = getServerURL(ctx.server);
|
||||
// ctx.serverB = await startServer();
|
||||
// ctx.serverBURL = getServerURL(ctx.serverB);
|
||||
|
||||
const bundlePath = path.resolve(__dirname, '../../dist/rrweb.js');
|
||||
ctx.code = fs.readFileSync(bundlePath, 'utf8');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
ctx.page = await ctx.browser.newPage();
|
||||
await ctx.page.goto('about:blank');
|
||||
await ctx.page.setContent(
|
||||
content.replace(/\{SERVER_URL\}/g, ctx.serverURL),
|
||||
);
|
||||
// await ctx.page.evaluate(ctx.code);
|
||||
ctx.events = [];
|
||||
await ctx.page.exposeFunction('emit', (e: eventWithTime) => {
|
||||
if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) {
|
||||
return;
|
||||
}
|
||||
ctx.events.push(e);
|
||||
});
|
||||
|
||||
ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
|
||||
await injectRecordScript(ctx.page.mainFrame());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ctx.page.close();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await ctx.browser.close();
|
||||
ctx.server.close();
|
||||
// ctx.serverB.close();
|
||||
});
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
||||
describe('cross origin iframes', function (this: ISuite) {
|
||||
jest.setTimeout(100_000);
|
||||
|
||||
describe('form.html', function (this: ISuite) {
|
||||
const ctx: ISuite = setup.call(
|
||||
this,
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<iframe src="{SERVER_URL}/html/form.html" style="width: 400px; height: 400px;"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
);
|
||||
|
||||
it("won't emit events if it isn't in the top level iframe", async () => {
|
||||
const el = (await ctx.page.$(
|
||||
'body > iframe',
|
||||
)) as puppeteer.ElementHandle<Element>;
|
||||
|
||||
const frame = await el.contentFrame();
|
||||
const events = await frame?.evaluate(
|
||||
() => ((window as unknown) as IWindow).snapshots,
|
||||
);
|
||||
expect(events).toMatchObject([]);
|
||||
});
|
||||
|
||||
it('will emit events if it is in the top level iframe', async () => {
|
||||
const events = await ctx.page.evaluate(
|
||||
() => ((window as unknown) as IWindow).snapshots,
|
||||
);
|
||||
expect(events.length).not.toBe(0);
|
||||
});
|
||||
|
||||
it('should emit contents of iframe', async () => {
|
||||
const events = await ctx.page.evaluate(
|
||||
() => ((window as unknown) as IWindow).snapshots,
|
||||
);
|
||||
await waitForRAF(ctx.page);
|
||||
// two events (full snapshot + meta) from main frame, and one full snapshot from iframe
|
||||
expect(events.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should emit full snapshot event from iframe as mutation event', async () => {
|
||||
const events = await ctx.page.evaluate(
|
||||
() => ((window as unknown) as IWindow).snapshots,
|
||||
);
|
||||
await waitForRAF(ctx.page);
|
||||
// two events from main frame, and two from iframe
|
||||
expect(events[events.length - 1]).toMatchObject({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
adds: [
|
||||
{
|
||||
parentId: expect.any(Number),
|
||||
node: {
|
||||
id: expect.any(Number),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should use unique id for child of iframes', async () => {
|
||||
const events: eventWithTime[] = await ctx.page.evaluate(
|
||||
() => ((window as unknown) as IWindow).snapshots,
|
||||
);
|
||||
await waitForRAF(ctx.page);
|
||||
expect(
|
||||
(events[events.length - 1].data as mutationData).adds[0].node.id,
|
||||
).not.toBe(1);
|
||||
});
|
||||
|
||||
it('should replace the existing DOM nodes on iframe navigation with `isAttachIframe`', async () => {
|
||||
await ctx.page.evaluate((url) => {
|
||||
const iframe = document.querySelector('iframe') as HTMLIFrameElement;
|
||||
iframe.src = `${url}/html/form.html?2`;
|
||||
}, ctx.serverURL);
|
||||
await waitForRAF(ctx.page); // loads iframe
|
||||
|
||||
await injectRecordScript(ctx.page.mainFrame().childFrames()[0]); // injects script into new iframe
|
||||
|
||||
const events: eventWithTime[] = await ctx.page.evaluate(
|
||||
() => ((window as unknown) as IWindow).snapshots,
|
||||
);
|
||||
expect(
|
||||
(events[events.length - 1].data as mutationData).removes,
|
||||
).toMatchObject([]);
|
||||
expect(
|
||||
(events[events.length - 1].data as mutationData).isAttachIframe,
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map input events correctly', async () => {
|
||||
const frame = ctx.page.mainFrame().childFrames()[0];
|
||||
await frame.type('input[type="text"]', 'test');
|
||||
await frame.click('input[type="radio"]');
|
||||
await frame.click('input[type="checkbox"]');
|
||||
await frame.type('input[type="password"]', 'password');
|
||||
await frame.type('textarea', 'textarea test');
|
||||
await frame.select('select', '1');
|
||||
|
||||
const snapshots = (await ctx.page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('should map scroll events correctly', async () => {
|
||||
// force scrollbars in iframe
|
||||
ctx.page.evaluate(() => {
|
||||
const iframe = document.querySelector('iframe') as HTMLIFrameElement;
|
||||
iframe.style.width = '50px';
|
||||
iframe.style.height = '50px';
|
||||
});
|
||||
|
||||
await waitForRAF(ctx.page);
|
||||
const frame = ctx.page.mainFrame().childFrames()[0];
|
||||
|
||||
// scroll a little
|
||||
frame.evaluate(() => {
|
||||
window.scrollTo(0, 10);
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
|
||||
const snapshots = (await ctx.page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
});
|
||||
|
||||
describe('move-node.html', function (this: ISuite) {
|
||||
const ctx: ISuite = setup.call(
|
||||
this,
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<iframe src="{SERVER_URL}/html/move-node.html"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
);
|
||||
|
||||
it('should record DOM node movement', async () => {
|
||||
const frame = ctx.page.mainFrame().childFrames()[0];
|
||||
await frame.evaluate(() => {
|
||||
const div = document.createElement('div');
|
||||
const span = document.querySelector('span')!;
|
||||
document.body.appendChild(div);
|
||||
div.appendChild(span);
|
||||
});
|
||||
const snapshots = (await ctx.page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('should record DOM node removal', async () => {
|
||||
const frame = ctx.page.mainFrame().childFrames()[0];
|
||||
await frame.evaluate(() => {
|
||||
const span = document.querySelector('span')!;
|
||||
span.remove();
|
||||
});
|
||||
const snapshots = (await ctx.page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('should record DOM attribute changes', async () => {
|
||||
const frame = ctx.page.mainFrame().childFrames()[0];
|
||||
await frame.evaluate(() => {
|
||||
const span = document.querySelector('span')!;
|
||||
span.className = 'added-class-name';
|
||||
});
|
||||
const snapshots = (await ctx.page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('should record DOM text changes', async () => {
|
||||
const frame = ctx.page.mainFrame().childFrames()[0];
|
||||
await frame.evaluate(() => {
|
||||
const b = document.querySelector('b')!;
|
||||
b.childNodes[0].textContent = 'replaced text';
|
||||
});
|
||||
const snapshots = (await ctx.page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('should record canvas elements', async () => {
|
||||
const frame = ctx.page.mainFrame().childFrames()[0];
|
||||
await frame.evaluate(() => {
|
||||
var canvas = document.createElement('canvas');
|
||||
var gl = canvas.getContext('webgl')!;
|
||||
var program = gl.createProgram()!;
|
||||
gl.linkProgram(program);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
document.body.appendChild(canvas);
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
const snapshots = (await ctx.page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('should record custom events', async () => {
|
||||
const frame = ctx.page.mainFrame().childFrames()[0];
|
||||
await frame.evaluate(() => {
|
||||
((window as unknown) as IWindow).rrweb.addCustomEvent('test', {
|
||||
id: 1,
|
||||
parentId: 1,
|
||||
nextId: 2,
|
||||
});
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
const snapshots = (await ctx.page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('captures mutations on adopted stylesheets', async () => {
|
||||
const frame = ctx.page.mainFrame().childFrames()[0];
|
||||
await ctx.page.evaluate(() => {
|
||||
const sheet = new CSSStyleSheet();
|
||||
// Add stylesheet to a document.
|
||||
document.adoptedStyleSheets = [sheet];
|
||||
});
|
||||
await frame.evaluate(() => {
|
||||
const sheet = new CSSStyleSheet();
|
||||
// Add stylesheet to a document.
|
||||
document.adoptedStyleSheets = [sheet];
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
await ctx.page.evaluate(() => {
|
||||
document.adoptedStyleSheets![0].replace!('div { color: yellow; }');
|
||||
});
|
||||
await frame.evaluate(() => {
|
||||
document.adoptedStyleSheets![0].replace!('h1 { color: blue; }');
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
await ctx.page.evaluate(() => {
|
||||
document.adoptedStyleSheets![0].replaceSync!(
|
||||
'div { display: inline ; }',
|
||||
);
|
||||
});
|
||||
await frame.evaluate(() => {
|
||||
document.adoptedStyleSheets![0].replaceSync!(
|
||||
'h1 { font-size: large; }',
|
||||
);
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
await ctx.page.evaluate(() => {
|
||||
(document.adoptedStyleSheets![0]
|
||||
.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green');
|
||||
(document.adoptedStyleSheets![0]
|
||||
.cssRules[0] as CSSStyleRule).style.removeProperty('display');
|
||||
});
|
||||
await frame.evaluate(() => {
|
||||
(document.adoptedStyleSheets![0]
|
||||
.cssRules[0] as CSSStyleRule).style.setProperty(
|
||||
'font-size',
|
||||
'medium',
|
||||
'important',
|
||||
);
|
||||
document.adoptedStyleSheets![0].insertRule('h2 { color: red; }');
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
await ctx.page.evaluate(() => {
|
||||
document.adoptedStyleSheets![0].insertRule(
|
||||
'body { border: 2px solid blue; }',
|
||||
1,
|
||||
);
|
||||
});
|
||||
await frame.evaluate(() => {
|
||||
document.adoptedStyleSheets![0].deleteRule(0);
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
|
||||
const snapshots = (await ctx.page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
it('captures mutations on stylesheets', async () => {
|
||||
const frame = ctx.page.mainFrame().childFrames()[0];
|
||||
await ctx.page.evaluate(() => {
|
||||
// Add stylesheet to a document.
|
||||
const style = document.createElement('style');
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
await frame.evaluate(() => {
|
||||
// Add stylesheet to a document.
|
||||
const style = document.createElement('style');
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
await ctx.page.evaluate(() => {
|
||||
document.styleSheets[0].insertRule('div { color: yellow; }');
|
||||
});
|
||||
await frame.evaluate(() => {
|
||||
document.styleSheets[0].insertRule('h1 { color: blue; }');
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
await ctx.page.evaluate(() => {
|
||||
(document.styleSheets[0].cssRules[0] as CSSStyleRule).style.setProperty(
|
||||
'color',
|
||||
'green',
|
||||
);
|
||||
(document.styleSheets[0]
|
||||
.cssRules[0] as CSSStyleRule).style.removeProperty('display');
|
||||
});
|
||||
await frame.evaluate(() => {
|
||||
(document.styleSheets[0].cssRules[0] as CSSStyleRule).style.setProperty(
|
||||
'font-size',
|
||||
'medium',
|
||||
'important',
|
||||
);
|
||||
document.styleSheets[0].insertRule('h2 { color: red; }');
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
await ctx.page.evaluate(() => {
|
||||
document.styleSheets[0].insertRule(
|
||||
'body { border: 2px solid blue; }',
|
||||
1,
|
||||
);
|
||||
});
|
||||
await frame.evaluate(() => {
|
||||
document.styleSheets[0].deleteRule(0);
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
|
||||
const snapshots = (await ctx.page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
});
|
||||
|
||||
describe('audio.html', function (this: ISuite) {
|
||||
jest.setTimeout(100_000);
|
||||
|
||||
const ctx: ISuite = setup.call(
|
||||
this,
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<iframe src="{SERVER_URL}/html/audio.html"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
);
|
||||
|
||||
it('should emit contents of iframe once', async () => {
|
||||
const frame = ctx.page.mainFrame().childFrames()[0];
|
||||
await frame.evaluate(() => {
|
||||
const el = document.querySelector('audio')!;
|
||||
el.play();
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
const snapshots = (await ctx.page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('same origin iframes', function (this: ISuite) {
|
||||
jest.setTimeout(100_000);
|
||||
|
||||
const ctx: ISuite = setup.call(
|
||||
this,
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<iframe src="about:blank"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
);
|
||||
|
||||
it('should emit contents of iframe once', async () => {
|
||||
const events = await ctx.page.evaluate(
|
||||
() => ((window as unknown) as IWindow).snapshots,
|
||||
);
|
||||
await waitForRAF(ctx.page);
|
||||
// two events (full snapshot + meta) from main frame,
|
||||
// and two (full snapshot + mutation) from iframe
|
||||
expect(events.length).toBe(4);
|
||||
assertSnapshot(events);
|
||||
});
|
||||
});
|
||||
@@ -191,6 +191,11 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
|
||||
}),
|
||||
null,
|
||||
2,
|
||||
).replace(
|
||||
// servers might get run on a random port,
|
||||
// so we need to normalize the port number
|
||||
/http:\/\/localhost:\d+/g,
|
||||
'http://localhost:3030',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -216,6 +216,23 @@ export type SamplingStrategy = Partial<{
|
||||
canvas: 'all' | number;
|
||||
}>;
|
||||
|
||||
export interface ICrossOriginIframeMirror {
|
||||
getId(
|
||||
iframe: HTMLIFrameElement,
|
||||
remoteId: number,
|
||||
parentToRemoteMap?: Map<number, number>,
|
||||
remoteToParentMap?: Map<number, number>,
|
||||
): number;
|
||||
getIds(iframe: HTMLIFrameElement, remoteId: number[]): number[];
|
||||
getRemoteId(
|
||||
iframe: HTMLIFrameElement,
|
||||
parentId: number,
|
||||
map?: Map<number, number>,
|
||||
): number;
|
||||
getRemoteIds(iframe: HTMLIFrameElement, parentId: number[]): number[];
|
||||
reset(iframe?: HTMLIFrameElement): void;
|
||||
}
|
||||
|
||||
export type RecordPlugin<TOptions = unknown> = {
|
||||
name: string;
|
||||
observer?: (
|
||||
@@ -224,7 +241,11 @@ export type RecordPlugin<TOptions = unknown> = {
|
||||
options: TOptions,
|
||||
) => listenerHandler;
|
||||
eventProcessor?: <TExtend>(event: eventWithTime) => eventWithTime & TExtend;
|
||||
getMirror?: (mirror: Mirror) => void;
|
||||
getMirror?: (mirrors: {
|
||||
nodeMirror: Mirror;
|
||||
crossOriginIframeMirror: ICrossOriginIframeMirror;
|
||||
crossOriginIframeStyleMirror: ICrossOriginIframeMirror;
|
||||
}) => void;
|
||||
options: TOptions;
|
||||
};
|
||||
|
||||
@@ -636,3 +657,16 @@ declare global {
|
||||
export type IWindow = Window & typeof globalThis;
|
||||
|
||||
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
|
||||
export type GetTypedKeys<Obj extends object, ValueType> = TakeTypeHelper<
|
||||
Obj,
|
||||
ValueType
|
||||
>[keyof TakeTypeHelper<Obj, ValueType>];
|
||||
export type TakeTypeHelper<Obj extends object, ValueType> = {
|
||||
[K in keyof Obj]: Obj[K] extends ValueType ? K : never;
|
||||
};
|
||||
|
||||
export type TakeTypedKeyValues<Obj extends object, Type> = Pick<
|
||||
Obj,
|
||||
TakeTypeHelper<Obj, Type>[keyof TakeTypeHelper<Obj, Type>]
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user