Canvas recording: Preserve drawing buffer (#1273)
* Upgrade jest to 29 and puppeteer to 16 in rrweb * Apply formatting changes * Upgrade rrweb's puppeteer to v20 * Apply formatting changes * Canvas: Reduce flickering and capturing of empty canvas elements Turn on `preserveDrawingBuffer` by default for canvas FPS recording. Has some negative performance implications, but really helps when capturing canvas. * Apply formatting changes * Include all test image snapshots in ci * Apply formatting changes * Allow more flexibility when capturing hover * Apply formatting changes * Create tiny-chairs-build.md * Apply formatting changes * Update hover.test.ts * Apply formatting changes * Document snapshotFormat jest config * Freeze `yarn.lock` in ci for reproducible dependencies * Apply formatting changes * Apply formatting changes * Revert to old style of puppeteer evaluation script notation * Apply formatting changes * Make test less flaky * Apply formatting changes * Apply formatting changes * Make tests less flaky * Apply formatting changes * Make test more robust * Apply formatting changes * Apply formatting changes * Add debugging code for test * Apply formatting changes * Also test not ignored input * Apply formatting changes * Apply formatting changes * Apply formatting changes * escape ignoreSelector * Apply formatting changes * Apply formatting changes
This commit is contained in:
7
.changeset/tiny-chairs-build.md
Normal file
7
.changeset/tiny-chairs-build.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'rrweb': patch
|
||||
---
|
||||
|
||||
Canvas FPS recording: override `preserveDrawingBuffer: true` on canvas creation.
|
||||
Canvas replay: fix flickering canvas elemenrs.
|
||||
Canvas FPS recording: fix bug that wipes webgl(2) canvas backgrounds while recording.
|
||||
4
.github/workflows/ci-cd.yml
vendored
4
.github/workflows/ci-cd.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build Project
|
||||
run: NODE_OPTIONS='--max-old-space-size=4096' yarn build:all
|
||||
@@ -37,5 +37,5 @@ jobs:
|
||||
if: failure()
|
||||
with:
|
||||
name: image-diff
|
||||
path: packages/rrweb/test/e2e/__image_snapshots__/__diff_output__/*.png
|
||||
path: packages/rrweb/test/*/__image_snapshots__/__diff_output__/*.png
|
||||
if-no-files-found: ignore
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
with:
|
||||
publish: yarn run release
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
|
||||
2
.github/workflows/style-check.yml
vendored
2
.github/workflows/style-check.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
node-version: 16
|
||||
cache: 'yarn'
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build Packages
|
||||
run: NODE_OPTIONS='--max-old-space-size=4096' yarn build:all
|
||||
- name: Eslint Check
|
||||
|
||||
@@ -6,4 +6,13 @@ export default {
|
||||
moduleNameMapper: {
|
||||
'\\.css$': 'identity-obj-proxy',
|
||||
},
|
||||
/**
|
||||
* Keeps old (pre-jest 29) snapshot format
|
||||
* its a bit ugly and harder to read than the new format,
|
||||
* so we might want to remove this in its own PR
|
||||
*/
|
||||
snapshotFormat: {
|
||||
escapeString: true,
|
||||
printBasicPrototype: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -53,11 +53,10 @@
|
||||
"@types/chai": "^4.1.6",
|
||||
"@types/dom-mediacapture-transform": "^0.1.3",
|
||||
"@types/inquirer": "^8.2.1",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/jest-image-snapshot": "^5.1.0",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/jest-image-snapshot": "^6.1.0",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/offscreencanvas": "^2019.6.4",
|
||||
"@types/puppeteer": "^5.4.4",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"cross-env": "^5.2.0",
|
||||
"esbuild": "^0.14.38",
|
||||
@@ -65,10 +64,11 @@
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"ignore-styles": "^5.0.1",
|
||||
"inquirer": "^9.0.0",
|
||||
"jest": "^27.5.1",
|
||||
"jest-image-snapshot": "^5.2.0",
|
||||
"jest-snapshot": "^23.6.0",
|
||||
"puppeteer": "^11.0.0",
|
||||
"jest": "^29.6.0",
|
||||
"jest-environment-jsdom": "^29.6.0",
|
||||
"jest-image-snapshot": "^6.2.0",
|
||||
"jest-snapshot": "^29.6.2",
|
||||
"puppeteer": "^20.9.0",
|
||||
"rollup": "^2.68.0",
|
||||
"rollup-plugin-esbuild": "^4.9.1",
|
||||
"rollup-plugin-postcss": "^3.1.1",
|
||||
@@ -76,7 +76,7 @@
|
||||
"rollup-plugin-typescript2": "^0.31.2",
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"simple-peer-light": "^9.10.0",
|
||||
"ts-jest": "^27.1.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.3.1"
|
||||
},
|
||||
|
||||
@@ -114,6 +114,7 @@ export class CanvasManager {
|
||||
win,
|
||||
blockClass,
|
||||
blockSelector,
|
||||
true,
|
||||
);
|
||||
const snapshotInProgressMap: Map<number, boolean> = new Map();
|
||||
const worker =
|
||||
@@ -198,9 +199,12 @@ export class CanvasManager {
|
||||
) {
|
||||
// Hack to load canvas back into memory so `createImageBitmap` can grab it's contents.
|
||||
// Context: https://twitter.com/Juice10/status/1499775271758704643
|
||||
// This hack might change the background color of the canvas in the unlikely event that
|
||||
// Preferably we set `preserveDrawingBuffer` to true, but that's not always possible,
|
||||
// especially when canvas is loaded before rrweb.
|
||||
// This hack can wipe the background color of the canvas in the (unlikely) event that
|
||||
// the canvas background was changed but clear was not called directly afterwards.
|
||||
context?.clear(context.COLOR_BUFFER_BIT);
|
||||
// Example of this hack having negative side effect: https://visgl.github.io/react-map-gl/examples/layers
|
||||
context.clear(context.COLOR_BUFFER_BIT);
|
||||
}
|
||||
}
|
||||
const bitmap = await createImageBitmap(canvas);
|
||||
@@ -238,6 +242,7 @@ export class CanvasManager {
|
||||
win,
|
||||
blockClass,
|
||||
blockSelector,
|
||||
false,
|
||||
);
|
||||
const canvas2DReset = initCanvas2DMutationObserver(
|
||||
this.processMutation.bind(this),
|
||||
|
||||
@@ -2,10 +2,15 @@ import type { ICanvas } from 'rrweb-snapshot';
|
||||
import type { blockClass, IWindow, listenerHandler } from '@rrweb/types';
|
||||
import { isBlocked, patch } from '../../../utils';
|
||||
|
||||
function getNormalizedContextName(contextType: string) {
|
||||
return contextType === 'experimental-webgl' ? 'webgl' : contextType;
|
||||
}
|
||||
|
||||
export default function initCanvasContextObserver(
|
||||
win: IWindow,
|
||||
blockClass: blockClass,
|
||||
blockSelector: string | null,
|
||||
setPreserveDrawingBufferToTrue: boolean,
|
||||
): listenerHandler {
|
||||
const handlers: listenerHandler[] = [];
|
||||
try {
|
||||
@@ -25,7 +30,24 @@ export default function initCanvasContextObserver(
|
||||
...args: Array<unknown>
|
||||
) {
|
||||
if (!isBlocked(this, blockClass, blockSelector, true)) {
|
||||
if (!('__context' in this)) this.__context = contextType;
|
||||
const ctxName = getNormalizedContextName(contextType);
|
||||
if (!('__context' in this)) this.__context = ctxName;
|
||||
|
||||
if (
|
||||
setPreserveDrawingBufferToTrue &&
|
||||
['webgl', 'webgl2'].includes(ctxName)
|
||||
) {
|
||||
if (args[0] && typeof args[0] === 'object') {
|
||||
const contextAttributes = args[0] as WebGLContextAttributes;
|
||||
if (!contextAttributes.preserveDrawingBuffer) {
|
||||
contextAttributes.preserveDrawingBuffer = true;
|
||||
}
|
||||
} else {
|
||||
args.splice(0, 1, {
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return original.apply(this, [contextType, ...args]);
|
||||
};
|
||||
|
||||
@@ -4,48 +4,63 @@ import { deserializeArg } from './deserialize-args';
|
||||
|
||||
export default async function canvasMutation({
|
||||
event,
|
||||
mutation,
|
||||
mutations,
|
||||
target,
|
||||
imageMap,
|
||||
errorHandler,
|
||||
}: {
|
||||
event: Parameters<Replayer['applyIncremental']>[0];
|
||||
mutation: canvasMutationCommand;
|
||||
mutations: canvasMutationCommand[];
|
||||
target: HTMLCanvasElement;
|
||||
imageMap: Replayer['imageMap'];
|
||||
errorHandler: Replayer['warnCanvasMutationFailed'];
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const ctx = target.getContext('2d')!;
|
||||
const ctx = target.getContext('2d');
|
||||
|
||||
if (mutation.setter) {
|
||||
// skip some read-only type checks
|
||||
(ctx as unknown as Record<string, unknown>)[mutation.property] =
|
||||
mutation.args[0];
|
||||
return;
|
||||
}
|
||||
const original = ctx[
|
||||
mutation.property as Exclude<keyof typeof ctx, 'canvas'>
|
||||
] as (ctx: CanvasRenderingContext2D, args: unknown[]) => void;
|
||||
|
||||
/**
|
||||
* We have serialized the image source into base64 string during recording,
|
||||
* which has been preloaded before replay.
|
||||
* So we can get call drawImage SYNCHRONOUSLY which avoid some fragile cast.
|
||||
*/
|
||||
if (
|
||||
mutation.property === 'drawImage' &&
|
||||
typeof mutation.args[0] === 'string'
|
||||
) {
|
||||
imageMap.get(event);
|
||||
original.apply(ctx, mutation.args);
|
||||
} else {
|
||||
const args = await Promise.all(
|
||||
mutation.args.map(deserializeArg(imageMap, ctx)),
|
||||
);
|
||||
original.apply(ctx, args);
|
||||
}
|
||||
} catch (error) {
|
||||
errorHandler(mutation, error);
|
||||
if (!ctx) {
|
||||
errorHandler(mutations[0], new Error('Canvas context is null'));
|
||||
return;
|
||||
}
|
||||
|
||||
// step 1, deserialize args, they may be async
|
||||
const mutationArgsPromises = mutations.map(
|
||||
async (mutation: canvasMutationCommand): Promise<unknown[]> => {
|
||||
return Promise.all(mutation.args.map(deserializeArg(imageMap, ctx)));
|
||||
},
|
||||
);
|
||||
const args = await Promise.all(mutationArgsPromises);
|
||||
// step 2 apply all mutations
|
||||
args.forEach((args, index) => {
|
||||
const mutation = mutations[index];
|
||||
try {
|
||||
if (mutation.setter) {
|
||||
// skip some read-only type checks
|
||||
(ctx as unknown as Record<string, unknown>)[mutation.property] =
|
||||
mutation.args[0];
|
||||
return;
|
||||
}
|
||||
const original = ctx[
|
||||
mutation.property as Exclude<keyof typeof ctx, 'canvas'>
|
||||
] as (ctx: CanvasRenderingContext2D, args: unknown[]) => void;
|
||||
|
||||
/**
|
||||
* We have serialized the image source into base64 string during recording,
|
||||
* which has been preloaded before replay.
|
||||
* So we can get call drawImage SYNCHRONOUSLY which avoid some fragile cast.
|
||||
*/
|
||||
if (
|
||||
mutation.property === 'drawImage' &&
|
||||
typeof mutation.args[0] === 'string'
|
||||
) {
|
||||
imageMap.get(event);
|
||||
original.apply(ctx, mutation.args);
|
||||
} else {
|
||||
original.apply(ctx, args);
|
||||
}
|
||||
} catch (error) {
|
||||
errorHandler(mutation, error);
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,16 +46,13 @@ export default async function canvasMutation({
|
||||
return;
|
||||
}
|
||||
// default is '2d' for backwards compatibility (rrweb below 1.1.x)
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const command = commands[i];
|
||||
await canvas2DMutation({
|
||||
event,
|
||||
mutation: command,
|
||||
target,
|
||||
imageMap,
|
||||
errorHandler,
|
||||
});
|
||||
}
|
||||
await canvas2DMutation({
|
||||
event,
|
||||
mutations: commands,
|
||||
target,
|
||||
imageMap,
|
||||
errorHandler,
|
||||
});
|
||||
} catch (error) {
|
||||
errorHandler(mutation, error);
|
||||
}
|
||||
|
||||
@@ -8712,13 +8712,11 @@ exports[`record integration tests should not record input events on ignored elem
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"label\\",
|
||||
\\"attributes\\": {
|
||||
\\"for\\": \\"ignore text\\"
|
||||
},
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\" \\",
|
||||
\\"textContent\\": \\"Input ignored here: \\",
|
||||
\\"id\\": 21
|
||||
},
|
||||
{
|
||||
@@ -8733,7 +8731,7 @@ exports[`record integration tests should not record input events on ignored elem
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\" \\",
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 23
|
||||
}
|
||||
],
|
||||
@@ -8741,8 +8739,74 @@ exports[`record integration tests should not record input events on ignored elem
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 24
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"label\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"Input ignored by selector here: \\",
|
||||
\\"id\\": 26
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"input\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"text\\",
|
||||
\\"data-rr-ignore\\": \\"\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 27
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 28
|
||||
}
|
||||
],
|
||||
\\"id\\": 25
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 29
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"label\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"Input not ignored here: \\",
|
||||
\\"id\\": 31
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"input\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"text\\",
|
||||
\\"class\\": \\"dont-ignore\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 32
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 33
|
||||
}
|
||||
],
|
||||
\\"id\\": 30
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 34
|
||||
}
|
||||
],
|
||||
\\"id\\": 18
|
||||
@@ -8750,7 +8814,7 @@ exports[`record integration tests should not record input events on ignored elem
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\\\n \\",
|
||||
\\"id\\": 25
|
||||
\\"id\\": 35
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
@@ -8760,15 +8824,15 @@ exports[`record integration tests should not record input events on ignored elem
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||
\\"id\\": 27
|
||||
\\"id\\": 37
|
||||
}
|
||||
],
|
||||
\\"id\\": 26
|
||||
\\"id\\": 36
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
||||
\\"id\\": 28
|
||||
\\"id\\": 38
|
||||
}
|
||||
],
|
||||
\\"id\\": 16
|
||||
@@ -8792,6 +8856,128 @@ exports[`record integration tests should not record input events on ignored elem
|
||||
\\"type\\": 5,
|
||||
\\"id\\": 22
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 6,
|
||||
\\"id\\": 22
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 5,
|
||||
\\"id\\": 27
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 6,
|
||||
\\"id\\": 27
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 2,
|
||||
\\"type\\": 5,
|
||||
\\"id\\": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 5,
|
||||
\\"text\\": \\"n\\",
|
||||
\\"isChecked\\": false,
|
||||
\\"id\\": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 5,
|
||||
\\"text\\": \\"no\\",
|
||||
\\"isChecked\\": false,
|
||||
\\"id\\": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 5,
|
||||
\\"text\\": \\"not\\",
|
||||
\\"isChecked\\": false,
|
||||
\\"id\\": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 5,
|
||||
\\"text\\": \\"not \\",
|
||||
\\"isChecked\\": false,
|
||||
\\"id\\": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 5,
|
||||
\\"text\\": \\"not s\\",
|
||||
\\"isChecked\\": false,
|
||||
\\"id\\": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 5,
|
||||
\\"text\\": \\"not se\\",
|
||||
\\"isChecked\\": false,
|
||||
\\"id\\": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 5,
|
||||
\\"text\\": \\"not sec\\",
|
||||
\\"isChecked\\": false,
|
||||
\\"id\\": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 5,
|
||||
\\"text\\": \\"not secr\\",
|
||||
\\"isChecked\\": false,
|
||||
\\"id\\": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 5,
|
||||
\\"text\\": \\"not secre\\",
|
||||
\\"isChecked\\": false,
|
||||
\\"id\\": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 5,
|
||||
\\"text\\": \\"not secret\\",
|
||||
\\"isChecked\\": false,
|
||||
\\"id\\": 32
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(200, 100);
|
||||
ctx.stroke();
|
||||
window.canvasMutationApplied = true;
|
||||
}, 10);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -9,8 +9,15 @@
|
||||
|
||||
<body>
|
||||
<form>
|
||||
<label for="ignore text"> <input type="text" class="rr-ignore" /> </label>
|
||||
<label for="ignore selector"> <input type="text" data-rr-ignore /> </label>
|
||||
<label
|
||||
>Input ignored here: <input type="text" class="rr-ignore" />
|
||||
</label>
|
||||
<label
|
||||
>Input ignored by selector here: <input type="text" data-rr-ignore />
|
||||
</label>
|
||||
<label
|
||||
>Input not ignored here: <input type="text" class="dont-ignore" />
|
||||
</label>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getServerURL,
|
||||
launchPuppeteer,
|
||||
waitForRAF,
|
||||
waitForIFrameLoad,
|
||||
replaceLast,
|
||||
generateRecordSnippet,
|
||||
ISuite,
|
||||
@@ -71,7 +72,7 @@ describe('record integration tests', function (this: ISuite) {
|
||||
// also tap on the span
|
||||
const span = await page.waitForSelector('span');
|
||||
const center = await page.evaluate((el) => {
|
||||
const { x, y, width, height } = el.getBoundingClientRect();
|
||||
const { x, y, width, height } = el!.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.round(x + width / 2),
|
||||
y: Math.round(y + height / 2),
|
||||
@@ -81,7 +82,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
|
||||
await page.click('a');
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -186,7 +189,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
li.removeAttribute('aria-label');
|
||||
});
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
});
|
||||
|
||||
@@ -306,11 +311,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
|
||||
await page.type('.rr-ignore', 'secret');
|
||||
await page.type('[data-rr-ignore]', 'secret');
|
||||
await page.type('.dont-ignore', 'not secret');
|
||||
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
await assertSnapshot(page);
|
||||
});
|
||||
|
||||
it('should not record input values if maskAllInputs is enabled', async () => {
|
||||
@@ -547,6 +550,7 @@ describe('record integration tests', function (this: ISuite) {
|
||||
recordCanvas: true,
|
||||
}),
|
||||
);
|
||||
await page.waitForFunction('window.canvasMutationApplied');
|
||||
await waitForRAF(page);
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
@@ -581,10 +585,7 @@ describe('record integration tests', function (this: ISuite) {
|
||||
|
||||
await page.type('#input', 'moo');
|
||||
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
assertSnapshot(snapshots);
|
||||
await assertSnapshot(page);
|
||||
});
|
||||
|
||||
it('should record webgl canvas mutations', async () => {
|
||||
@@ -757,13 +758,9 @@ describe('record integration tests', function (this: ISuite) {
|
||||
await page.goto(`${serverURL}/html`);
|
||||
await page.setContent(getHtml.call(this, 'main.html'));
|
||||
|
||||
await page.waitForSelector('#two');
|
||||
const frameIdTwo = await page.frames()[2];
|
||||
await frameIdTwo.waitForSelector('#four');
|
||||
const frameIdFour = frameIdTwo.childFrames()[1];
|
||||
await frameIdFour.waitForSelector('#five');
|
||||
|
||||
await page.waitForTimeout(50);
|
||||
const frameIdTwo = await waitForIFrameLoad(page, '#two');
|
||||
const frameIdFour = await waitForIFrameLoad(frameIdTwo, '#four');
|
||||
await waitForIFrameLoad(frameIdFour, '#five');
|
||||
|
||||
const snapshots = (await page.evaluate(
|
||||
'window.snapshots',
|
||||
|
||||
@@ -37,7 +37,7 @@ exports[`cross origin iframes audio.html should emit contents of iframe once 1`]
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -202,7 +202,7 @@ exports[`cross origin iframes audio.html should emit contents of iframe once 1`]
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -370,7 +370,7 @@ exports[`cross origin iframes blank.html should filter out forwarded cross origi
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -448,7 +448,7 @@ exports[`cross origin iframes blank.html should filter out forwarded cross origi
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -544,7 +544,7 @@ exports[`cross origin iframes blank.html should filter out forwarded cross origi
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -632,7 +632,7 @@ exports[`cross origin iframes blank.html should record same-origin iframe in cro
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -710,7 +710,7 @@ exports[`cross origin iframes blank.html should record same-origin iframe in cro
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -877,7 +877,7 @@ exports[`cross origin iframes blank.html should support packFn option in record(
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -956,7 +956,7 @@ exports[`cross origin iframes blank.html should support packFn option in record(
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -1045,7 +1045,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] =
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -1211,7 +1211,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] =
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -2042,7 +2042,7 @@ exports[`cross origin iframes form.html should map scroll events correctly 1`] =
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -2208,7 +2208,7 @@ exports[`cross origin iframes form.html should map scroll events correctly 1`] =
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -2682,7 +2682,7 @@ exports[`cross origin iframes move-node.html captures mutations on adopted style
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -2768,7 +2768,7 @@ exports[`cross origin iframes move-node.html captures mutations on adopted style
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -3096,7 +3096,7 @@ exports[`cross origin iframes move-node.html captures mutations on stylesheets 1
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -3182,7 +3182,7 @@ exports[`cross origin iframes move-node.html captures mutations on stylesheets 1
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -3515,7 +3515,7 @@ exports[`cross origin iframes move-node.html should record DOM attribute changes
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -3601,7 +3601,7 @@ exports[`cross origin iframes move-node.html should record DOM attribute changes
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -3803,7 +3803,7 @@ exports[`cross origin iframes move-node.html should record DOM node movement 1`]
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -3889,7 +3889,7 @@ exports[`cross origin iframes move-node.html should record DOM node movement 1`]
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -4188,7 +4188,7 @@ exports[`cross origin iframes move-node.html should record DOM node removal 1`]
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -4274,7 +4274,7 @@ exports[`cross origin iframes move-node.html should record DOM node removal 1`]
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -4474,7 +4474,7 @@ exports[`cross origin iframes move-node.html should record DOM text changes 1`]
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -4560,7 +4560,7 @@ exports[`cross origin iframes move-node.html should record DOM text changes 1`]
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -4760,7 +4760,7 @@ exports[`cross origin iframes move-node.html should record canvas elements 1`] =
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -4846,7 +4846,7 @@ exports[`cross origin iframes move-node.html should record canvas elements 1`] =
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -5083,7 +5083,7 @@ exports[`cross origin iframes move-node.html should record custom events 1`] = `
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -5169,7 +5169,7 @@ exports[`cross origin iframes move-node.html should record custom events 1`] = `
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -5365,7 +5365,7 @@ exports[`same origin iframes should emit contents of iframe once 1`] = `
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
@@ -5479,7 +5479,7 @@ exports[`same origin iframes should emit contents of iframe once 1`] = `
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"\\"
|
||||
\\"type\\": \\"text/javascript\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 11,
|
||||
|
||||
81
packages/rrweb/test/replay/2d-mutation.test.ts
Normal file
81
packages/rrweb/test/replay/2d-mutation.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { polyfillWebGLGlobals } from '../utils';
|
||||
polyfillWebGLGlobals();
|
||||
|
||||
import canvas2DMutation from '../../src/replay/canvas/2d';
|
||||
import type { Replayer } from '../../src/replay';
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
describe('canvas2DMutation', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
canvas = document.createElement('canvas');
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should execute all mutations after args are parsed', async () => {
|
||||
let resolve: (value: unknown) => void;
|
||||
const promise = new Promise((r) => {
|
||||
resolve = r;
|
||||
});
|
||||
const context = {
|
||||
clearRect: jest.fn(),
|
||||
drawImage: jest.fn(),
|
||||
} as unknown as CanvasRenderingContext2D;
|
||||
jest.spyOn(canvas, 'getContext').mockImplementation(() => {
|
||||
return context;
|
||||
});
|
||||
|
||||
const createImageBitmapMock = jest.fn(() => {
|
||||
return new Promise((r) => {
|
||||
setTimeout(r, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
(global as any).createImageBitmap = createImageBitmapMock;
|
||||
|
||||
const mutation = canvas2DMutation({
|
||||
event: {} as Parameters<Replayer['applyIncremental']>[0],
|
||||
mutations: [
|
||||
{
|
||||
property: 'clearRect',
|
||||
args: [0, 0, 1000, 1000],
|
||||
},
|
||||
{
|
||||
property: 'drawImage',
|
||||
args: [
|
||||
{
|
||||
rr_type: 'ImageBitmap',
|
||||
args: [],
|
||||
},
|
||||
0,
|
||||
0,
|
||||
],
|
||||
},
|
||||
],
|
||||
target: canvas,
|
||||
imageMap: new Map(),
|
||||
errorHandler: () => {},
|
||||
});
|
||||
|
||||
await jest.advanceTimersByTimeAsync(100);
|
||||
|
||||
await expect(createImageBitmapMock).toHaveBeenCalled();
|
||||
|
||||
expect(context.clearRect).not.toBeCalled();
|
||||
expect(context.drawImage).not.toBeCalled();
|
||||
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
|
||||
await mutation;
|
||||
|
||||
expect(context.clearRect).toHaveBeenCalledWith(0, 0, 1000, 1000);
|
||||
expect(context.drawImage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
@@ -65,7 +65,9 @@ describe('replayer', function () {
|
||||
await waitForRAF(page);
|
||||
|
||||
const image = await page.screenshot();
|
||||
expect(image).toMatchImageSnapshot();
|
||||
expect(image).toMatchImageSnapshot({
|
||||
failureThreshold: 40,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Optional,
|
||||
mouseInteractionData,
|
||||
event,
|
||||
pluginEvent,
|
||||
} from '@rrweb/types';
|
||||
import type { recordOptions } from '../src/types';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
@@ -20,7 +21,7 @@ export async function launchPuppeteer(
|
||||
options?: Parameters<(typeof puppeteer)['launch']>[0],
|
||||
) {
|
||||
return await puppeteer.launch({
|
||||
headless: process.env.PUPPETEER_HEADLESS ? true : false,
|
||||
headless: process.env.PUPPETEER_HEADLESS ? 'new' : false,
|
||||
defaultViewport: {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
@@ -108,7 +109,8 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
|
||||
.filter((s) => {
|
||||
if (
|
||||
s.type === EventType.IncrementalSnapshot &&
|
||||
s.data.source === IncrementalSource.MouseMove
|
||||
(s.data.source === IncrementalSource.MouseMove ||
|
||||
s.data.source === IncrementalSource.ViewportResize)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -195,6 +197,33 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
|
||||
if (s.data.currentTime) {
|
||||
s.data.currentTime = Math.round(s.data.currentTime * 10) / 10;
|
||||
}
|
||||
} else if (
|
||||
s.type === EventType.Plugin &&
|
||||
s.data.plugin === 'rrweb/console@1'
|
||||
) {
|
||||
const pluginPayload = (
|
||||
s as pluginEvent<{
|
||||
trace: string[];
|
||||
payload: string[];
|
||||
}>
|
||||
).data.payload;
|
||||
|
||||
if (pluginPayload?.trace.length) {
|
||||
pluginPayload.trace = pluginPayload.trace.map((trace) => {
|
||||
return trace.replace(
|
||||
/^pptr:evaluate;.*?:(\d+:\d+)/,
|
||||
'__puppeteer_evaluation_script__:$1',
|
||||
);
|
||||
});
|
||||
}
|
||||
if (pluginPayload?.payload.length) {
|
||||
pluginPayload.payload = pluginPayload.payload.map((payload) => {
|
||||
return payload.replace(
|
||||
/pptr:evaluate;.*?:(\d+:\d+)/g,
|
||||
'__puppeteer_evaluation_script__:$1',
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
delete (s as Optional<eventWithTime, 'timestamp'>).timestamp;
|
||||
return s as event;
|
||||
@@ -260,7 +289,24 @@ function stringifyDomSnapshot(mhtml: string): string {
|
||||
return newResult.map((asset) => Object.values(asset).join('\n')).join('\n\n');
|
||||
}
|
||||
|
||||
export function assertSnapshot(snapshots: eventWithTime[]) {
|
||||
export async function assertSnapshot(
|
||||
snapshotsOrPage: eventWithTime[] | puppeteer.Page,
|
||||
) {
|
||||
let snapshots: eventWithTime[];
|
||||
if (!Array.isArray(snapshotsOrPage)) {
|
||||
// make sure page has finished executing js
|
||||
await waitForRAF(snapshotsOrPage);
|
||||
await snapshotsOrPage.waitForFunction(
|
||||
'window.snapshots && window.snapshots.length > 0',
|
||||
);
|
||||
|
||||
snapshots = (await snapshotsOrPage.evaluate(
|
||||
'window.snapshots',
|
||||
)) as eventWithTime[];
|
||||
} else {
|
||||
snapshots = snapshotsOrPage;
|
||||
}
|
||||
|
||||
expect(snapshots).toBeDefined();
|
||||
expect(stringifySnapshots(snapshots)).toMatchSnapshot();
|
||||
}
|
||||
@@ -590,14 +636,59 @@ export async function waitForRAF(
|
||||
});
|
||||
}
|
||||
|
||||
export async function waitForIFrameLoad(
|
||||
page: puppeteer.Frame | puppeteer.Page,
|
||||
iframeSelector: string,
|
||||
timeout = 10000,
|
||||
): Promise<puppeteer.Frame> {
|
||||
const el = await page.waitForSelector(iframeSelector);
|
||||
if (!el)
|
||||
throw new Error('Waiting for iframe load has timed out - no element found');
|
||||
|
||||
let frame = await el.contentFrame();
|
||||
if (frame && frame.isDetached()) {
|
||||
throw new Error(
|
||||
'Waiting for iframe load has timed out - frame is detached',
|
||||
);
|
||||
}
|
||||
if (frame && frame.url() !== '') {
|
||||
return frame;
|
||||
}
|
||||
|
||||
await page.$eval(
|
||||
iframeSelector,
|
||||
(el, timeout) => {
|
||||
const p = new Promise((resolve, reject) => {
|
||||
(el as HTMLIFrameElement).onload = () => {
|
||||
resolve(el as HTMLIFrameElement);
|
||||
};
|
||||
setTimeout(() => {
|
||||
reject(
|
||||
new Error(
|
||||
'Waiting for iframe load has timed out - onload not fired',
|
||||
),
|
||||
);
|
||||
}, timeout);
|
||||
});
|
||||
return p;
|
||||
},
|
||||
timeout,
|
||||
);
|
||||
|
||||
frame = await el.contentFrame();
|
||||
if (!frame)
|
||||
throw new Error('Waiting for iframe load has timed out - no frame found');
|
||||
return frame;
|
||||
}
|
||||
|
||||
export function generateRecordSnippet(options: recordOptions<eventWithTime>) {
|
||||
return `
|
||||
window.snapshots = [];
|
||||
rrweb.record({
|
||||
emit: event => {
|
||||
if (!window.snapshots) window.snapshots = [];
|
||||
window.snapshots.push(event);
|
||||
},
|
||||
ignoreSelector: ${options.ignoreSelector},
|
||||
ignoreSelector: ${JSON.stringify(options.ignoreSelector)},
|
||||
maskTextSelector: ${JSON.stringify(options.maskTextSelector)},
|
||||
maskAllInputs: ${options.maskAllInputs},
|
||||
maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
|
||||
|
||||
Reference in New Issue
Block a user