Skip to content

Commit

Permalink
feat(replay): Use vitest instead of jest (#11899)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
billyvg committed May 8, 2024
1 parent 4dcfb24 commit 4cc5301
Show file tree
Hide file tree
Showing 47 changed files with 628 additions and 635 deletions.
2 changes: 1 addition & 1 deletion packages/replay-internal/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module.exports = {
files: ['src/**/*.ts'],
},
{
files: ['jest.setup.ts', 'jest.config.ts'],
files: ['test.setup.ts', 'vitest.config.ts'],
parserOptions: {
project: ['tsconfig.test.json'],
},
Expand Down
17 changes: 0 additions & 17 deletions packages/replay-internal/jest.config.ts

This file was deleted.

9 changes: 6 additions & 3 deletions packages/replay-internal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@
"build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm",
"circularDepCheck": "madge --circular src/index.ts",
"clean": "rimraf build sentry-replay-*.tgz",
"fix": "eslint . --format stylish --fix",
"fix": "run-s fix:biome fix:eslint",
"fix:eslint": "eslint . --format stylish --fix",
"fix:biome": "biome check --apply .",
"lint": "eslint . --format stylish",
"test": "jest",
"test:watch": "jest --watch",
"test": "vitest",
"test:watch": "vitest --watch",
"yalc:publish": "ts-node ../../scripts/prepack.ts --bundles && yalc publish ./build/npm --push --sig"
},
"repository": {
Expand All @@ -73,6 +75,7 @@
"@sentry-internal/rrweb": "2.15.0",
"@sentry-internal/rrweb-snapshot": "2.15.0",
"fflate": "^0.8.1",
"jest-matcher-utils": "^29.0.0",
"jsdom-worker": "^0.2.1"
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { TextEncoder } from 'util';
import { printDiffOrStringify } from 'jest-matcher-utils';
import { vi } from 'vitest';
import type { Mocked, MockedFunction } from 'vitest';

/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { getClient } from '@sentry/core';
import type { ReplayRecordingData, Transport } from '@sentry/types';
import * as SentryUtils from '@sentry/utils';

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

// eslint-disable-next-line @typescript-eslint/no-explicit-any
(global as any).TextEncoder = TextEncoder;

type MockTransport = jest.MockedFunction<Transport['send']>;
type MockTransport = MockedFunction<Transport['send']>;

jest.spyOn(SentryUtils, 'isBrowser').mockImplementation(() => true);
vi.spyOn(SentryUtils, 'isBrowser').mockImplementation(() => true);

type EnvelopeHeader = {
event_id: string;
Expand All @@ -36,7 +36,7 @@ type SentReplayExpected = {
};

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

const options = {
Expand All @@ -47,12 +47,12 @@ const toHaveSameSession = function (received: jest.Mocked<ReplayContainer>, expe
return {
pass,
message: () =>
`${this.utils.matcherHint(
'toHaveSameSession',
undefined,
undefined,
options,
)}\n\n${this.utils.printDiffOrStringify(expected, received.session, 'Expected', 'Received')}`,
`${this.utils.matcherHint('toHaveSameSession', undefined, undefined, options)}\n\n${printDiffOrStringify(
expected,
received.session,
'Expected',
'Received',
)}`,
};
};

Expand Down Expand Up @@ -101,6 +101,7 @@ function checkCallForSentReplay(
: (expected as SentReplayExpected);

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

Expand Down Expand Up @@ -152,7 +153,7 @@ function getReplayCalls(calls: any[][][]): any[][][] {
*/
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const toHaveSentReplay = function (
_received: jest.Mocked<ReplayContainer>,
_received: Mocked<ReplayContainer>,
expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean },
) {
const { calls } = (getClient()?.getTransport()?.send as MockTransport).mock;
Expand Down Expand Up @@ -194,12 +195,7 @@ const toHaveSentReplay = function (
: 'Expected Replay to have been sent, but a request was not attempted'
: `${this.utils.matcherHint('toHaveSentReplay', undefined, undefined, options)}\n\n${results
.map(({ key, expectedVal, actualVal }: Result) =>
this.utils.printDiffOrStringify(
expectedVal,
actualVal,
`Expected (key: ${key})`,
`Received (key: ${key})`,
),
printDiffOrStringify(expectedVal, actualVal, `Expected (key: ${key})`, `Received (key: ${key})`),
)
.join('\n')}`,
};
Expand All @@ -211,7 +207,7 @@ const toHaveSentReplay = function (
*/
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const toHaveLastSentReplay = function (
_received: jest.Mocked<ReplayContainer>,
_received: Mocked<ReplayContainer>,
expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean },
) {
const { calls } = (getClient()?.getTransport()?.send as MockTransport).mock;
Expand All @@ -235,12 +231,7 @@ const toHaveLastSentReplay = function (
: 'Expected Replay to have last been sent, but a request was not attempted'
: `${this.utils.matcherHint('toHaveSentReplay', undefined, undefined, options)}\n\n${results
.map(({ key, expectedVal, actualVal }: Result) =>
this.utils.printDiffOrStringify(
expectedVal,
actualVal,
`Expected (key: ${key})`,
`Received (key: ${key})`,
),
printDiffOrStringify(expectedVal, actualVal, `Expected (key: ${key})`, `Received (key: ${key})`),
)
.join('\n')}`,
};
Expand All @@ -252,18 +243,13 @@ expect.extend({
toHaveLastSentReplay,
});

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
interface AsymmetricMatchers {
toHaveSentReplay(expected?: SentReplayExpected): void;
toHaveLastSentReplay(expected?: SentReplayExpected): void;
toHaveSameSession(expected: undefined | Session): void;
}
interface Matchers<R> {
toHaveSentReplay(expected?: SentReplayExpected): R;
toHaveLastSentReplay(expected?: SentReplayExpected): R;
toHaveSameSession(expected: undefined | Session): R;
}
}
interface CustomMatchers<R = unknown> {
toHaveSentReplay(expected?: SentReplayExpected): R;
toHaveLastSentReplay(expected?: SentReplayExpected): R;
toHaveSameSession(expected: undefined | Session): R;
}

declare module 'vitest' {
type Assertion<T = any> = CustomMatchers<T>;
type AsymmetricMatchersContaining = CustomMatchers;
}
27 changes: 14 additions & 13 deletions packages/replay-internal/test/integration/autoSaveSession.test.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,42 @@
import { vi } from 'vitest';

import { EventType } from '@sentry-internal/rrweb';

import { saveSession } from '../../src/session/saveSession';
import type { RecordingEvent } from '../../src/types';
import { addEvent } from '../../src/util/addEvent';
import { resetSdkMock } from '../mocks/resetSdkMock';
import { useFakeTimers } from '../utils/use-fake-timers';

useFakeTimers();

vi.mock('../../src/session/saveSession', () => {
return {
saveSession: vi.fn(),
};
});

describe('Integration | autoSaveSession', () => {
afterEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});

test.each([
['with stickySession=true', true, 1],
['with stickySession=false', false, 0],
])('%s', async (_: string, stickySession: boolean, addSummand: number) => {
const saveSessionSpy = jest.fn();

jest.mock('../../src/session/saveSession', () => {
return {
saveSession: saveSessionSpy,
};
});

const { replay } = await resetSdkMock({
replayOptions: {
stickySession,
},
});

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

replay['_updateSessionActivity']();

expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 4);
expect(saveSession).toHaveBeenCalledTimes(addSummand * 4);

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

addEvent(replay, event);

await replay['_runFlush']();
await Promise.all([replay['_runFlush'](), vi.runAllTimersAsync()]);

expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 5);
expect(saveSession).toHaveBeenCalledTimes(addSummand * 5);
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { vi } from 'vitest';
import type { MockInstance, MockedFunction } from 'vitest';

import * as SentryBrowserUtils from '@sentry-internal/browser-utils';
import * as SentryCore from '@sentry/core';
import type { Transport } from '@sentry/types';
Expand All @@ -14,24 +17,19 @@ import { useFakeTimers } from '../utils/use-fake-timers';

useFakeTimers();

async function advanceTimers(time: number) {
jest.advanceTimersByTime(time);
await new Promise(process.nextTick);
}

type MockTransportSend = jest.MockedFunction<Transport['send']>;
type MockTransportSend = MockedFunction<Transport['send']>;

describe('Integration | beforeAddRecordingEvent', () => {
let replay: ReplayContainer;
let integration: Replay;
let mockTransportSend: MockTransportSend;
let mockSendReplayRequest: jest.SpyInstance<any>;
let mockSendReplayRequest: MockInstance<any>;
let domHandler: DomHandler;
const { record: mockRecord } = mockRrweb();

beforeAll(async () => {
jest.setSystemTime(new Date(BASE_TIMESTAMP));
jest.spyOn(SentryBrowserUtils, 'addClickKeypressInstrumentationHandler').mockImplementation(handler => {
vi.setSystemTime(new Date(BASE_TIMESTAMP));
vi.spyOn(SentryBrowserUtils, 'addClickKeypressInstrumentationHandler').mockImplementation(handler => {
domHandler = handler;
});

Expand Down Expand Up @@ -69,14 +67,14 @@ describe('Integration | beforeAddRecordingEvent', () => {
},
}));

mockSendReplayRequest = jest.spyOn(SendReplayRequest, 'sendReplayRequest');
mockSendReplayRequest = vi.spyOn(SendReplayRequest, 'sendReplayRequest');

jest.runAllTimers();
vi.runAllTimers();
mockTransportSend = SentryCore.getClient()?.getTransport()?.send as MockTransportSend;
});

beforeEach(() => {
jest.setSystemTime(new Date(BASE_TIMESTAMP));
vi.setSystemTime(new Date(BASE_TIMESTAMP));
mockRecord.takeFullSnapshot.mockClear();
mockTransportSend.mockClear();

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

afterEach(async () => {
jest.runAllTimers();
vi.runAllTimers();
await new Promise(process.nextTick);
jest.setSystemTime(new Date(BASE_TIMESTAMP));
vi.setSystemTime(new Date(BASE_TIMESTAMP));
clearSession(replay);
});

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

await advanceTimers(5000);
await vi.runAllTimersAsync();

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

integration.start();

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

jest.runAllTimers();
await new Promise(process.nextTick);
await vi.runAllTimersAsync();

expect(replay).not.toHaveLastSentReplay();
expect(replay.isEnabled()).toBe(true);
Expand Down

0 comments on commit 4cc5301

Please sign in to comment.