migrate to jest (#721)

* migrate rrweb-snapshot tests to jest

* migrate rrweb tests to jest
This commit is contained in:
yz-yu
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 4b6a7fbf2e
commit 23e00bdf66
26 changed files with 9313 additions and 7983 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,176 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ordering-events 1`] = `
"file-frame-1
<html>
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\" />
</head>
<body>
<div class=\\"replayer-wrapper\\">
<div class=\\"replayer-mouse\\"></div>
<canvas
class=\\"replayer-mouse-tail\\"
width=\\"1000\\"
height=\\"800\\"
style=\\"display: inherit\\"
></canvas
><iframe
sandbox=\\"allow-same-origin\\"
scrolling=\\"no\\"
width=\\"1000\\"
height=\\"800\\"
style=\\"display: inherit; pointer-events: none\\"
></iframe>
</div>
</body>
</html>
file-frame-2
<!DOCTYPE html>
<html lang=\\"en\\">
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\" />
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"file-cid-0\\" />
</head>
<body>
<span>Final - correct</span>
</body>
</html>
file-cid-0
@charset \\"utf-8\\";
.rr-block { background: currentcolor; }
noscript { display: none !important; }
html.rrweb-paused * { animation-play-state: paused !important; }
"
`;
exports[`style-sheet-remove-events-play-at-2500 1`] = `
"file-frame-1
<html>
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\" />
</head>
<body>
<div class=\\"replayer-wrapper\\">
<div class=\\"replayer-mouse\\"></div>
<canvas
class=\\"replayer-mouse-tail\\"
width=\\"1000\\"
height=\\"800\\"
style=\\"display: inherit\\"
></canvas
><iframe
sandbox=\\"allow-same-origin\\"
scrolling=\\"no\\"
width=\\"1000\\"
height=\\"800\\"
style=\\"display: inherit; pointer-events: none\\"
></iframe>
</div>
</body>
</html>
file-frame-2
<html>
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\" />
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"file-cid-0\\" />
</head>
<body></body>
</html>
file-cid-0
@charset \\"utf-8\\";
.rr-block { background: currentcolor; }
noscript { display: none !important; }
html.rrweb-paused * { animation-play-state: paused !important; }
"
`;
exports[`style-sheet-rule-events-pause-at-2500 1`] = `
"file-frame-4
<html>
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\">
<title></title>
</head>
<body>
<div class=\\"replayer-wrapper\\">
<div class=\\"replayer-mouse\\"></div>
<canvas class=\\"replayer-mouse-tail\\" width=\\"1000\\" height=\\"800\\" style=
\\"display: inherit;\\"></canvas><iframe sandbox=\\"allow-same-origin\\" scrolling=
\\"no\\" width=\\"1000\\" height=\\"800\\" style=
\\"display: inherit; pointer-events: none;\\"></iframe>
</div>
</body>
</html>
file-frame-5
<!DOCTYPE html>
<html lang=\\"en\\">
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\">
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"file-cid-0\\">
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"file-cid-1\\">
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"file-cid-2\\">
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"file-cid-3\\">
<title></title>
</head>
<body>
<a class=\\"css-added-at-1000-deleted-at-2500\\">string</a>
</body>
</html>
file-cid-0
@charset \\"utf-8\\";
.rr-block { background: currentcolor; }
noscript { display: none !important; }
html.rrweb-paused * { animation-play-state: paused !important; }
file-cid-1
@charset \\"utf-8\\";
.c011xx { padding: 1.3125rem; flex: 0 0 auto; width: 100%; }
file-cid-2
@charset \\"utf-8\\";
.c01x { opacity: 1; transform: translateX(0px); }
.css-added-at-400 { border: 1px solid blue; }
file-cid-3
@charset \\"utf-8\\";
.css-1uxxxx3 { position: fixed; top: 0px; right: 0px; left: 4rem; z-index: 15; flex-shrink: 0; height: 0.25rem; overflow: hidden; background-color: rgb(17, 171, 209); }
.css-1c9xxxx { height: 0.25rem; background-color: rgb(190, 232, 242); opacity: 0; transition: opacity 0.5s ease 0s; }
.css-lsxxx { padding-left: 4rem; }
"
`;
exports[`style-sheet-rule-events-play-at-1500 1`] = `
exports[`replayer can fast forward past StyleSheetRule changes on virtual elements 1`] = `
"file-frame-4
<html>
<head>
@@ -250,8 +80,19 @@ file-cid-3
"
`;
exports[`style-sheet-rule-events-play-at-2500 1`] = `
"file-frame-4
exports[`replayer can fast forward past StyleSheetRule deletion on virtual elements 1`] = `
"file-frame-0
<html>
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\" />
</head>
<body></body>
</html>
"
`;
exports[`replayer can handle removing style elements 1`] = `
"file-frame-1
<html>
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\" />
@@ -277,18 +118,114 @@ exports[`style-sheet-rule-events-play-at-2500 1`] = `
</html>
file-frame-5
file-frame-2
<html>
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\" />
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"file-cid-0\\" />
</head>
<body></body>
</html>
file-cid-0
@charset \\"utf-8\\";
.rr-block { background: currentcolor; }
noscript { display: none !important; }
html.rrweb-paused * { animation-play-state: paused !important; }
"
`;
exports[`replayer replays same timestamp events in correct order (with addAction) 1`] = `
"file-frame-1
<html>
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\" />
</head>
<body>
<div class=\\"replayer-wrapper\\">
<div class=\\"replayer-mouse\\"></div>
<canvas
class=\\"replayer-mouse-tail\\"
width=\\"1000\\"
height=\\"800\\"
style=\\"display: inherit\\"
></canvas
><iframe
sandbox=\\"allow-same-origin\\"
scrolling=\\"no\\"
width=\\"1000\\"
height=\\"800\\"
style=\\"display: inherit; pointer-events: none\\"
></iframe>
</div>
</body>
</html>
file-frame-2
<!DOCTYPE html>
<html lang=\\"en\\">
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\" />
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"file-cid-0\\" />
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"file-cid-1\\" />
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"file-cid-2\\" />
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"file-cid-3\\" />
</head>
<body>
<a class=\\"css-added-at-1000-deleted-at-2500\\">string</a>
<span>Final - correct</span>
</body>
</html>
file-cid-0
@charset \\"utf-8\\";
.rr-block { background: currentcolor; }
noscript { display: none !important; }
html.rrweb-paused * { animation-play-state: paused !important; }
"
`;
exports[`replayer replays same timestamp events in correct order 1`] = `
"file-frame-1
<html>
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\" />
</head>
<body>
<div class=\\"replayer-wrapper\\">
<div class=\\"replayer-mouse\\"></div>
<canvas
class=\\"replayer-mouse-tail\\"
width=\\"1000\\"
height=\\"800\\"
style=\\"display: inherit\\"
></canvas
><iframe
sandbox=\\"allow-same-origin\\"
scrolling=\\"no\\"
width=\\"1000\\"
height=\\"800\\"
style=\\"display: inherit; pointer-events: none\\"
></iframe>
</div>
</body>
</html>
file-frame-2
<!DOCTYPE html>
<html lang=\\"en\\">
<head>
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\" />
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"file-cid-0\\" />
</head>
<body>
<span>Final - correct</span>
</body>
</html>
@@ -301,29 +238,5 @@ file-cid-0
noscript { display: none !important; }
html.rrweb-paused * { animation-play-state: paused !important; }
file-cid-1
@charset \\"utf-8\\";
.c011xx { padding: 1.3125rem; flex: 0 0 auto; width: 100%; }
file-cid-2
@charset \\"utf-8\\";
.c01x { opacity: 1; transform: translateX(0px); }
.css-added-at-400 { border: 1px solid blue; }
file-cid-3
@charset \\"utf-8\\";
.css-1uxxxx3 { position: fixed; top: 0px; right: 0px; left: 4rem; z-index: 15; flex-shrink: 0; height: 0.25rem; overflow: hidden; background-color: rgb(17, 171, 209); }
.css-1c9xxxx { height: 0.25rem; background-color: rgb(190, 232, 242); opacity: 0; transition: opacity 0.5s ease 0s; }
.css-lsxxx { padding-left: 4rem; }
"
`;

View File

@@ -20,6 +20,6 @@
iframe2.src = './html/frame1.html';
setTimeout(() => {
document.body.appendChild(iframe2);
}, 10);
}, 100);
</script>
</html>

