From b02d81661d58490c1b16822541c89678a940ee47 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 29 Jun 2026 18:18:05 +0300 Subject: [PATCH] migrate native-mock tests to vitest (phase 2) --- __tests__/lib/images.test.ts | 34 ++++++++++--- eslint-suppressions.json | 8 ---- src/analytics/identifiers/session.test.ts | 48 ++++++++++--------- src/analytics/metrics/client.test.ts | 44 +++++++++-------- .../__tests__/persisted-query-storage.test.ts | 4 +- src/logger/__tests__/logger.test.ts | 40 ++++++++-------- src/storage/__tests__/index.test.ts | 4 +- 7 files changed, 100 insertions(+), 82 deletions(-) diff --git a/__tests__/lib/images.test.ts b/__tests__/lib/images.test.ts index f0f948365c..1f8a324e73 100644 --- a/__tests__/lib/images.test.ts +++ b/__tests__/lib/images.test.ts @@ -1,5 +1,6 @@ import {createDownloadResumable, deleteAsync} from 'expo-file-system/legacy' import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' +import {type Mock, vi} from 'vitest' import {IMAGE_SIZE_CONFIG_2K_1MB} from '../../src/lib/constants' import { @@ -8,6 +9,27 @@ import { } from '../../src/lib/media/manip' import {getResizedDimensions} from '../../src/lib/media/util' +vi.mock('expo-file-system/legacy', () => ({ + cacheDirectory: 'file://cache/', + EncodingType: {UTF8: 'utf8', Base64: 'base64'}, + StorageAccessFramework: {}, + getInfoAsync: vi.fn().mockResolvedValue({exists: true, size: 100}), + deleteAsync: vi.fn(), + moveAsync: vi.fn().mockResolvedValue(undefined), + makeDirectoryAsync: vi.fn().mockResolvedValue(undefined), + writeAsStringAsync: vi.fn().mockResolvedValue(undefined), + createDownloadResumable: vi.fn(), +})) + +vi.mock('expo-media-library', () => ({ + usePermissions: vi.fn(() => [true]), +})) + +vi.mock('expo-image-manipulator', () => ({ + manipulateAsync: vi.fn().mockResolvedValue({uri: 'file://resized-image'}), + SaveFormat: {JPEG: 'jpeg', WEBP: 'webp'}, +})) + const mockResizedImage = { path: 'file://resized-image.jpg', size: 100, @@ -17,10 +39,10 @@ const mockResizedImage = { } describe('downloadAndResize', () => { - const errorSpy = jest.spyOn(global.console, 'error') + const errorSpy = vi.spyOn(global.console, 'error') beforeEach(() => { - const mockedCreateResizedImage = manipulateAsync as jest.Mock + const mockedCreateResizedImage = manipulateAsync as Mock mockedCreateResizedImage.mockResolvedValue({ uri: 'file://resized-image.jpg', ...mockResizedImage, @@ -28,14 +50,14 @@ describe('downloadAndResize', () => { }) afterEach(() => { - jest.clearAllMocks() + vi.clearAllMocks() }) it('should return resized image for valid URI and options', async () => { - const mockedFetch = createDownloadResumable as jest.Mock + const mockedFetch = createDownloadResumable as Mock mockedFetch.mockReturnValue({ - cancelAsync: jest.fn(), - downloadAsync: jest + cancelAsync: vi.fn(), + downloadAsync: vi .fn() .mockResolvedValue({uri: 'file://resized-image.jpg'}), }) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 152355c62a..9b3965af3d 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -114,14 +114,6 @@ "count": 1 } }, - "src/analytics/identifiers/session.test.ts": { - "@typescript-eslint/no-unsafe-call": { - "count": 9 - }, - "@typescript-eslint/no-unsafe-member-access": { - "count": 9 - } - }, "src/analytics/metadata.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 diff --git a/src/analytics/identifiers/session.test.ts b/src/analytics/identifiers/session.test.ts index ab334ff497..699a6226b3 100644 --- a/src/analytics/identifiers/session.test.ts +++ b/src/analytics/identifiers/session.test.ts @@ -1,39 +1,41 @@ -jest.mock('#/storage', () => ({ +import {vi} from 'vitest' + +vi.mock('#/storage', () => ({ device: { - get: jest.fn(), - set: jest.fn(), + get: vi.fn(), + set: vi.fn(), }, })) -jest.mock('#/analytics/identifiers/util', () => ({ - isSessionIdExpired: jest.fn(), +vi.mock('#/analytics/identifiers/util', () => ({ + isSessionIdExpired: vi.fn(), })) -jest.mock('#/lib/appState', () => ({ - onAppStateChange: jest.fn(() => ({remove: jest.fn()})), +vi.mock('#/lib/appState', () => ({ + onAppStateChange: vi.fn(() => ({remove: vi.fn()})), })) beforeEach(() => { - jest.resetModules() - jest.clearAllMocks() + vi.resetModules() + vi.clearAllMocks() }) -function getMocks() { - const {device} = require('#/storage') - const {isSessionIdExpired} = require('#/analytics/identifiers/util') +async function getMocks() { + const {device} = await import('#/storage') + const {isSessionIdExpired} = await import('#/analytics/identifiers/util') return { - device: jest.mocked(device), - isSessionIdExpired: jest.mocked(isSessionIdExpired), + device: vi.mocked(device), + isSessionIdExpired: vi.mocked(isSessionIdExpired), } } describe('session initialization', () => { - it('creates new session and sets timestamp when none exists', () => { - const {device, isSessionIdExpired} = getMocks() + it('creates new session and sets timestamp when none exists', async () => { + const {device, isSessionIdExpired} = await getMocks() device.get.mockReturnValue(undefined) isSessionIdExpired.mockReturnValue(false) - const {getInitialSessionId} = require('./session') + const {getInitialSessionId} = await import('./session') const id = getInitialSessionId() expect(id).toBeDefined() @@ -45,8 +47,8 @@ describe('session initialization', () => { ) }) - it('reuses existing session when not expired', () => { - const {device, isSessionIdExpired} = getMocks() + it('reuses existing session when not expired', async () => { + const {device, isSessionIdExpired} = await getMocks() const existingId = 'existing-session-id' device.get.mockImplementation((key: string[]) => { if (key[0] === 'nativeSessionId') return existingId @@ -55,13 +57,13 @@ describe('session initialization', () => { }) isSessionIdExpired.mockReturnValue(false) - const {getInitialSessionId} = require('./session') + const {getInitialSessionId} = await import('./session') expect(getInitialSessionId()).toBe(existingId) }) - it('creates new session when existing is expired', () => { - const {device, isSessionIdExpired} = getMocks() + it('creates new session when existing is expired', async () => { + const {device, isSessionIdExpired} = await getMocks() const existingId = 'existing-session-id' device.get.mockImplementation((key: string[]) => { if (key[0] === 'nativeSessionId') return existingId @@ -70,7 +72,7 @@ describe('session initialization', () => { }) isSessionIdExpired.mockReturnValue(true) - const {getInitialSessionId} = require('./session') + const {getInitialSessionId} = await import('./session') const id = getInitialSessionId() expect(id).not.toBe(existingId) diff --git a/src/analytics/metrics/client.test.ts b/src/analytics/metrics/client.test.ts index 000e2894bf..45059b7bc1 100644 --- a/src/analytics/metrics/client.test.ts +++ b/src/analytics/metrics/client.test.ts @@ -1,26 +1,28 @@ +import {type Mock, vi} from 'vitest' + import {MetricsClient} from './client' let appStateCallback: (state: string) => void -jest.mock('#/lib/appState', () => ({ - onAppStateChange: jest.fn(cb => { +vi.mock('#/lib/appState', () => ({ + onAppStateChange: vi.fn(cb => { appStateCallback = cb - return {remove: jest.fn()} + return {remove: vi.fn()} }), })) -jest.mock('#/logger', () => ({ +vi.mock('#/logger', () => ({ Logger: { create: () => ({ - info: jest.fn(), - debug: jest.fn(), - error: jest.fn(), + info: vi.fn(), + debug: vi.fn(), + error: vi.fn(), }), Context: {Metric: 'metric'}, }, })) -jest.mock('#/env', () => ({ +vi.mock('#/env', () => ({ METRICS_API_HOST: 'https://test.metrics.api', IS_WEB: false, })) @@ -31,13 +33,13 @@ type TestEvents = { } describe('MetricsClient', () => { - let fetchMock: jest.Mock + let fetchMock: Mock let fetchRequests: {body: any}[] beforeEach(() => { - jest.useFakeTimers({advanceTimers: true}) + vi.useFakeTimers({shouldAdvanceTime: true}) fetchRequests = [] - fetchMock = jest.fn().mockImplementation(async (_url, options) => { + fetchMock = vi.fn().mockImplementation(async (_url, options) => { const body = JSON.parse(options.body) fetchRequests.push({body}) return {ok: true, status: 200} @@ -46,8 +48,8 @@ describe('MetricsClient', () => { }) afterEach(() => { - jest.useRealTimers() - jest.clearAllMocks() + vi.useRealTimers() + vi.clearAllMocks() }) it('flushes events on interval', async () => { @@ -58,7 +60,7 @@ describe('MetricsClient', () => { expect(fetchRequests).toHaveLength(0) // Advance past the 10 second interval - await jest.advanceTimersByTimeAsync(10_000) + await vi.advanceTimersByTimeAsync(10_000) expect(fetchRequests).toHaveLength(1) expect(fetchRequests[0].body.events).toHaveLength(2) @@ -81,7 +83,7 @@ describe('MetricsClient', () => { client.track('click', {button: 'btn-trigger'}) // Allow microtasks to run - await jest.advanceTimersByTimeAsync(0) + await vi.advanceTimersByTimeAsync(0) expect(fetchRequests).toHaveLength(1) expect(fetchRequests[0].body.events).toHaveLength(6) @@ -112,14 +114,14 @@ describe('MetricsClient', () => { client.track('click', {button: 'submit'}) // Trigger flush via interval - await jest.advanceTimersByTimeAsync(10_000) + await vi.advanceTimersByTimeAsync(10_000) expect(requestCount).toBe(1) expect(fetchRequests).toHaveLength(0) // Simulate app coming to foreground to trigger retry appStateCallback('active') - await jest.advanceTimersByTimeAsync(0) + await vi.advanceTimersByTimeAsync(0) expect(requestCount).toBe(2) expect(fetchRequests).toHaveLength(1) @@ -144,19 +146,19 @@ describe('MetricsClient', () => { client.track('click', {button: 'submit'}) // First flush fails - await jest.advanceTimersByTimeAsync(10_000) + await vi.advanceTimersByTimeAsync(10_000) expect(requestCount).toBe(1) // Retry also fails appStateCallback('active') - await jest.advanceTimersByTimeAsync(0) + await vi.advanceTimersByTimeAsync(0) expect(requestCount).toBe(2) // Another foreground event should not retry again (events are dropped) appStateCallback('active') - await jest.advanceTimersByTimeAsync(0) + await vi.advanceTimersByTimeAsync(0) expect(requestCount).toBe(2) // No additional requests }) @@ -169,7 +171,7 @@ describe('MetricsClient', () => { // Simulate app going to background appStateCallback('background') - await jest.advanceTimersByTimeAsync(0) + await vi.advanceTimersByTimeAsync(0) expect(fetchRequests).toHaveLength(1) }) diff --git a/src/lib/__tests__/persisted-query-storage.test.ts b/src/lib/__tests__/persisted-query-storage.test.ts index fe6a61884b..a9a78387c5 100644 --- a/src/lib/__tests__/persisted-query-storage.test.ts +++ b/src/lib/__tests__/persisted-query-storage.test.ts @@ -1,6 +1,6 @@ -import {beforeEach, describe, expect, it, jest} from '@jest/globals' +import {vi} from 'vitest' -jest.mock('@bsky.app/react-native-mmkv', () => ({ +vi.mock('@bsky.app/react-native-mmkv', () => ({ MMKV: class MMKVMock { _store = new Map() diff --git a/src/logger/__tests__/logger.test.ts b/src/logger/__tests__/logger.test.ts index 2f6915a4b3..d15681d8e8 100644 --- a/src/logger/__tests__/logger.test.ts +++ b/src/logger/__tests__/logger.test.ts @@ -1,19 +1,19 @@ -import {beforeAll, describe, expect, jest, test} from '@jest/globals' import * as Sentry from '@sentry/react-native' import {nanoid} from 'nanoid/non-secure' +import {vi} from 'vitest' import {Logger} from '#/logger' import {sentryTransport} from '#/logger/transports/sentry' import {LogLevel} from '#/logger/types' -jest.mock('@sentry/react-native', () => ({ - addBreadcrumb: jest.fn(), - captureException: jest.fn(), - captureMessage: jest.fn(), +vi.mock('@sentry/react-native', () => ({ + addBreadcrumb: vi.fn(), + captureException: vi.fn(), + captureMessage: vi.fn(), })) beforeAll(() => { - jest.useFakeTimers() + vi.useFakeTimers() }) describe('general functionality', () => { @@ -41,7 +41,7 @@ describe('general functionality', () => { const timestamp = Date.now() const logger = new Logger({}) - const mockTransport = jest.fn() + const mockTransport = vi.fn() logger.addTransport(mockTransport) @@ -63,7 +63,7 @@ describe('general functionality', () => { metadata: {bar: true}, }) - const mockTransport = jest.fn() + const mockTransport = vi.fn() logger.addTransport(mockTransport) @@ -83,7 +83,7 @@ describe('general functionality', () => { const timestamp = Date.now() const logger = new Logger({}) - const mockTransport = jest.fn() + const mockTransport = vi.fn() const remove = logger.addTransport(mockTransport) @@ -181,7 +181,7 @@ describe('general functionality', () => { level: 'log', timestamp: sentryTimestamp, }) - jest.runAllTimers() + vi.runAllTimers() expect(Sentry.captureMessage).toHaveBeenCalledWith(message, { level: 'log', tags: {category: 'logger'}, @@ -203,7 +203,7 @@ describe('general functionality', () => { level: 'warning', timestamp: sentryTimestamp, }) - jest.runAllTimers() + vi.runAllTimers() expect(Sentry.captureMessage).toHaveBeenCalledWith(message, { level: 'warning', tags: {category: 'logger'}, @@ -262,7 +262,7 @@ describe('general functionality', () => { test('add/remove transport', () => { const timestamp = Date.now() const logger = new Logger({}) - const mockTransport = jest.fn() + const mockTransport = vi.fn() const remove = logger.addTransport(mockTransport) @@ -286,7 +286,7 @@ describe('general functionality', () => { describe('create', () => { test('create', () => { - const mockTransport = jest.fn() + const mockTransport = vi.fn() const timestamp = Date.now() const message = nanoid() const logger = Logger.create(Logger.Context.Default) @@ -306,7 +306,7 @@ describe('create', () => { describe('debug contexts', () => { test('specific', () => { - const mockTransport = jest.fn() + const mockTransport = vi.fn() const timestamp = Date.now() const message = nanoid() const logger = new Logger({ @@ -328,7 +328,7 @@ describe('debug contexts', () => { }) test('namespaced', () => { - const mockTransport = jest.fn() + const mockTransport = vi.fn() const timestamp = Date.now() const message = nanoid() const logger = new Logger({ @@ -351,7 +351,7 @@ describe('debug contexts', () => { }) test('ignores inactive', () => { - const mockTransport = jest.fn() + const mockTransport = vi.fn() const timestamp = Date.now() const message = nanoid() const logger = new Logger({ @@ -380,7 +380,7 @@ describe('supports levels', () => { level: LogLevel.Debug, }) const message = nanoid() - const mockTransport = jest.fn() + const mockTransport = vi.fn() logger.addTransport(mockTransport) @@ -428,7 +428,7 @@ describe('supports levels', () => { level: LogLevel.Info, }) const message = nanoid() - const mockTransport = jest.fn() + const mockTransport = vi.fn() logger.addTransport(mockTransport) @@ -451,7 +451,7 @@ describe('supports levels', () => { level: LogLevel.Warn, }) const message = nanoid() - const mockTransport = jest.fn() + const mockTransport = vi.fn() logger.addTransport(mockTransport) @@ -477,7 +477,7 @@ describe('supports levels', () => { level: LogLevel.Error, }) const message = nanoid() - const mockTransport = jest.fn() + const mockTransport = vi.fn() logger.addTransport(mockTransport) diff --git a/src/storage/__tests__/index.test.ts b/src/storage/__tests__/index.test.ts index 0b57d365ff..f784a87ffe 100644 --- a/src/storage/__tests__/index.test.ts +++ b/src/storage/__tests__/index.test.ts @@ -1,8 +1,8 @@ -import {beforeEach, expect, jest, test} from '@jest/globals' +import {vi} from 'vitest' import {Storage} from '#/storage' -jest.mock('@bsky.app/react-native-mmkv', () => ({ +vi.mock('@bsky.app/react-native-mmkv', () => ({ MMKV: class MMKVMock { _store = new Map()