Record canvas snapshots N times per second (#859)
* Only record canvas when recordCanvas is true * All should be compiled first Makes recompiling+debugging a lot faster * Add support for compiling web workes Replaces @rollup/plugin-typescript for rollup-plugin-typescript2 as the former is incompatible with rollup-plugin-web-worker-loader * Update yarn.lock * Upgrade to typescript 4.5.5 * add support for replay of ImageBitmap in 2d canvas * Snapshot canvases in a web-worker on FPS basis * Fix performance of canvas recording and playback * Wait for all images to be preloaded before checking results * flatten base64 strings, as encoding isn't consistent * Cleanup * Add serializing to 2d canvases as well * Disable blob serialize test We don't have any code for it yet * Upgrade @rollup/plugin-commonjs to 21.0.2 Fixes https://linguinecode.com/post/import-export-appear-at-the-top-level * Move canvas recording options to `sampling` Based on: https://github.com/rrweb-io/rrweb/pull/859#discussion_r846582146
This commit is contained in:
@@ -130,7 +130,7 @@ describe('e2e webgl', () => {
|
||||
});
|
||||
replayer.play(500);
|
||||
`);
|
||||
await page.waitForTimeout(50);
|
||||
await waitForRAF(page);
|
||||
|
||||
const element = await page.$('iframe');
|
||||
const frameImage = await element!.screenshot();
|
||||
@@ -165,7 +165,7 @@ describe('e2e webgl', () => {
|
||||
// wait for iframe to get added and `preloadAllImages` to ge called
|
||||
await page.waitForSelector('iframe');
|
||||
await page.evaluate(`replayer.play(500);`);
|
||||
await page.waitForTimeout(50);
|
||||
await waitForRAF(page);
|
||||
|
||||
const element = await page.$('iframe');
|
||||
const frameImage = await element!.screenshot();
|
||||
|
||||
@@ -32,79 +32,84 @@
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
// example from https://www.creativebloq.com/javascript/get-started-webgl-draw-square-7112981
|
||||
const canvas = document.getElementById('myCanvas');
|
||||
const ctx = canvas.getContext('webgl2');
|
||||
ctx.viewport(0, 0, canvas.width, canvas.height);
|
||||
ctx.clearColor(0, 0.5, 0, 1);
|
||||
ctx.clear(ctx.COLOR_BUFFER_BIT);
|
||||
setTimeout(() => {
|
||||
// example from https://www.creativebloq.com/javascript/get-started-webgl-draw-square-7112981
|
||||
const canvas = document.getElementById('myCanvas');
|
||||
const ctx = canvas.getContext('webgl2');
|
||||
ctx.viewport(0, 0, canvas.width, canvas.height);
|
||||
ctx.clearColor(0, 0.5, 0, 1);
|
||||
ctx.clear(ctx.COLOR_BUFFER_BIT);
|
||||
|
||||
const v = document.getElementById('vertex').firstChild.nodeValue;
|
||||
const f = document.getElementById('fragment').firstChild.nodeValue;
|
||||
const v = document.getElementById('vertex').firstChild.nodeValue;
|
||||
const f = document.getElementById('fragment').firstChild.nodeValue;
|
||||
|
||||
const vs = ctx.createShader(ctx.VERTEX_SHADER);
|
||||
ctx.shaderSource(vs, v);
|
||||
ctx.compileShader(vs);
|
||||
const vs = ctx.createShader(ctx.VERTEX_SHADER);
|
||||
ctx.shaderSource(vs, v);
|
||||
ctx.compileShader(vs);
|
||||
|
||||
const fs = ctx.createShader(ctx.FRAGMENT_SHADER);
|
||||
ctx.shaderSource(fs, f);
|
||||
ctx.compileShader(fs);
|
||||
const fs = ctx.createShader(ctx.FRAGMENT_SHADER);
|
||||
ctx.shaderSource(fs, f);
|
||||
ctx.compileShader(fs);
|
||||
|
||||
program = ctx.createProgram();
|
||||
ctx.attachShader(program, vs);
|
||||
ctx.attachShader(program, fs);
|
||||
ctx.linkProgram(program);
|
||||
program = ctx.createProgram();
|
||||
ctx.attachShader(program, vs);
|
||||
ctx.attachShader(program, fs);
|
||||
ctx.linkProgram(program);
|
||||
|
||||
if (!ctx.getShaderParameter(vs, ctx.COMPILE_STATUS))
|
||||
console.log(ctx.getShaderInfoLog(vs));
|
||||
if (!ctx.getShaderParameter(vs, ctx.COMPILE_STATUS))
|
||||
console.log(ctx.getShaderInfoLog(vs));
|
||||
|
||||
if (!ctx.getShaderParameter(fs, ctx.COMPILE_STATUS))
|
||||
console.log(ctx.getShaderInfoLog(fs));
|
||||
if (!ctx.getShaderParameter(fs, ctx.COMPILE_STATUS))
|
||||
console.log(ctx.getShaderInfoLog(fs));
|
||||
|
||||
if (!ctx.getProgramParameter(program, ctx.LINK_STATUS))
|
||||
console.log(ctx.getProgramInfoLog(program));
|
||||
if (!ctx.getProgramParameter(program, ctx.LINK_STATUS))
|
||||
console.log(ctx.getProgramInfoLog(program));
|
||||
|
||||
const aspect = canvas.width / canvas.height;
|
||||
const aspect = canvas.width / canvas.height;
|
||||
|
||||
const vertices = new Float32Array([
|
||||
-0.5,
|
||||
0.5 * aspect,
|
||||
0.5,
|
||||
0.5 * aspect,
|
||||
0.5,
|
||||
-0.5 * aspect, // Triangle 1
|
||||
-0.5,
|
||||
0.5 * aspect,
|
||||
0.5,
|
||||
-0.5 * aspect,
|
||||
-0.5,
|
||||
-0.5 * aspect, // Triangle 2
|
||||
]);
|
||||
const vertices = new Float32Array([
|
||||
-0.5,
|
||||
0.5 * aspect,
|
||||
0.5,
|
||||
0.5 * aspect,
|
||||
0.5,
|
||||
-0.5 * aspect, // Triangle 1
|
||||
-0.5,
|
||||
0.5 * aspect,
|
||||
0.5,
|
||||
-0.5 * aspect,
|
||||
-0.5,
|
||||
-0.5 * aspect, // Triangle 2
|
||||
]);
|
||||
|
||||
vbuffer = ctx.createBuffer();
|
||||
ctx.bindBuffer(ctx.ARRAY_BUFFER, vbuffer);
|
||||
ctx.bufferData(ctx.ARRAY_BUFFER, vertices, ctx.STATIC_DRAW);
|
||||
vbuffer = ctx.createBuffer();
|
||||
ctx.bindBuffer(ctx.ARRAY_BUFFER, vbuffer);
|
||||
ctx.bufferData(ctx.ARRAY_BUFFER, vertices, ctx.STATIC_DRAW);
|
||||
|
||||
itemSize = 2;
|
||||
numItems = vertices.length / itemSize;
|
||||
itemSize = 2;
|
||||
numItems = vertices.length / itemSize;
|
||||
|
||||
ctx.useProgram(program);
|
||||
ctx.useProgram(program);
|
||||
|
||||
const uColor = ctx.getUniformLocation(program, 'uColor');
|
||||
ctx.uniform4fv(uColor, [0.0, 0.3, 0.0, 1.0]);
|
||||
const uColor = ctx.getUniformLocation(program, 'uColor');
|
||||
ctx.uniform4fv(uColor, [0.0, 0.3, 0.0, 1.0]);
|
||||
|
||||
const aVertexPosition = ctx.getAttribLocation(program, 'aVertexPosition');
|
||||
ctx.enableVertexAttribArray(aVertexPosition);
|
||||
ctx.vertexAttribPointer(
|
||||
aVertexPosition,
|
||||
itemSize,
|
||||
ctx.FLOAT,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
const aVertexPosition = ctx.getAttribLocation(
|
||||
program,
|
||||
'aVertexPosition',
|
||||
);
|
||||
ctx.enableVertexAttribArray(aVertexPosition);
|
||||
ctx.vertexAttribPointer(
|
||||
aVertexPosition,
|
||||
itemSize,
|
||||
ctx.FLOAT,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
|
||||
ctx.drawArrays(ctx.TRIANGLES, 0, numItems);
|
||||
ctx.drawArrays(ctx.TRIANGLES, 0, numItems);
|
||||
}, 1);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
IncrementalSource,
|
||||
styleSheetRuleData,
|
||||
} from '../src/types';
|
||||
import { assertSnapshot, launchPuppeteer } from './utils';
|
||||
import { assertSnapshot, launchPuppeteer, waitForRAF } from './utils';
|
||||
|
||||
interface ISuite {
|
||||
code: string;
|
||||
@@ -368,7 +368,7 @@ describe('record iframes', function (this: ISuite) {
|
||||
emit: ((window as unknown) as IWindow).emit,
|
||||
});
|
||||
});
|
||||
await ctx.page.waitForTimeout(10);
|
||||
await waitForRAF(ctx.page);
|
||||
// console.log(JSON.stringify(ctx.events));
|
||||
|
||||
expect(ctx.events.length).toEqual(3);
|
||||
|
||||
@@ -1,5 +1,164 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`record webgl recordCanvas FPS should record snapshots 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\\": \\"canvas\\",
|
||||
\\"attributes\\": {
|
||||
\\"id\\": \\"canvas\\"
|
||||
},
|
||||
\\"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\\": 9,
|
||||
\\"id\\": 7,
|
||||
\\"type\\": 0,
|
||||
\\"commands\\": [
|
||||
{
|
||||
\\"property\\": \\"clearRect\\",
|
||||
\\"args\\": [
|
||||
0,
|
||||
0,
|
||||
300,
|
||||
150
|
||||
]
|
||||
},
|
||||
{
|
||||
\\"property\\": \\"drawImage\\",
|
||||
\\"args\\": [
|
||||
{
|
||||
\\"rr_type\\": \\"ImageBitmap\\",
|
||||
\\"args\\": [
|
||||
{
|
||||
\\"rr_type\\": \\"Blob\\",
|
||||
\\"data\\": [
|
||||
{
|
||||
\\"rr_type\\": \\"ArrayBuffer\\",
|
||||
\\"base64\\": \\"base64-0\\"
|
||||
}
|
||||
],
|
||||
\\"type\\": \\"image/png\\"
|
||||
}
|
||||
]
|
||||
},
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 9,
|
||||
\\"id\\": 7,
|
||||
\\"type\\": 0,
|
||||
\\"commands\\": [
|
||||
{
|
||||
\\"property\\": \\"clearRect\\",
|
||||
\\"args\\": [
|
||||
0,
|
||||
0,
|
||||
300,
|
||||
150
|
||||
]
|
||||
},
|
||||
{
|
||||
\\"property\\": \\"drawImage\\",
|
||||
\\"args\\": [
|
||||
{
|
||||
\\"rr_type\\": \\"ImageBitmap\\",
|
||||
\\"args\\": [
|
||||
{
|
||||
\\"rr_type\\": \\"Blob\\",
|
||||
\\"data\\": [
|
||||
{
|
||||
\\"rr_type\\": \\"ArrayBuffer\\",
|
||||
\\"base64\\": \\"base64-1\\"
|
||||
}
|
||||
],
|
||||
\\"type\\": \\"image/png\\"
|
||||
}
|
||||
]
|
||||
},
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record webgl should batch events by RAF 1`] = `
|
||||
"[
|
||||
{
|
||||
|
||||
@@ -149,6 +149,16 @@ describe('serializeArg', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should support HTMLCanvasElements saved to image', async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
// polyfill canvas.toDataURL as it doesn't exist in jsdom
|
||||
canvas.toDataURL = () => 'data:image/png;base64,...';
|
||||
expect(serializeArg(canvas, window, context)).toMatchObject({
|
||||
rr_type: 'HTMLImageElement',
|
||||
src: 'data:image/png;base64,...',
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize ImageData', async () => {
|
||||
const arr = new Uint8ClampedArray(40000);
|
||||
|
||||
@@ -176,4 +186,19 @@ describe('serializeArg', () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// we do not yet support async serializing which is needed to call Blob.arrayBuffer()
|
||||
it.skip('should serialize a blob', async () => {
|
||||
const arrayBuffer = new Uint8Array([1, 2, 0, 4]).buffer;
|
||||
const blob = new Blob([arrayBuffer], { type: 'image/png' });
|
||||
const expected = {
|
||||
rr_type: 'ArrayBuffer',
|
||||
base64: 'AQIABA==',
|
||||
};
|
||||
|
||||
expect(await serializeArg(blob, window, context)).toStrictEqual({
|
||||
rr_type: 'Blob',
|
||||
args: [expected, { type: 'image/png' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,12 @@ import {
|
||||
IncrementalSource,
|
||||
CanvasContext,
|
||||
} from '../../src/types';
|
||||
import { assertSnapshot, launchPuppeteer, waitForRAF } from '../utils';
|
||||
import {
|
||||
assertSnapshot,
|
||||
launchPuppeteer,
|
||||
stripBase64,
|
||||
waitForRAF,
|
||||
} from '../utils';
|
||||
import { ICanvas } from 'rrweb-snapshot';
|
||||
|
||||
interface ISuite {
|
||||
@@ -31,7 +36,11 @@ interface IWindow extends Window {
|
||||
emit: (e: eventWithTime) => undefined;
|
||||
}
|
||||
|
||||
const setup = function (this: ISuite, content: string): ISuite {
|
||||
const setup = function (
|
||||
this: ISuite,
|
||||
content: string,
|
||||
canvasSample: 'all' | number = 'all',
|
||||
): ISuite {
|
||||
const ctx = {} as ISuite;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -56,13 +65,16 @@ const setup = function (this: ISuite, content: string): ISuite {
|
||||
|
||||
ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
|
||||
|
||||
await ctx.page.evaluate(() => {
|
||||
await ctx.page.evaluate((canvasSample) => {
|
||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||
record({
|
||||
recordCanvas: true,
|
||||
sampling: {
|
||||
canvas: canvasSample,
|
||||
},
|
||||
emit: ((window as unknown) as IWindow).emit,
|
||||
});
|
||||
});
|
||||
}, canvasSample);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -257,4 +269,52 @@ describe('record webgl', function (this: ISuite) {
|
||||
assertSnapshot(ctx.events);
|
||||
expect(ctx.events.length).toEqual(5);
|
||||
});
|
||||
|
||||
describe('recordCanvas FPS', function (this: ISuite) {
|
||||
jest.setTimeout(10_000);
|
||||
|
||||
const maxFPS = 60;
|
||||
|
||||
const ctx: ISuite = setup.call(
|
||||
this,
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
maxFPS,
|
||||
);
|
||||
|
||||
it('should record snapshots', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
|
||||
const gl = canvas.getContext('webgl', { preserveDrawingBuffer: true })!;
|
||||
// Set the clear color to darkish green.
|
||||
gl.clearColor(0.0, 0.5, 0.0, 1.0);
|
||||
// Clear the context with the newly set color. This is
|
||||
// the function call that actually does the drawing.
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
});
|
||||
|
||||
await ctx.page.waitForTimeout(200); // give it some time buffer
|
||||
|
||||
await ctx.page.evaluate(() => {
|
||||
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
|
||||
const gl = canvas.getContext('webgl', { preserveDrawingBuffer: true })!;
|
||||
// Set the clear color to darkish blue.
|
||||
gl.clearColor(0.0, 0.0, 0.5, 1.0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
});
|
||||
|
||||
await ctx.page.waitForTimeout(200);
|
||||
|
||||
await waitForRAF(ctx.page);
|
||||
|
||||
// should yield a frame for each change at a max of 60fps
|
||||
assertSnapshot(stripBase64(ctx.events));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { deserializeArg } from '../../src/replay/canvas/deserialize-args';
|
||||
import { polyfillWebGLGlobals } from '../utils';
|
||||
polyfillWebGLGlobals();
|
||||
|
||||
import { deserializeArg } from '../../src/replay/canvas/webgl';
|
||||
|
||||
let context: WebGLRenderingContext | WebGL2RenderingContext;
|
||||
describe('deserializeArg', () => {
|
||||
beforeEach(() => {
|
||||
@@ -14,7 +13,7 @@ describe('deserializeArg', () => {
|
||||
});
|
||||
it('should deserialize Float32Array values', async () => {
|
||||
expect(
|
||||
deserializeArg(
|
||||
await deserializeArg(
|
||||
new Map(),
|
||||
context,
|
||||
)({
|
||||
@@ -26,7 +25,7 @@ describe('deserializeArg', () => {
|
||||
|
||||
it('should deserialize Float64Array values', async () => {
|
||||
expect(
|
||||
deserializeArg(
|
||||
await deserializeArg(
|
||||
new Map(),
|
||||
context,
|
||||
)({
|
||||
@@ -39,7 +38,7 @@ describe('deserializeArg', () => {
|
||||
it('should deserialize ArrayBuffer values', async () => {
|
||||
const contents = [1, 2, 0, 4];
|
||||
expect(
|
||||
deserializeArg(
|
||||
await deserializeArg(
|
||||
new Map(),
|
||||
context,
|
||||
)({
|
||||
@@ -51,7 +50,7 @@ describe('deserializeArg', () => {
|
||||
|
||||
it('should deserialize DataView values', async () => {
|
||||
expect(
|
||||
deserializeArg(
|
||||
await deserializeArg(
|
||||
new Map(),
|
||||
context,
|
||||
)({
|
||||
@@ -70,7 +69,7 @@ describe('deserializeArg', () => {
|
||||
|
||||
it('should leave arrays intact', async () => {
|
||||
const array = [1, 2, 3, 4];
|
||||
expect(deserializeArg(new Map(), context)(array)).toEqual(array);
|
||||
expect(await deserializeArg(new Map(), context)(array)).toEqual(array);
|
||||
});
|
||||
|
||||
it('should deserialize complex objects', async () => {
|
||||
@@ -89,22 +88,20 @@ describe('deserializeArg', () => {
|
||||
5,
|
||||
6,
|
||||
];
|
||||
expect(deserializeArg(new Map(), context)(serializedArg)).toStrictEqual([
|
||||
new DataView(new ArrayBuffer(16), 0, 16),
|
||||
5,
|
||||
6,
|
||||
]);
|
||||
expect(
|
||||
await deserializeArg(new Map(), context)(serializedArg),
|
||||
).toStrictEqual([new DataView(new ArrayBuffer(16), 0, 16), 5, 6]);
|
||||
});
|
||||
|
||||
it('should leave null as-is', async () => {
|
||||
expect(deserializeArg(new Map(), context)(null)).toStrictEqual(null);
|
||||
expect(await deserializeArg(new Map(), context)(null)).toStrictEqual(null);
|
||||
});
|
||||
|
||||
it('should support HTMLImageElements', async () => {
|
||||
const image = new Image();
|
||||
image.src = 'http://example.com/image.png';
|
||||
expect(
|
||||
deserializeArg(
|
||||
await deserializeArg(
|
||||
new Map(),
|
||||
context,
|
||||
)({
|
||||
@@ -121,7 +118,7 @@ describe('deserializeArg', () => {
|
||||
imageMap.set(image.src, image);
|
||||
|
||||
expect(
|
||||
deserializeArg(
|
||||
await deserializeArg(
|
||||
imageMap,
|
||||
context,
|
||||
)({
|
||||
@@ -130,4 +127,77 @@ describe('deserializeArg', () => {
|
||||
}),
|
||||
).toBe(image);
|
||||
});
|
||||
|
||||
it('should support blobs', async () => {
|
||||
const arrayBuffer = new Uint8Array([1, 2, 0, 4]).buffer;
|
||||
const expected = new Blob([arrayBuffer], { type: 'image/png' });
|
||||
|
||||
const deserialized = await deserializeArg(
|
||||
new Map(),
|
||||
context,
|
||||
)({
|
||||
rr_type: 'Blob',
|
||||
data: [
|
||||
{
|
||||
rr_type: 'ArrayBuffer',
|
||||
base64: 'AQIABA==',
|
||||
},
|
||||
],
|
||||
type: 'image/png',
|
||||
});
|
||||
|
||||
// `expect(blob).toEqual(otherBlob)` doesn't really do anything yet
|
||||
// jest hasn't implemented a propper way to compare blobs
|
||||
// more info: https://github.com/facebook/jest/issues/7372
|
||||
// because JSDOM doesn't support most functions needed for comparison:
|
||||
// more info: https://github.com/jsdom/jsdom/issues/2555
|
||||
expect(deserialized).toEqual(expected);
|
||||
// thats why we test size of the blob as well
|
||||
expect(deserialized.size).toEqual(expected.size);
|
||||
});
|
||||
|
||||
describe('isUnchanged', () => {
|
||||
it('should set isUnchanged:true when non of the args are changed', async () => {
|
||||
const status = {
|
||||
isUnchanged: true,
|
||||
};
|
||||
|
||||
await deserializeArg(new Map(), context, status)(true);
|
||||
expect(status.isUnchanged).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set isUnchanged: false when args are deserialzed', async () => {
|
||||
const status = {
|
||||
isUnchanged: true,
|
||||
};
|
||||
|
||||
await deserializeArg(
|
||||
new Map(),
|
||||
context,
|
||||
status,
|
||||
)({
|
||||
rr_type: 'Float64Array',
|
||||
args: [[-1, -1, 3, -1, -1, 3]],
|
||||
});
|
||||
expect(status.isUnchanged).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should set isUnchanged: false when nested args are deserialzed', async () => {
|
||||
const status = {
|
||||
isUnchanged: true,
|
||||
};
|
||||
|
||||
await deserializeArg(
|
||||
new Map(),
|
||||
context,
|
||||
status,
|
||||
)([
|
||||
{
|
||||
rr_type: 'Float64Array',
|
||||
args: [[-1, -1, 3, -1, -1, 3]],
|
||||
},
|
||||
]);
|
||||
expect(status.isUnchanged).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Replayer } from '../../src/replay';
|
||||
import {} from '../../src/types';
|
||||
import {
|
||||
CanvasContext,
|
||||
SerializedWebGlArg,
|
||||
CanvasArg,
|
||||
IncrementalSource,
|
||||
EventType,
|
||||
eventWithTime,
|
||||
@@ -16,9 +16,7 @@ import {
|
||||
|
||||
let replayer: Replayer;
|
||||
|
||||
const canvasMutationEventWithArgs = (
|
||||
args: SerializedWebGlArg[],
|
||||
): eventWithTime => {
|
||||
const canvasMutationEventWithArgs = (args: CanvasArg[]): eventWithTime => {
|
||||
return {
|
||||
timestamp: 100,
|
||||
type: EventType.IncrementalSnapshot,
|
||||
@@ -67,11 +65,11 @@ describe('preloadAllImages', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should preload nested image', () => {
|
||||
it('should preload nested image', async () => {
|
||||
replayer.service.state.context.events = [
|
||||
canvasMutationEventWithArgs([
|
||||
{
|
||||
rr_type: 'something',
|
||||
rr_type: 'Array',
|
||||
args: [
|
||||
{
|
||||
rr_type: 'HTMLImageElement',
|
||||
@@ -82,7 +80,7 @@ describe('preloadAllImages', () => {
|
||||
]),
|
||||
];
|
||||
|
||||
(replayer as any).preloadAllImages();
|
||||
await (replayer as any).preloadAllImages();
|
||||
|
||||
const expectedImage = new Image();
|
||||
expectedImage.src = 'http://example.com';
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
import { polyfillWebGLGlobals } from '../utils';
|
||||
polyfillWebGLGlobals();
|
||||
|
||||
import webglMutation, { variableListFor } from '../../src/replay/canvas/webgl';
|
||||
import webglMutation from '../../src/replay/canvas/webgl';
|
||||
import { CanvasContext } from '../../src/types';
|
||||
import { variableListFor } from '../../src/replay/canvas/deserialize-args';
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
describe('webglMutation', () => {
|
||||
@@ -30,7 +31,7 @@ describe('webglMutation', () => {
|
||||
|
||||
expect(variableListFor(context, 'WebGLShader')).toHaveLength(0);
|
||||
|
||||
webglMutation({
|
||||
await webglMutation({
|
||||
mutation: {
|
||||
property: 'createShader',
|
||||
args: [35633],
|
||||
|
||||
@@ -220,6 +220,39 @@ export async function assertDomSnapshot(
|
||||
expect(stringifyDomSnapshot(data)).toMatchSnapshot();
|
||||
}
|
||||
|
||||
export function stripBase64(events: eventWithTime[]) {
|
||||
const base64Strings: string[] = [];
|
||||
function walk<T>(obj: T): T {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
if (Array.isArray(obj)) return (obj.map((e) => walk(e)) as unknown) as T;
|
||||
const newObj: Partial<T> = {};
|
||||
for (let prop in obj) {
|
||||
const value = obj[prop];
|
||||
if (prop === 'base64' && typeof value === 'string') {
|
||||
let index = base64Strings.indexOf(value);
|
||||
if (index === -1) {
|
||||
index = base64Strings.push(value) - 1;
|
||||
}
|
||||
(newObj as any)[prop] = `base64-${index}`;
|
||||
} else {
|
||||
(newObj as any)[prop] = walk(value);
|
||||
}
|
||||
}
|
||||
return newObj as T;
|
||||
}
|
||||
|
||||
return events.map((event) => {
|
||||
if (
|
||||
event.type === EventType.IncrementalSnapshot &&
|
||||
event.data.source === IncrementalSource.CanvasMutation
|
||||
) {
|
||||
const newData = walk(event.data);
|
||||
return { ...event, data: newData };
|
||||
}
|
||||
return event;
|
||||
});
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
export const sampleEvents: eventWithTime[] = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user