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:
116
docs/recipes/cross-origin-iframes.md
Normal file
116
docs/recipes/cross-origin-iframes.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# Cross origin iframes
|
||||||
|
|
||||||
|
By default browsers make it difficult to access the contents of an iframe that is hosted on a different domain. This is a security feature to prevent malicious sites from accessing sensitive information on other sites. It is possible to work around this security feature, but it is not recommended unless you [are very strict](https://stackoverflow.com/a/21629575) about allowing only the sites you trust to embed your website inside of an iframe.
|
||||||
|
Since if you allow recording cross origin iframes, any malicious website can embed your website and as long as they have rrweb running they can record all the contents of your website.
|
||||||
|
|
||||||
|
## How to record cross origin iframes
|
||||||
|
|
||||||
|
Enable recording cross-origin iframes in your parent page:
|
||||||
|
|
||||||
|
```js
|
||||||
|
rrweb.record({
|
||||||
|
emit(event) {}, // all events will be emitted here, including events from cross origin iframes
|
||||||
|
recordCrossOriginIframes: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable replaying cross-origin iframes in your child page:
|
||||||
|
|
||||||
|
```js
|
||||||
|
rrweb.record({
|
||||||
|
emit(event) {}, // this is required for rrweb, but the child page will not emit any events
|
||||||
|
recordCrossOriginIframes: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Considerations
|
||||||
|
|
||||||
|
When cross origin iframe recording is turned on rrweb will check to see if it is being run in a top level window.
|
||||||
|
If it isn't it'll send the events to the parent window via `postMessage`.
|
||||||
|
|
||||||
|
If you don't have rrweb running in the top level window, the events will be lost when `recordCrossOriginIframes` is turned on.
|
||||||
|
|
||||||
|
If the top level window is a malicious website it can listen to the events and send them to a server of its choosing.
|
||||||
|
|
||||||
|
Or if a malicious script is running in on your page they can listen in on `postMessage` and as communication between the child and parent window is not encrypted. And they can see the events.
|
||||||
|
|
||||||
|
## Options for injecting rrweb into cross origin iframes
|
||||||
|
|
||||||
|
### 1. Website owners, add rrweb in the iframes
|
||||||
|
|
||||||
|
If you own the website that with the iframe and the website that is being embedded in an iframe, you can add rrweb to both pages via a script tag.
|
||||||
|
|
||||||
|
### 2. Browser extension
|
||||||
|
|
||||||
|
See https://developer.chrome.com/docs/extensions/mv3/content_scripts/#functionality
|
||||||
|
|
||||||
|
### 3. Puppeteer script
|
||||||
|
|
||||||
|
```js
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
|
||||||
|
async function injectRecording(frame) {
|
||||||
|
await frame.evaluate((rrwebCode) => {
|
||||||
|
if (window.__IS_RECORDING__) return;
|
||||||
|
window.__IS_RECORDING__ = true;
|
||||||
|
|
||||||
|
(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);
|
||||||
|
|
||||||
|
window.rrweb.record({
|
||||||
|
emit: (event) => {
|
||||||
|
window._captureEvent(event);
|
||||||
|
},
|
||||||
|
recordCrossOriginIframes: true,
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch();
|
||||||
|
const page = (await browser.pages())[0];
|
||||||
|
|
||||||
|
const events = []; // contains all events from all frames
|
||||||
|
|
||||||
|
await page.exposeFunction('_captureEvent', (event) => {
|
||||||
|
events.push(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('framenavigated', async (frame) => {
|
||||||
|
await injectRecording(frame); // injects rrweb into the iframe
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('https://example.com');
|
||||||
|
|
||||||
|
// your events are in the events array
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Electron
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'rrweb-recording-script.js'),
|
||||||
|
// this turns on preload inside iframes, but disables node integration
|
||||||
|
nodeIntegrationInSubFrames: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
See https://www.electronjs.org/docs/latest/tutorial/tutorial-preload
|
||||||
|
And https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
|
||||||
113
docs/recipes/cross-origin-iframes.zh_CN.md
Normal file
113
docs/recipes/cross-origin-iframes.zh_CN.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# 跨域 IFrame 录制
|
||||||
|
|
||||||
|
默认情况下,浏览器很难访问托管在不同域上的 iframe 的内容。 这是一项安全功能,可防止恶意站点访问其他站点上的敏感信息。 rrweb 提供了一个解决方案,但如果您对网站的安全有严格的要求https://stackoverflow.com/a/21629575, 只允许您信任的网站将您的网站嵌入 iframe 中,那我们不建议使用我们的解决方案。
|
||||||
|
因为如果您允许记录跨源 iframe,任何恶意网站都可以嵌入您的网站,并且只要它们运行 rrweb,它们就可以记录您网站的所有内容。
|
||||||
|
|
||||||
|
## 如何记录跨源 iframe
|
||||||
|
|
||||||
|
在父页面中启用录制跨域 iframe:
|
||||||
|
|
||||||
|
```js
|
||||||
|
rrweb.record({
|
||||||
|
emit(event) {}, // 所有事件都将在此处发出,包括来自跨源 iframe 的事件
|
||||||
|
recordCrossOriginIframes: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
在您的子页面中启用重放跨域 iframe:
|
||||||
|
|
||||||
|
```js
|
||||||
|
rrweb.record({
|
||||||
|
emit(event) {}, // 这是 rrweb 所必需的,但子页面不会发出任何事件
|
||||||
|
recordCrossOriginIframes: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
当跨源 iframe 录制开启时,rrweb 将检查它是否正在顶级窗口中运行。 如果不是,它将通过 `postMessage` 将事件发送到父窗口。
|
||||||
|
|
||||||
|
如果您没有在顶层窗口中运行 rrweb,则打开 `recordCrossOriginIframes` 时事件将丢失。
|
||||||
|
|
||||||
|
如果顶层窗口是一个恶意网站,它可以监听事件并将它们发送到它选择的服务器。或者如果您的页面上正在运行恶意脚本,则它们可以监听“postMessage”,又因为子窗口和父窗口之间的通信未加密,所以他们可以获取到所有事件。
|
||||||
|
|
||||||
|
## 将 rrweb 注入跨源 iframe 的选项
|
||||||
|
|
||||||
|
### 1. 网站所有者,在 iframe 中添加 rrweb
|
||||||
|
|
||||||
|
如果您拥有使用 iframe 的网站和嵌入在 iframe 中的网站,您可以通过脚本标签将 rrweb 添加到这两个页面。
|
||||||
|
|
||||||
|
### 2. 浏览器扩展
|
||||||
|
|
||||||
|
请查看 https://developer.chrome.com/docs/extensions/mv3/content_scripts/#functionality
|
||||||
|
|
||||||
|
### 3. Puppeteer script
|
||||||
|
|
||||||
|
```js
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
|
||||||
|
async function injectRecording(frame) {
|
||||||
|
await frame.evaluate((rrwebCode) => {
|
||||||
|
if (window.__IS_RECORDING__) return;
|
||||||
|
window.__IS_RECORDING__ = true;
|
||||||
|
|
||||||
|
(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);
|
||||||
|
|
||||||
|
window.rrweb.record({
|
||||||
|
emit: (event) => {
|
||||||
|
window._captureEvent(event);
|
||||||
|
},
|
||||||
|
recordCrossOriginIframes: true,
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch();
|
||||||
|
const page = (await browser.pages())[0];
|
||||||
|
|
||||||
|
const events = []; // 包含来自所有Frame的所有事件
|
||||||
|
|
||||||
|
await page.exposeFunction('_captureEvent', (event) => {
|
||||||
|
events.push(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('framenavigated', async (frame) => {
|
||||||
|
await injectRecording(frame); // 将 rrweb 注入 iframe
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('https://example.com');
|
||||||
|
|
||||||
|
// 您的事件将在事件数组中
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Electron
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'rrweb-recording-script.js'),
|
||||||
|
// 这会打开 iframe 内的预加载,但会禁用节点集成
|
||||||
|
nodeIntegrationInSubFrames: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
请查看 https://www.electronjs.org/docs/latest/tutorial/tutorial-preload
|
||||||
|
和 https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
|
||||||
@@ -67,4 +67,4 @@ source -> IncrementalSource.Font
|
|||||||
data -> fontData
|
data -> fontData
|
||||||
```
|
```
|
||||||
|
|
||||||
enum IncrementalSource's definition can be found in this [list](https://github.com/rrweb-io/rrweb/blob/master/packages/rrweb/typings/types.d.ts#L62).
|
enum IncrementalSource's definition can be found in this [list](https://github.com/rrweb-io/rrweb/blob/98e71cd0d23628cd1fbdbe47664a65748084c4a4/packages/types/src/index.ts#L69).
|
||||||
|
|||||||
53
guide.md
53
guide.md
@@ -135,32 +135,33 @@ setInterval(save, 10 * 1000);
|
|||||||
|
|
||||||
The parameter of `rrweb.record` accepts the following options.
|
The parameter of `rrweb.record` accepts the following options.
|
||||||
|
|
||||||
| key | default | description |
|
| key | default | description |
|
||||||
| -------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| emit | required | the callback function to get emitted events |
|
| emit | required | the callback function to get emitted events |
|
||||||
| checkoutEveryNth | - | take a full snapshot after every N events<br />refer to the [checkout](#checkout) chapter |
|
| checkoutEveryNth | - | take a full snapshot after every N events<br />refer to the [checkout](#checkout) chapter |
|
||||||
| checkoutEveryNms | - | take a full snapshot after every N ms<br />refer to the [checkout](#checkout) chapter |
|
| checkoutEveryNms | - | take a full snapshot after every N ms<br />refer to the [checkout](#checkout) chapter |
|
||||||
| blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter |
|
| blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter |
|
||||||
| blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter |
|
| blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter |
|
||||||
| ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter |
|
| ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter |
|
||||||
| ignoreCSSAttributes | null | array of CSS attributes that should be ignored |
|
| ignoreCSSAttributes | null | array of CSS attributes that should be ignored |
|
||||||
| maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter |
|
| maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter |
|
||||||
| maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter |
|
| maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter |
|
||||||
| maskAllInputs | false | mask all input content as \* |
|
| maskAllInputs | false | mask all input content as \* |
|
||||||
| maskInputOptions | { password: true } | mask some kinds of input \*<br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) |
|
| maskInputOptions | { password: true } | mask some kinds of input \*<br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) |
|
||||||
| maskInputFn | - | customize mask input content recording logic |
|
| maskInputFn | - | customize mask input content recording logic |
|
||||||
| maskTextFn | - | customize mask text content recording logic |
|
| maskTextFn | - | customize mask text content recording logic |
|
||||||
| slimDOMOptions | {} | remove unnecessary parts of the DOM <br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) |
|
| slimDOMOptions | {} | remove unnecessary parts of the DOM <br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) |
|
||||||
| dataURLOptions | {} | Canvas image format and quality ,This parameter will be passed to the OffscreenCanvas.convertToBlob(),Using this parameter effectively reduces the size of the recorded data |
|
| dataURLOptions | {} | Canvas image format and quality ,This parameter will be passed to the OffscreenCanvas.convertToBlob(),Using this parameter effectively reduces the size of the recorded data |
|
||||||
| inlineStylesheet | true | whether to inline the stylesheet in the events |
|
| inlineStylesheet | true | whether to inline the stylesheet in the events |
|
||||||
| hooks | {} | hooks for events<br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) |
|
| hooks | {} | hooks for events<br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) |
|
||||||
| packFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) |
|
| packFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) |
|
||||||
| sampling | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) |
|
| sampling | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) |
|
||||||
| recordCanvas | false | Whether to record the canvas element. Available options:<br/>`false`, <br/>`true` |
|
| recordCanvas | false | Whether to record the canvas element. Available options:<br/>`false`, <br/>`true` |
|
||||||
| inlineImages | false | whether to record the image content |
|
| recordCrossOriginIframes | false | Whether to record cross origin iframes. rrweb has to be injected in each child iframe for this to work. Available options:<br/>`false`, <br/>`true` |
|
||||||
| collectFonts | false | whether to collect fonts in the website |
|
| inlineImages | false | whether to record the image content |
|
||||||
| userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) |
|
| collectFonts | false | whether to collect fonts in the website |
|
||||||
| plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) |
|
| userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) |
|
||||||
|
| plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) |
|
||||||
|
|
||||||
#### Privacy
|
#### Privacy
|
||||||
|
|
||||||
|
|||||||
@@ -131,32 +131,33 @@ setInterval(save, 10 * 1000);
|
|||||||
|
|
||||||
`rrweb.record(config)` 的 config 部分接受以下参数
|
`rrweb.record(config)` 的 config 部分接受以下参数
|
||||||
|
|
||||||
| key | 默认值 | 功能 |
|
| key | 默认值 | 功能 |
|
||||||
| -------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| emit | 必填 | 获取当前录制的数据 |
|
| emit | 必填 | 获取当前录制的数据 |
|
||||||
| checkoutEveryNth | - | 每 N 次事件重新制作一次全量快照<br />详见[“重新制作快照”](#重新制作快照)章节 |
|
| checkoutEveryNth | - | 每 N 次事件重新制作一次全量快照<br />详见[“重新制作快照”](#重新制作快照)章节 |
|
||||||
| checkoutEveryNms | - | 每 N 毫秒重新制作一次全量快照<br />详见[“重新制作快照”](#重新制作快照)章节 |
|
| checkoutEveryNms | - | 每 N 毫秒重新制作一次全量快照<br />详见[“重新制作快照”](#重新制作快照)章节 |
|
||||||
| blockClass | 'rr-block' | 字符串或正则表达式,可用于自定义屏蔽元素的类名,详见[“隐私”](#隐私)章节 |
|
| blockClass | 'rr-block' | 字符串或正则表达式,可用于自定义屏蔽元素的类名,详见[“隐私”](#隐私)章节 |
|
||||||
| blockSelector | null | 所有 element.matches(blockSelector)为 true 的元素都不会被录制,回放时取而代之的是一个同等宽高的占位元素 |
|
| blockSelector | null | 所有 element.matches(blockSelector)为 true 的元素都不会被录制,回放时取而代之的是一个同等宽高的占位元素 |
|
||||||
| ignoreClass | 'rr-ignore' | 字符串或正则表达式,可用于自定义忽略元素的类名,详见[“隐私”](#隐私)章节 |
|
| ignoreClass | 'rr-ignore' | 字符串或正则表达式,可用于自定义忽略元素的类名,详见[“隐私”](#隐私)章节 |
|
||||||
| ignoreCSSAttributes | null | 应该被忽略的 CSS 属性数组 |
|
| ignoreCSSAttributes | null | 应该被忽略的 CSS 属性数组 |
|
||||||
| maskTextClass | 'rr-mask' | 字符串或正则表达式,可用于自定义忽略元素 text 内容的类名,详见[“隐私”](#隐私)章节 |
|
| maskTextClass | 'rr-mask' | 字符串或正则表达式,可用于自定义忽略元素 text 内容的类名,详见[“隐私”](#隐私)章节 |
|
||||||
| maskTextSelector | null | 所有 element.matches(maskTextSelector)为 true 的元素及其子元素的 text 内容将会被屏蔽 |
|
| maskTextSelector | null | 所有 element.matches(maskTextSelector)为 true 的元素及其子元素的 text 内容将会被屏蔽 |
|
||||||
| maskAllInputs | false | 将所有输入内容记录为 \* |
|
| maskAllInputs | false | 将所有输入内容记录为 \* |
|
||||||
| maskInputOptions | { password: true } | 选择将特定类型的输入框内容记录为 \*<br />类型详见[列表](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) |
|
| maskInputOptions | { password: true } | 选择将特定类型的输入框内容记录为 \*<br />类型详见[列表](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) |
|
||||||
| maskInputFn | - | 自定义特定类型的输入框内容记录逻辑 |
|
| maskInputFn | - | 自定义特定类型的输入框内容记录逻辑 |
|
||||||
| maskTextFn | - | 自定义文字内容的记录逻辑 |
|
| maskTextFn | - | 自定义文字内容的记录逻辑 |
|
||||||
| slimDOMOptions | {} | 去除 DOM 中不必要的部分 <br />类型详见[列表](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) |
|
| slimDOMOptions | {} | 去除 DOM 中不必要的部分 <br />类型详见[列表](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) |
|
||||||
| inlineStylesheet | true | 是否将样式表内联 |
|
| inlineStylesheet | true | 是否将样式表内联 |
|
||||||
| hooks | {} | 各类事件的回调<br />类型详见[列表](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) |
|
| hooks | {} | 各类事件的回调<br />类型详见[列表](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) |
|
||||||
| packFn | - | 数据压缩函数,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) |
|
| packFn | - | 数据压缩函数,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) |
|
||||||
| sampling | - | 数据抽样策略,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) |
|
| sampling | - | 数据抽样策略,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) |
|
||||||
| dataURLOptions | {} | Canvas 图像快照的格式和质量,这个参数将传递给 OffscreenCanvas.convertToBlob(),使用这个参数能有效减小录制数据的大小 |
|
| dataURLOptions | {} | Canvas 图像快照的格式和质量,这个参数将传递给 OffscreenCanvas.convertToBlob(),使用这个参数能有效减小录制数据的大小 |
|
||||||
| recordCanvas | false | 是否记录 canvas 内容, 可用选项:false, true |
|
| recordCanvas | false | 是否记录 canvas 内容, 可用选项:`false`, `true` |
|
||||||
| inlineImages | false | 是否将图片内容记内联录制 |
|
| recordCrossOriginIframes | false | 是否记录 cross origin iframes。 必须在每个子 iframe 中注入 rrweb 才能使其工作。 可用选项:`false`, `true` |
|
||||||
| collectFonts | false | 是否记录页面中的字体文件 |
|
| inlineImages | false | 是否将图片内容记内联录制 |
|
||||||
| userTriggeredOnInput | false | [什么是 `userTriggered`](https://github.com/rrweb-io/rrweb/pull/495) |
|
| collectFonts | false | 是否记录页面中的字体文件 |
|
||||||
| plugins | [] | 加载插件以获得额外的录制功能. [什么是插件?](./docs/recipes/plugin.zh_CN.md) |
|
| userTriggeredOnInput | false | [什么是 `userTriggered`](https://github.com/rrweb-io/rrweb/pull/495) |
|
||||||
|
| plugins | [] | 加载插件以获得额外的录制功能. [什么是插件?](./docs/recipes/plugin.zh_CN.md) |
|
||||||
|
|
||||||
#### 隐私
|
#### 隐私
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"concurrently": "^7.1.0",
|
"concurrently": "^7.1.0",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"eslint-plugin-compat": "^4.0.2",
|
"eslint-plugin-compat": "^4.0.2",
|
||||||
"eslint-plugin-jest": "^26.5.3",
|
"eslint-plugin-jest": "^27.1.3",
|
||||||
"eslint-plugin-tsdoc": "^0.2.16",
|
"eslint-plugin-tsdoc": "^0.2.16",
|
||||||
"lerna": "^4.0.0",
|
"lerna": "^4.0.0",
|
||||||
"markdownlint": "^0.25.1",
|
"markdownlint": "^0.25.1",
|
||||||
|
|||||||
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 { playerConfig } from 'rrweb/typings/types';
|
||||||
import type { eventWithTime } from '@rrweb/types';
|
import { eventWithTime } from '@rrweb/types';
|
||||||
import { Replayer, mirror } from 'rrweb';
|
import { Replayer, mirror } from 'rrweb';
|
||||||
import { SvelteComponent } from 'svelte';
|
import { SvelteComponent } from 'svelte';
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import snapshot, {
|
|||||||
needMaskingText,
|
needMaskingText,
|
||||||
classMatchesRegex,
|
classMatchesRegex,
|
||||||
IGNORED_NODE,
|
IGNORED_NODE,
|
||||||
|
genId,
|
||||||
} from './snapshot';
|
} from './snapshot';
|
||||||
import rebuild, {
|
import rebuild, {
|
||||||
buildNodeWithSN,
|
buildNodeWithSN,
|
||||||
@@ -28,4 +29,5 @@ export {
|
|||||||
needMaskingText,
|
needMaskingText,
|
||||||
classMatchesRegex,
|
classMatchesRegex,
|
||||||
IGNORED_NODE,
|
IGNORED_NODE,
|
||||||
|
genId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const tagNameRegex = new RegExp('[^a-z0-9-_:]');
|
|||||||
|
|
||||||
export const IGNORED_NODE = -2;
|
export const IGNORED_NODE = -2;
|
||||||
|
|
||||||
function genId(): number {
|
export function genId(): number {
|
||||||
return _id++;
|
return _id++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
"@types/dom-mediacapture-transform": "^0.1.3",
|
"@types/dom-mediacapture-transform": "^0.1.3",
|
||||||
"@types/inquirer": "^8.2.1",
|
"@types/inquirer": "^8.2.1",
|
||||||
"@types/jest": "^27.4.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/node": "^17.0.21",
|
||||||
"@types/offscreencanvas": "^2019.6.4",
|
"@types/offscreencanvas": "^2019.6.4",
|
||||||
"@types/prettier": "^2.3.2",
|
"@types/prettier": "^2.3.2",
|
||||||
@@ -63,10 +63,10 @@
|
|||||||
"ignore-styles": "^5.0.1",
|
"ignore-styles": "^5.0.1",
|
||||||
"inquirer": "^9.0.0",
|
"inquirer": "^9.0.0",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"jest-image-snapshot": "^4.5.1",
|
"jest-image-snapshot": "^5.2.0",
|
||||||
"jest-snapshot": "^23.6.0",
|
"jest-snapshot": "^23.6.0",
|
||||||
"prettier": "2.2.1",
|
"prettier": "2.2.1",
|
||||||
"puppeteer": "^9.1.1",
|
"puppeteer": "^11.0.0",
|
||||||
"rollup": "^2.68.0",
|
"rollup": "^2.68.0",
|
||||||
"rollup-plugin-esbuild": "^4.9.1",
|
"rollup-plugin-esbuild": "^4.9.1",
|
||||||
"rollup-plugin-postcss": "^3.1.1",
|
"rollup-plugin-postcss": "^3.1.1",
|
||||||
|
|||||||
@@ -21,6 +21,43 @@ void (async () => {
|
|||||||
const code = getCode();
|
const code = getCode();
|
||||||
let events = [];
|
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');
|
await start('https://react-redux.realworld.io');
|
||||||
|
|
||||||
const fakeGoto = async (page, url) => {
|
const fakeGoto = async (page, url) => {
|
||||||
@@ -44,8 +81,7 @@ void (async () => {
|
|||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'url',
|
name: 'url',
|
||||||
message:
|
message: `Enter the url you want to record, e.g [${defaultURL}]: `,
|
||||||
`Enter the url you want to record, e.g [${defaultURL}]: `,
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -116,34 +152,18 @@ void (async () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.goto(url, {
|
|
||||||
waitUntil: 'domcontentloaded',
|
|
||||||
timeout: 300000,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.exposeFunction('_replLog', (event) => {
|
await page.exposeFunction('_replLog', (event) => {
|
||||||
events.push(event);
|
events.push(event);
|
||||||
});
|
});
|
||||||
await page.evaluate(`;${code}
|
|
||||||
window.__IS_RECORDING__ = true
|
page.on('framenavigated', async (frame) => {
|
||||||
rrweb.record({
|
await injectRecording(frame);
|
||||||
emit: event => window._replLog(event),
|
});
|
||||||
recordCanvas: true,
|
|
||||||
collectFonts: true
|
await page.goto(url, {
|
||||||
});
|
waitUntil: 'domcontentloaded',
|
||||||
`);
|
timeout: 300000,
|
||||||
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
|
|
||||||
});
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.once('done', async (shouldReplay) => {
|
emitter.once('done', async (shouldReplay) => {
|
||||||
@@ -211,9 +231,9 @@ void (async () => {
|
|||||||
<script>
|
<script>
|
||||||
/*<!--*/
|
/*<!--*/
|
||||||
const events = ${JSON.stringify(events).replace(
|
const events = ${JSON.stringify(events).replace(
|
||||||
/<\/script>/g,
|
/<\/script>/g,
|
||||||
'<\\/script>',
|
'<\\/script>',
|
||||||
)};
|
)};
|
||||||
/*-->*/
|
/*-->*/
|
||||||
const replayer = new rrweb.Replayer(events, {
|
const replayer = new rrweb.Replayer(events, {
|
||||||
UNSAFE_replayCanvas: true
|
UNSAFE_replayCanvas: true
|
||||||
|
|||||||
@@ -17,41 +17,62 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const emitter = new EventEmitter();
|
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) {
|
async function injectRecording(frame) {
|
||||||
try {
|
await frame.evaluate(
|
||||||
await page.addScriptTag({ url: `${serverURL}/rrweb.js` });
|
(rrwebCode, pluginCode) => {
|
||||||
await page.addScriptTag({
|
|
||||||
url: `${serverURL}/plugins/canvas-webrtc-record.js`,
|
|
||||||
});
|
|
||||||
await page.evaluate((serverURL) => {
|
|
||||||
const win = window;
|
const win = window;
|
||||||
|
if (win.__IS_RECORDING__) return;
|
||||||
win.__IS_RECORDING__ = true;
|
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({
|
(async () => {
|
||||||
emit: (event) => {
|
function loadScript(code) {
|
||||||
win.events.push(event);
|
const s = document.createElement('script');
|
||||||
win._captureEvent(event);
|
s.type = 'text/javascript';
|
||||||
},
|
s.innerHTML = code;
|
||||||
plugins: [window.plugin.initPlugin()],
|
if (document.head) {
|
||||||
recordCanvas: false,
|
document.head.append(s);
|
||||||
collectFonts: true,
|
} else {
|
||||||
inlineImages: true,
|
requestAnimationFrame(() => {
|
||||||
});
|
document.head.append(s);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
}
|
||||||
console.error(error);
|
}
|
||||||
}
|
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) {
|
async function startReplay(page, serverURL, recordedPage) {
|
||||||
@@ -209,6 +230,17 @@ void (async () => {
|
|||||||
resizeWindow(replayerPage, 0, 800, 800, 800),
|
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, {
|
await recordedPage.goto(url, {
|
||||||
waitUntil: 'domcontentloaded',
|
waitUntil: 'domcontentloaded',
|
||||||
timeout: 300000,
|
timeout: 300000,
|
||||||
@@ -216,24 +248,6 @@ void (async () => {
|
|||||||
|
|
||||||
if (!replayerPage) throw new Error('No replayer page found');
|
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 () => {
|
emitter.once('done', async () => {
|
||||||
const pages = [
|
const pages = [
|
||||||
...(await recordingBrowser.pages()),
|
...(await recordingBrowser.pages()),
|
||||||
|
|||||||
@@ -1,14 +1,39 @@
|
|||||||
import type { Mirror } from 'rrweb-snapshot';
|
import type { Mirror } from 'rrweb-snapshot';
|
||||||
import SimplePeer from 'simple-peer-light';
|
import SimplePeer from 'simple-peer-light';
|
||||||
import type { RecordPlugin } from '@rrweb/types';
|
import type { RecordPlugin, ICrossOriginIframeMirror } from '@rrweb/types';
|
||||||
import type { WebRTCDataChannel } from '../types';
|
import type { WebRTCDataChannel } from '../types';
|
||||||
|
|
||||||
export const PLUGIN_NAME = 'rrweb/canvas-webrtc@1';
|
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 {
|
export class RRWebPluginCanvasWebRTCRecord {
|
||||||
private peer: SimplePeer.Instance | null = null;
|
private peer: SimplePeer.Instance | null = null;
|
||||||
private mirror: Mirror;
|
private mirror: Mirror;
|
||||||
|
private crossOriginIframeMirror: ICrossOriginIframeMirror;
|
||||||
private streamMap: Map<number, MediaStream> = new Map();
|
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;
|
private signalSendCallback: (msg: RTCSessionDescriptionInit) => void;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
@@ -19,14 +44,19 @@ export class RRWebPluginCanvasWebRTCRecord {
|
|||||||
peer?: SimplePeer.Instance;
|
peer?: SimplePeer.Instance;
|
||||||
}) {
|
}) {
|
||||||
this.signalSendCallback = signalSendCallback;
|
this.signalSendCallback = signalSendCallback;
|
||||||
|
window.addEventListener(
|
||||||
|
'message',
|
||||||
|
this.windowPostMessageHandler.bind(this),
|
||||||
|
);
|
||||||
if (peer) this.peer = peer;
|
if (peer) this.peer = peer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public initPlugin(): RecordPlugin {
|
public initPlugin(): RecordPlugin {
|
||||||
return {
|
return {
|
||||||
name: PLUGIN_NAME,
|
name: PLUGIN_NAME,
|
||||||
getMirror: (mirror) => {
|
getMirror: ({ nodeMirror, crossOriginIframeMirror }) => {
|
||||||
this.mirror = mirror;
|
this.mirror = nodeMirror;
|
||||||
|
this.crossOriginIframeMirror = crossOriginIframeMirror;
|
||||||
},
|
},
|
||||||
options: {},
|
options: {},
|
||||||
};
|
};
|
||||||
@@ -37,58 +67,239 @@ export class RRWebPluginCanvasWebRTCRecord {
|
|||||||
this.peer?.signal(signal);
|
this.peer?.signal(signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public signalReceiveFromCrossOriginIframe(
|
||||||
|
signal: RTCSessionDescriptionInit,
|
||||||
|
source: WindowProxy,
|
||||||
|
) {
|
||||||
|
const peer = this.setupPeer(source);
|
||||||
|
peer.signal(signal);
|
||||||
|
}
|
||||||
|
|
||||||
private startStream(id: number, stream: MediaStream) {
|
private startStream(id: number, stream: MediaStream) {
|
||||||
if (!this.peer) return this.setupPeer();
|
if (!this.peer) this.setupPeer();
|
||||||
|
|
||||||
const data: WebRTCDataChannel = {
|
const data: WebRTCDataChannel = {
|
||||||
nodeId: id,
|
nodeId: id,
|
||||||
streamId: stream.id,
|
streamId: stream.id,
|
||||||
};
|
};
|
||||||
this.peer?.send(JSON.stringify(data));
|
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() {
|
public setupPeer(source?: WindowProxy): SimplePeer.Instance {
|
||||||
if (!this.peer) {
|
let peer: SimplePeer.Instance;
|
||||||
this.peer = new SimplePeer({
|
|
||||||
|
if (!source) {
|
||||||
|
if (this.peer) return this.peer;
|
||||||
|
|
||||||
|
peer = this.peer = new SimplePeer({
|
||||||
initiator: true,
|
initiator: true,
|
||||||
// trickle: false, // only create one WebRTC offer per session
|
// trickle: false, // only create one WebRTC offer per session
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
const peerFromMap = this.windowPeerMap.get(source);
|
||||||
|
|
||||||
this.peer.on('error', (err: Error) => {
|
if (peerFromMap) return peerFromMap;
|
||||||
this.peer = null;
|
|
||||||
console.log('error', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.peer.on('close', () => {
|
peer = new SimplePeer({
|
||||||
this.peer = null;
|
initiator: false,
|
||||||
console.log('closing');
|
// trickle: false, // only create one WebRTC offer per session
|
||||||
});
|
|
||||||
|
|
||||||
this.peer.on('signal', (data: RTCSessionDescriptionInit) => {
|
|
||||||
this.signalSendCallback(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.peer.on('connect', () => {
|
|
||||||
for (const [id, stream] of this.streamMap) {
|
|
||||||
this.startStream(id, stream);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
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;
|
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;
|
if (stream) return stream;
|
||||||
|
|
||||||
const el = this.mirror.getNode(id) as HTMLCanvasElement | null;
|
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();
|
stream = el.captureStream();
|
||||||
this.streamMap.set(id, stream);
|
this.streamMap.set(rootId || id, stream);
|
||||||
this.setupPeer();
|
this.setupPeer();
|
||||||
|
|
||||||
return stream;
|
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);
|
this.canvasFoundCallback(node, context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getMirror: (mirror: Mirror) => {
|
getMirror: (options) => {
|
||||||
this.mirror = mirror;
|
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 { 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';
|
import type { StylesheetManager } from './stylesheet-manager';
|
||||||
|
|
||||||
export class IframeManager {
|
export class IframeManager {
|
||||||
private iframes: WeakMap<HTMLIFrameElement, true> = new WeakMap();
|
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 mutationCb: mutationCallBack;
|
||||||
|
private wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void;
|
||||||
private loadListener?: (iframeEl: HTMLIFrameElement) => unknown;
|
private loadListener?: (iframeEl: HTMLIFrameElement) => unknown;
|
||||||
private stylesheetManager: StylesheetManager;
|
private stylesheetManager: StylesheetManager;
|
||||||
|
private recordCrossOriginIframes: boolean;
|
||||||
|
|
||||||
constructor(options: {
|
constructor(options: {
|
||||||
|
mirror: Mirror;
|
||||||
mutationCb: mutationCallBack;
|
mutationCb: mutationCallBack;
|
||||||
stylesheetManager: StylesheetManager;
|
stylesheetManager: StylesheetManager;
|
||||||
|
recordCrossOriginIframes: boolean;
|
||||||
|
wrappedEmit: (e: eventWithTime, isCheckout?: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
this.mutationCb = options.mutationCb;
|
this.mutationCb = options.mutationCb;
|
||||||
|
this.wrappedEmit = options.wrappedEmit;
|
||||||
this.stylesheetManager = options.stylesheetManager;
|
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) {
|
public addIframe(iframeEl: HTMLIFrameElement) {
|
||||||
this.iframes.set(iframeEl, true);
|
this.iframes.set(iframeEl, true);
|
||||||
|
if (iframeEl.contentWindow)
|
||||||
|
this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addLoadListener(cb: (iframeEl: HTMLIFrameElement) => unknown) {
|
public addLoadListener(cb: (iframeEl: HTMLIFrameElement) => unknown) {
|
||||||
@@ -27,12 +56,11 @@ export class IframeManager {
|
|||||||
public attachIframe(
|
public attachIframe(
|
||||||
iframeEl: HTMLIFrameElement,
|
iframeEl: HTMLIFrameElement,
|
||||||
childSn: serializedNodeWithId,
|
childSn: serializedNodeWithId,
|
||||||
mirror: Mirror,
|
|
||||||
) {
|
) {
|
||||||
this.mutationCb({
|
this.mutationCb({
|
||||||
adds: [
|
adds: [
|
||||||
{
|
{
|
||||||
parentId: mirror.getId(iframeEl),
|
parentId: this.mirror.getId(iframeEl),
|
||||||
nextId: null,
|
nextId: null,
|
||||||
node: childSn,
|
node: childSn,
|
||||||
},
|
},
|
||||||
@@ -51,7 +79,199 @@ export class IframeManager {
|
|||||||
)
|
)
|
||||||
this.stylesheetManager.adoptStyleSheets(
|
this.stylesheetManager.adoptStyleSheets(
|
||||||
iframeEl.contentDocument.adoptedStyleSheets,
|
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,
|
canvasMutationParam,
|
||||||
adoptedStyleSheetParam,
|
adoptedStyleSheetParam,
|
||||||
} from '@rrweb/types';
|
} from '@rrweb/types';
|
||||||
|
import type { CrossOriginIframeMessageEventContent } from '../types';
|
||||||
import { IframeManager } from './iframe-manager';
|
import { IframeManager } from './iframe-manager';
|
||||||
import { ShadowDomManager } from './shadow-dom-manager';
|
import { ShadowDomManager } from './shadow-dom-manager';
|
||||||
import { CanvasManager } from './observers/canvas/canvas-manager';
|
import { CanvasManager } from './observers/canvas/canvas-manager';
|
||||||
@@ -69,6 +70,7 @@ function record<T = eventWithTime>(
|
|||||||
dataURLOptions = {},
|
dataURLOptions = {},
|
||||||
mousemoveWait,
|
mousemoveWait,
|
||||||
recordCanvas = false,
|
recordCanvas = false,
|
||||||
|
recordCrossOriginIframes = false,
|
||||||
userTriggeredOnInput = false,
|
userTriggeredOnInput = false,
|
||||||
collectFonts = false,
|
collectFonts = false,
|
||||||
inlineImages = false,
|
inlineImages = false,
|
||||||
@@ -77,8 +79,22 @@ function record<T = eventWithTime>(
|
|||||||
ignoreCSSAttributes = new Set([]),
|
ignoreCSSAttributes = new Set([]),
|
||||||
} = options;
|
} = 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
|
// runtime checks for user options
|
||||||
if (!emit) {
|
if (inEmittingFrame && !emit) {
|
||||||
throw new Error('emit function is required');
|
throw new Error('emit function is required');
|
||||||
}
|
}
|
||||||
// move departed options to new options
|
// move departed options to new options
|
||||||
@@ -138,13 +154,6 @@ function record<T = eventWithTime>(
|
|||||||
let lastFullSnapshotEvent: eventWithTime;
|
let lastFullSnapshotEvent: eventWithTime;
|
||||||
let incrementalSnapshotCount = 0;
|
let incrementalSnapshotCount = 0;
|
||||||
|
|
||||||
/**
|
|
||||||
* Exposes mirror to the plugins
|
|
||||||
*/
|
|
||||||
for (const plugin of plugins || []) {
|
|
||||||
if (plugin.getMirror) plugin.getMirror(mirror);
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventProcessor = (e: eventWithTime): T => {
|
const eventProcessor = (e: eventWithTime): T => {
|
||||||
for (const plugin of plugins || []) {
|
for (const plugin of plugins || []) {
|
||||||
if (plugin.eventProcessor) {
|
if (plugin.eventProcessor) {
|
||||||
@@ -170,7 +179,17 @@ function record<T = eventWithTime>(
|
|||||||
mutationBuffers.forEach((buf) => buf.unfreeze());
|
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) {
|
if (e.type === EventType.FullSnapshot) {
|
||||||
lastFullSnapshotEvent = e;
|
lastFullSnapshotEvent = e;
|
||||||
incrementalSnapshotCount = 0;
|
incrementalSnapshotCount = 0;
|
||||||
@@ -244,10 +263,26 @@ function record<T = eventWithTime>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const iframeManager = new IframeManager({
|
const iframeManager = new IframeManager({
|
||||||
|
mirror,
|
||||||
mutationCb: wrappedMutationEmit,
|
mutationCb: wrappedMutationEmit,
|
||||||
stylesheetManager: stylesheetManager,
|
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({
|
canvasManager = new CanvasManager({
|
||||||
recordCanvas,
|
recordCanvas,
|
||||||
mutationCb: wrappedCanvasMutationEmit,
|
mutationCb: wrappedCanvasMutationEmit,
|
||||||
@@ -326,7 +361,7 @@ function record<T = eventWithTime>(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onIframeLoad: (iframe, childSn) => {
|
onIframeLoad: (iframe, childSn) => {
|
||||||
iframeManager.attachIframe(iframe, childSn, mirror);
|
iframeManager.attachIframe(iframe, childSn);
|
||||||
shadowDomManager.observeAttachShadow(iframe);
|
shadowDomManager.observeAttachShadow(iframe);
|
||||||
},
|
},
|
||||||
onStylesheetLoad: (linkEl, childSn) => {
|
onStylesheetLoad: (linkEl, childSn) => {
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ export default class MutationBuffer {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onIframeLoad: (iframe, childSn) => {
|
onIframeLoad: (iframe, childSn) => {
|
||||||
this.iframeManager.attachIframe(iframe, childSn, this.mirror);
|
this.iframeManager.attachIframe(iframe, childSn);
|
||||||
this.shadowDomManager.observeAttachShadow(iframe);
|
this.shadowDomManager.observeAttachShadow(iframe);
|
||||||
},
|
},
|
||||||
onStylesheetLoad: (link, childSn) => {
|
onStylesheetLoad: (link, childSn) => {
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ export class Replayer {
|
|||||||
* Exposes mirror to the plugins
|
* Exposes mirror to the plugins
|
||||||
*/
|
*/
|
||||||
for (const plugin of this.config.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, () => {
|
this.emitter.on(ReplayerEvents.Flush, () => {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
serializedNodeWithId,
|
|
||||||
Mirror,
|
Mirror,
|
||||||
INode,
|
|
||||||
MaskInputOptions,
|
MaskInputOptions,
|
||||||
SlimDOMOptions,
|
SlimDOMOptions,
|
||||||
MaskInputFn,
|
MaskInputFn,
|
||||||
@@ -61,6 +59,7 @@ export type recordOptions<T> = {
|
|||||||
sampling?: SamplingStrategy;
|
sampling?: SamplingStrategy;
|
||||||
dataURLOptions?: DataURLOptions;
|
dataURLOptions?: DataURLOptions;
|
||||||
recordCanvas?: boolean;
|
recordCanvas?: boolean;
|
||||||
|
recordCrossOriginIframes?: boolean;
|
||||||
userTriggeredOnInput?: boolean;
|
userTriggeredOnInput?: boolean;
|
||||||
collectFonts?: boolean;
|
collectFonts?: boolean;
|
||||||
inlineImages?: boolean;
|
inlineImages?: boolean;
|
||||||
@@ -152,7 +151,7 @@ export type ReplayPlugin = {
|
|||||||
node: Node | RRNode,
|
node: Node | RRNode,
|
||||||
context: { id: number; replayer: Replayer },
|
context: { id: number; replayer: Replayer },
|
||||||
) => void;
|
) => void;
|
||||||
getMirror?: (mirror: Mirror) => void;
|
getMirror?: (mirrors: { nodeMirror: Mirror }) => void;
|
||||||
};
|
};
|
||||||
export type playerConfig = {
|
export type playerConfig = {
|
||||||
speed: number;
|
speed: number;
|
||||||
@@ -194,3 +193,10 @@ declare global {
|
|||||||
FontFace: typeof FontFace;
|
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.idStyleMap = new Map();
|
||||||
this.id = 1;
|
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,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n\\\\n\\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 7
|
\\"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
|
\\"id\\": 3
|
||||||
@@ -4023,7 +4042,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n\\\\n\\",
|
\\"textContent\\": \\"\\\\n\\\\n\\",
|
||||||
\\"id\\": 8
|
\\"id\\": 11
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -4033,7 +4052,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 10
|
\\"id\\": 13
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -4043,40 +4062,22 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||||
\\"id\\": 12
|
\\"id\\": 15
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 11
|
\\"id\\": 14
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 13
|
\\"id\\": 16
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"br\\",
|
\\"tagName\\": \\"br\\",
|
||||||
\\"attributes\\": {},
|
\\"attributes\\": {},
|
||||||
\\"childNodes\\": [],
|
\\"childNodes\\": [],
|
||||||
\\"id\\": 14
|
\\"id\\": 17
|
||||||
},
|
|
||||||
{
|
|
||||||
\\"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
@@ -4085,15 +4086,33 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"br\\",
|
\\"tagName\\": \\"h1\\",
|
||||||
\\"attributes\\": {},
|
\\"attributes\\": {},
|
||||||
\\"childNodes\\": [],
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"Verify that block class bugs are fixed\\",
|
||||||
|
\\"id\\": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
\\"id\\": 19
|
\\"id\\": 19
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 20
|
\\"id\\": 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"br\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 23
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -4105,7 +4124,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 22
|
\\"id\\": 25
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -4117,7 +4136,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 24
|
\\"id\\": 27
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -4127,91 +4146,91 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"VISIBLE\\",
|
\\"textContent\\": \\"VISIBLE\\",
|
||||||
\\"id\\": 26
|
\\"id\\": 29
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 25
|
\\"id\\": 28
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 27
|
\\"id\\": 30
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 23
|
\\"id\\": 26
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"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
|
\\"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,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 32
|
\\"id\\": 35
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"div\\",
|
\\"tagName\\": \\"div\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"class\\": \\"rr-block\\",
|
\\"class\\": \\"rr-block\\",
|
||||||
\\"rr_width\\": \\"1904px\\",
|
\\"rr_width\\": \\"200px\\",
|
||||||
\\"rr_height\\": \\"21px\\"
|
\\"rr_height\\": \\"33px\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [],
|
\\"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
|
\\"id\\": 36
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 37
|
||||||
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"br\\",
|
\\"tagName\\": \\"br\\",
|
||||||
\\"attributes\\": {},
|
\\"attributes\\": {},
|
||||||
\\"childNodes\\": [],
|
\\"childNodes\\": [],
|
||||||
\\"id\\": 37
|
\\"id\\": 38
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"br\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 39
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"br\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 40
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 38
|
\\"id\\": 41
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -4223,49 +4242,49 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"MUTATE\\",
|
\\"textContent\\": \\"MUTATE\\",
|
||||||
\\"id\\": 40
|
\\"id\\": 43
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 39
|
\\"id\\": 42
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 41
|
\\"id\\": 44
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 21
|
\\"id\\": 24
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"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
|
\\"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,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 46
|
\\"id\\": 49
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -4277,7 +4296,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 48
|
\\"id\\": 51
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -4289,7 +4308,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 50
|
\\"id\\": 53
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -4299,91 +4318,91 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"VISIBLE\\",
|
\\"textContent\\": \\"VISIBLE\\",
|
||||||
\\"id\\": 52
|
\\"id\\": 55
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 51
|
\\"id\\": 54
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 53
|
\\"id\\": 56
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 49
|
\\"id\\": 52
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"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
|
\\"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,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 58
|
\\"id\\": 61
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"div\\",
|
\\"tagName\\": \\"div\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"class\\": \\"rr-block\\",
|
\\"class\\": \\"rr-block\\",
|
||||||
\\"rr_width\\": \\"1904px\\",
|
\\"rr_width\\": \\"200px\\",
|
||||||
\\"rr_height\\": \\"21px\\"
|
\\"rr_height\\": \\"33px\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [],
|
\\"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
|
\\"id\\": 62
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 63
|
||||||
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"br\\",
|
\\"tagName\\": \\"br\\",
|
||||||
\\"attributes\\": {},
|
\\"attributes\\": {},
|
||||||
\\"childNodes\\": [],
|
\\"childNodes\\": [],
|
||||||
\\"id\\": 63
|
\\"id\\": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"br\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 65
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"br\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 66
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 64
|
\\"id\\": 67
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -4395,23 +4414,23 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"MUTATE\\",
|
\\"textContent\\": \\"MUTATE\\",
|
||||||
\\"id\\": 66
|
\\"id\\": 69
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 65
|
\\"id\\": 68
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 67
|
\\"id\\": 70
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 47
|
\\"id\\": 50
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n\\\\n \\",
|
\\"textContent\\": \\"\\\\n\\\\n \\",
|
||||||
\\"id\\": 68
|
\\"id\\": 71
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -4421,18 +4440,18 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||||
\\"id\\": 70
|
\\"id\\": 73
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 69
|
\\"id\\": 72
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\",
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\",
|
||||||
\\"id\\": 71
|
\\"id\\": 74
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 9
|
\\"id\\": 12
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 2
|
\\"id\\": 2
|
||||||
@@ -4452,7 +4471,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
\\"data\\": {
|
\\"data\\": {
|
||||||
\\"source\\": 2,
|
\\"source\\": 2,
|
||||||
\\"type\\": 1,
|
\\"type\\": 1,
|
||||||
\\"id\\": 39
|
\\"id\\": 42
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -4460,7 +4479,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
\\"data\\": {
|
\\"data\\": {
|
||||||
\\"source\\": 2,
|
\\"source\\": 2,
|
||||||
\\"type\\": 5,
|
\\"type\\": 5,
|
||||||
\\"id\\": 39
|
\\"id\\": 42
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -4468,7 +4487,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
\\"data\\": {
|
\\"data\\": {
|
||||||
\\"source\\": 2,
|
\\"source\\": 2,
|
||||||
\\"type\\": 0,
|
\\"type\\": 0,
|
||||||
\\"id\\": 39
|
\\"id\\": 42
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -4476,7 +4495,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
\\"data\\": {
|
\\"data\\": {
|
||||||
\\"source\\": 2,
|
\\"source\\": 2,
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"id\\": 39
|
\\"id\\": 42
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -4486,7 +4505,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
\\"texts\\": [],
|
\\"texts\\": [],
|
||||||
\\"attributes\\": [
|
\\"attributes\\": [
|
||||||
{
|
{
|
||||||
\\"id\\": 33,
|
\\"id\\": 36,
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"class\\": \\"notB\\"
|
\\"class\\": \\"notB\\"
|
||||||
}
|
}
|
||||||
@@ -4495,50 +4514,19 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
\\"removes\\": [],
|
\\"removes\\": [],
|
||||||
\\"adds\\": [
|
\\"adds\\": [
|
||||||
{
|
{
|
||||||
\\"parentId\\": 23,
|
\\"parentId\\": 26,
|
||||||
\\"nextId\\": null,
|
\\"nextId\\": null,
|
||||||
\\"node\\": {
|
\\"node\\": {
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"div\\",
|
\\"tagName\\": \\"div\\",
|
||||||
\\"attributes\\": {},
|
\\"attributes\\": {},
|
||||||
\\"childNodes\\": [],
|
\\"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
|
\\"id\\": 75
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"parentId\\": 72,
|
\\"parentId\\": 75,
|
||||||
\\"nextId\\": 73,
|
\\"nextId\\": null,
|
||||||
\\"node\\": {
|
\\"node\\": {
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"div\\",
|
\\"tagName\\": \\"div\\",
|
||||||
@@ -4563,71 +4551,13 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
\\"nextId\\": null,
|
\\"nextId\\": null,
|
||||||
\\"node\\": {
|
\\"node\\": {
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"I1I1 VISIBLE\\",
|
\\"textContent\\": \\"I1I2 VISIBLE\\",
|
||||||
\\"id\\": 78
|
\\"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,
|
\\"parentId\\": 75,
|
||||||
\\"attributes\\": {
|
\\"nextId\\": 76,
|
||||||
\\"class\\": \\"notB\\"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
\\"removes\\": [],
|
|
||||||
\\"adds\\": [
|
|
||||||
{
|
|
||||||
\\"parentId\\": 49,
|
|
||||||
\\"nextId\\": null,
|
|
||||||
\\"node\\": {
|
\\"node\\": {
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"div\\",
|
\\"tagName\\": \\"div\\",
|
||||||
@@ -4641,7 +4571,7 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
\\"nextId\\": null,
|
\\"nextId\\": null,
|
||||||
\\"node\\": {
|
\\"node\\": {
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"div\\",
|
\\"tagName\\": \\"button\\",
|
||||||
\\"attributes\\": {},
|
\\"attributes\\": {},
|
||||||
\\"childNodes\\": [],
|
\\"childNodes\\": [],
|
||||||
\\"id\\": 80
|
\\"id\\": 80
|
||||||
@@ -4651,25 +4581,83 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
\\"parentId\\": 80,
|
\\"parentId\\": 80,
|
||||||
\\"nextId\\": null,
|
\\"nextId\\": null,
|
||||||
\\"node\\": {
|
\\"node\\": {
|
||||||
\\"type\\": 2,
|
\\"type\\": 3,
|
||||||
\\"tagName\\": \\"button\\",
|
\\"textContent\\": \\"I1I1 VISIBLE\\",
|
||||||
\\"attributes\\": {},
|
|
||||||
\\"childNodes\\": [],
|
|
||||||
\\"id\\": 81
|
\\"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,
|
\\"nextId\\": null,
|
||||||
\\"node\\": {
|
\\"node\\": {
|
||||||
\\"type\\": 3,
|
\\"type\\": 2,
|
||||||
\\"textContent\\": \\"I1I2 VISIBLE\\",
|
\\"tagName\\": \\"div\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [],
|
||||||
\\"id\\": 82
|
\\"id\\": 82
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"parentId\\": 79,
|
\\"parentId\\": 82,
|
||||||
\\"nextId\\": 80,
|
\\"nextId\\": null,
|
||||||
\\"node\\": {
|
\\"node\\": {
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"div\\",
|
\\"tagName\\": \\"div\\",
|
||||||
@@ -4694,9 +4682,40 @@ exports[`record integration tests mutations should work when blocked class is un
|
|||||||
\\"nextId\\": null,
|
\\"nextId\\": null,
|
||||||
\\"node\\": {
|
\\"node\\": {
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"I1I1 VISIBLE\\",
|
\\"textContent\\": \\"I1I2 VISIBLE\\",
|
||||||
\\"id\\": 85
|
\\"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`] = `
|
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`] = `
|
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`] = `
|
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) => {
|
const hideMouseAnimation = async (p: puppeteer.Page) => {
|
||||||
await p.addStyleTag({
|
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);
|
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();
|
page = await browser.newPage();
|
||||||
|
|
||||||
@@ -108,9 +112,8 @@ describe('e2e webgl', () => {
|
|||||||
`);
|
`);
|
||||||
await waitForRAF(page);
|
await waitForRAF(page);
|
||||||
|
|
||||||
const element = await page.$('iframe');
|
const frameImage = await page!.screenshot();
|
||||||
const frameImage = await element!.screenshot();
|
await waitForRAF(page);
|
||||||
|
|
||||||
expect(frameImage).toMatchImageSnapshot();
|
expect(frameImage).toMatchImageSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,8 +125,11 @@ describe('e2e webgl', () => {
|
|||||||
getHtml.call(this, 'canvas-webgl-image.html', { recordCanvas: true }),
|
getHtml.call(this, 'canvas-webgl-image.html', { recordCanvas: true }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await waitForRAF(page);
|
||||||
await page.waitForTimeout(100);
|
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();
|
page = await browser.newPage();
|
||||||
|
|
||||||
@@ -143,9 +149,7 @@ describe('e2e webgl', () => {
|
|||||||
await page.evaluate(`replayer.play(500);`);
|
await page.evaluate(`replayer.play(500);`);
|
||||||
await waitForRAF(page);
|
await waitForRAF(page);
|
||||||
|
|
||||||
const element = await page.$('iframe');
|
const frameImage = await page!.screenshot();
|
||||||
const frameImage = await element!.screenshot();
|
|
||||||
|
|
||||||
expect(frameImage).toMatchImageSnapshot();
|
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>
|
<head>
|
||||||
<title>Uber Application for Codegen Testing</title>
|
<title>Uber Application for Codegen Testing</title>
|
||||||
|
<style>
|
||||||
|
#b-class,
|
||||||
|
#b-class-2 {
|
||||||
|
height: 33px;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script>
|
||||||
function mutate1() {
|
function mutate1() {
|
||||||
const bClassDiv = document.getElementById("b-class");
|
const bClassDiv = document.getElementById('b-class');
|
||||||
bClassDiv.className = "notB";
|
bClassDiv.className = 'notB';
|
||||||
|
|
||||||
const removeBlockedButton = document.getElementById("remove");
|
const removeBlockedButton = document.getElementById('remove');
|
||||||
removeBlockedButton.remove();
|
removeBlockedButton.remove();
|
||||||
|
|
||||||
const visibleCollection = document.getElementsByClassName("visible");
|
const visibleCollection = document.getElementsByClassName('visible');
|
||||||
const i1Div = document.createElement("div");
|
const i1Div = document.createElement('div');
|
||||||
const i1i1Div = document.createElement("div");
|
const i1i1Div = document.createElement('div');
|
||||||
const i1i2Div = document.createElement("div");
|
const i1i2Div = document.createElement('div');
|
||||||
|
|
||||||
const i1i1Button = document.createElement("button");
|
const i1i1Button = document.createElement('button');
|
||||||
i1i1Button.innerHTML = "I1I1 VISIBLE";
|
i1i1Button.innerHTML = 'I1I1 VISIBLE';
|
||||||
i1i1Div.appendChild(i1i1Button);
|
i1i1Div.appendChild(i1i1Button);
|
||||||
|
|
||||||
const i1i2Button = document.createElement("button");
|
const i1i2Button = document.createElement('button');
|
||||||
i1i2Button.innerHTML = "I1I2 VISIBLE";
|
i1i2Button.innerHTML = 'I1I2 VISIBLE';
|
||||||
i1i2Div.appendChild(i1i2Button);
|
i1i2Div.appendChild(i1i2Button);
|
||||||
|
|
||||||
i1Div.appendChild(i1i1Div);
|
i1Div.appendChild(i1i1Div);
|
||||||
i1Div.appendChild(i1i2Div);
|
i1Div.appendChild(i1i2Div);
|
||||||
visibleCollection[0].appendChild(i1Div);
|
visibleCollection[0].appendChild(i1Div);
|
||||||
}
|
}
|
||||||
function mutate2() {
|
function mutate2() {
|
||||||
const bClassDiv = document.getElementById("b-class-2");
|
const bClassDiv = document.getElementById('b-class-2');
|
||||||
bClassDiv.className = "notB";
|
bClassDiv.className = 'notB';
|
||||||
|
|
||||||
const removeBlockedButton = document.getElementById("remove2");
|
const removeBlockedButton = document.getElementById('remove2');
|
||||||
const innerButton = document.createElement("button");
|
const innerButton = document.createElement('button');
|
||||||
innerButton.innerHTML = "INNER BLOCKED";
|
innerButton.innerHTML = 'INNER BLOCKED';
|
||||||
removeBlockedButton.appendChild(innerButton)
|
removeBlockedButton.appendChild(innerButton);
|
||||||
removeBlockedButton.remove();
|
removeBlockedButton.remove();
|
||||||
|
|
||||||
const visibleCollection = document.getElementsByClassName("visible2");
|
const visibleCollection = document.getElementsByClassName('visible2');
|
||||||
const i1Div = document.createElement("div");
|
const i1Div = document.createElement('div');
|
||||||
const i1i1Div = document.createElement("div");
|
const i1i1Div = document.createElement('div');
|
||||||
const i1i2Div = document.createElement("div");
|
const i1i2Div = document.createElement('div');
|
||||||
|
|
||||||
const i1i1Button = document.createElement("button");
|
const i1i1Button = document.createElement('button');
|
||||||
i1i1Button.innerHTML = "I1I1 VISIBLE";
|
i1i1Button.innerHTML = 'I1I1 VISIBLE';
|
||||||
i1i1Div.appendChild(i1i1Button);
|
i1i1Div.appendChild(i1i1Button);
|
||||||
|
|
||||||
const i1i2Button = document.createElement("button");
|
const i1i2Button = document.createElement('button');
|
||||||
i1i2Button.innerHTML = "I1I2 VISIBLE";
|
i1i2Button.innerHTML = 'I1I2 VISIBLE';
|
||||||
i1i2Div.appendChild(i1i2Button);
|
i1i2Div.appendChild(i1i2Button);
|
||||||
|
|
||||||
i1Div.appendChild(i1i1Div);
|
i1Div.appendChild(i1i1Div);
|
||||||
i1Div.appendChild(i1i2Div);
|
i1Div.appendChild(i1i2Div);
|
||||||
visibleCollection[0].appendChild(i1Div);
|
visibleCollection[0].appendChild(i1Div);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<br/>
|
<br />
|
||||||
<h1>
|
<h1>Verify that block class bugs are fixed</h1>
|
||||||
Verify that block class bugs are fixed
|
<br />
|
||||||
</h1>
|
|
||||||
<br/>
|
|
||||||
<div class="first">
|
<div class="first">
|
||||||
<div class="visible">
|
<div class="visible">
|
||||||
<button>VISIBLE</button>
|
<button>VISIBLE</button>
|
||||||
</div>
|
</div>
|
||||||
<br/><br/><br/>
|
<br /><br /><br />
|
||||||
<div class="rr-block" id="b-class">
|
<div class="rr-block" id="b-class">
|
||||||
<button id="remove">BLOCKED</button>
|
<button id="remove">BLOCKED</button>
|
||||||
</div>
|
</div>
|
||||||
<br/><br/><br/>
|
<br /><br /><br />
|
||||||
<button onclick="mutate1()">MUTATE</button>
|
<button onclick="mutate1()">MUTATE</button>
|
||||||
</div>
|
</div>
|
||||||
<br/><br/><br/>
|
<br /><br /><br />
|
||||||
<div class="second">
|
<div class="second">
|
||||||
<div class="visible2">
|
<div class="visible2">
|
||||||
<button>VISIBLE</button>
|
<button>VISIBLE</button>
|
||||||
</div>
|
</div>
|
||||||
<br/><br/><br/>
|
<br /><br /><br />
|
||||||
<div class="rr-block" id="b-class-2">
|
<div class="rr-block" id="b-class-2">
|
||||||
<button id="remove2">BLOCKED</button>
|
<button id="remove2">BLOCKED</button>
|
||||||
</div>
|
</div>
|
||||||
<br/><br/><br/>
|
<br /><br /><br />
|
||||||
<button onclick="mutate2()">MUTATE</button>
|
<button onclick="mutate2()">MUTATE</button>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</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.type('textarea', 'textarea test');
|
||||||
await page.select('select', '1');
|
await page.select('select', '1');
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,7 +93,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
p.appendChild(document.createElement('span'));
|
p.appendChild(document.createElement('span'));
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,7 +115,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
p.innerText = 'mutated';
|
p.innerText = 'mutated';
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,7 +135,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
document.body.setAttribute('test', 'true');
|
document.body.setAttribute('test', 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,7 +154,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
'document.getElementById("select2-drop").setAttribute("style", document.getElementById("select2-drop").style.cssText + "color:black !important")',
|
'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);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -180,7 +190,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
|
|
||||||
await waitForRAF(page);
|
await waitForRAF(page);
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -191,7 +203,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
|
|
||||||
await page.type('.rr-ignore', 'secret');
|
await page.type('.rr-ignore', 'secret');
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,7 +223,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
await page.type('textarea', 'textarea test');
|
await page.type('textarea', 'textarea test');
|
||||||
await page.select('select', '1');
|
await page.select('select', '1');
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,7 +249,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
await page.type('input[type="password"]', 'password');
|
await page.type('input[type="password"]', 'password');
|
||||||
await page.select('select', '1');
|
await page.select('select', '1');
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,7 +268,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
|
|
||||||
await page.type('input[type="password"]', 'secr3t');
|
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);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -268,7 +288,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
await page.type('textarea', 'textarea test');
|
await page.type('textarea', 'textarea test');
|
||||||
await page.select('select', '1');
|
await page.select('select', '1');
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -281,7 +303,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
await page.evaluate(`document.getElementById('text').innerText = '1'`);
|
await page.evaluate(`document.getElementById('text').innerText = '1'`);
|
||||||
await page.click('#text');
|
await page.click('#text');
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -301,7 +325,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
nextElement.parentNode!.insertBefore(el, nextElement);
|
nextElement.parentNode!.insertBefore(el, nextElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -310,13 +336,19 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
await page.goto('about: blank');
|
await page.goto('about: blank');
|
||||||
await page.setContent(getHtml.call(this, 'blocked-unblocked.html'));
|
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();
|
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();
|
await elements2[0].click();
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -334,7 +366,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
p.removeChild(span);
|
p.removeChild(span);
|
||||||
div.appendChild(span);
|
div.appendChild(span);
|
||||||
});
|
});
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -349,7 +383,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
document.body.appendChild(div);
|
document.body.appendChild(div);
|
||||||
div.appendChild(span);
|
div.appendChild(span);
|
||||||
});
|
});
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -358,7 +394,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
await page.goto('about:blank');
|
await page.goto('about:blank');
|
||||||
await page.setContent(getHtml.call(this, 'react-styled-components.html'));
|
await page.setContent(getHtml.call(this, 'react-styled-components.html'));
|
||||||
await page.click('.toggle');
|
await page.click('.toggle');
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -371,7 +409,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await waitForRAF(page);
|
await waitForRAF(page);
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
for (const event of snapshots) {
|
for (const event of snapshots) {
|
||||||
if (event.type === EventType.FullSnapshot) {
|
if (event.type === EventType.FullSnapshot) {
|
||||||
visitSnapshot(event.data.node, (n) => {
|
visitSnapshot(event.data.node, (n) => {
|
||||||
@@ -393,7 +433,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await page.waitForTimeout(50);
|
await page.waitForTimeout(50);
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -406,7 +448,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await waitForRAF(page);
|
await waitForRAF(page);
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
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);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -483,11 +529,15 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await waitForRAF(page);
|
||||||
await page.frames()[1].evaluate(() => {
|
await page.frames()[1].evaluate(() => {
|
||||||
console.log('from iframe');
|
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);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -504,7 +554,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
|
|
||||||
await page.waitForTimeout(50);
|
await page.waitForTimeout(50);
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -519,7 +571,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
await page.waitForSelector('img'); // wait for image to get added
|
await page.waitForSelector('img'); // wait for image to get added
|
||||||
await waitForRAF(page); // wait for image to be captured
|
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);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -534,7 +588,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
await page.waitForTimeout(50); // wait for image to get added
|
await page.waitForTimeout(50); // wait for image to get added
|
||||||
await waitForRAF(page); // wait for image to be captured
|
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);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -555,7 +611,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
await page.waitForTimeout(50); // wait for image to get added
|
await page.waitForTimeout(50); // wait for image to get added
|
||||||
await waitForRAF(page); // wait for image to be captured
|
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);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -603,7 +661,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
});
|
});
|
||||||
await page.waitForTimeout(50);
|
await page.waitForTimeout(50);
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -646,7 +706,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
});
|
});
|
||||||
await waitForRAF(page); // wait till browser sent snapshots
|
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);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -674,7 +736,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitForRAF(page); // wait for snapshot to be updated
|
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);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -706,7 +770,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
});
|
});
|
||||||
await waitForRAF(page); // wait till browser sent snapshots
|
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);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -746,7 +812,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
});
|
});
|
||||||
await waitForRAF(page); // wait till browser sent snapshots
|
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);
|
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);
|
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);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -794,7 +866,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
p.innerText = 'mutated';
|
p.innerText = 'mutated';
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,14 @@ import {
|
|||||||
styleSheetRuleData,
|
styleSheetRuleData,
|
||||||
selectionData,
|
selectionData,
|
||||||
} from '@rrweb/types';
|
} from '@rrweb/types';
|
||||||
import { assertSnapshot, launchPuppeteer, waitForRAF } from './utils';
|
import {
|
||||||
|
assertSnapshot,
|
||||||
|
getServerURL,
|
||||||
|
launchPuppeteer,
|
||||||
|
startServer,
|
||||||
|
waitForRAF,
|
||||||
|
} from './utils';
|
||||||
|
import type { Server } from 'http';
|
||||||
|
|
||||||
interface ISuite {
|
interface ISuite {
|
||||||
code: string;
|
code: string;
|
||||||
@@ -465,54 +472,63 @@ describe('record', function (this: ISuite) {
|
|||||||
|
|
||||||
it('captures mutations on adopted stylesheets', async () => {
|
it('captures mutations on adopted stylesheets', async () => {
|
||||||
await ctx.page.evaluate(() => {
|
await ctx.page.evaluate(() => {
|
||||||
document.body.innerHTML = `
|
return new Promise((resolve) => {
|
||||||
|
document.body.innerHTML = `
|
||||||
<div>div in outermost document</div>
|
<div>div in outermost document</div>
|
||||||
<iframe></iframe>
|
<iframe></iframe>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const sheet = new CSSStyleSheet();
|
const sheet = new CSSStyleSheet();
|
||||||
// Add stylesheet to a document.
|
// Add stylesheet to a document.
|
||||||
|
|
||||||
document.adoptedStyleSheets = [sheet];
|
document.adoptedStyleSheets = [sheet];
|
||||||
|
|
||||||
const iframe = document.querySelector('iframe');
|
const iframe = document.querySelector('iframe');
|
||||||
const sheet2 = new (iframe!.contentWindow! as Window &
|
const sheet2 = new (iframe!.contentWindow! as Window &
|
||||||
typeof globalThis).CSSStyleSheet();
|
typeof globalThis).CSSStyleSheet();
|
||||||
|
|
||||||
// Add stylesheet to an IFrame document.
|
// Add stylesheet to an IFrame document.
|
||||||
iframe!.contentDocument!.adoptedStyleSheets = [sheet2];
|
iframe!.contentDocument!.adoptedStyleSheets = [sheet2];
|
||||||
iframe!.contentDocument!.body.innerHTML = '<h1>h1 in iframe</h1>';
|
iframe!.contentDocument!.body.innerHTML = '<h1>h1 in iframe</h1>';
|
||||||
|
|
||||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||||
record({
|
record({
|
||||||
emit: ((window as unknown) as IWindow).emit,
|
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);
|
await waitForRAF(ctx.page);
|
||||||
assertSnapshot(ctx.events);
|
assertSnapshot(ctx.events);
|
||||||
@@ -602,70 +618,91 @@ describe('record', function (this: ISuite) {
|
|||||||
assertSnapshot(ctx.events);
|
assertSnapshot(ctx.events);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('captures stylesheets that are still loading', async () => {
|
describe('loading stylesheets', () => {
|
||||||
await ctx.page.evaluate(() => {
|
let server: Server;
|
||||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
let serverURL: string;
|
||||||
|
|
||||||
record({
|
beforeAll(async () => {
|
||||||
inlineStylesheet: true,
|
server = await startServer();
|
||||||
emit: ((window as unknown) as IWindow).emit,
|
serverURL = getServerURL(server);
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// `blob:` URLs are not available immediately, so we need to wait for the browser to load them
|
beforeEach(async () => {
|
||||||
await waitForRAF(ctx.page);
|
ctx.page = await ctx.browser.newPage();
|
||||||
// 'blob' URL is different in every execution so we need to remove it from the snapshot.
|
await ctx.page.goto(`${serverURL}/html/hello-world.html`);
|
||||||
const filteredEvents = JSON.parse(
|
await ctx.page.evaluate(ctx.code);
|
||||||
JSON.stringify(ctx.events).replace(/blob\:[\w\d-/]+"/, 'blob:null"'),
|
ctx.events = [];
|
||||||
);
|
await ctx.page.exposeFunction('emit', (e: eventWithTime) => {
|
||||||
assertSnapshot(filteredEvents);
|
if (
|
||||||
});
|
e.type === EventType.DomContentLoaded ||
|
||||||
|
e.type === EventType.Load
|
||||||
it('captures stylesheets in iframes that are still loading', async () => {
|
) {
|
||||||
await ctx.page.evaluate(() => {
|
return;
|
||||||
const iframe = document.createElement('iframe');
|
}
|
||||||
iframe.setAttribute('src', 'about:blank');
|
ctx.events.push(e);
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const linkEl = document.createElement('link');
|
ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
|
||||||
linkEl.setAttribute('rel', 'stylesheet');
|
|
||||||
linkEl.setAttribute(
|
|
||||||
'href',
|
|
||||||
URL.createObjectURL(
|
|
||||||
new Blob(['body { color: pink; }'], {
|
|
||||||
type: 'text/css',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
iframeDoc.head.appendChild(linkEl);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// `blob:` URLs are not available immediately, so we need to wait for the browser to load them
|
afterAll(async () => {
|
||||||
await waitForRAF(ctx.page);
|
await server.close();
|
||||||
const filteredEvents = JSON.parse(
|
});
|
||||||
JSON.stringify(ctx.events).replace(/blob\:[\w\d-/]+"/, 'blob:null"'),
|
|
||||||
);
|
it('captures stylesheets that are still loading', async () => {
|
||||||
assertSnapshot(filteredEvents);
|
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 () => {
|
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 () => {
|
it('captures adopted stylesheets in shadow doms and iframe', async () => {
|
||||||
await ctx.page.evaluate(() => {
|
await ctx.page.evaluate(() => {
|
||||||
document.body.innerHTML = `
|
return new Promise((resolve) => {
|
||||||
|
document.body.innerHTML = `
|
||||||
<div>div in outermost document</div>
|
<div>div in outermost document</div>
|
||||||
<div id="shadow-host1"></div>
|
<div id="shadow-host1"></div>
|
||||||
<div id="shadow-host2"></div>
|
<div id="shadow-host2"></div>
|
||||||
<iframe></iframe>
|
<iframe></iframe>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const sheet = new CSSStyleSheet();
|
const sheet = new CSSStyleSheet();
|
||||||
sheet.replaceSync!(
|
sheet.replaceSync!(
|
||||||
'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}',
|
'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}',
|
||||||
);
|
);
|
||||||
// Add stylesheet to a document.
|
// Add stylesheet to a document.
|
||||||
|
|
||||||
document.adoptedStyleSheets = [sheet];
|
document.adoptedStyleSheets = [sheet];
|
||||||
|
|
||||||
// Add stylesheet to a shadow host.
|
// Add stylesheet to a shadow host.
|
||||||
const host = document.querySelector('#shadow-host1');
|
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');
|
|
||||||
const shadow = host!.attachShadow({ mode: 'open' });
|
const shadow = host!.attachShadow({ mode: 'open' });
|
||||||
shadow.innerHTML =
|
shadow.innerHTML =
|
||||||
'<div>div in shadow dom 2</div><span>span in shadow dom 2</span>';
|
'<div>div in shadow dom 1</div><span>span in shadow dom 1</span>';
|
||||||
const sheet4 = new CSSStyleSheet();
|
const sheet2 = new CSSStyleSheet();
|
||||||
sheet4.replaceSync!('span { color: green; }');
|
|
||||||
shadow.adoptedStyleSheets = [sheet, sheet4];
|
|
||||||
|
|
||||||
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();
|
typeof globalThis).CSSStyleSheet();
|
||||||
sheet5.replaceSync!('h2 { color: purple; }');
|
sheet3.replaceSync!('h1 { color: blue; }');
|
||||||
iframe!.contentDocument!.adoptedStyleSheets = [sheet5, sheet3];
|
|
||||||
}, 10);
|
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
|
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,
|
null,
|
||||||
2,
|
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;
|
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> = {
|
export type RecordPlugin<TOptions = unknown> = {
|
||||||
name: string;
|
name: string;
|
||||||
observer?: (
|
observer?: (
|
||||||
@@ -224,7 +241,11 @@ export type RecordPlugin<TOptions = unknown> = {
|
|||||||
options: TOptions,
|
options: TOptions,
|
||||||
) => listenerHandler;
|
) => listenerHandler;
|
||||||
eventProcessor?: <TExtend>(event: eventWithTime) => eventWithTime & TExtend;
|
eventProcessor?: <TExtend>(event: eventWithTime) => eventWithTime & TExtend;
|
||||||
getMirror?: (mirror: Mirror) => void;
|
getMirror?: (mirrors: {
|
||||||
|
nodeMirror: Mirror;
|
||||||
|
crossOriginIframeMirror: ICrossOriginIframeMirror;
|
||||||
|
crossOriginIframeStyleMirror: ICrossOriginIframeMirror;
|
||||||
|
}) => void;
|
||||||
options: TOptions;
|
options: TOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -636,3 +657,16 @@ declare global {
|
|||||||
export type IWindow = Window & typeof globalThis;
|
export type IWindow = Window & typeof globalThis;
|
||||||
|
|
||||||
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
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>]
|
||||||
|
>;
|
||||||
|
|||||||
135
yarn.lock
135
yarn.lock
@@ -2188,10 +2188,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/istanbul-lib-report" "*"
|
"@types/istanbul-lib-report" "*"
|
||||||
|
|
||||||
"@types/jest-image-snapshot@^4.3.1":
|
"@types/jest-image-snapshot@^5.1.0":
|
||||||
version "4.3.1"
|
version "5.1.0"
|
||||||
resolved "https://registry.npmjs.org/@types/jest-image-snapshot/-/jest-image-snapshot-4.3.1.tgz"
|
resolved "https://registry.yarnpkg.com/@types/jest-image-snapshot/-/jest-image-snapshot-5.1.0.tgz#aa355ec40625fcb338fd31c935791bc8fde72bcf"
|
||||||
integrity sha512-WDdUruGF14C53axe/mNDgQP2YIhtcwXrwmmVP8eOGyfNTVD+FbxWjWR7RTU+lzEy4K6V6+z7nkVDm/auI/r3xQ==
|
integrity sha512-pfCz6dclA8mDxwXN/x/PuYBCPwzGuYcTfOVZvDAikC1GLXg/CwECF9UgtWse0tR42c6OaL7LPEnN7i/Dm86KkQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/jest" "*"
|
"@types/jest" "*"
|
||||||
"@types/pixelmatch" "*"
|
"@types/pixelmatch" "*"
|
||||||
@@ -4159,7 +4159,7 @@ debug@2.6.9, debug@^2.6.9:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
ms "2.0.0"
|
||||||
|
|
||||||
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2:
|
debug@4, debug@4.3.2, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2:
|
||||||
version "4.3.2"
|
version "4.3.2"
|
||||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz"
|
resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz"
|
||||||
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
|
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
|
||||||
@@ -4287,6 +4287,11 @@ devtools-protocol@0.0.869402:
|
|||||||
resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz"
|
resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz"
|
||||||
integrity sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==
|
integrity sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==
|
||||||
|
|
||||||
|
devtools-protocol@0.0.901419:
|
||||||
|
version "0.0.901419"
|
||||||
|
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd"
|
||||||
|
integrity sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==
|
||||||
|
|
||||||
dezalgo@^1.0.0:
|
dezalgo@^1.0.0:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz"
|
resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz"
|
||||||
@@ -4909,10 +4914,10 @@ eslint-plugin-compat@^4.0.2:
|
|||||||
lodash.memoize "4.1.2"
|
lodash.memoize "4.1.2"
|
||||||
semver "7.3.5"
|
semver "7.3.5"
|
||||||
|
|
||||||
eslint-plugin-jest@^26.5.3:
|
eslint-plugin-jest@^27.1.3:
|
||||||
version "26.5.3"
|
version "27.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-26.5.3.tgz#a3ceeaf4a757878342b8b00eca92379b246e5505"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.1.3.tgz#9f359eeac0c720a825f658e7e261a9eef869dc8d"
|
||||||
integrity sha512-sICclUqJQnR1bFRZGLN2jnSVsYOsmPYYnroGCIMVSvTS3y8XR3yjzy1EcTQmk6typ5pRgyIWzbjqxK6cZHEZuQ==
|
integrity sha512-7DrIfYRQPa7JQd1Le8G/BJsfYHVUKQdJQ/6vULSp/4NjKZmSMJ/605G2hhScEra++SiH68zPEjLnrO74nHrMLg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/utils" "^5.10.0"
|
"@typescript-eslint/utils" "^5.10.0"
|
||||||
|
|
||||||
@@ -5225,17 +5230,7 @@ extglob@^0.3.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-extglob "^1.0.0"
|
is-extglob "^1.0.0"
|
||||||
|
|
||||||
extract-zip@^1.6.6:
|
extract-zip@2.0.1, extract-zip@^2.0.0:
|
||||||
version "1.7.0"
|
|
||||||
resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz"
|
|
||||||
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
|
|
||||||
dependencies:
|
|
||||||
concat-stream "^1.6.2"
|
|
||||||
debug "^2.6.9"
|
|
||||||
mkdirp "^0.5.4"
|
|
||||||
yauzl "^2.10.0"
|
|
||||||
|
|
||||||
extract-zip@^2.0.0:
|
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz"
|
||||||
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
|
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
|
||||||
@@ -5246,6 +5241,16 @@ extract-zip@^2.0.0:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@types/yauzl" "^2.9.1"
|
"@types/yauzl" "^2.9.1"
|
||||||
|
|
||||||
|
extract-zip@^1.6.6:
|
||||||
|
version "1.7.0"
|
||||||
|
resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz"
|
||||||
|
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
|
||||||
|
dependencies:
|
||||||
|
concat-stream "^1.6.2"
|
||||||
|
debug "^2.6.9"
|
||||||
|
mkdirp "^0.5.4"
|
||||||
|
yauzl "^2.10.0"
|
||||||
|
|
||||||
extsprintf@1.3.0:
|
extsprintf@1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz"
|
resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz"
|
||||||
@@ -6066,6 +6071,14 @@ http-signature@~1.2.0:
|
|||||||
jsprim "^1.2.2"
|
jsprim "^1.2.2"
|
||||||
sshpk "^1.7.0"
|
sshpk "^1.7.0"
|
||||||
|
|
||||||
|
https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz"
|
||||||
|
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
|
||||||
|
dependencies:
|
||||||
|
agent-base "6"
|
||||||
|
debug "4"
|
||||||
|
|
||||||
https-proxy-agent@^2.2.1:
|
https-proxy-agent@^2.2.1:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz"
|
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz"
|
||||||
@@ -6074,14 +6087,6 @@ https-proxy-agent@^2.2.1:
|
|||||||
agent-base "^4.3.0"
|
agent-base "^4.3.0"
|
||||||
debug "^3.1.0"
|
debug "^3.1.0"
|
||||||
|
|
||||||
https-proxy-agent@^5.0.0:
|
|
||||||
version "5.0.0"
|
|
||||||
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz"
|
|
||||||
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
|
|
||||||
dependencies:
|
|
||||||
agent-base "6"
|
|
||||||
debug "4"
|
|
||||||
|
|
||||||
human-signals@^2.1.0:
|
human-signals@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz"
|
||||||
@@ -7090,10 +7095,10 @@ jest-haste-map@^27.5.1:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "^2.3.2"
|
fsevents "^2.3.2"
|
||||||
|
|
||||||
jest-image-snapshot@^4.5.1:
|
jest-image-snapshot@^5.2.0:
|
||||||
version "4.5.1"
|
version "5.2.0"
|
||||||
resolved "https://registry.npmjs.org/jest-image-snapshot/-/jest-image-snapshot-4.5.1.tgz"
|
resolved "https://registry.yarnpkg.com/jest-image-snapshot/-/jest-image-snapshot-5.2.0.tgz#4af046935b465f0460aa73e890717bbc25d431e9"
|
||||||
integrity sha512-0YkgupgkkCx0wIZkxvqs/oNiUT0X0d2WTpUhaAp+Dy6CpqBUZMRTIZo4KR1f+dqmx6WXrLCvecjnHLIsLkI+gQ==
|
integrity sha512-msKQqsxr4ZS8S3FQ6ot1SPlDKc4pCfyKY3SxU9LEoASj1zoEfglDYjmxNX53pxpNf7Fp7CJZvwP4xkNXVQgEXA==
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk "^1.1.3"
|
chalk "^1.1.3"
|
||||||
get-stdin "^5.0.1"
|
get-stdin "^5.0.1"
|
||||||
@@ -8575,6 +8580,13 @@ nice-try@^1.0.4:
|
|||||||
resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz"
|
resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz"
|
||||||
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
||||||
|
|
||||||
|
node-fetch@2.6.5:
|
||||||
|
version "2.6.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
|
||||||
|
integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
|
||||||
|
dependencies:
|
||||||
|
whatwg-url "^5.0.0"
|
||||||
|
|
||||||
node-fetch@^2.6.1:
|
node-fetch@^2.6.1:
|
||||||
version "2.6.7"
|
version "2.6.7"
|
||||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz"
|
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz"
|
||||||
@@ -9331,6 +9343,13 @@ pixelmatch@^5.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
pngjs "^4.0.1"
|
pngjs "^4.0.1"
|
||||||
|
|
||||||
|
pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz"
|
||||||
|
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
|
||||||
|
dependencies:
|
||||||
|
find-up "^4.0.0"
|
||||||
|
|
||||||
"pkg-dir@< 6 >= 5":
|
"pkg-dir@< 6 >= 5":
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz"
|
resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz"
|
||||||
@@ -9338,13 +9357,6 @@ pixelmatch@^5.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
find-up "^5.0.0"
|
find-up "^5.0.0"
|
||||||
|
|
||||||
pkg-dir@^4.1.0, pkg-dir@^4.2.0:
|
|
||||||
version "4.2.0"
|
|
||||||
resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz"
|
|
||||||
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
|
|
||||||
dependencies:
|
|
||||||
find-up "^4.0.0"
|
|
||||||
|
|
||||||
pngjs@^3.4.0:
|
pngjs@^3.4.0:
|
||||||
version "3.4.0"
|
version "3.4.0"
|
||||||
resolved "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz"
|
resolved "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz"
|
||||||
@@ -9790,7 +9802,7 @@ process-nextick-args@~2.0.0:
|
|||||||
resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz"
|
||||||
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
|
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
|
||||||
|
|
||||||
progress@^2.0.1:
|
progress@2.0.3, progress@^2.0.1:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz"
|
resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz"
|
||||||
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
||||||
@@ -9851,7 +9863,7 @@ proxy-addr@~2.0.5:
|
|||||||
forwarded "0.2.0"
|
forwarded "0.2.0"
|
||||||
ipaddr.js "1.9.1"
|
ipaddr.js "1.9.1"
|
||||||
|
|
||||||
proxy-from-env@^1.0.0, proxy-from-env@^1.1.0:
|
proxy-from-env@1.1.0, proxy-from-env@^1.0.0, proxy-from-env@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
||||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
@@ -9888,6 +9900,24 @@ puppeteer@^1.15.0:
|
|||||||
rimraf "^2.6.1"
|
rimraf "^2.6.1"
|
||||||
ws "^6.1.0"
|
ws "^6.1.0"
|
||||||
|
|
||||||
|
puppeteer@^11.0.0:
|
||||||
|
version "11.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-11.0.0.tgz#0808719c38e15315ecc1b1c28911f1c9054d201f"
|
||||||
|
integrity sha512-6rPFqN1ABjn4shgOICGDBITTRV09EjXVqhDERBDKwCLz0UyBxeeBH6Ay0vQUJ84VACmlxwzOIzVEJXThcF3aNg==
|
||||||
|
dependencies:
|
||||||
|
debug "4.3.2"
|
||||||
|
devtools-protocol "0.0.901419"
|
||||||
|
extract-zip "2.0.1"
|
||||||
|
https-proxy-agent "5.0.0"
|
||||||
|
node-fetch "2.6.5"
|
||||||
|
pkg-dir "4.2.0"
|
||||||
|
progress "2.0.3"
|
||||||
|
proxy-from-env "1.1.0"
|
||||||
|
rimraf "3.0.2"
|
||||||
|
tar-fs "2.1.1"
|
||||||
|
unbzip2-stream "1.4.3"
|
||||||
|
ws "8.2.3"
|
||||||
|
|
||||||
puppeteer@^9.1.1:
|
puppeteer@^9.1.1:
|
||||||
version "9.1.1"
|
version "9.1.1"
|
||||||
resolved "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz"
|
resolved "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz"
|
||||||
@@ -10298,6 +10328,13 @@ rgba-regex@^1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz"
|
||||||
integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
|
integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
|
||||||
|
|
||||||
|
rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz"
|
||||||
|
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||||
|
dependencies:
|
||||||
|
glob "^7.1.3"
|
||||||
|
|
||||||
rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
|
rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
|
||||||
version "2.7.1"
|
version "2.7.1"
|
||||||
resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz"
|
resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz"
|
||||||
@@ -10305,13 +10342,6 @@ rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
rimraf@^3.0.0, rimraf@^3.0.2:
|
|
||||||
version "3.0.2"
|
|
||||||
resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz"
|
|
||||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
|
||||||
dependencies:
|
|
||||||
glob "^7.1.3"
|
|
||||||
|
|
||||||
rollup-plugin-css-only@^3.1.0:
|
rollup-plugin-css-only@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz"
|
resolved "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz"
|
||||||
@@ -11147,7 +11177,7 @@ symbol-tree@^3.2.4:
|
|||||||
resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz"
|
resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz"
|
||||||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
||||||
|
|
||||||
tar-fs@^2.0.0:
|
tar-fs@2.1.1, tar-fs@^2.0.0:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz"
|
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz"
|
||||||
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
|
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
|
||||||
@@ -11677,7 +11707,7 @@ unbox-primitive@^1.0.1:
|
|||||||
has-symbols "^1.0.2"
|
has-symbols "^1.0.2"
|
||||||
which-boxed-primitive "^1.0.2"
|
which-boxed-primitive "^1.0.2"
|
||||||
|
|
||||||
unbzip2-stream@^1.3.3:
|
unbzip2-stream@1.4.3, unbzip2-stream@^1.3.3:
|
||||||
version "1.4.3"
|
version "1.4.3"
|
||||||
resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz"
|
resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz"
|
||||||
integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
|
integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
|
||||||
@@ -12078,6 +12108,11 @@ write-pkg@^4.0.0:
|
|||||||
type-fest "^0.4.1"
|
type-fest "^0.4.1"
|
||||||
write-json-file "^3.2.0"
|
write-json-file "^3.2.0"
|
||||||
|
|
||||||
|
ws@8.2.3:
|
||||||
|
version "8.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
|
||||||
|
integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
|
||||||
|
|
||||||
ws@^6.1.0:
|
ws@^6.1.0:
|
||||||
version "6.2.2"
|
version "6.2.2"
|
||||||
resolved "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz"
|
resolved "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user