migrate native-mock tests to vitest (phase 2)

This commit is contained in:
Samuel Newman
2026-06-29 18:18:05 +03:00
parent a70caf1829
commit b02d81661d
7 changed files with 100 additions and 82 deletions
+28 -6
View File
@@ -1,5 +1,6 @@
import {createDownloadResumable, deleteAsync} from 'expo-file-system/legacy' import {createDownloadResumable, deleteAsync} from 'expo-file-system/legacy'
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import {type Mock, vi} from 'vitest'
import {IMAGE_SIZE_CONFIG_2K_1MB} from '../../src/lib/constants' import {IMAGE_SIZE_CONFIG_2K_1MB} from '../../src/lib/constants'
import { import {
@@ -8,6 +9,27 @@ import {
} from '../../src/lib/media/manip' } from '../../src/lib/media/manip'
import {getResizedDimensions} from '../../src/lib/media/util' 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 = { const mockResizedImage = {
path: 'file://resized-image.jpg', path: 'file://resized-image.jpg',
size: 100, size: 100,
@@ -17,10 +39,10 @@ const mockResizedImage = {
} }
describe('downloadAndResize', () => { describe('downloadAndResize', () => {
const errorSpy = jest.spyOn(global.console, 'error') const errorSpy = vi.spyOn(global.console, 'error')
beforeEach(() => { beforeEach(() => {
const mockedCreateResizedImage = manipulateAsync as jest.Mock const mockedCreateResizedImage = manipulateAsync as Mock
mockedCreateResizedImage.mockResolvedValue({ mockedCreateResizedImage.mockResolvedValue({
uri: 'file://resized-image.jpg', uri: 'file://resized-image.jpg',
...mockResizedImage, ...mockResizedImage,
@@ -28,14 +50,14 @@ describe('downloadAndResize', () => {
}) })
afterEach(() => { afterEach(() => {
jest.clearAllMocks() vi.clearAllMocks()
}) })
it('should return resized image for valid URI and options', async () => { it('should return resized image for valid URI and options', async () => {
const mockedFetch = createDownloadResumable as jest.Mock const mockedFetch = createDownloadResumable as Mock
mockedFetch.mockReturnValue({ mockedFetch.mockReturnValue({
cancelAsync: jest.fn(), cancelAsync: vi.fn(),
downloadAsync: jest downloadAsync: vi
.fn() .fn()
.mockResolvedValue({uri: 'file://resized-image.jpg'}), .mockResolvedValue({uri: 'file://resized-image.jpg'}),
}) })
-8
View File
@@ -114,14 +114,6 @@
"count": 1 "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": { "src/analytics/metadata.ts": {
"@typescript-eslint/no-explicit-any": { "@typescript-eslint/no-explicit-any": {
"count": 1 "count": 1
+25 -23
View File
@@ -1,39 +1,41 @@
jest.mock('#/storage', () => ({ import {vi} from 'vitest'
vi.mock('#/storage', () => ({
device: { device: {
get: jest.fn(), get: vi.fn(),
set: jest.fn(), set: vi.fn(),
}, },
})) }))
jest.mock('#/analytics/identifiers/util', () => ({ vi.mock('#/analytics/identifiers/util', () => ({
isSessionIdExpired: jest.fn(), isSessionIdExpired: vi.fn(),
})) }))
jest.mock('#/lib/appState', () => ({ vi.mock('#/lib/appState', () => ({
onAppStateChange: jest.fn(() => ({remove: jest.fn()})), onAppStateChange: vi.fn(() => ({remove: vi.fn()})),
})) }))
beforeEach(() => { beforeEach(() => {
jest.resetModules() vi.resetModules()
jest.clearAllMocks() vi.clearAllMocks()
}) })
function getMocks() { async function getMocks() {
const {device} = require('#/storage') const {device} = await import('#/storage')
const {isSessionIdExpired} = require('#/analytics/identifiers/util') const {isSessionIdExpired} = await import('#/analytics/identifiers/util')
return { return {
device: jest.mocked(device), device: vi.mocked(device),
isSessionIdExpired: jest.mocked(isSessionIdExpired), isSessionIdExpired: vi.mocked(isSessionIdExpired),
} }
} }
describe('session initialization', () => { describe('session initialization', () => {
it('creates new session and sets timestamp when none exists', () => { it('creates new session and sets timestamp when none exists', async () => {
const {device, isSessionIdExpired} = getMocks() const {device, isSessionIdExpired} = await getMocks()
device.get.mockReturnValue(undefined) device.get.mockReturnValue(undefined)
isSessionIdExpired.mockReturnValue(false) isSessionIdExpired.mockReturnValue(false)
const {getInitialSessionId} = require('./session') const {getInitialSessionId} = await import('./session')
const id = getInitialSessionId() const id = getInitialSessionId()
expect(id).toBeDefined() expect(id).toBeDefined()
@@ -45,8 +47,8 @@ describe('session initialization', () => {
) )
}) })
it('reuses existing session when not expired', () => { it('reuses existing session when not expired', async () => {
const {device, isSessionIdExpired} = getMocks() const {device, isSessionIdExpired} = await getMocks()
const existingId = 'existing-session-id' const existingId = 'existing-session-id'
device.get.mockImplementation((key: string[]) => { device.get.mockImplementation((key: string[]) => {
if (key[0] === 'nativeSessionId') return existingId if (key[0] === 'nativeSessionId') return existingId
@@ -55,13 +57,13 @@ describe('session initialization', () => {
}) })
isSessionIdExpired.mockReturnValue(false) isSessionIdExpired.mockReturnValue(false)
const {getInitialSessionId} = require('./session') const {getInitialSessionId} = await import('./session')
expect(getInitialSessionId()).toBe(existingId) expect(getInitialSessionId()).toBe(existingId)
}) })
it('creates new session when existing is expired', () => { it('creates new session when existing is expired', async () => {
const {device, isSessionIdExpired} = getMocks() const {device, isSessionIdExpired} = await getMocks()
const existingId = 'existing-session-id' const existingId = 'existing-session-id'
device.get.mockImplementation((key: string[]) => { device.get.mockImplementation((key: string[]) => {
if (key[0] === 'nativeSessionId') return existingId if (key[0] === 'nativeSessionId') return existingId
@@ -70,7 +72,7 @@ describe('session initialization', () => {
}) })
isSessionIdExpired.mockReturnValue(true) isSessionIdExpired.mockReturnValue(true)
const {getInitialSessionId} = require('./session') const {getInitialSessionId} = await import('./session')
const id = getInitialSessionId() const id = getInitialSessionId()
expect(id).not.toBe(existingId) expect(id).not.toBe(existingId)
+23 -21
View File
@@ -1,26 +1,28 @@
import {type Mock, vi} from 'vitest'
import {MetricsClient} from './client' import {MetricsClient} from './client'
let appStateCallback: (state: string) => void let appStateCallback: (state: string) => void
jest.mock('#/lib/appState', () => ({ vi.mock('#/lib/appState', () => ({
onAppStateChange: jest.fn(cb => { onAppStateChange: vi.fn(cb => {
appStateCallback = cb appStateCallback = cb
return {remove: jest.fn()} return {remove: vi.fn()}
}), }),
})) }))
jest.mock('#/logger', () => ({ vi.mock('#/logger', () => ({
Logger: { Logger: {
create: () => ({ create: () => ({
info: jest.fn(), info: vi.fn(),
debug: jest.fn(), debug: vi.fn(),
error: jest.fn(), error: vi.fn(),
}), }),
Context: {Metric: 'metric'}, Context: {Metric: 'metric'},
}, },
})) }))
jest.mock('#/env', () => ({ vi.mock('#/env', () => ({
METRICS_API_HOST: 'https://test.metrics.api', METRICS_API_HOST: 'https://test.metrics.api',
IS_WEB: false, IS_WEB: false,
})) }))
@@ -31,13 +33,13 @@ type TestEvents = {
} }
describe('MetricsClient', () => { describe('MetricsClient', () => {
let fetchMock: jest.Mock let fetchMock: Mock
let fetchRequests: {body: any}[] let fetchRequests: {body: any}[]
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers({advanceTimers: true}) vi.useFakeTimers({shouldAdvanceTime: true})
fetchRequests = [] fetchRequests = []
fetchMock = jest.fn().mockImplementation(async (_url, options) => { fetchMock = vi.fn().mockImplementation(async (_url, options) => {
const body = JSON.parse(options.body) const body = JSON.parse(options.body)
fetchRequests.push({body}) fetchRequests.push({body})
return {ok: true, status: 200} return {ok: true, status: 200}
@@ -46,8 +48,8 @@ describe('MetricsClient', () => {
}) })
afterEach(() => { afterEach(() => {
jest.useRealTimers() vi.useRealTimers()
jest.clearAllMocks() vi.clearAllMocks()
}) })
it('flushes events on interval', async () => { it('flushes events on interval', async () => {
@@ -58,7 +60,7 @@ describe('MetricsClient', () => {
expect(fetchRequests).toHaveLength(0) expect(fetchRequests).toHaveLength(0)
// Advance past the 10 second interval // Advance past the 10 second interval
await jest.advanceTimersByTimeAsync(10_000) await vi.advanceTimersByTimeAsync(10_000)
expect(fetchRequests).toHaveLength(1) expect(fetchRequests).toHaveLength(1)
expect(fetchRequests[0].body.events).toHaveLength(2) expect(fetchRequests[0].body.events).toHaveLength(2)
@@ -81,7 +83,7 @@ describe('MetricsClient', () => {
client.track('click', {button: 'btn-trigger'}) client.track('click', {button: 'btn-trigger'})
// Allow microtasks to run // Allow microtasks to run
await jest.advanceTimersByTimeAsync(0) await vi.advanceTimersByTimeAsync(0)
expect(fetchRequests).toHaveLength(1) expect(fetchRequests).toHaveLength(1)
expect(fetchRequests[0].body.events).toHaveLength(6) expect(fetchRequests[0].body.events).toHaveLength(6)
@@ -112,14 +114,14 @@ describe('MetricsClient', () => {
client.track('click', {button: 'submit'}) client.track('click', {button: 'submit'})
// Trigger flush via interval // Trigger flush via interval
await jest.advanceTimersByTimeAsync(10_000) await vi.advanceTimersByTimeAsync(10_000)
expect(requestCount).toBe(1) expect(requestCount).toBe(1)
expect(fetchRequests).toHaveLength(0) expect(fetchRequests).toHaveLength(0)
// Simulate app coming to foreground to trigger retry // Simulate app coming to foreground to trigger retry
appStateCallback('active') appStateCallback('active')
await jest.advanceTimersByTimeAsync(0) await vi.advanceTimersByTimeAsync(0)
expect(requestCount).toBe(2) expect(requestCount).toBe(2)
expect(fetchRequests).toHaveLength(1) expect(fetchRequests).toHaveLength(1)
@@ -144,19 +146,19 @@ describe('MetricsClient', () => {
client.track('click', {button: 'submit'}) client.track('click', {button: 'submit'})
// First flush fails // First flush fails
await jest.advanceTimersByTimeAsync(10_000) await vi.advanceTimersByTimeAsync(10_000)
expect(requestCount).toBe(1) expect(requestCount).toBe(1)
// Retry also fails // Retry also fails
appStateCallback('active') appStateCallback('active')
await jest.advanceTimersByTimeAsync(0) await vi.advanceTimersByTimeAsync(0)
expect(requestCount).toBe(2) expect(requestCount).toBe(2)
// Another foreground event should not retry again (events are dropped) // Another foreground event should not retry again (events are dropped)
appStateCallback('active') appStateCallback('active')
await jest.advanceTimersByTimeAsync(0) await vi.advanceTimersByTimeAsync(0)
expect(requestCount).toBe(2) // No additional requests expect(requestCount).toBe(2) // No additional requests
}) })
@@ -169,7 +171,7 @@ describe('MetricsClient', () => {
// Simulate app going to background // Simulate app going to background
appStateCallback('background') appStateCallback('background')
await jest.advanceTimersByTimeAsync(0) await vi.advanceTimersByTimeAsync(0)
expect(fetchRequests).toHaveLength(1) expect(fetchRequests).toHaveLength(1)
}) })
@@ -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 { MMKV: class MMKVMock {
_store = new Map<string, string>() _store = new Map<string, string>()
+20 -20
View File
@@ -1,19 +1,19 @@
import {beforeAll, describe, expect, jest, test} from '@jest/globals'
import * as Sentry from '@sentry/react-native' import * as Sentry from '@sentry/react-native'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {vi} from 'vitest'
import {Logger} from '#/logger' import {Logger} from '#/logger'
import {sentryTransport} from '#/logger/transports/sentry' import {sentryTransport} from '#/logger/transports/sentry'
import {LogLevel} from '#/logger/types' import {LogLevel} from '#/logger/types'
jest.mock('@sentry/react-native', () => ({ vi.mock('@sentry/react-native', () => ({
addBreadcrumb: jest.fn(), addBreadcrumb: vi.fn(),
captureException: jest.fn(), captureException: vi.fn(),
captureMessage: jest.fn(), captureMessage: vi.fn(),
})) }))
beforeAll(() => { beforeAll(() => {
jest.useFakeTimers() vi.useFakeTimers()
}) })
describe('general functionality', () => { describe('general functionality', () => {
@@ -41,7 +41,7 @@ describe('general functionality', () => {
const timestamp = Date.now() const timestamp = Date.now()
const logger = new Logger({}) const logger = new Logger({})
const mockTransport = jest.fn() const mockTransport = vi.fn()
logger.addTransport(mockTransport) logger.addTransport(mockTransport)
@@ -63,7 +63,7 @@ describe('general functionality', () => {
metadata: {bar: true}, metadata: {bar: true},
}) })
const mockTransport = jest.fn() const mockTransport = vi.fn()
logger.addTransport(mockTransport) logger.addTransport(mockTransport)
@@ -83,7 +83,7 @@ describe('general functionality', () => {
const timestamp = Date.now() const timestamp = Date.now()
const logger = new Logger({}) const logger = new Logger({})
const mockTransport = jest.fn() const mockTransport = vi.fn()
const remove = logger.addTransport(mockTransport) const remove = logger.addTransport(mockTransport)
@@ -181,7 +181,7 @@ describe('general functionality', () => {
level: 'log', level: 'log',
timestamp: sentryTimestamp, timestamp: sentryTimestamp,
}) })
jest.runAllTimers() vi.runAllTimers()
expect(Sentry.captureMessage).toHaveBeenCalledWith(message, { expect(Sentry.captureMessage).toHaveBeenCalledWith(message, {
level: 'log', level: 'log',
tags: {category: 'logger'}, tags: {category: 'logger'},
@@ -203,7 +203,7 @@ describe('general functionality', () => {
level: 'warning', level: 'warning',
timestamp: sentryTimestamp, timestamp: sentryTimestamp,
}) })
jest.runAllTimers() vi.runAllTimers()
expect(Sentry.captureMessage).toHaveBeenCalledWith(message, { expect(Sentry.captureMessage).toHaveBeenCalledWith(message, {
level: 'warning', level: 'warning',
tags: {category: 'logger'}, tags: {category: 'logger'},
@@ -262,7 +262,7 @@ describe('general functionality', () => {
test('add/remove transport', () => { test('add/remove transport', () => {
const timestamp = Date.now() const timestamp = Date.now()
const logger = new Logger({}) const logger = new Logger({})
const mockTransport = jest.fn() const mockTransport = vi.fn()
const remove = logger.addTransport(mockTransport) const remove = logger.addTransport(mockTransport)
@@ -286,7 +286,7 @@ describe('general functionality', () => {
describe('create', () => { describe('create', () => {
test('create', () => { test('create', () => {
const mockTransport = jest.fn() const mockTransport = vi.fn()
const timestamp = Date.now() const timestamp = Date.now()
const message = nanoid() const message = nanoid()
const logger = Logger.create(Logger.Context.Default) const logger = Logger.create(Logger.Context.Default)
@@ -306,7 +306,7 @@ describe('create', () => {
describe('debug contexts', () => { describe('debug contexts', () => {
test('specific', () => { test('specific', () => {
const mockTransport = jest.fn() const mockTransport = vi.fn()
const timestamp = Date.now() const timestamp = Date.now()
const message = nanoid() const message = nanoid()
const logger = new Logger({ const logger = new Logger({
@@ -328,7 +328,7 @@ describe('debug contexts', () => {
}) })
test('namespaced', () => { test('namespaced', () => {
const mockTransport = jest.fn() const mockTransport = vi.fn()
const timestamp = Date.now() const timestamp = Date.now()
const message = nanoid() const message = nanoid()
const logger = new Logger({ const logger = new Logger({
@@ -351,7 +351,7 @@ describe('debug contexts', () => {
}) })
test('ignores inactive', () => { test('ignores inactive', () => {
const mockTransport = jest.fn() const mockTransport = vi.fn()
const timestamp = Date.now() const timestamp = Date.now()
const message = nanoid() const message = nanoid()
const logger = new Logger({ const logger = new Logger({
@@ -380,7 +380,7 @@ describe('supports levels', () => {
level: LogLevel.Debug, level: LogLevel.Debug,
}) })
const message = nanoid() const message = nanoid()
const mockTransport = jest.fn() const mockTransport = vi.fn()
logger.addTransport(mockTransport) logger.addTransport(mockTransport)
@@ -428,7 +428,7 @@ describe('supports levels', () => {
level: LogLevel.Info, level: LogLevel.Info,
}) })
const message = nanoid() const message = nanoid()
const mockTransport = jest.fn() const mockTransport = vi.fn()
logger.addTransport(mockTransport) logger.addTransport(mockTransport)
@@ -451,7 +451,7 @@ describe('supports levels', () => {
level: LogLevel.Warn, level: LogLevel.Warn,
}) })
const message = nanoid() const message = nanoid()
const mockTransport = jest.fn() const mockTransport = vi.fn()
logger.addTransport(mockTransport) logger.addTransport(mockTransport)
@@ -477,7 +477,7 @@ describe('supports levels', () => {
level: LogLevel.Error, level: LogLevel.Error,
}) })
const message = nanoid() const message = nanoid()
const mockTransport = jest.fn() const mockTransport = vi.fn()
logger.addTransport(mockTransport) logger.addTransport(mockTransport)
+2 -2
View File
@@ -1,8 +1,8 @@
import {beforeEach, expect, jest, test} from '@jest/globals' import {vi} from 'vitest'
import {Storage} from '#/storage' import {Storage} from '#/storage'
jest.mock('@bsky.app/react-native-mmkv', () => ({ vi.mock('@bsky.app/react-native-mmkv', () => ({
MMKV: class MMKVMock { MMKV: class MMKVMock {
_store = new Map() _store = new Map()