View File

@@ -4,12 +4,10 @@ import * as http from 'http';
import * as url from 'url';
import * as puppeteer from 'puppeteer';
import { assertSnapshot, launchPuppeteer } from './utils';
import { Suite } from 'mocha';
import { expect } from 'chai';
import { recordOptions, eventWithTime, EventType } from '../src/types';
import { visitSnapshot, NodeType } from 'rrweb-snapshot';
interface ISuite extends Suite {
interface ISuite {
server: http.Server;
code: string;
browser: puppeteer.Browser;
@@ -19,7 +17,7 @@ interface IMimeType {
[key: string]: string;
}
const server = () =>
const startServer = () =>
new Promise<http.Server>((resolve) => {
const mimeType: IMimeType = {
'.html': 'text/html',
@@ -53,7 +51,7 @@ const server = () =>
});
describe('record integration tests', function (this: ISuite) {
this.timeout(10_000);
jest.setTimeout(10_000);
const getHtml = (
fileName: string,
@@ -65,7 +63,7 @@ describe('record integration tests', function (this: ISuite) {
'</body>',
`
<script>
${this.code}
${code}
window.Date.now = () => new Date(Date.UTC(2018, 10, 15, 8)).valueOf();
window.snapshots = [];
rrweb.record({
@@ -86,9 +84,13 @@ describe('record integration tests', function (this: ISuite) {
);
};
before(async () => {
this.server = await server();
this.browser = await launchPuppeteer();
let server: ISuite['server'];
let code: ISuite['code'];
let browser: ISuite['browser'];
beforeAll(async () => {
server = await startServer();
browser = await launchPuppeteer();
const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js');
const pluginsCode = [
@@ -96,16 +98,16 @@ describe('record integration tests', function (this: ISuite) {
]
.map((path) => fs.readFileSync(path, 'utf8'))
.join();
this.code = fs.readFileSync(bundlePath, 'utf8') + pluginsCode;
code = fs.readFileSync(bundlePath, 'utf8') + pluginsCode;
});
after(async () => {
await this.browser.close();
this.server.close();
afterAll(async () => {
await browser.close();
server.close();
});
it('can record form interactions', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'form.html'));
@@ -116,11 +118,11 @@ describe('record integration tests', function (this: ISuite) {
await page.select('select', '1');
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'form');
assertSnapshot(snapshots);
});
it('can record childList mutations', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'mutation-observer.html'));
@@ -134,11 +136,11 @@ describe('record integration tests', function (this: ISuite) {
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'child-list');
assertSnapshot(snapshots);
});
it('can record character data muatations', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'mutation-observer.html'));
@@ -154,11 +156,11 @@ describe('record integration tests', function (this: ISuite) {
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'character-data');
assertSnapshot(snapshots);
});
it('can record attribute mutation', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'mutation-observer.html'));
@@ -172,11 +174,11 @@ describe('record integration tests', function (this: ISuite) {
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'attributes');
assertSnapshot(snapshots);
});
it('can record node mutations', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'select2.html'), {
waitUntil: 'networkidle0',
@@ -189,11 +191,11 @@ describe('record integration tests', function (this: ISuite) {
'document.getElementById("select2-drop").setAttribute("style", document.getElementById("select2-drop").style.cssText + "color:black !important")',
);
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'select2');
assertSnapshot(snapshots);
});
it('can freeze mutations', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'mutation-observer.html'));
@@ -215,22 +217,22 @@ describe('record integration tests', function (this: ISuite) {
document.body.removeChild(ul);
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'frozen');
assertSnapshot(snapshots);
});
it('should not record input events on ignored elements', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'ignore.html'));
await page.type('.rr-ignore', 'secret');
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'ignore');
assertSnapshot(snapshots);
});
it('should not record input values if maskAllInputs is enabled', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'form.html', { maskAllInputs: true }),
@@ -244,11 +246,11 @@ describe('record integration tests', function (this: ISuite) {
await page.select('select', '1');
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'mask');
assertSnapshot(snapshots);
});
it('can use maskInputOptions to configure which type of inputs should be masked', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'form.html', {
@@ -268,11 +270,11 @@ describe('record integration tests', function (this: ISuite) {
await page.select('select', '1');
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'maskInputOptions');
assertSnapshot(snapshots);
});
it('should mask value attribute with maskInputOptions', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'password.html', {
@@ -285,11 +287,11 @@ describe('record integration tests', function (this: ISuite) {
await page.type('input[type="password"]', 'secr3t');
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'maskPassword');
assertSnapshot(snapshots);
});
it('should record input userTriggered values if userTriggeredOnInput is enabled', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'form.html', { userTriggeredOnInput: true }),
@@ -303,11 +305,11 @@ describe('record integration tests', function (this: ISuite) {
await page.select('select', '1');
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'userTriggered');
assertSnapshot(snapshots);
});
it('should not record blocked elements and its child nodes', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'block.html'));
@@ -316,11 +318,11 @@ describe('record integration tests', function (this: ISuite) {
await page.click('#text');
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'block');
assertSnapshot(snapshots);
});
it('should not record blocked elements dynamically added', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'block.html'));
@@ -336,11 +338,11 @@ describe('record integration tests', function (this: ISuite) {
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'block 2');
assertSnapshot(snapshots);
});
it('should record DOM node movement 1', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'move-node.html'));
@@ -354,11 +356,11 @@ describe('record integration tests', function (this: ISuite) {
div.appendChild(span);
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'move-node-1');
assertSnapshot(snapshots);
});
it('should record DOM node movement 2', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'move-node.html'));
@@ -369,20 +371,20 @@ describe('record integration tests', function (this: ISuite) {
div.appendChild(span);
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'move-node-2');
assertSnapshot(snapshots);
});
it('should record dynamic CSS changes', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'react-styled-components.html'));
await page.click('.toggle');
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'react-styled-components');
assertSnapshot(snapshots);
});
it('should record canvas mutations', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'canvas.html', {
@@ -400,11 +402,11 @@ describe('record integration tests', function (this: ISuite) {
});
}
}
assertSnapshot(snapshots, __filename, 'canvas');
assertSnapshot(snapshots);
});
it('will serialize node before record', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'mutation-observer.html'));
@@ -419,11 +421,11 @@ describe('record integration tests', function (this: ISuite) {
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'serialize-before-record');
assertSnapshot(snapshots);
});
it('will defer missing next node mutation', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'shuffle.html'));
@@ -441,11 +443,11 @@ describe('record integration tests', function (this: ISuite) {
return parent.innerText;
});
expect(text).to.equal('4\n3\n2\n1\n5');
expect(text).toEqual('4\n3\n2\n1\n5');
});
it('should record console messages', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'log.html', {
@@ -454,7 +456,7 @@ describe('record integration tests', function (this: ISuite) {
);
await page.evaluate(() => {
console.assert(0 == 0, 'assert');
console.assert(0 === 0, 'assert');
console.count('count');
console.countReset('count');
console.debug('debug');
@@ -479,23 +481,22 @@ describe('record integration tests', function (this: ISuite) {
console.log('from iframe');
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'log');
assertSnapshot(snapshots);
});
it('should nest record iframe', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto(`http://localhost:3030/html`);
await page.setContent(getHtml.call(this, 'main.html'));
await page.waitForTimeout(500);
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'iframe');
assertSnapshot(snapshots);
});
it('should record shadow DOM', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'shadow-dom.html'));
@@ -528,11 +529,11 @@ describe('record integration tests', function (this: ISuite) {
await page.waitForTimeout(50);
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'shadow-dom');
assertSnapshot(snapshots);
});
it('should mask texts', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'mask-text.html', {
@@ -541,11 +542,11 @@ describe('record integration tests', function (this: ISuite) {
);
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'mask-text');
assertSnapshot(snapshots);
});
it('should mask texts using maskTextFn', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'mask-text.html', {
@@ -555,11 +556,11 @@ describe('record integration tests', function (this: ISuite) {
);
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'mask-text-fn');
assertSnapshot(snapshots);
});
it('can mask character data mutations', async () => {
const page: puppeteer.Page = await this.browser.newPage();
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'mutation-observer.html'));
@@ -576,6 +577,6 @@ describe('record integration tests', function (this: ISuite) {
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'mask-character-data');
assertSnapshot(snapshots);
});
});

