Skip to content

Commit ae3b87f

Browse files
billyvgandreiborza
authored andcommitted
feat(replay): Use vitest instead of jest (#11899)
Use vitest instead of jest. Some notes: * Use vi/jest `*Async` fake timer functions instead of `process.nextTick` * Our `useFakeTimers` module was setting an interval which was causing all sorts of flakes in tests with the updated fake timers library * removed the interval in `use-fake-timers` module which seemed to be unnecessary and was causing lots of flakes in our tests (also was able to remove all of the random tick values in timestamps due to this)
1 parent 3632e4c commit ae3b87f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+628
-635
lines changed

packages/replay-internal/.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module.exports = {
1010
files: ['src/**/*.ts'],
1111
},
1212
{
13-
files: ['jest.setup.ts', 'jest.config.ts'],
13+
files: ['test.setup.ts', 'vitest.config.ts'],
1414
parserOptions: {
1515
project: ['tsconfig.test.json'],
1616
},

packages/replay-internal/jest.config.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

packages/replay-internal/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@
5151
"build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm",
5252
"circularDepCheck": "madge --circular src/index.ts",
5353
"clean": "rimraf build sentry-replay-*.tgz",
54-
"fix": "eslint . --format stylish --fix",
54+
"fix": "run-s fix:biome fix:eslint",
55+
"fix:eslint": "eslint . --format stylish --fix",
56+
"fix:biome": "biome check --apply .",
5557
"lint": "eslint . --format stylish",
56-
"test": "jest",
57-
"test:watch": "jest --watch",
58+
"test": "vitest",
59+
"test:watch": "vitest --watch",
5860
"yalc:publish": "ts-node ../../scripts/prepack.ts --bundles && yalc publish ./build/npm --push --sig"
5961
},
6062
"repository": {
@@ -73,6 +75,7 @@
7375
"@sentry-internal/rrweb": "2.15.0",
7476
"@sentry-internal/rrweb-snapshot": "2.15.0",
7577
"fflate": "^0.8.1",
78+
"jest-matcher-utils": "^29.0.0",
7679
"jsdom-worker": "^0.2.1"
7780
},
7881
"dependencies": {

packages/replay-internal/jest.setup.ts renamed to packages/replay-internal/test.setup.ts

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import { TextEncoder } from 'util';
1+
import { printDiffOrStringify } from 'jest-matcher-utils';
2+
import { vi } from 'vitest';
3+
import type { Mocked, MockedFunction } from 'vitest';
4+
25
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
36
import { getClient } from '@sentry/core';
47
import type { ReplayRecordingData, Transport } from '@sentry/types';
58
import * as SentryUtils from '@sentry/utils';
69

710
import type { ReplayContainer, Session } from './src/types';
811

9-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10-
(global as any).TextEncoder = TextEncoder;
11-
12-
type MockTransport = jest.MockedFunction<Transport['send']>;
12+
type MockTransport = MockedFunction<Transport['send']>;
1313

14-
jest.spyOn(SentryUtils, 'isBrowser').mockImplementation(() => true);
14+
vi.spyOn(SentryUtils, 'isBrowser').mockImplementation(() => true);
1515

1616
type EnvelopeHeader = {
1717
event_id: string;
@@ -36,7 +36,7 @@ type SentReplayExpected = {
3636
};
3737

3838
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
39-
const toHaveSameSession = function (received: jest.Mocked<ReplayContainer>, expected: undefined | Session) {
39+
const toHaveSameSession = function (received: Mocked<ReplayContainer>, expected: undefined | Session) {
4040
const pass = this.equals(received.session?.id, expected?.id) as boolean;
4141

4242
const options = {
@@ -47,12 +47,12 @@ const toHaveSameSession = function (received: jest.Mocked<ReplayContainer>, expe
4747
return {
4848
pass,
4949
message: () =>
50-
`${this.utils.matcherHint(
51-
'toHaveSameSession',
52-
undefined,
53-
undefined,
54-
options,
55-
)}\n\n${this.utils.printDiffOrStringify(expected, received.session, 'Expected', 'Received')}`,
50+
`${this.utils.matcherHint('toHaveSameSession', undefined, undefined, options)}\n\n${printDiffOrStringify(
51+
expected,
52+
received.session,
53+
'Expected',
54+
'Received',
55+
)}`,
5656
};
5757
};
5858

@@ -101,6 +101,7 @@ function checkCallForSentReplay(
101101
: (expected as SentReplayExpected);
102102

103103
if (isObjectContaining) {
104+
// eslint-disable-next-line no-console
104105
console.warn('`expect.objectContaining` is unnecessary when using the `toHaveSentReplay` matcher');
105106
}
106107

@@ -152,7 +153,7 @@ function getReplayCalls(calls: any[][][]): any[][][] {
152153
*/
153154
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
154155
const toHaveSentReplay = function (
155-
_received: jest.Mocked<ReplayContainer>,
156+
_received: Mocked<ReplayContainer>,
156157
expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean },
157158
) {
158159
const { calls } = (getClient()?.getTransport()?.send as MockTransport).mock;
@@ -194,12 +195,7 @@ const toHaveSentReplay = function (
194195
: 'Expected Replay to have been sent, but a request was not attempted'
195196
: `${this.utils.matcherHint('toHaveSentReplay', undefined, undefined, options)}\n\n${results
196197
.map(({ key, expectedVal, actualVal }: Result) =>
197-
this.utils.printDiffOrStringify(
198-
expectedVal,
199-
actualVal,
200-
`Expected (key: ${key})`,
201-
`Received (key: ${key})`,
202-
),
198+
printDiffOrStringify(expectedVal, actualVal, `Expected (key: ${key})`, `Received (key: ${key})`),
203199
)
204200
.join('\n')}`,
205201
};
@@ -211,7 +207,7 @@ const toHaveSentReplay = function (
211207
*/
212208
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
213209
const toHaveLastSentReplay = function (
214-
_received: jest.Mocked<ReplayContainer>,
210+
_received: Mocked<ReplayContainer>,
215211
expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean },
216212
) {
217213
const { calls } = (getClient()?.getTransport()?.send as MockTransport).mock;
@@ -235,12 +231,7 @@ const toHaveLastSentReplay = function (
235231
: 'Expected Replay to have last been sent, but a request was not attempted'
236232
: `${this.utils.matcherHint('toHaveSentReplay', undefined, undefined, options)}\n\n${results
237233
.map(({ key, expectedVal, actualVal }: Result) =>
238-
this.utils.printDiffOrStringify(
239-
expectedVal,
240-
actualVal,
241-
`Expected (key: ${key})`,
242-
`Received (key: ${key})`,
243-
),
234+
printDiffOrStringify(expectedVal, actualVal, `Expected (key: ${key})`, `Received (key: ${key})`),
244235
)
245236
.join('\n')}`,
246237
};
@@ -252,18 +243,13 @@ expect.extend({
252243
toHaveLastSentReplay,
253244
});
254245

255-
declare global {
256-
// eslint-disable-next-line @typescript-eslint/no-namespace
257-
namespace jest {
258-
interface AsymmetricMatchers {
259-
toHaveSentReplay(expected?: SentReplayExpected): void;
260-
toHaveLastSentReplay(expected?: SentReplayExpected): void;
261-
toHaveSameSession(expected: undefined | Session): void;
262-
}
263-
interface Matchers<R> {
264-
toHaveSentReplay(expected?: SentReplayExpected): R;
265-
toHaveLastSentReplay(expected?: SentReplayExpected): R;
266-
toHaveSameSession(expected: undefined | Session): R;
267-
}
268-
}
246+
interface CustomMatchers<R = unknown> {
247+
toHaveSentReplay(expected?: SentReplayExpected): R;
248+
toHaveLastSentReplay(expected?: SentReplayExpected): R;
249+
toHaveSameSession(expected: undefined | Session): R;
250+
}
251+
252+
declare module 'vitest' {
253+
type Assertion<T = any> = CustomMatchers<T>;
254+
type AsymmetricMatchersContaining = CustomMatchers;
269255
}
Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,42 @@
1+
import { vi } from 'vitest';
2+
13
import { EventType } from '@sentry-internal/rrweb';
24

5+
import { saveSession } from '../../src/session/saveSession';
36
import type { RecordingEvent } from '../../src/types';
47
import { addEvent } from '../../src/util/addEvent';
58
import { resetSdkMock } from '../mocks/resetSdkMock';
69
import { useFakeTimers } from '../utils/use-fake-timers';
710

811
useFakeTimers();
912

13+
vi.mock('../../src/session/saveSession', () => {
14+
return {
15+
saveSession: vi.fn(),
16+
};
17+
});
18+
1019
describe('Integration | autoSaveSession', () => {
1120
afterEach(() => {
12-
jest.clearAllMocks();
21+
vi.clearAllMocks();
1322
});
1423

1524
test.each([
1625
['with stickySession=true', true, 1],
1726
['with stickySession=false', false, 0],
1827
])('%s', async (_: string, stickySession: boolean, addSummand: number) => {
19-
const saveSessionSpy = jest.fn();
20-
21-
jest.mock('../../src/session/saveSession', () => {
22-
return {
23-
saveSession: saveSessionSpy,
24-
};
25-
});
26-
2728
const { replay } = await resetSdkMock({
2829
replayOptions: {
2930
stickySession,
3031
},
3132
});
3233

3334
// Initially called up to three times: once for start, then once for replay.updateSessionActivity & once for segmentId increase
34-
expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 3);
35+
expect(saveSession).toHaveBeenCalledTimes(addSummand * 3);
3536

3637
replay['_updateSessionActivity']();
3738

38-
expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 4);
39+
expect(saveSession).toHaveBeenCalledTimes(addSummand * 4);
3940

4041
// In order for runFlush to actually do something, we need to add an event
4142
const event = {
@@ -48,8 +49,8 @@ describe('Integration | autoSaveSession', () => {
4849

4950
addEvent(replay, event);
5051

51-
await replay['_runFlush']();
52+
await Promise.all([replay['_runFlush'](), vi.runAllTimersAsync()]);
5253

53-
expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 5);
54+
expect(saveSession).toHaveBeenCalledTimes(addSummand * 5);
5455
});
5556
});

packages/replay-internal/test/integration/beforeAddRecordingEvent.test.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { vi } from 'vitest';
2+
import type { MockInstance, MockedFunction } from 'vitest';
3+
14
import * as SentryBrowserUtils from '@sentry-internal/browser-utils';
25
import * as SentryCore from '@sentry/core';
36
import type { Transport } from '@sentry/types';
@@ -14,24 +17,19 @@ import { useFakeTimers } from '../utils/use-fake-timers';
1417

1518
useFakeTimers();
1619

17-
async function advanceTimers(time: number) {
18-
jest.advanceTimersByTime(time);
19-
await new Promise(process.nextTick);
20-
}
21-
22-
type MockTransportSend = jest.MockedFunction<Transport['send']>;
20+
type MockTransportSend = MockedFunction<Transport['send']>;
2321

2422
describe('Integration | beforeAddRecordingEvent', () => {
2523
let replay: ReplayContainer;
2624
let integration: Replay;
2725
let mockTransportSend: MockTransportSend;
28-
let mockSendReplayRequest: jest.SpyInstance<any>;
26+
let mockSendReplayRequest: MockInstance<any>;
2927
let domHandler: DomHandler;
3028
const { record: mockRecord } = mockRrweb();
3129

3230
beforeAll(async () => {
33-
jest.setSystemTime(new Date(BASE_TIMESTAMP));
34-
jest.spyOn(SentryBrowserUtils, 'addClickKeypressInstrumentationHandler').mockImplementation(handler => {
31+
vi.setSystemTime(new Date(BASE_TIMESTAMP));
32+
vi.spyOn(SentryBrowserUtils, 'addClickKeypressInstrumentationHandler').mockImplementation(handler => {
3533
domHandler = handler;
3634
});
3735

@@ -69,14 +67,14 @@ describe('Integration | beforeAddRecordingEvent', () => {
6967
},
7068
}));
7169

72-
mockSendReplayRequest = jest.spyOn(SendReplayRequest, 'sendReplayRequest');
70+
mockSendReplayRequest = vi.spyOn(SendReplayRequest, 'sendReplayRequest');
7371

74-
jest.runAllTimers();
72+
vi.runAllTimers();
7573
mockTransportSend = SentryCore.getClient()?.getTransport()?.send as MockTransportSend;
7674
});
7775

7876
beforeEach(() => {
79-
jest.setSystemTime(new Date(BASE_TIMESTAMP));
77+
vi.setSystemTime(new Date(BASE_TIMESTAMP));
8078
mockRecord.takeFullSnapshot.mockClear();
8179
mockTransportSend.mockClear();
8280

@@ -90,9 +88,9 @@ describe('Integration | beforeAddRecordingEvent', () => {
9088
});
9189

9290
afterEach(async () => {
93-
jest.runAllTimers();
91+
vi.runAllTimers();
9492
await new Promise(process.nextTick);
95-
jest.setSystemTime(new Date(BASE_TIMESTAMP));
93+
vi.setSystemTime(new Date(BASE_TIMESTAMP));
9694
clearSession(replay);
9795
});
9896

@@ -106,7 +104,7 @@ describe('Integration | beforeAddRecordingEvent', () => {
106104
event: new Event('click'),
107105
});
108106

109-
await advanceTimers(5000);
107+
await vi.runAllTimersAsync();
110108

111109
expect(replay).toHaveLastSentReplay({
112110
recordingPayloadHeader: { segment_id: 0 },
@@ -135,8 +133,7 @@ describe('Integration | beforeAddRecordingEvent', () => {
135133

136134
integration.start();
137135

138-
jest.runAllTimers();
139-
await new Promise(process.nextTick);
136+
await vi.runAllTimersAsync();
140137
expect(replay).toHaveLastSentReplay({
141138
recordingPayloadHeader: { segment_id: 0 },
142139
recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }]),
@@ -174,8 +171,7 @@ describe('Integration | beforeAddRecordingEvent', () => {
174171
]),
175172
);
176173

177-
jest.runAllTimers();
178-
await new Promise(process.nextTick);
174+
await vi.runAllTimersAsync();
179175

180176
expect(replay).not.toHaveLastSentReplay();
181177
expect(replay.isEnabled()).toBe(true);

0 commit comments

Comments
 (0)