wip - claude --resume 742969c2-ab50-4a79-b7bd-3971e9fd314d

This commit is contained in:
Samuel Newman
2026-05-15 21:58:48 +03:00
parent f04b350f05
commit 1c60734e84
40 changed files with 1209 additions and 1571 deletions
+1 -2
View File
@@ -82,5 +82,4 @@ jobs:
- name: Check & compile i18n
run: pnpm intl:build
- name: Run tests
run: |
NODE_ENV=test pnpm test --forceExit
run: pnpm test-ci
+7 -6
View File
@@ -1,5 +1,6 @@
import React, {type ReactNode} from 'react'
import {FlatList, Modal, ScrollView, TextInput, View} from 'react-native'
import {vi} from 'vitest'
const BottomSheetModalContext = React.createContext(null)
BottomSheetModalContext.displayName = 'BottomSheetModalContext'
@@ -33,12 +34,12 @@ const BottomSheetScrollView = (props: any) => <ScrollView {...props} />
const BottomSheetFlatList = (props: any) => <FlatList {...props} />
const BottomSheetTextInput = (props: any) => <TextInput {...props} />
const useBottomSheet = jest.fn()
const useBottomSheetModal = jest.fn()
const useBottomSheetSpringConfigs = jest.fn()
const useBottomSheetTimingConfigs = jest.fn()
const useBottomSheetInternal = jest.fn()
const useBottomSheetDynamicSnapPoints = jest.fn()
const useBottomSheet = vi.fn()
const useBottomSheetModal = vi.fn()
const useBottomSheetSpringConfigs = vi.fn()
const useBottomSheetTimingConfigs = vi.fn()
const useBottomSheetInternal = vi.fn()
const useBottomSheetDynamicSnapPoints = vi.fn()
export {useBottomSheet}
export {useBottomSheetModal}
+3 -1
View File
@@ -1 +1,3 @@
export const getLocales = jest.fn().mockResolvedValue([])
import {vi} from 'vitest'
export const getLocales = vi.fn().mockResolvedValue([])
+3 -1
View File
@@ -1,3 +1,5 @@
export const CID = jest.fn().mockImplementation(() => {
import {vi} from 'vitest'
export const CID = vi.fn().mockImplementation(() => {
return {}
})
+4 -2
View File
@@ -1,6 +1,8 @@
export const from = jest.fn().mockImplementation(() => {
import {vi} from 'vitest'
export const from = vi.fn().mockImplementation(() => {
return {
digest: jest.fn().mockImplementation(() => {
digest: vi.fn().mockImplementation(() => {
return Promise.resolve('')
}),
}
+4 -2
View File
@@ -1,3 +1,5 @@
jest.mock('sentry-expo', () => ({
init: () => jest.fn(),
import {vi} from 'vitest'
vi.mock('sentry-expo', () => ({
init: () => vi.fn(),
}))
+2
View File
@@ -1,3 +1,5 @@
import {describe, expect, it} from 'vitest'
import {bundleAsync} from '../../../src/lib/async/bundle'
describe('bundle', () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import {describe, expect, it} from '@jest/globals'
import {describe, expect, it} from 'vitest'
import tldts from 'tldts'
import {isEmailMaybeInvalid} from '#/lib/strings/email'
+2
View File
@@ -1,3 +1,5 @@
import {describe, expect, it} from 'vitest'
import {isNetworkError} from '../../src/lib/strings/errors'
describe('isNetworkError', () => {
+6 -5
View File
@@ -1,5 +1,6 @@
import {createDownloadResumable, deleteAsync} from 'expo-file-system/legacy'
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
import {
downloadAndResize,
@@ -16,10 +17,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,
@@ -27,13 +28,13 @@ 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(),
cancelAsync: vi.fn(),
downloadAsync: jest
.fn()
.mockResolvedValue({uri: 'file://resized-image.jpg'}),
+2
View File
@@ -1,3 +1,5 @@
import {describe, expect, it} from 'vitest'
import {getLikelyType, LikelyType} from '../../src/lib/link-meta/link-meta'
describe('getLikelyType', () => {
+2
View File
@@ -1,3 +1,5 @@
import {describe, expect, it} from 'vitest'
import {clamp} from '../../src/lib/numbers'
describe('clamp', () => {
+1
View File
@@ -1,5 +1,6 @@
import {RichText} from '@atproto/api'
import {i18n} from '@lingui/core'
import {describe, expect, it} from 'vitest'
import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
import {
+2
View File
@@ -1,3 +1,5 @@
import {describe, expect, it} from 'vitest'
import {type IsValidHandle, validateServiceHandle} from '#/lib/strings/handles'
describe('handle validation', () => {
@@ -1,3 +1,5 @@
import {describe, expect, it} from 'vitest'
import {
getMentionAt,
insertMentionAt,
+1 -1
View File
@@ -1,4 +1,4 @@
import {describe, expect, it} from '@jest/globals'
import {describe, expect, it} from 'vitest'
import {
isPossiblyAUrl,
+1 -3
View File
@@ -1,6 +1,5 @@
module.exports = function (api) {
api.cache(true)
const isTestEnv = process.env.NODE_ENV === 'test'
return {
presets: [
[
@@ -9,8 +8,7 @@ module.exports = function (api) {
lazyImports: true,
native: {
// Disable ESM -> CJS compilation because Metro takes care of it.
// However, we need it in Jest tests since those run without Metro.
disableImportExportTransform: !isTestEnv,
disableImportExportTransform: true,
},
},
],
@@ -1,6 +1,12 @@
const {RuleTester} = require('eslint')
const tseslint = require('typescript-eslint')
const avoidUnwrappedText = require('../avoid-unwrapped-text')
import {RuleTester} from 'eslint'
import tseslint from 'typescript-eslint'
import {describe, it} from 'vitest'
import avoidUnwrappedText from '../avoid-unwrapped-text.js'
RuleTester.describe = describe
RuleTester.it = it
RuleTester.itOnly = it.only
const ruleTester = new RuleTester({
languageOptions: {
+9 -3
View File
@@ -1,6 +1,12 @@
const {RuleTester} = require('eslint')
const tseslint = require('typescript-eslint')
const linguiMsgRule = require('../lingui-msg-rule')
import {RuleTester} from 'eslint'
import tseslint from 'typescript-eslint'
import {describe, it} from 'vitest'
import linguiMsgRule from '../lingui-msg-rule.js'
RuleTester.describe = describe
RuleTester.it = it
RuleTester.itOnly = it.only
const ruleTester = new RuleTester({
languageOptions: {
-112
View File
@@ -1,112 +0,0 @@
/* global jest */
import 'react-native-gesture-handler/jestSetup'
import {configure} from '@testing-library/react-native'
configure({asyncUtilTimeout: 20000})
jest.mock('@react-native-async-storage/async-storage', () =>
require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
)
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => {
// eslint-disable-next-line import-x/no-nodejs-modules
const {EventEmitter} = require('events')
return {
__esModule: true,
default: EventEmitter,
}
})
jest.mock('@fortawesome/react-native-fontawesome', () => ({
FontAwesomeIcon: '',
}))
jest.mock('react-native-safe-area-context', () => {
const inset = {top: 0, right: 0, bottom: 0, left: 0}
return {
SafeAreaProvider: jest.fn().mockImplementation(({children}) => children),
SafeAreaConsumer: jest
.fn()
.mockImplementation(({children}) => children(inset)),
useSafeAreaInsets: jest.fn().mockImplementation(() => inset),
}
})
jest.mock('expo-file-system/legacy', () => ({
getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
deleteAsync: jest.fn(),
moveAsync: jest.fn().mockResolvedValue(undefined),
createDownloadResumable: jest.fn(),
}))
jest.mock('expo-image-manipulator', () => ({
manipulateAsync: jest.fn().mockResolvedValue({
uri: 'file://resized-image',
}),
SaveFormat: {
JPEG: 'jpeg',
WEBP: 'webp',
},
}))
jest.mock('expo-camera', () => ({
Camera: {
useCameraPermissions: jest.fn(() => [true]),
},
}))
jest.mock('expo-media-library', () => ({
__esModule: true, // this property makes it work
default: jest.fn(),
usePermissions: jest.fn(() => [true]),
}))
jest.mock('@bsky.app/expo-guess-language', () => ({
guessLanguageSync: jest
.fn()
.mockReturnValue([{language: 'en', confidence: 1}]),
guessLanguageAsync: jest
.fn()
.mockResolvedValue([{language: 'en', confidence: 1}]),
}))
jest.mock('sentry-expo', () => ({
init: () => jest.fn(),
Native: {
ReactNativeTracing: jest.fn().mockImplementation(() => ({
start: jest.fn(),
stop: jest.fn(),
})),
ReactNavigationInstrumentation: jest.fn(),
},
}))
jest.mock('crypto', () => ({}))
jest.mock('expo-application', () => ({
nativeApplicationVersion: '1.0.0',
nativeBuildVersion: '1',
}))
jest.mock('expo-modules-core', () => ({
requireNativeModule: jest.fn().mockImplementation(moduleName => {
if (moduleName === 'ExpoPlatformInfo') {
return {
getIsReducedMotionEnabled: () => false,
}
}
if (moduleName === 'BottomSheet') {
return {
dismissAll: () => {},
}
}
}),
requireNativeViewManager: jest.fn().mockImplementation(_ => {
return () => null
}),
createPermissionHook: () => () => [true],
}))
jest.mock('expo-localization', () => ({
getLocales: () => [],
}))
+8 -48
View File
@@ -44,10 +44,10 @@
"build-embed": "cd bskyembed && yarn build && yarn build-snippet && cd .. && node ./scripts/post-embed-build.js",
"start": "expo start --dev-client",
"start:prod": "expo start --dev-client --no-dev --minify",
"test": "NODE_ENV=test jest --forceExit --testTimeout=20000 --bail",
"test-watch": "NODE_ENV=test jest --watchAll",
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "NODE_ENV=test jest --coverage",
"test": "NODE_ENV=test vitest run --bail=1",
"test-watch": "NODE_ENV=test vitest",
"test-ci": "NODE_ENV=test vitest run --reporter=default --reporter=junit --outputFile.junit=./junit.xml",
"test-coverage": "NODE_ENV=test vitest run --coverage",
"lint": "eslint --cache --quiet src",
"lint-native": "swiftlint ./modules && ktlint ./modules",
"lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules",
@@ -226,7 +226,7 @@
"react-native-uitextview": "^1.4.0",
"react-native-uuid": "^2.0.3",
"react-native-view-shot": "^4.0.3",
"react-native-web": "^0.21.0",
"react-native-web": "^0.21.2",
"react-native-web-webview": "^1.0.2",
"react-native-webview": "^13.15.0",
"react-remove-scroll-bar": "^2.3.8",
@@ -250,12 +250,11 @@
"@eslint/js": "^9.39.2",
"@lingui/babel-plugin-lingui-macro": "^5.9.2",
"@lingui/cli": "^5.9.2",
"@lingui/vite-plugin": "^6.0.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@react-native/babel-preset": "0.81.5",
"@react-native/typescript-config": "^0.81.5",
"@sentry/webpack-plugin": "^3.2.2",
"@testing-library/react-native": "^13.2.0",
"@types/jest": "29.5.14",
"@types/lodash.chunk": "^4.2.7",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.shuffle": "^4.2.7",
@@ -263,7 +262,7 @@
"@types/react": "^19.1.17",
"@types/react-dom": "^19.1.11",
"@typescript/native-preview": "^7.0.0-dev.20260428.1",
"babel-jest": "^29.7.0",
"@vitest/coverage-v8": "^4.0.0",
"babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"babel-preset-expo": "~54.0.10",
@@ -282,9 +281,6 @@
"globals": "^17.0.0",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"jest": "^29.7.0",
"jest-expo": "~54.0.17",
"jest-junit": "^16.0.0",
"lint-staged": "^13.2.3",
"prettier": "^3.8.3",
"react-native-dotenv": "^3.4.11",
@@ -293,45 +289,9 @@
"ts-plugin-sort-import-suggestions": "^1.0.4",
"typescript": "^6.0.2",
"typescript-eslint": "^8.58.0",
"vitest": "^4.0.0",
"webpack-bundle-analyzer": "^4.10.1"
},
"jest": {
"preset": "jest-expo/ios",
"setupFilesAfterEnv": [
"./jest/jestSetup.js"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
"transform": {
"\\.[jt]sx?$": "babel-jest"
},
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|@discord|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match)"
],
"modulePathIgnorePatterns": [
"__tests__/.*/__mocks__",
"__e2e__/.*",
"bskylink/.*"
],
"coveragePathIgnorePatterns": [
"<rootDir>/node_modules/",
"<rootDir>/src/platform",
"<rootDir>/src/third-party",
"<rootDir>/src/view/com/util",
"<rootDir>/src/state/lib",
"<rootDir>/__tests__/test-utils.js"
],
"reporters": [
"default",
"jest-junit"
]
},
"browserslist": {
"production": [
">0.2%",
+834 -1277
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,10 +1,10 @@
import {jest} from '@jest/globals'
import {beforeEach, describe, expect, it, vi} from 'vitest'
import {transparentifyColor} from '../colorGeneration'
describe('transparentifyColor', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
it('converts hsl() to hsla()', () => {
+25 -23
View File
@@ -1,39 +1,41 @@
jest.mock('#/storage', () => ({
import {beforeEach, describe, expect, it, 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)
+23 -21
View File
@@ -1,26 +1,28 @@
import {afterEach, beforeEach, describe, expect, it, 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)
})
@@ -1,11 +1,11 @@
import {describe, test} from '@jest/globals'
import {describe, expect, test, vi} from 'vitest'
import {
computeCompletedState,
syncCompletedState,
} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
jest.mock('../../../state/queries/nuxs')
vi.mock('../../../state/queries/nuxs')
describe('computeCompletedState', () => {
test(`initial state`, () => {
@@ -67,8 +67,8 @@ describe('computeCompletedState', () => {
describe('syncCompletedState', () => {
describe('!nuxIsReady', () => {
test(`!completedForDevice, no-op`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
const save = vi.fn()
const setCompletedForDevice = vi.fn()
syncCompletedState({
nuxIsReady: false,
nuxIsCompleted: false,
@@ -83,8 +83,8 @@ describe('syncCompletedState', () => {
})
test(`completedForDevice, no-op`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
const save = vi.fn()
const setCompletedForDevice = vi.fn()
syncCompletedState({
nuxIsReady: false,
nuxIsCompleted: false,
@@ -103,8 +103,8 @@ describe('syncCompletedState', () => {
describe(`!nuxIsCompleted`, () => {
describe(`!nuxIsOptimisticallyCompleted`, () => {
test(`!completedForDevice, no-op`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
const save = vi.fn()
const setCompletedForDevice = vi.fn()
syncCompletedState({
nuxIsReady: true,
nuxIsCompleted: false,
@@ -119,8 +119,8 @@ describe('syncCompletedState', () => {
})
test(`completedForDevice, syncs to server`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
const save = vi.fn()
const setCompletedForDevice = vi.fn()
syncCompletedState({
nuxIsReady: true,
nuxIsCompleted: false,
@@ -141,8 +141,8 @@ describe('syncCompletedState', () => {
*/
describe(`nuxIsOptimisticallyCompleted`, () => {
test(`completedForDevice, no-op`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
const save = vi.fn()
const setCompletedForDevice = vi.fn()
syncCompletedState({
nuxIsReady: true,
nuxIsCompleted: false,
@@ -160,8 +160,8 @@ describe('syncCompletedState', () => {
describe(`nuxIsCompleted`, () => {
test(`!completedForDevice, syncs to device`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
const save = vi.fn()
const setCompletedForDevice = vi.fn()
syncCompletedState({
nuxIsReady: true,
nuxIsCompleted: true,
@@ -176,8 +176,8 @@ describe('syncCompletedState', () => {
})
test(`completedForDevice, no-op`, () => {
const save = jest.fn()
const setCompletedForDevice = jest.fn()
const save = vi.fn()
const setCompletedForDevice = vi.fn()
syncCompletedState({
nuxIsReady: true,
nuxIsCompleted: true,
+1 -1
View File
@@ -1,4 +1,4 @@
import {describe, expect, it} from '@jest/globals'
import {describe, expect, it} from 'vitest'
import {parseLinkingUrl} from '../parseLinkingUrl'
@@ -1,6 +1,6 @@
import {beforeEach, describe, expect, it, jest} from '@jest/globals'
import {beforeEach, describe, expect, it, vi} from 'vitest'
jest.mock('@bsky.app/react-native-mmkv', () => ({
vi.mock('@bsky.app/react-native-mmkv', () => ({
MMKV: class MMKVMock {
_store = new Map<string, string>()
+1 -1
View File
@@ -1,4 +1,4 @@
import {describe, expect, it} from '@jest/globals'
import {describe, expect, it} from 'vitest'
import {addDays, subDays, subHours, subMinutes, subSeconds} from 'date-fns'
import {dateDiff} from '../useTimeAgo'
+1 -1
View File
@@ -1,4 +1,4 @@
import {expect, test} from '@jest/globals'
import {expect, test} from 'vitest'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {AppLanguage} from '#/locale/languages'
+1 -1
View File
@@ -1,4 +1,4 @@
import {expect, test} from '@jest/globals'
import {expect, test} from 'vitest'
import {add, type ConsoleTransportEntry, getEntries} from '#/logger/logDump'
import {LogContext, LogLevel} from '#/logger/types'
+20 -20
View File
@@ -1,4 +1,4 @@
import {beforeAll, describe, expect, jest, test} from '@jest/globals'
import {beforeAll, describe, expect, test, vi} from 'vitest'
import * as Sentry from '@sentry/react-native'
import {nanoid} from 'nanoid/non-secure'
@@ -6,14 +6,14 @@ 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)
+1 -1
View File
@@ -1,4 +1,4 @@
import {describe, expect, it} from '@jest/globals'
import {describe, expect, it} from 'vitest'
import {parseSearchQuery} from '#/screens/Search/utils'
+1 -1
View File
@@ -1,4 +1,4 @@
import {describe, it} from '@jest/globals'
import {describe, it} from 'vitest'
describe(`#/state/messages/convo`, () => {
describe(`init`, () => {
+5 -5
View File
@@ -1,25 +1,25 @@
import {jest} from '@jest/globals'
import {vi} from 'vitest'
export {Nux} from '#/state/queries/nuxs/definitions'
export const useNuxs = jest.fn(() => {
export const useNuxs = vi.fn(() => {
return {
nuxs: undefined,
status: 'loading' as const,
}
})
export const useNux = jest.fn((id: string) => {
export const useNux = vi.fn((id: string) => {
return {
nux: undefined,
status: 'loading' as const,
}
})
export const useSaveNux = jest.fn(() => {
export const useSaveNux = vi.fn(() => {
return {}
})
export const useResetNuxs = jest.fn(() => {
export const useResetNuxs = vi.fn(() => {
return {}
})
+6 -6
View File
@@ -1,21 +1,21 @@
import {BskyAgent} from '@atproto/api'
import {describe, expect, it, jest} from '@jest/globals'
import {describe, expect, it, vi} from 'vitest'
import {agentToSessionAccountOrThrow} from '../agent'
import {type Action, getInitialState, reducer, type State} from '../reducer'
jest.mock('jwt-decode', () => ({
vi.mock('jwt-decode', () => ({
jwtDecode(_token: string) {
return {}
},
}))
jest.mock('../../birthdate')
jest.mock('../../../ageAssurance/data')
jest.mock('../../../ageAssurance/state', () => ({
vi.mock('../../birthdate')
vi.mock('../../../ageAssurance/data')
vi.mock('../../../ageAssurance/state', () => ({
getAndComputeAgeAssuranceState: () => ({}),
}))
jest.mock('#/lib/notifications/notifications', () => ({
vi.mock('#/lib/notifications/notifications', () => ({
unregisterPushToken(_agents: BskyAgent[]) {
return Promise.resolve()
},
+2 -2
View File
@@ -1,8 +1,8 @@
import {beforeEach, expect, jest, test} from '@jest/globals'
import {beforeEach, expect, test, 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()
+1 -1
View File
@@ -4,7 +4,7 @@
"jsx": "react-jsx",
"module": "esnext",
"lib": ["dom", "esnext"],
"types": ["node", "jest"],
"types": ["node"],
"paths": {
"#/*": ["./src/*"],
"crypto": ["./src/platform/crypto.ts"],
+63
View File
@@ -0,0 +1,63 @@
import {fileURLToPath} from 'node:url'
import {lingui} from '@lingui/vite-plugin'
import {defineConfig} from 'vitest/config'
export default defineConfig({
plugins: [lingui()],
define: {
__DEV__: 'true',
},
resolve: {
tsconfigPaths: true,
alias: {
crypto: fileURLToPath(new URL('./src/platform/crypto.ts', import.meta.url)),
},
},
test: {
globals: false,
environment: 'node',
setupFiles: ['./vitest/setup.ts'],
include: [
'__tests__/**/*.test.{ts,tsx,js,jsx}',
'src/**/*.test.{ts,tsx}',
'src/**/__tests__/**/*.{ts,tsx}',
'eslint/**/*.test.{js,ts}',
],
exclude: [
'**/node_modules/**',
'**/__mocks__/**',
'__e2e__/**',
'bskylink/**',
'bskyembed/**',
],
testTimeout: 20000,
coverage: {
provider: 'v8',
exclude: [
'node_modules/**',
'src/platform/**',
'src/third-party/**',
'src/view/com/util/**',
'src/state/lib/**',
'__tests__/test-utils.js',
],
},
server: {
deps: {
inline: [
/^react-native($|\/)/,
/^@react-native($|\/)/,
/^expo($|-|\/)/,
/^@expo($|\/)/,
/^@react-navigation($|\/)/,
/^react-native-svg($|\/)/,
/^nanoid($|\/)/,
/^@sentry($|\/)/,
/^sentry-expo($|\/)/,
/^bcp-47-match($|\/)/,
],
},
},
},
})
+134
View File
@@ -0,0 +1,134 @@
import {EventEmitter} from 'node:events'
import {vi} from 'vitest'
// react-native ships Flow syntax in its index.js entry point, which Vitest's
// rolldown parser cannot handle. None of the tests exercise actual RN runtime
// behavior, so stub the whole module to satisfy transitive imports.
vi.mock('react-native', () => ({
Platform: {OS: 'ios', select: (obj: any) => obj.ios ?? obj.default},
StyleSheet: {create: (s: any) => s, flatten: (s: any) => s, hairlineWidth: 1},
View: 'View',
Text: 'Text',
ScrollView: 'ScrollView',
FlatList: 'FlatList',
TextInput: 'TextInput',
Modal: 'Modal',
Image: 'Image',
Pressable: 'Pressable',
TouchableOpacity: 'TouchableOpacity',
TouchableWithoutFeedback: 'TouchableWithoutFeedback',
NativeModules: {},
NativeEventEmitter: EventEmitter,
AppState: {addEventListener: () => ({remove: () => {}}), currentState: 'active'},
Linking: {openURL: vi.fn(), canOpenURL: vi.fn().mockResolvedValue(true)},
Dimensions: {get: () => ({width: 375, height: 812}), addEventListener: () => ({remove: () => {}})},
PixelRatio: {get: () => 2, getFontScale: () => 1},
Keyboard: {addListener: () => ({remove: () => {}}), dismiss: () => {}},
InteractionManager: {runAfterInteractions: (fn: () => void) => fn()},
Animated: {View: 'AnimatedView', Value: class {}, timing: () => ({start: () => {}})},
UIManager: {},
}))
vi.mock('@react-native-async-storage/async-storage', () =>
require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
)
vi.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => ({
__esModule: true,
default: EventEmitter,
}))
vi.mock('@fortawesome/react-native-fontawesome', () => ({
FontAwesomeIcon: '',
}))
vi.mock('react-native-safe-area-context', () => {
const inset = {top: 0, right: 0, bottom: 0, left: 0}
return {
SafeAreaProvider: vi.fn().mockImplementation(({children}) => children),
SafeAreaConsumer: vi
.fn()
.mockImplementation(({children}) => children(inset)),
useSafeAreaInsets: vi.fn().mockImplementation(() => inset),
}
})
vi.mock('expo-file-system/legacy', () => ({
getInfoAsync: vi.fn().mockResolvedValue({exists: true, size: 100}),
deleteAsync: vi.fn(),
moveAsync: vi.fn().mockResolvedValue(undefined),
createDownloadResumable: vi.fn(),
}))
vi.mock('expo-image-manipulator', () => ({
manipulateAsync: vi.fn().mockResolvedValue({
uri: 'file://resized-image',
}),
SaveFormat: {
JPEG: 'jpeg',
WEBP: 'webp',
},
}))
vi.mock('expo-camera', () => ({
Camera: {
useCameraPermissions: vi.fn(() => [true]),
},
}))
vi.mock('expo-media-library', () => ({
__esModule: true,
default: vi.fn(),
usePermissions: vi.fn(() => [true]),
}))
vi.mock('@bsky.app/expo-guess-language', () => ({
guessLanguageSync: vi
.fn()
.mockReturnValue([{language: 'en', confidence: 1}]),
guessLanguageAsync: vi
.fn()
.mockResolvedValue([{language: 'en', confidence: 1}]),
}))
vi.mock('sentry-expo', () => ({
init: () => vi.fn(),
Native: {
ReactNativeTracing: vi.fn().mockImplementation(() => ({
start: vi.fn(),
stop: vi.fn(),
})),
ReactNavigationInstrumentation: vi.fn(),
},
}))
vi.mock('crypto', () => ({}))
vi.mock('expo-application', () => ({
nativeApplicationVersion: '1.0.0',
nativeBuildVersion: '1',
}))
vi.mock('expo-modules-core', () => ({
requireNativeModule: vi.fn().mockImplementation(moduleName => {
if (moduleName === 'ExpoPlatformInfo') {
return {
getIsReducedMotionEnabled: () => false,
}
}
if (moduleName === 'BottomSheet') {
return {
dismissAll: () => {},
}
}
}),
requireNativeViewManager: vi.fn().mockImplementation(_ => {
return () => null
}),
createPermissionHook: () => () => [true],
}))
vi.mock('expo-localization', () => ({
getLocales: () => [],
}))