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/*
|
node-version: lts/*
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: yarn
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build Project
|
- name: Build Project
|
||||||
run: NODE_OPTIONS='--max-old-space-size=4096' yarn build:all
|
run: NODE_OPTIONS='--max-old-space-size=4096' yarn build:all
|
||||||
@@ -37,5 +37,5 @@ jobs:
|
|||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: image-diff
|
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
|
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/*
|
node-version: lts/*
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: yarn
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: Create Release Pull Request or Publish to npm
|
- name: Create Release Pull Request or Publish to npm
|
||||||
id: changesets
|
id: changesets
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
publish: yarn run release
|
publish: yarn run release
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_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
|
node-version: 16
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: yarn
|
run: yarn install --frozen-lockfile
|
||||||
- name: Build Packages
|
- name: Build Packages
|
||||||
run: NODE_OPTIONS='--max-old-space-size=4096' yarn build:all
|
run: NODE_OPTIONS='--max-old-space-size=4096' yarn build:all
|
||||||
- name: Eslint Check
|
- name: Eslint Check
|
||||||
|
|||||||
@@ -6,4 +6,13 @@ export default {
|
|||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'\\.css$': 'identity-obj-proxy',
|
'\\.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/chai": "^4.1.6",
|
||||||
"@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": "^29.5.0",
|
||||||
"@types/jest-image-snapshot": "^5.1.0",
|
"@types/jest-image-snapshot": "^6.1.0",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
"@types/offscreencanvas": "^2019.6.4",
|
"@types/offscreencanvas": "^2019.6.4",
|
||||||
"@types/puppeteer": "^5.4.4",
|
|
||||||
"construct-style-sheets-polyfill": "^3.1.0",
|
"construct-style-sheets-polyfill": "^3.1.0",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"esbuild": "^0.14.38",
|
"esbuild": "^0.14.38",
|
||||||
@@ -65,10 +64,11 @@
|
|||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"ignore-styles": "^5.0.1",
|
"ignore-styles": "^5.0.1",
|
||||||
"inquirer": "^9.0.0",
|
"inquirer": "^9.0.0",
|
||||||
"jest": "^27.5.1",
|
"jest": "^29.6.0",
|
||||||
"jest-image-snapshot": "^5.2.0",
|
"jest-environment-jsdom": "^29.6.0",
|
||||||
"jest-snapshot": "^23.6.0",
|
"jest-image-snapshot": "^6.2.0",
|
||||||
"puppeteer": "^11.0.0",
|
"jest-snapshot": "^29.6.2",
|
||||||
|
"puppeteer": "^20.9.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",
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
"rollup-plugin-typescript2": "^0.31.2",
|
"rollup-plugin-typescript2": "^0.31.2",
|
||||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||||
"simple-peer-light": "^9.10.0",
|
"simple-peer-light": "^9.10.0",
|
||||||
"ts-jest": "^27.1.3",
|
"ts-jest": "^29.1.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tslib": "^2.3.1"
|
"tslib": "^2.3.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export class CanvasManager {
|
|||||||
win,
|
win,
|
||||||
blockClass,
|
blockClass,
|
||||||
blockSelector,
|
blockSelector,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
const snapshotInProgressMap: Map<number, boolean> = new Map();
|
const snapshotInProgressMap: Map<number, boolean> = new Map();
|
||||||
const worker =
|
const worker =
|
||||||
@@ -198,9 +199,12 @@ export class CanvasManager {
|
|||||||
) {
|
) {
|
||||||
// Hack to load canvas back into memory so `createImageBitmap` can grab it's contents.
|
// Hack to load canvas back into memory so `createImageBitmap` can grab it's contents.
|
||||||
// Context: https://twitter.com/Juice10/status/1499775271758704643
|
// 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.
|
// 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);
|
const bitmap = await createImageBitmap(canvas);
|
||||||
@@ -238,6 +242,7 @@ export class CanvasManager {
|
|||||||
win,
|
win,
|
||||||
blockClass,
|
blockClass,
|
||||||
blockSelector,
|
blockSelector,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
const canvas2DReset = initCanvas2DMutationObserver(
|
const canvas2DReset = initCanvas2DMutationObserver(
|
||||||
this.processMutation.bind(this),
|
this.processMutation.bind(this),
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ import type { ICanvas } from 'rrweb-snapshot';
|
|||||||
import type { blockClass, IWindow, listenerHandler } from '@rrweb/types';
|
import type { blockClass, IWindow, listenerHandler } from '@rrweb/types';
|
||||||
import { isBlocked, patch } from '../../../utils';
|
import { isBlocked, patch } from '../../../utils';
|
||||||
|
|
||||||
|
function getNormalizedContextName(contextType: string) {
|
||||||
|
return contextType === 'experimental-webgl' ? 'webgl' : contextType;
|
||||||
|
}
|
||||||
|
|
||||||
export default function initCanvasContextObserver(
|
export default function initCanvasContextObserver(
|
||||||
win: IWindow,
|
win: IWindow,
|
||||||
blockClass: blockClass,
|
blockClass: blockClass,
|
||||||
blockSelector: string | null,
|
blockSelector: string | null,
|
||||||
|
setPreserveDrawingBufferToTrue: boolean,
|
||||||
): listenerHandler {
|
): listenerHandler {
|
||||||
const handlers: listenerHandler[] = [];
|
const handlers: listenerHandler[] = [];
|
||||||
try {
|
try {
|
||||||
@@ -25,7 +30,24 @@ export default function initCanvasContextObserver(
|
|||||||
...args: Array<unknown>
|
...args: Array<unknown>
|
||||||
) {
|
) {
|
||||||
if (!isBlocked(this, blockClass, blockSelector, true)) {
|
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]);
|
return original.apply(this, [contextType, ...args]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,48 +4,63 @@ import { deserializeArg } from './deserialize-args';
|
|||||||
|
|
||||||
export default async function canvasMutation({
|
export default async function canvasMutation({
|
||||||
event,
|
event,
|
||||||
mutation,
|
mutations,
|
||||||
target,
|
target,
|
||||||
imageMap,
|
imageMap,
|
||||||
errorHandler,
|
errorHandler,
|
||||||
}: {
|
}: {
|
||||||
event: Parameters<Replayer['applyIncremental']>[0];
|
event: Parameters<Replayer['applyIncremental']>[0];
|
||||||
mutation: canvasMutationCommand;
|
mutations: canvasMutationCommand[];
|
||||||
target: HTMLCanvasElement;
|
target: HTMLCanvasElement;
|
||||||
imageMap: Replayer['imageMap'];
|
imageMap: Replayer['imageMap'];
|
||||||
errorHandler: Replayer['warnCanvasMutationFailed'];
|
errorHandler: Replayer['warnCanvasMutationFailed'];
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
try {
|
const ctx = target.getContext('2d');
|
||||||
const ctx = target.getContext('2d')!;
|
|
||||||
|
|
||||||
if (mutation.setter) {
|
if (!ctx) {
|
||||||
// skip some read-only type checks
|
errorHandler(mutations[0], new Error('Canvas context is null'));
|
||||||
(ctx as unknown as Record<string, unknown>)[mutation.property] =
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
// default is '2d' for backwards compatibility (rrweb below 1.1.x)
|
// default is '2d' for backwards compatibility (rrweb below 1.1.x)
|
||||||
for (let i = 0; i < commands.length; i++) {
|
await canvas2DMutation({
|
||||||
const command = commands[i];
|
event,
|
||||||
await canvas2DMutation({
|
mutations: commands,
|
||||||
event,
|
target,
|
||||||
mutation: command,
|
imageMap,
|
||||||
target,
|
errorHandler,
|
||||||
imageMap,
|
});
|
||||||
errorHandler,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorHandler(mutation, error);
|
errorHandler(mutation, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8712,13 +8712,11 @@ exports[`record integration tests should not record input events on ignored elem
|
|||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"label\\",
|
\\"tagName\\": \\"label\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {},
|
||||||
\\"for\\": \\"ignore text\\"
|
|
||||||
},
|
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\" \\",
|
\\"textContent\\": \\"Input ignored here: \\",
|
||||||
\\"id\\": 21
|
\\"id\\": 21
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -8733,7 +8731,7 @@ exports[`record integration tests should not record input events on ignored elem
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\" \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 23
|
\\"id\\": 23
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -8741,8 +8739,74 @@ exports[`record integration tests should not record input events on ignored elem
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 24
|
\\"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
|
\\"id\\": 18
|
||||||
@@ -8750,7 +8814,7 @@ exports[`record integration tests should not record input events on ignored elem
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\",
|
\\"textContent\\": \\"\\\\n \\\\n \\",
|
||||||
\\"id\\": 25
|
\\"id\\": 35
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -8760,15 +8824,15 @@ exports[`record integration tests should not record input events on ignored elem
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||||
\\"id\\": 27
|
\\"id\\": 37
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 26
|
\\"id\\": 36
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
||||||
\\"id\\": 28
|
\\"id\\": 38
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 16
|
\\"id\\": 16
|
||||||
@@ -8792,6 +8856,128 @@ exports[`record integration tests should not record input events on ignored elem
|
|||||||
\\"type\\": 5,
|
\\"type\\": 5,
|
||||||
\\"id\\": 22
|
\\"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.moveTo(0, 0);
|
||||||
ctx.lineTo(200, 100);
|
ctx.lineTo(200, 100);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
window.canvasMutationApplied = true;
|
||||||
}, 10);
|
}, 10);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -9,8 +9,15 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<form>
|
<form>
|
||||||
<label for="ignore text"> <input type="text" class="rr-ignore" /> </label>
|
<label
|
||||||
<label for="ignore selector"> <input type="text" data-rr-ignore /> </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>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
getServerURL,
|
getServerURL,
|
||||||
launchPuppeteer,
|
launchPuppeteer,
|
||||||
waitForRAF,
|
waitForRAF,
|
||||||
|
waitForIFrameLoad,
|
||||||
replaceLast,
|
replaceLast,
|
||||||
generateRecordSnippet,
|
generateRecordSnippet,
|
||||||
ISuite,
|
ISuite,
|
||||||
@@ -71,7 +72,7 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
// also tap on the span
|
// also tap on the span
|
||||||
const span = await page.waitForSelector('span');
|
const span = await page.waitForSelector('span');
|
||||||
const center = await page.evaluate((el) => {
|
const center = await page.evaluate((el) => {
|
||||||
const { x, y, width, height } = el.getBoundingClientRect();
|
const { x, y, width, height } = el!.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
x: Math.round(x + width / 2),
|
x: Math.round(x + width / 2),
|
||||||
y: Math.round(y + height / 2),
|
y: Math.round(y + height / 2),
|
||||||
@@ -81,7 +82,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
|
|
||||||
await page.click('a');
|
await page.click('a');
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -186,7 +189,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
li.removeAttribute('aria-label');
|
li.removeAttribute('aria-label');
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshots = await page.evaluate('window.snapshots');
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -306,11 +311,9 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
|
|
||||||
await page.type('.rr-ignore', 'secret');
|
await page.type('.rr-ignore', 'secret');
|
||||||
await page.type('[data-rr-ignore]', 'secret');
|
await page.type('[data-rr-ignore]', 'secret');
|
||||||
|
await page.type('.dont-ignore', 'not secret');
|
||||||
|
|
||||||
const snapshots = (await page.evaluate(
|
await assertSnapshot(page);
|
||||||
'window.snapshots',
|
|
||||||
)) as eventWithTime[];
|
|
||||||
assertSnapshot(snapshots);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not record input values if maskAllInputs is enabled', async () => {
|
it('should not record input values if maskAllInputs is enabled', async () => {
|
||||||
@@ -547,6 +550,7 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
recordCanvas: true,
|
recordCanvas: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
await page.waitForFunction('window.canvasMutationApplied');
|
||||||
await waitForRAF(page);
|
await waitForRAF(page);
|
||||||
const snapshots = (await page.evaluate(
|
const snapshots = (await page.evaluate(
|
||||||
'window.snapshots',
|
'window.snapshots',
|
||||||
@@ -581,10 +585,7 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
|
|
||||||
await page.type('#input', 'moo');
|
await page.type('#input', 'moo');
|
||||||
|
|
||||||
const snapshots = (await page.evaluate(
|
await assertSnapshot(page);
|
||||||
'window.snapshots',
|
|
||||||
)) as eventWithTime[];
|
|
||||||
assertSnapshot(snapshots);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should record webgl canvas mutations', async () => {
|
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.goto(`${serverURL}/html`);
|
||||||
await page.setContent(getHtml.call(this, 'main.html'));
|
await page.setContent(getHtml.call(this, 'main.html'));
|
||||||
|
|
||||||
await page.waitForSelector('#two');
|
const frameIdTwo = await waitForIFrameLoad(page, '#two');
|
||||||
const frameIdTwo = await page.frames()[2];
|
const frameIdFour = await waitForIFrameLoad(frameIdTwo, '#four');
|
||||||
await frameIdTwo.waitForSelector('#four');
|
await waitForIFrameLoad(frameIdFour, '#five');
|
||||||
const frameIdFour = frameIdTwo.childFrames()[1];
|
|
||||||
await frameIdFour.waitForSelector('#five');
|
|
||||||
|
|
||||||
await page.waitForTimeout(50);
|
|
||||||
|
|
||||||
const snapshots = (await page.evaluate(
|
const snapshots = (await page.evaluate(
|
||||||
'window.snapshots',
|
'window.snapshots',
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ exports[`cross origin iframes audio.html should emit contents of iframe once 1`]
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -202,7 +202,7 @@ exports[`cross origin iframes audio.html should emit contents of iframe once 1`]
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -370,7 +370,7 @@ exports[`cross origin iframes blank.html should filter out forwarded cross origi
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -448,7 +448,7 @@ exports[`cross origin iframes blank.html should filter out forwarded cross origi
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -544,7 +544,7 @@ exports[`cross origin iframes blank.html should filter out forwarded cross origi
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -632,7 +632,7 @@ exports[`cross origin iframes blank.html should record same-origin iframe in cro
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -710,7 +710,7 @@ exports[`cross origin iframes blank.html should record same-origin iframe in cro
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -877,7 +877,7 @@ exports[`cross origin iframes blank.html should support packFn option in record(
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -956,7 +956,7 @@ exports[`cross origin iframes blank.html should support packFn option in record(
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -1045,7 +1045,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] =
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -1211,7 +1211,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] =
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -2042,7 +2042,7 @@ exports[`cross origin iframes form.html should map scroll events correctly 1`] =
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -2208,7 +2208,7 @@ exports[`cross origin iframes form.html should map scroll events correctly 1`] =
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -2682,7 +2682,7 @@ exports[`cross origin iframes move-node.html captures mutations on adopted style
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -2768,7 +2768,7 @@ exports[`cross origin iframes move-node.html captures mutations on adopted style
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -3096,7 +3096,7 @@ exports[`cross origin iframes move-node.html captures mutations on stylesheets 1
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -3182,7 +3182,7 @@ exports[`cross origin iframes move-node.html captures mutations on stylesheets 1
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -3515,7 +3515,7 @@ exports[`cross origin iframes move-node.html should record DOM attribute changes
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -3601,7 +3601,7 @@ exports[`cross origin iframes move-node.html should record DOM attribute changes
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -3803,7 +3803,7 @@ exports[`cross origin iframes move-node.html should record DOM node movement 1`]
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -3889,7 +3889,7 @@ exports[`cross origin iframes move-node.html should record DOM node movement 1`]
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -4188,7 +4188,7 @@ exports[`cross origin iframes move-node.html should record DOM node removal 1`]
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -4274,7 +4274,7 @@ exports[`cross origin iframes move-node.html should record DOM node removal 1`]
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -4474,7 +4474,7 @@ exports[`cross origin iframes move-node.html should record DOM text changes 1`]
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -4560,7 +4560,7 @@ exports[`cross origin iframes move-node.html should record DOM text changes 1`]
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -4760,7 +4760,7 @@ exports[`cross origin iframes move-node.html should record canvas elements 1`] =
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -4846,7 +4846,7 @@ exports[`cross origin iframes move-node.html should record canvas elements 1`] =
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -5083,7 +5083,7 @@ exports[`cross origin iframes move-node.html should record custom events 1`] = `
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -5169,7 +5169,7 @@ exports[`cross origin iframes move-node.html should record custom events 1`] = `
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -5365,7 +5365,7 @@ exports[`same origin iframes should emit contents of iframe once 1`] = `
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -5479,7 +5479,7 @@ exports[`same origin iframes should emit contents of iframe once 1`] = `
|
|||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
\\"tagName\\": \\"script\\",
|
\\"tagName\\": \\"script\\",
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"type\\": \\"\\"
|
\\"type\\": \\"text/javascript\\"
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [],
|
\\"childNodes\\": [],
|
||||||
\\"rootId\\": 11,
|
\\"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);
|
await waitForRAF(page);
|
||||||
|
|
||||||
const image = await page.screenshot();
|
const image = await page.screenshot();
|
||||||
expect(image).toMatchImageSnapshot();
|
expect(image).toMatchImageSnapshot({
|
||||||
|
failureThreshold: 40,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Optional,
|
Optional,
|
||||||
mouseInteractionData,
|
mouseInteractionData,
|
||||||
event,
|
event,
|
||||||
|
pluginEvent,
|
||||||
} from '@rrweb/types';
|
} from '@rrweb/types';
|
||||||
import type { recordOptions } from '../src/types';
|
import type { recordOptions } from '../src/types';
|
||||||
import * as puppeteer from 'puppeteer';
|
import * as puppeteer from 'puppeteer';
|
||||||
@@ -20,7 +21,7 @@ export async function launchPuppeteer(
|
|||||||
options?: Parameters<(typeof puppeteer)['launch']>[0],
|
options?: Parameters<(typeof puppeteer)['launch']>[0],
|
||||||
) {
|
) {
|
||||||
return await puppeteer.launch({
|
return await puppeteer.launch({
|
||||||
headless: process.env.PUPPETEER_HEADLESS ? true : false,
|
headless: process.env.PUPPETEER_HEADLESS ? 'new' : false,
|
||||||
defaultViewport: {
|
defaultViewport: {
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
@@ -108,7 +109,8 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
|
|||||||
.filter((s) => {
|
.filter((s) => {
|
||||||
if (
|
if (
|
||||||
s.type === EventType.IncrementalSnapshot &&
|
s.type === EventType.IncrementalSnapshot &&
|
||||||
s.data.source === IncrementalSource.MouseMove
|
(s.data.source === IncrementalSource.MouseMove ||
|
||||||
|
s.data.source === IncrementalSource.ViewportResize)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -195,6 +197,33 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
|
|||||||
if (s.data.currentTime) {
|
if (s.data.currentTime) {
|
||||||
s.data.currentTime = Math.round(s.data.currentTime * 10) / 10;
|
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;
|
delete (s as Optional<eventWithTime, 'timestamp'>).timestamp;
|
||||||
return s as event;
|
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');
|
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(snapshots).toBeDefined();
|
||||||
expect(stringifySnapshots(snapshots)).toMatchSnapshot();
|
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>) {
|
export function generateRecordSnippet(options: recordOptions<eventWithTime>) {
|
||||||
return `
|
return `
|
||||||
window.snapshots = [];
|
|
||||||
rrweb.record({
|
rrweb.record({
|
||||||
emit: event => {
|
emit: event => {
|
||||||
|
if (!window.snapshots) window.snapshots = [];
|
||||||
window.snapshots.push(event);
|
window.snapshots.push(event);
|
||||||
},
|
},
|
||||||
ignoreSelector: ${options.ignoreSelector},
|
ignoreSelector: ${JSON.stringify(options.ignoreSelector)},
|
||||||
maskTextSelector: ${JSON.stringify(options.maskTextSelector)},
|
maskTextSelector: ${JSON.stringify(options.maskTextSelector)},
|
||||||
maskAllInputs: ${options.maskAllInputs},
|
maskAllInputs: ${options.maskAllInputs},
|
||||||
maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
|
maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
|
||||||
|
|||||||
Reference in New Issue
Block a user