View File

@@ -1,4 +1,3 @@
import { expect } from 'chai';
import { discardPriorSnapshots } from '../src/replay/machine';
import { sampleEvents } from './utils';
import { EventType } from '../src/types';
@@ -17,7 +16,7 @@ const nextNextEvents = nextEvents.map((e) => ({
describe('get last session', () => {
it('will return all the events when there is only one session', () => {
expect(discardPriorSnapshots(events, events[0].timestamp)).to.deep.equal(events);
expect(discardPriorSnapshots(events, events[0].timestamp)).toEqual(events);
});
it('will return last session when there is more than one in the events', () => {
@@ -27,7 +26,7 @@ describe('get last session', () => {
multiple,
nextNextEvents[nextNextEvents.length - 1].timestamp,
),
).to.deep.equal(nextNextEvents);
).toEqual(nextNextEvents);
});
it('will return last session when baseline time is future time', () => {
@@ -37,11 +36,11 @@ describe('get last session', () => {
multiple,
nextNextEvents[nextNextEvents.length - 1].timestamp + 1000,
),
).to.deep.equal(nextNextEvents);
).toEqual(nextNextEvents);
});
it('will return all sessions when baseline time is prior time', () => {
expect(discardPriorSnapshots(events, events[0].timestamp - 1000)).to.deep.equal(
expect(discardPriorSnapshots(events, events[0].timestamp - 1000)).toEqual(
events,
);
});

View File

@@ -1,5 +1,3 @@
import { expect } from 'chai';
import { matchSnapshot } from './utils';
import { pack, unpack } from '../src/packer';
import { eventWithTime, EventType } from '../src/types';
import { MARK } from '../src/packer/base';
@@ -13,30 +11,29 @@ const event: eventWithTime = {
describe('pack', () => {
it('can pack event', () => {
const packedData = pack(event);
const result = matchSnapshot(packedData, __filename, 'pack');
expect(result.pass).to.true;
expect(packedData).toMatchSnapshot();
});
});
describe('unpack', () => {
it('is compatible with unpacked data 1', () => {
const result = unpack((event as unknown) as string);
expect(result).to.deep.equal(event);
expect(result).toEqual(event);
});
it('is compatible with unpacked data 2', () => {
const result = unpack(JSON.stringify(event));
expect(result).to.deep.equal(event);
expect(result).toEqual(event);
});
it('stop on unknown data format', () => {
expect(() => unpack('[""]')).to.throw('');
expect(() => unpack('[""]')).toThrow('');
});
it('can unpack packed data', () => {
const packedData = pack(event);
const result = unpack(packedData);
expect(result).to.deep.equal({
expect(result).toEqual({
...event,
v: MARK,
});

View File

@@ -3,7 +3,6 @@
import * as fs from 'fs';
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import { expect } from 'chai';
import {
recordOptions,
listenerHandler,
@@ -13,9 +12,8 @@ import {
styleSheetRuleData,
} from '../src/types';
import { assertSnapshot, launchPuppeteer } from './utils';
import { Suite } from 'mocha';
interface ISuite extends Suite {
interface ISuite {
code: string;
browser: puppeteer.Browser;
page: puppeteer.Page;
@@ -32,44 +30,47 @@ interface IWindow extends Window {
emit: (e: eventWithTime) => undefined;
}
const setup = async function (this: ISuite, content: string) {
before(async () => {
this.browser = await launchPuppeteer();
const setup = function (this: ISuite, content: string): ISuite {
const ctx = {} as ISuite;
beforeAll(async () => {
ctx.browser = await launchPuppeteer();
const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js');
this.code = fs.readFileSync(bundlePath, 'utf8');
ctx.code = fs.readFileSync(bundlePath, 'utf8');
});
beforeEach(async () => {
const page: puppeteer.Page = await this.browser.newPage();
await page.goto('about:blank');
await page.setContent(content);
await page.evaluate(this.code);
this.page = page;
this.events = [];
await this.page.exposeFunction('emit', (e: eventWithTime) => {
ctx.page = await ctx.browser.newPage();
await ctx.page.goto('about:blank');
await ctx.page.setContent(content);
await ctx.page.evaluate(ctx.code);
ctx.events = [];
await ctx.page.exposeFunction('emit', (e: eventWithTime) => {
if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) {
return;
}
this.events.push(e);
ctx.events.push(e);
});
page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
});
afterEach(async () => {
await this.page.close();
await ctx.page.close();
});
after(async () => {
await this.browser.close();
afterAll(async () => {
await ctx.browser.close();
});
return ctx;
};
describe('record', function (this: ISuite) {
this.timeout(10_000);
jest.setTimeout(10_000);
setup.call(
const ctx: ISuite = setup.call(
this,
`
<!DOCTYPE html>
@@ -82,7 +83,7 @@ describe('record', function (this: ISuite) {
);
it('will only have one full snapshot without checkout config', async () => {
await this.page.evaluate(() => {
await ctx.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
record({
emit: ((window as unknown) as IWindow).emit,
@@ -90,24 +91,23 @@ describe('record', function (this: ISuite) {
});
let count = 30;
while (count--) {
await this.page.type('input', 'a');
await ctx.page.type('input', 'a');
}
await this.page.waitForTimeout(10);
expect(this.events.length).to.equal(33);
await ctx.page.waitForTimeout(10);
expect(ctx.events.length).toEqual(33);
expect(
this.events.filter(
(event: eventWithTime) => event.type === EventType.Meta,
).length,
).to.equal(1);
ctx.events.filter((event: eventWithTime) => event.type === EventType.Meta)
.length,
).toEqual(1);
expect(
this.events.filter(
ctx.events.filter(
(event: eventWithTime) => event.type === EventType.FullSnapshot,
).length,
).to.equal(1);
).toEqual(1);
});
it('can checkout full snapshot by count', async () => {
await this.page.evaluate(() => {
await ctx.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
record({
emit: ((window as unknown) as IWindow).emit,
@@ -116,28 +116,27 @@ describe('record', function (this: ISuite) {
});
let count = 30;
while (count--) {
await this.page.type('input', 'a');
await ctx.page.type('input', 'a');
}
await this.page.waitForTimeout(10);
expect(this.events.length).to.equal(39);
await ctx.page.waitForTimeout(10);
expect(ctx.events.length).toEqual(39);
expect(
this.events.filter(
(event: eventWithTime) => event.type === EventType.Meta,
).length,
).to.equal(4);
ctx.events.filter((event: eventWithTime) => event.type === EventType.Meta)
.length,
).toEqual(4);
expect(
this.events.filter(
ctx.events.filter(
(event: eventWithTime) => event.type === EventType.FullSnapshot,
).length,
).to.equal(4);
expect(this.events[1].type).to.equal(EventType.FullSnapshot);
expect(this.events[13].type).to.equal(EventType.FullSnapshot);
expect(this.events[25].type).to.equal(EventType.FullSnapshot);
expect(this.events[37].type).to.equal(EventType.FullSnapshot);
).toEqual(4);
expect(ctx.events[1].type).toEqual(EventType.FullSnapshot);
expect(ctx.events[13].type).toEqual(EventType.FullSnapshot);
expect(ctx.events[25].type).toEqual(EventType.FullSnapshot);
expect(ctx.events[37].type).toEqual(EventType.FullSnapshot);
});
it('can checkout full snapshot by time', async () => {
await this.page.evaluate(() => {
await ctx.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
record({
emit: ((window as unknown) as IWindow).emit,
@@ -146,30 +145,29 @@ describe('record', function (this: ISuite) {
});
let count = 30;
while (count--) {
await this.page.type('input', 'a');
await ctx.page.type('input', 'a');
}
await this.page.waitForTimeout(300);
expect(this.events.length).to.equal(33); // before first automatic snapshot
await this.page.waitForTimeout(200); // could be 33 or 35 events by now depending on speed of test env
await this.page.type('input', 'a');
await this.page.waitForTimeout(10);
expect(this.events.length).to.equal(36); // additionally includes the 2 checkout events
await ctx.page.waitForTimeout(300);
expect(ctx.events.length).toEqual(33); // before first automatic snapshot
await ctx.page.waitForTimeout(200); // could be 33 or 35 events by now depending on speed of test env
await ctx.page.type('input', 'a');
await ctx.page.waitForTimeout(10);
expect(ctx.events.length).toEqual(36); // additionally includes the 2 checkout events
expect(
this.events.filter(
(event: eventWithTime) => event.type === EventType.Meta,
).length,
).to.equal(2);
ctx.events.filter((event: eventWithTime) => event.type === EventType.Meta)
.length,
).toEqual(2);
expect(
this.events.filter(
ctx.events.filter(
(event: eventWithTime) => event.type === EventType.FullSnapshot,
).length,
).to.equal(2);
expect(this.events[1].type).to.equal(EventType.FullSnapshot);
expect(this.events[35].type).to.equal(EventType.FullSnapshot);
).toEqual(2);
expect(ctx.events[1].type).toEqual(EventType.FullSnapshot);
expect(ctx.events[35].type).toEqual(EventType.FullSnapshot);
});
it('is safe to checkout during async callbacks', async () => {
await this.page.evaluate(() => {
await ctx.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
record({
emit: ((window as unknown) as IWindow).emit,
@@ -190,12 +188,12 @@ describe('record', function (this: ISuite) {
document.body.appendChild(span);
}, 10);
});
await this.page.waitForTimeout(100);
assertSnapshot(this.events, __filename, 'async-checkout');
await ctx.page.waitForTimeout(100);
assertSnapshot(ctx.events);
});
it('can add custom event', async () => {
await this.page.evaluate(() => {
await ctx.page.evaluate(() => {
const { record, addCustomEvent } = ((window as unknown) as IWindow).rrweb;
record({
emit: ((window as unknown) as IWindow).emit,
@@ -205,12 +203,12 @@ describe('record', function (this: ISuite) {
a: 'b',
});
});
await this.page.waitForTimeout(50);
assertSnapshot(this.events, __filename, 'custom-event');
await ctx.page.waitForTimeout(50);
assertSnapshot(ctx.events);
});
it('captures stylesheet rules', async () => {
await this.page.evaluate(() => {
await ctx.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
record({
@@ -236,8 +234,8 @@ describe('record', function (this: ISuite) {
styleSheet.insertRule('body { color: #ccc; }');
}, 10);
});
await this.page.waitForTimeout(50);
const styleSheetRuleEvents = this.events.filter(
await ctx.page.waitForTimeout(50);
const styleSheetRuleEvents = ctx.events.filter(
(e) =>
e.type === EventType.IncrementalSnapshot &&
e.data.source === IncrementalSource.StyleSheetRule,
@@ -249,14 +247,18 @@ describe('record', function (this: ISuite) {
Boolean((e.data as styleSheetRuleData).removes),
).length;
// pre-serialization insert/delete should be ignored
expect(addRules.length).to.equal(2);
expect((addRules[0].data as styleSheetRuleData).adds).to.deep.include({rule: "body { color: #fff; }"});
expect(removeRuleCount).to.equal(1);
assertSnapshot(this.events, __filename, 'stylesheet-rules');
expect(addRules.length).toEqual(2);
expect((addRules[0].data as styleSheetRuleData).adds).toEqual([
{
rule: 'body { color: #fff; }',
},
]);
expect(removeRuleCount).toEqual(1);
assertSnapshot(ctx.events);
});
const captureNestedStylesheetRulesTest = async () => {
await this.page.evaluate(() => {
await ctx.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
record({
@@ -283,8 +285,8 @@ describe('record', function (this: ISuite) {
atMediaRule.insertRule('body { color: #ccc; }', 0);
}, 10);
});
await this.page.waitForTimeout(50);
const styleSheetRuleEvents = this.events.filter(
await ctx.page.waitForTimeout(50);
const styleSheetRuleEvents = ctx.events.filter(
(e) =>
e.type === EventType.IncrementalSnapshot &&
e.data.source === IncrementalSource.StyleSheetRule,
@@ -296,9 +298,9 @@ describe('record', function (this: ISuite) {
Boolean((e.data as styleSheetRuleData).removes),
).length;
// sync insert/delete should be ignored
expect(addRuleCount).to.equal(2);
expect(removeRuleCount).to.equal(1);
assertSnapshot(this.events, __filename, 'nested-stylesheet-rules');
expect(addRuleCount).toEqual(2);
expect(removeRuleCount).toEqual(1);
assertSnapshot(ctx.events);
};
it('captures nested stylesheet rules', captureNestedStylesheetRulesTest);
@@ -306,18 +308,18 @@ describe('record', function (this: ISuite) {
// Safari currently doesn't support CSSGroupingRule, let's test without that
// https://caniuse.com/?search=CSSGroupingRule
beforeEach(async () => {
await this.page.evaluate(() => {
await ctx.page.evaluate(() => {
/* @ts-ignore: override CSSGroupingRule */
CSSGroupingRule = undefined;
});
// load a fresh rrweb recorder without CSSGroupingRule
await this.page.evaluate(this.code);
await ctx.page.evaluate(ctx.code);
});
it('captures nested stylesheet rules', captureNestedStylesheetRulesTest);
});
it('captures style property changes', async () => {
await this.page.evaluate(() => {
await ctx.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
record({
@@ -330,21 +332,24 @@ describe('record', function (this: ISuite) {
const styleSheet = <CSSStyleSheet>styleElement.sheet;
styleSheet.insertRule('body { background: #000; }');
setTimeout(() => {
(styleSheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green');
(styleSheet.cssRules[0] as CSSStyleRule).style.removeProperty('background');
(styleSheet.cssRules[0] as CSSStyleRule).style.setProperty(
'color',
'green',
);
(styleSheet.cssRules[0] as CSSStyleRule).style.removeProperty(
'background',
);
}, 0);
});
await this.page.waitForTimeout(50);
assertSnapshot(this.events, __filename, 'stylesheet-properties');
await ctx.page.waitForTimeout(50);
assertSnapshot(ctx.events);
});
});
describe('record iframes', function (this: ISuite) {
this.timeout(10_000);
jest.setTimeout(10_000);
setup.call(
const ctx: ISuite = setup.call(
this,
`
<!DOCTYPE html>
@@ -357,31 +362,31 @@ describe('record iframes', function (this: ISuite) {
);
it('captures iframe content in correct order', async () => {
await this.page.evaluate(() => {
await ctx.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
record({
emit: ((window as unknown) as IWindow).emit,
});
});
await this.page.waitForTimeout(10);
// console.log(JSON.stringify(this.events));
await ctx.page.waitForTimeout(10);
// console.log(JSON.stringify(ctx.events));
expect(this.events.length).to.equal(3);
const eventTypes = this.events
expect(ctx.events.length).toEqual(3);
const eventTypes = ctx.events
.filter(
(e) =>
e.type === EventType.IncrementalSnapshot ||
e.type === EventType.FullSnapshot,
)
.map((e) => e.type);
expect(eventTypes).to.have.ordered.members([
expect(eventTypes).toEqual([
EventType.FullSnapshot,
EventType.IncrementalSnapshot,
]);
});
it('captures stylesheet mutations in iframes', async () => {
await this.page.evaluate(() => {
await ctx.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
record({
// need to reference window.top for when we are in an iframe!
@@ -391,7 +396,6 @@ describe('record iframes', function (this: ISuite) {
const iframe = document.querySelector('iframe');
// outer timeout is needed to wait for initStyleSheetObserver on iframe to be set up
setTimeout(() => {
const idoc = (iframe as HTMLIFrameElement).contentDocument!;
const styleElement = idoc.createElement('style');
@@ -400,8 +404,11 @@ describe('record iframes', function (this: ISuite) {
const styleSheet = <CSSStyleSheet>styleElement.sheet;
styleSheet.insertRule('@media {}');
const atMediaRule = styleSheet.cssRules[0] as CSSMediaRule;
const atRuleIdx0 = atMediaRule.insertRule('body { background: #000; }', 0);
const ruleIdx0 = styleSheet.insertRule('body { background: #000; }'); // inserted before above
const atRuleIdx0 = atMediaRule.insertRule(
'body { background: #000; }',
0,
);
const ruleIdx0 = styleSheet.insertRule('body { background: #000; }'); // inserted before above
// pre-serialization insert/delete above should be ignored
setTimeout(() => {
styleSheet.insertRule('body { color: #fff; }');
@@ -409,19 +416,22 @@ describe('record iframes', function (this: ISuite) {
}, 0);
setTimeout(() => {
styleSheet.deleteRule(ruleIdx0);
(styleSheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green');
(styleSheet.cssRules[0] as CSSStyleRule).style.setProperty(
'color',
'green',
);
}, 5);
setTimeout(() =>{
setTimeout(() => {
atMediaRule.deleteRule(atRuleIdx0);
}, 10);
}, 10);
});
await this.page.waitForTimeout(50);
const styleRelatedEvents = this.events.filter(
await ctx.page.waitForTimeout(50);
const styleRelatedEvents = ctx.events.filter(
(e) =>
e.type === EventType.IncrementalSnapshot &&
(e.data.source === IncrementalSource.StyleSheetRule ||
e.data.source === IncrementalSource.StyleDeclaration),
e.data.source === IncrementalSource.StyleDeclaration),
);
const addRuleCount = styleRelatedEvents.filter((e) =>
Boolean((e.data as styleSheetRuleData).adds),
@@ -429,10 +439,9 @@ describe('record iframes', function (this: ISuite) {
const removeRuleCount = styleRelatedEvents.filter((e) =>
Boolean((e.data as styleSheetRuleData).removes),
).length;
expect(styleRelatedEvents.length).to.equal(5);
expect(addRuleCount).to.equal(2);
expect(removeRuleCount).to.equal(2);
assertSnapshot(this.events, __filename, 'iframe-stylesheet-mutations');
expect(styleRelatedEvents.length).toEqual(5);
expect(addRuleCount).toEqual(2);
expect(removeRuleCount).toEqual(2);
assertSnapshot(ctx.events);
});
});

View File

@@ -1,4 +1,3 @@
import { expect } from 'chai';
import { JSDOM } from 'jsdom';
import {
applyVirtualStyleRulesToNode,
@@ -21,8 +20,8 @@ describe('virtual styles', () => {
];
applyVirtualStyleRulesToNode(virtualStyleRules, styleEl);
expect(styleEl.sheet?.cssRules?.length).to.equal(1);
expect(styleEl.sheet?.cssRules[0].cssText).to.equal(cssText);
expect(styleEl.sheet?.cssRules?.length).toEqual(1);
expect(styleEl.sheet?.cssRules[0].cssText).toEqual(cssText);
});
it('should insert rule at index 0 and keep exsisting rules', () => {
@@ -40,8 +39,8 @@ describe('virtual styles', () => {
];
applyVirtualStyleRulesToNode(virtualStyleRules, styleEl);
expect(styleEl.sheet?.cssRules?.length).to.equal(3);
expect(styleEl.sheet?.cssRules[0].cssText).to.equal(cssText);
expect(styleEl.sheet?.cssRules?.length).toEqual(3);
expect(styleEl.sheet?.cssRules[0].cssText).toEqual(cssText);
});
it('should delete rule at index 0', () => {
@@ -58,10 +57,8 @@ describe('virtual styles', () => {
];
applyVirtualStyleRulesToNode(virtualStyleRules, styleEl);
expect(styleEl.sheet?.cssRules?.length).to.equal(1);
expect(styleEl.sheet?.cssRules[0].cssText).to.equal(
'div {color: black;}',
);
expect(styleEl.sheet?.cssRules?.length).toEqual(1);
expect(styleEl.sheet?.cssRules[0].cssText).toEqual('div {color: black;}');
});
it('should restore a snapshot by inserting missing rules', () => {
@@ -82,7 +79,7 @@ describe('virtual styles', () => {
];
applyVirtualStyleRulesToNode(virtualStyleRules, styleEl);
expect(styleEl.sheet?.cssRules?.length).to.equal(2);
expect(styleEl.sheet?.cssRules?.length).toEqual(2);
});
it('should restore a snapshot by fixing order of rules', () => {
@@ -104,10 +101,10 @@ describe('virtual styles', () => {
];
applyVirtualStyleRulesToNode(virtualStyleRules, styleEl);
expect(styleEl.sheet?.cssRules?.length).to.equal(2);
expect(styleEl.sheet?.cssRules?.length).toEqual(2);
expect(
Array.from(styleEl.sheet?.cssRules || []).map((rule) => rule.cssText),
).to.have.ordered.members(cssTexts);
).toEqual(cssTexts);
});
// JSDOM/CSSOM is currently broken for this test
@@ -135,10 +132,10 @@ describe('virtual styles', () => {
expect(
(styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules?.length,
).to.equal(3);
).toEqual(3);
expect(
(styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules[0].cssText,
).to.equal(cssText);
).toEqual(cssText);
});
it('should delete rule at index [0,1]', () => {
@@ -159,10 +156,10 @@ describe('virtual styles', () => {
expect(
(styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules?.length,
).to.equal(1);
).toEqual(1);
expect(
(styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules[0].cssText,
).to.equal('a {color: blue;}');
).toEqual('a {color: blue;}');
});
});
});

View File

@@ -3,8 +3,6 @@
import * as fs from 'fs';
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import { expect } from 'chai';
import { Suite } from 'mocha';
import {
assertDomSnapshot,
launchPuppeteer,
@@ -14,47 +12,50 @@ import {
import styleSheetRuleEvents from './events/style-sheet-rule-events';
import orderingEvents from './events/ordering';
interface ISuite extends Suite {
interface ISuite {
code: string;
browser: puppeteer.Browser;
page: puppeteer.Page;
}
describe('replayer', function (this: ISuite) {
this.timeout(10_000);
describe('replayer', function () {
jest.setTimeout(10_000);
before(async () => {
this.browser = await launchPuppeteer();
let code: ISuite['code'];
let browser: ISuite['browser'];
let page: ISuite['page'];
beforeAll(async () => {
browser = await launchPuppeteer();
const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js');
this.code = fs.readFileSync(bundlePath, 'utf8');
code = fs.readFileSync(bundlePath, 'utf8');
});
beforeEach(async () => {
const page: puppeteer.Page = await this.browser.newPage();
page = await browser.newPage();
await page.goto('about:blank');
await page.evaluate(this.code);
await page.evaluate(code);
await page.evaluate(`let events = ${JSON.stringify(events)}`);
this.page = page;
page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
});
afterEach(async () => {
await this.page.close();
await page.close();
});
after(async () => {
await this.browser.close();
afterAll(async () => {
await browser.close();
});
it('can get meta data', async () => {
const meta = await this.page.evaluate(`
const meta = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.getMetaData();
`);
expect(meta).to.deep.equal({
expect(meta).toEqual({
startTime: events[0].timestamp,
endTime: events[events.length - 1].timestamp,
totalTime: events[events.length - 1].timestamp - events[0].timestamp,
@@ -62,111 +63,107 @@ describe('replayer', function (this: ISuite) {
});
it('will start actions when play', async () => {
const actionLength = await this.page.evaluate(`
const actionLength = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.play();
replayer['timer']['actions'].length;
`);
expect(actionLength).to.equal(events.length);
expect(actionLength).toEqual(events.length);
});
it('will clean actions when pause', async () => {
const actionLength = await this.page.evaluate(`
const actionLength = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.play();
replayer.pause();
replayer['timer']['actions'].length;
`);
expect(actionLength).to.equal(0);
expect(actionLength).toEqual(0);
});
it('can play at any time offset', async () => {
const actionLength = await this.page.evaluate(`
const actionLength = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.play(1500);
replayer['timer']['actions'].length;
`);
expect(actionLength).to.equal(
expect(actionLength).toEqual(
events.filter((e) => e.timestamp - events[0].timestamp >= 1500).length,
);
});
it('can play a second time in the future', async () => {
const actionLength = await this.page.evaluate(`
const actionLength = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.play(500);
replayer.play(1500);
replayer['timer']['actions'].length;
`);
expect(actionLength).to.equal(
expect(actionLength).toEqual(
events.filter((e) => e.timestamp - events[0].timestamp >= 1500).length,
);
});
it('can play a second time to the past', async () => {
const actionLength = await this.page.evaluate(`
const actionLength = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.play(1500);
replayer.play(500);
replayer['timer']['actions'].length;
`);
expect(actionLength).to.equal(
expect(actionLength).toEqual(
events.filter((e) => e.timestamp - events[0].timestamp >= 500).length,
);
});
it('can pause at any time offset', async () => {
const actionLength = await this.page.evaluate(`
const actionLength = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.pause(2500);
replayer['timer']['actions'].length;
`);
const currentTime = await this.page.evaluate(`
const currentTime = await page.evaluate(`
replayer.getCurrentTime();
`);
const currentState = await this.page.evaluate(`
const currentState = await page.evaluate(`
replayer['service']['state']['value'];
`);
expect(actionLength).to.equal(0);
expect(currentTime).to.equal(2500);
expect(currentState).to.equal('paused');
expect(actionLength).toEqual(0);
expect(currentTime).toEqual(2500);
expect(currentState).toEqual('paused');
});
it('can fast forward past StyleSheetRule changes on virtual elements', async () => {
await this.page.evaluate(
`events = ${JSON.stringify(styleSheetRuleEvents)}`,
);
const actionLength = await this.page.evaluate(`
await page.evaluate(`events = ${JSON.stringify(styleSheetRuleEvents)}`);
const actionLength = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.play(1500);
replayer['timer']['actions'].length;
`);
expect(actionLength).to.equal(
expect(actionLength).toEqual(
styleSheetRuleEvents.filter(
(e) => e.timestamp - styleSheetRuleEvents[0].timestamp >= 1500,
).length,
);
await assertDomSnapshot(
this.page,
page,
__filename,
'style-sheet-rule-events-play-at-1500',
);
});
it('should apply fast forwarded StyleSheetRules that where added', async () => {
await this.page.evaluate(
`events = ${JSON.stringify(styleSheetRuleEvents)}`,
);
const result = await this.page.evaluate(`
await page.evaluate(`events = ${JSON.stringify(styleSheetRuleEvents)}`);
const result = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.pause(1500);
@@ -176,55 +173,43 @@ describe('replayer', function (this: ISuite) {
rules.some((x) => x.selectorText === '.css-added-at-1000-deleted-at-2500');
`);
expect(result).to.equal(true);
expect(result).toEqual(true);
});
it('can handle removing style elements', async () => {
await this.page.evaluate(
`events = ${JSON.stringify(stylesheetRemoveEvents)}`,
);
const actionLength = await this.page.evaluate(`
await page.evaluate(`events = ${JSON.stringify(stylesheetRemoveEvents)}`);
const actionLength = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.play(2500);
replayer['timer']['actions'].length;
`);
expect(actionLength).to.equal(
expect(actionLength).toEqual(
stylesheetRemoveEvents.filter(
(e) => e.timestamp - stylesheetRemoveEvents[0].timestamp >= 2500,
).length,
);
await assertDomSnapshot(
this.page,
page,
__filename,
'style-sheet-remove-events-play-at-2500',
);
});
it('can fast forward past StyleSheetRule deletion on virtual elements', async () => {
await this.page.evaluate(
`events = ${JSON.stringify(styleSheetRuleEvents)}`,
);
const actionLength = await this.page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.play(2500);
replayer['timer']['actions'].length;
`);
await page.evaluate(`events = ${JSON.stringify(styleSheetRuleEvents)}`);
await assertDomSnapshot(
this.page,
page,
__filename,
'style-sheet-rule-events-play-at-2500',
);
});
it('should delete fast forwarded StyleSheetRules that where removed', async () => {
await this.page.evaluate(
`events = ${JSON.stringify(styleSheetRuleEvents)}`,
);
const result = await this.page.evaluate(`
await page.evaluate(`events = ${JSON.stringify(styleSheetRuleEvents)}`);
const result = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.pause(3000);
@@ -234,11 +219,11 @@ describe('replayer', function (this: ISuite) {
rules.some((x) => x.selectorText === '.css-added-at-1000-deleted-at-2500');
`);
expect(result).to.equal(false);
expect(result).toEqual(false);
});
it('can stream events in live mode', async () => {
const status = await this.page.evaluate(`
const status = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events, {
liveMode: true
@@ -246,46 +231,32 @@ describe('replayer', function (this: ISuite) {
replayer.startLive();
replayer.service.state.value;
`);
expect(status).to.equal('live');
expect(status).toEqual('live');
});
it('replays same timestamp events in correct order', async () => {
await this.page.evaluate(
`events = ${JSON.stringify(orderingEvents)}`,
);
await this.page.evaluate(`
await page.evaluate(`events = ${JSON.stringify(orderingEvents)}`);
await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.play();
`);
await this.page.waitForTimeout(50);
await page.waitForTimeout(50);
await assertDomSnapshot(
this.page,
__filename,
'ordering-events',
);
await assertDomSnapshot(page, __filename, 'ordering-events');
});
it('replays same timestamp events in correct order (with addAction)', async () => {
await this.page.evaluate(
`events = ${JSON.stringify(orderingEvents)}`,
);
await this.page.evaluate(`
await page.evaluate(`events = ${JSON.stringify(orderingEvents)}`);
await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events.slice(0, events.length-2));
replayer.play();
replayer.addEvent(events[events.length-2]);
replayer.addEvent(events[events.length-1]);
`);
await this.page.waitForTimeout(50);
await page.waitForTimeout(50);
await assertDomSnapshot(
this.page,
__filename,
'ordering-events',
);
await assertDomSnapshot(page, __filename, 'ordering-events');
});
});

View File

@@ -1,6 +1,4 @@
import { SnapshotState, toMatchSnapshot } from 'jest-snapshot';
import { NodeType } from 'rrweb-snapshot';
import { assert } from 'chai';
import {
EventType,
IncrementalSource,
@@ -21,24 +19,6 @@ export async function launchPuppeteer() {
});
}
export function matchSnapshot(
actual: string,
testFile: string,
testTitle: string,
) {
const snapshotState = new SnapshotState(testFile, {
updateSnapshot: process.env.SNAPSHOT_UPDATE ? 'all' : 'new',
});
const matcher = toMatchSnapshot.bind({
snapshotState,
currentTestName: testTitle,
});
const result = matcher(actual);
snapshotState.save();
return result;
}
/**
* Puppeteer may cast random mouse move which make our tests flaky.
* So we only do snapshot test with filtered events.
@@ -137,7 +117,7 @@ function stringifyDomSnapshot(mhtml: string): string {
.rewrite() // rewrite all links
.spit(); // return all contents
const newResult: { filename: string; content: string }[] = result.map(
const newResult: Array<{ filename: string; content: string }> = result.map(
(asset: { filename: string; content: string }) => {
let { filename, content } = asset;
let res: string | undefined;
@@ -152,13 +132,8 @@ function stringifyDomSnapshot(mhtml: string): string {
return newResult.map((asset) => Object.values(asset).join('\n')).join('\n\n');
}
export function assertSnapshot(
snapshots: eventWithTime[],
filename: string,
name: string,
) {
const result = matchSnapshot(stringifySnapshots(snapshots), filename, name);
assert(result.pass, result.pass ? '' : result.report());
export function assertSnapshot(snapshots: eventWithTime[]) {
expect(stringifySnapshots(snapshots)).toMatchSnapshot();
}
export async function assertDomSnapshot(
@@ -171,8 +146,7 @@ export async function assertDomSnapshot(
format: 'mhtml',
});
const result = matchSnapshot(stringifyDomSnapshot(data), filename, name);
assert(result.pass, result.pass ? '' : result.report());
expect(stringifyDomSnapshot(data)).toMatchSnapshot();
}
const now = Date.now();