Skip to content

feat(replay): Use vitest instead of jest #11899

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 .",
Comment on lines +54 to +56
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this, kept running into lint errors because I only want to yarn fix this package.

"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",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our custom matchers use a util fn from this lib to pretty print diffs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can possibly refactor this later to avoid this dependency, but all good for now!

"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,
};
});

Comment on lines -19 to -26
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets hoisted and yells at you for using vars outside of scope

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
Loading
Loading