From a70caf18296308333e118819b12770e412a85031 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 29 Jun 2026 18:11:31 +0300 Subject: [PATCH] migrate pure-logic tests to vitest (phase 1) --- __tests__/lib/email.test.ts | 1 - __tests__/lib/strings/url-helpers.test.ts | 2 - package.json | 1 + pnpm-lock.yaml | 16 ++- src/alf/util/__tests__/colors.test.ts | 4 +- .../__tests__/useAnnouncementState.test.ts | 32 ++--- src/features/inviteFriends/themes.test.ts | 2 - src/features/inviteFriends/urls.test.ts | 2 - src/lib/__tests__/parseLinkingUrl.test.ts | 2 - src/lib/hooks/__tests__/useTimeAgo.test.ts | 1 - src/locale/__tests__/helpers.test.ts | 2 - src/logger/__tests__/logDump.test.ts | 2 - .../Search/__tests__/searchParams.test.ts | 2 - src/screens/Search/__tests__/utils.test.ts | 2 - .../__tests__/utils.test.ts | 2 - src/state/messages/__tests__/convo.test.ts | 2 - .../__tests__/search-posts-params.test.ts | 2 - src/state/queries/nuxs/__mocks__/index.ts | 10 +- vitest.config.ts | 111 ++++++++++++++++-- vitest/expo-application-stub.ts | 9 ++ vitest/expo-stub.ts | 33 ++++++ vitest/mmkv-stub.ts | 24 ++++ vitest/react-native-stub.ts | 27 +++++ vitest/sentry-stub.ts | 16 +++ vitest/vitestSetup.ts | 36 ++++-- 25 files changed, 280 insertions(+), 63 deletions(-) create mode 100644 vitest/expo-application-stub.ts create mode 100644 vitest/expo-stub.ts create mode 100644 vitest/mmkv-stub.ts create mode 100644 vitest/react-native-stub.ts create mode 100644 vitest/sentry-stub.ts diff --git a/__tests__/lib/email.test.ts b/__tests__/lib/email.test.ts index 4dfda658f7..094b1aff0e 100644 --- a/__tests__/lib/email.test.ts +++ b/__tests__/lib/email.test.ts @@ -1,4 +1,3 @@ -import {describe, expect, it} from '@jest/globals' import tldts from 'tldts' import {isEmailMaybeInvalid} from '#/lib/strings/email' diff --git a/__tests__/lib/strings/url-helpers.test.ts b/__tests__/lib/strings/url-helpers.test.ts index dd1a096ef5..ee1d538b5a 100644 --- a/__tests__/lib/strings/url-helpers.test.ts +++ b/__tests__/lib/strings/url-helpers.test.ts @@ -1,5 +1,3 @@ -import {describe, expect, it} from '@jest/globals' - import { getChatInviteCodeFromUrl, isPossiblyAUrl, diff --git a/package.json b/package.json index a5b6a8a462..77f0419c25 100644 --- a/package.json +++ b/package.json @@ -307,6 +307,7 @@ "ts-plugin-sort-import-suggestions": "^1.0.4", "typescript": "^6.0.2", "typescript-eslint": "^8.58.0", + "vite-plugin-babel": "^1.7.3", "vitest": "^4.1.9", "webpack-bundle-analyzer": "^4.10.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68dee0394c..b50d04fede 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -868,6 +868,9 @@ importers: typescript-eslint: specifier: ^8.58.0 version: 8.59.3(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + vite-plugin-babel: + specifier: ^1.7.3 + version: 1.7.3(@babel/core@7.29.0)(vite@8.1.0(@types/node@24.12.4)(jiti@2.7.0)(terser@5.47.1)(yaml@2.9.0)) vitest: specifier: ^4.1.9 version: 4.1.9(@types/node@24.12.4)(@vitest/coverage-v8@4.1.9)(jsdom@20.0.3)(vite@8.1.0(@types/node@24.12.4)(jiti@2.7.0)(terser@5.47.1)(yaml@2.9.0)) @@ -9406,6 +9409,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-plugin-babel@1.7.3: + resolution: {integrity: sha512-PsomVbH74/XyqMzXSwBDqb/1o5sAqE68gPlHLfx4FiMGbvKDhaZVQNv/gj/7oRQ5BvuIlyTKBtNEA9MnirGwRA==} + peerDependencies: + '@babel/core': ^7.0.0 + vite: ^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vite@8.1.0: resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -13398,7 +13407,7 @@ snapshots: debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -19644,6 +19653,11 @@ snapshots: vary@1.1.2: {} + vite-plugin-babel@1.7.3(@babel/core@7.29.0)(vite@8.1.0(@types/node@24.12.4)(jiti@2.7.0)(terser@5.47.1)(yaml@2.9.0)): + dependencies: + '@babel/core': 7.29.0 + vite: 8.1.0(@types/node@24.12.4)(jiti@2.7.0)(terser@5.47.1)(yaml@2.9.0) + vite@8.1.0(@types/node@24.12.4)(jiti@2.7.0)(terser@5.47.1)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 diff --git a/src/alf/util/__tests__/colors.test.ts b/src/alf/util/__tests__/colors.test.ts index 1ecb7394bd..d755664023 100644 --- a/src/alf/util/__tests__/colors.test.ts +++ b/src/alf/util/__tests__/colors.test.ts @@ -1,10 +1,10 @@ -import {jest} from '@jest/globals' +import {vi} from 'vitest' import {transparentifyColor} from '../colorGeneration' describe('transparentifyColor', () => { beforeEach(() => { - jest.clearAllMocks() + vi.clearAllMocks() }) it('converts hsl() to hsla()', () => { diff --git a/src/components/PolicyUpdateOverlay/__tests__/useAnnouncementState.test.ts b/src/components/PolicyUpdateOverlay/__tests__/useAnnouncementState.test.ts index f6055bf34a..085da928af 100644 --- a/src/components/PolicyUpdateOverlay/__tests__/useAnnouncementState.test.ts +++ b/src/components/PolicyUpdateOverlay/__tests__/useAnnouncementState.test.ts @@ -1,11 +1,11 @@ -import {describe, test} from '@jest/globals' +import {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, diff --git a/src/features/inviteFriends/themes.test.ts b/src/features/inviteFriends/themes.test.ts index 607217dc5a..1376e4482b 100644 --- a/src/features/inviteFriends/themes.test.ts +++ b/src/features/inviteFriends/themes.test.ts @@ -1,5 +1,3 @@ -import {describe, expect, it} from '@jest/globals' - import {getInviteTheme, INVITE_THEME_KEYS, INVITE_THEMES} from './themes' describe('invite themes', () => { diff --git a/src/features/inviteFriends/urls.test.ts b/src/features/inviteFriends/urls.test.ts index e24c3ca1cc..3ff0fa4169 100644 --- a/src/features/inviteFriends/urls.test.ts +++ b/src/features/inviteFriends/urls.test.ts @@ -1,5 +1,3 @@ -import {describe, expect, it} from '@jest/globals' - import {getInviteDisplayUrl, getInviteShareUrl} from './urls' describe('invite URLs', () => { diff --git a/src/lib/__tests__/parseLinkingUrl.test.ts b/src/lib/__tests__/parseLinkingUrl.test.ts index 48bb927e01..1d5f699013 100644 --- a/src/lib/__tests__/parseLinkingUrl.test.ts +++ b/src/lib/__tests__/parseLinkingUrl.test.ts @@ -1,5 +1,3 @@ -import {describe, expect, it} from '@jest/globals' - import {parseLinkingUrl} from '../parseLinkingUrl' describe('parseLinkingUrl', () => { diff --git a/src/lib/hooks/__tests__/useTimeAgo.test.ts b/src/lib/hooks/__tests__/useTimeAgo.test.ts index 68eb6e43a9..23cf03886f 100644 --- a/src/lib/hooks/__tests__/useTimeAgo.test.ts +++ b/src/lib/hooks/__tests__/useTimeAgo.test.ts @@ -1,4 +1,3 @@ -import {describe, expect, it} from '@jest/globals' import {addDays, subDays, subHours, subMinutes, subSeconds} from 'date-fns' import {dateDiff} from '../useTimeAgo' diff --git a/src/locale/__tests__/helpers.test.ts b/src/locale/__tests__/helpers.test.ts index 44f0901f13..28ada3edff 100644 --- a/src/locale/__tests__/helpers.test.ts +++ b/src/locale/__tests__/helpers.test.ts @@ -1,5 +1,3 @@ -import {expect, test} from '@jest/globals' - import {sanitizeAppLanguageSetting} from '#/locale/helpers' import {AppLanguage} from '#/locale/languages' diff --git a/src/logger/__tests__/logDump.test.ts b/src/logger/__tests__/logDump.test.ts index 9575b70623..2bfdde91d3 100644 --- a/src/logger/__tests__/logDump.test.ts +++ b/src/logger/__tests__/logDump.test.ts @@ -1,5 +1,3 @@ -import {expect, test} from '@jest/globals' - import {add, type ConsoleTransportEntry, getEntries} from '#/logger/logDump' import {LogContext, LogLevel} from '#/logger/types' diff --git a/src/screens/Search/__tests__/searchParams.test.ts b/src/screens/Search/__tests__/searchParams.test.ts index fd12fd2466..a6cc5dada0 100644 --- a/src/screens/Search/__tests__/searchParams.test.ts +++ b/src/screens/Search/__tests__/searchParams.test.ts @@ -1,5 +1,3 @@ -import {describe, expect, it} from '@jest/globals' - import { countActiveFilters, definedFilterParams, diff --git a/src/screens/Search/__tests__/utils.test.ts b/src/screens/Search/__tests__/utils.test.ts index 81610cc59a..d0e643d700 100644 --- a/src/screens/Search/__tests__/utils.test.ts +++ b/src/screens/Search/__tests__/utils.test.ts @@ -1,5 +1,3 @@ -import {describe, expect, it} from '@jest/globals' - import {parseSearchQuery} from '#/screens/Search/utils' describe(`parseSearchQuery`, () => { diff --git a/src/screens/Search/components/AdvancedSearchDialog/__tests__/utils.test.ts b/src/screens/Search/components/AdvancedSearchDialog/__tests__/utils.test.ts index 8822c1337d..5e5967c277 100644 --- a/src/screens/Search/components/AdvancedSearchDialog/__tests__/utils.test.ts +++ b/src/screens/Search/components/AdvancedSearchDialog/__tests__/utils.test.ts @@ -1,5 +1,3 @@ -import {describe, expect, it} from '@jest/globals' - import { makeFilter, parseAdvancedSearch, diff --git a/src/state/messages/__tests__/convo.test.ts b/src/state/messages/__tests__/convo.test.ts index 34df5f94ab..f6214e32be 100644 --- a/src/state/messages/__tests__/convo.test.ts +++ b/src/state/messages/__tests__/convo.test.ts @@ -1,5 +1,3 @@ -import {describe, it} from '@jest/globals' - describe(`#/state/messages/convo`, () => { describe(`init`, () => { it.todo(`fails if sender and recipients aren't found`) diff --git a/src/state/queries/__tests__/search-posts-params.test.ts b/src/state/queries/__tests__/search-posts-params.test.ts index a35f319fea..516dd97b45 100644 --- a/src/state/queries/__tests__/search-posts-params.test.ts +++ b/src/state/queries/__tests__/search-posts-params.test.ts @@ -1,5 +1,3 @@ -import {describe, expect, it} from '@jest/globals' - import { buildSearchPostsV2Filters, extractSearchPostsParams, diff --git a/src/state/queries/nuxs/__mocks__/index.ts b/src/state/queries/nuxs/__mocks__/index.ts index c718b15940..8fa6f50846 100644 --- a/src/state/queries/nuxs/__mocks__/index.ts +++ b/src/state/queries/nuxs/__mocks__/index.ts @@ -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 {} }) diff --git a/vitest.config.ts b/vitest.config.ts index 581564b104..a0472c3bb1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,19 +1,37 @@ import {fileURLToPath} from 'node:url' -import react from '@vitejs/plugin-react' +import babel from 'vite-plugin-babel' import {defineConfig} from 'vitest/config' const r = (p: string) => fileURLToPath(new URL(p, import.meta.url)) export default defineConfig({ plugins: [ - react({ - babel: { + /* + * esbuild handles TS/JSX transpilation; babel runs only for the lingui + * macro, which must be expanded in every .ts/.tsx file that uses it (not + * just JSX files). We deliberately omit react-compiler, reanimated/plugin, + * and react-native-dotenv - those matter for render semantics and + * native/build output, none of which our tests exercise. + */ + babel({ + include: /\.[jt]sx?$/, + exclude: /node_modules/, + babelConfig: { + babelrc: false, + configFile: false, /* - * Only what tests actually need. We deliberately omit react-compiler, - * reanimated/plugin, and react-native-dotenv - those matter for render - * semantics and native/build output, none of which our tests exercise. + * The plugin runs with enforce: 'pre', so babel receives raw + * TypeScript before esbuild strips types - it needs the TS preset to + * parse it. preset-typescript auto-enables JSX for .tsx filenames. */ + /* + * Default onlyRemoveTypeImports: false so babel elides imports whose + * specifiers are all types (e.g. `import {type X} from '...'`). + * Otherwise such imports survive as side-effect imports and drag whole + * runtime module graphs (and their native deps) into tests. + */ + presets: ['@babel/preset-typescript'], plugins: [ '@lingui/babel-plugin-lingui-macro', '@babel/plugin-transform-class-static-block', @@ -22,6 +40,29 @@ export default defineConfig({ }), ], resolve: { + /* + * Mirror Metro's platform-specific resolution so Component.native.ts / + * Component.ios.ts win over Component.ts (matching the jest-expo/ios + * preset, which resolves ios then native). Without this, files like + * PlatformInfo/index.ts (which throws NotImplementedError) load instead of + * index.native.ts (which uses the stubbed requireNativeModule). + */ + extensions: [ + '.ios.ts', + '.ios.tsx', + '.ios.js', + '.ios.jsx', + '.native.ts', + '.native.tsx', + '.native.js', + '.native.jsx', + '.ts', + '.tsx', + '.mjs', + '.js', + '.jsx', + '.json', + ], alias: [ /* * Mirror the babel module-resolver aliases (also in tsconfig.json). @@ -29,6 +70,56 @@ export default defineConfig({ {find: /^#\/(.*)$/, replacement: r('./src/$1')}, {find: /^crypto$/, replacement: r('./src/platform/crypto.ts')}, + /* + * react-native's real entry is Flow-typed and boots a native runtime. + * Redirect it (and deep Libraries/* subpaths some packages import) to a + * small stub. Aliasing at resolution time is more robust than a + * setup-file vi.mock, which doesn't reach imports from externalized CJS + * deps like @bsky.app/react-native-mmkv. + */ + {find: /^react-native$/, replacement: r('./vitest/react-native-stub.ts')}, + { + find: /^react-native\/.*$/, + replacement: r('./vitest/react-native-stub.ts'), + }, + + /* + * Native MMKV module - binds to a native lib and imports react-native. + * Stubbed so transitive importers (#/storage) load. Tests that exercise + * storage logic still vi.mock this with their own in-memory impl, which + * takes precedence over the alias. + */ + { + find: /^@bsky\.app\/react-native-mmkv$/, + replacement: r('./vitest/mmkv-stub.ts'), + }, + + /* + * @sentry/react-native is CJS and requires react-native internally. + * Stubbed so modules that pull it in via #/logger load. logger.test.ts + * still vi.mocks it directly to assert on calls. + */ + { + find: /^@sentry\/react-native$/, + replacement: r('./vitest/sentry-stub.ts'), + }, + + /* + * expo-application pulls in the `expo` package's native winter runtime, + * which does bare CJS requires that don't resolve under Vitest. + */ + { + find: /^expo-application$/, + replacement: r('./vitest/expo-application-stub.ts'), + }, + + /* + * The bare `expo` package's entry runs the winter runtime side-effect on + * import (bare CJS requires that don't resolve under node). Stub it to + * the named exports test-reachable code uses. + */ + {find: /^expo$/, replacement: r('./vitest/expo-stub.ts')}, + /* * Former Jest moduleNameMapper entries. Vite's ESM resolution may make * some of these unnecessary, but they're harmless and proven, so keep @@ -72,7 +163,13 @@ export default defineConfig({ }, ], }, - define: {__DEV__: 'true'}, + /* + * Match jest's effective environment: under jest __DEV__ was true but + * JEST_WORKER_ID was set, so dev-only branches guarded by + * `__DEV__ && !process.env.JEST_WORKER_ID` took the production path. Setting + * __DEV__ to false reproduces that for the equivalent branches in Vitest. + */ + define: {__DEV__: 'false'}, test: { globals: true, environment: 'node', diff --git a/vitest/expo-application-stub.ts b/vitest/expo-application-stub.ts new file mode 100644 index 0000000000..f3ed370358 --- /dev/null +++ b/vitest/expo-application-stub.ts @@ -0,0 +1,9 @@ +/* + * Stub for expo-application. The real package transitively imports the `expo` + * package, whose native winter runtime does bare CJS requires that don't + * resolve under Vitest. #/env only reads nativeBuildVersion. + * + * Aliased in place of expo-application via resolve.alias in vitest.config.ts. + */ +export const nativeApplicationVersion = '1.0.0' +export const nativeBuildVersion = '1' diff --git a/vitest/expo-stub.ts b/vitest/expo-stub.ts new file mode 100644 index 0000000000..b4d570fba7 --- /dev/null +++ b/vitest/expo-stub.ts @@ -0,0 +1,33 @@ +/* + * Stub for the bare `expo` package. Importing the real entry runs Expo's + * "winter" runtime side-effect, which does bare CJS requires (./TextDecoder + * etc) that don't resolve under Vitest's node environment. Node already + * provides TextDecoder and friends, so we just need the named exports + * test-reachable code pulls from `expo`. + * + * Aliased in place of `expo` via resolve.alias in vitest.config.ts. + */ +import {vi} from 'vitest' + +export function requireNativeModule(moduleName: string) { + if (moduleName === 'ExpoPlatformInfo') { + return {getIsReducedMotionEnabled: () => false} + } + if (moduleName === 'BottomSheet') { + return {dismissAll: () => {}} + } + return {} +} + +export function requireOptionalNativeModule() { + return null +} + +export const requireNativeView = vi.fn(() => () => null) + +export class NativeModule {} +export class SharedObject {} +export class SharedRef {} + +export const useEvent = vi.fn() +export const useEventListener = vi.fn() diff --git a/vitest/mmkv-stub.ts b/vitest/mmkv-stub.ts new file mode 100644 index 0000000000..ef79cdc858 --- /dev/null +++ b/vitest/mmkv-stub.ts @@ -0,0 +1,24 @@ +/* + * Stub for @bsky.app/react-native-mmkv. The real package binds to a native + * module and imports `react-native`, neither of which works under Vitest's + * node environment. Tests that exercise storage logic mock this module's MMKV + * with their own in-memory implementation; this stub just provides a class + * shape so transitive importers (e.g. #/storage) load without booting RN. + * + * Aliased in place of @bsky.app/react-native-mmkv via resolve.alias in + * vitest.config.ts. + */ +export class MMKV { + getString() { + return undefined + } + set() {} + delete() {} + getAllKeys(): string[] { + return [] + } + clearAll() {} + addOnValueChangedListener() { + return {remove() {}} + } +} diff --git a/vitest/react-native-stub.ts b/vitest/react-native-stub.ts new file mode 100644 index 0000000000..3eb995a055 --- /dev/null +++ b/vitest/react-native-stub.ts @@ -0,0 +1,27 @@ +/* + * Stub for `react-native` used in unit tests. react-native's real entry is + * Flow-typed and boots a native runtime, neither of which works (or is needed) + * under Vitest's node environment. This is aliased in place of `react-native` + * via resolve.alias in vitest.config.ts. + * + * Only the surface our test-reachable modules actually touch is implemented: + * - Image.getSize (src/lib/media/manip.ts) + * - NativeModules / Platform (@bsky.app/react-native-mmkv) + * Extend as new transitive usages appear. + */ +import {vi} from 'vitest' + +export const Image = {getSize: vi.fn()} + +export const NativeModules: Record = {} + +export const Platform = { + OS: 'ios' as const, + select: (o: {ios?: T; android?: T; native?: T; default?: T}): T => + (o.ios ?? o.native ?? o.default) as T, +} + +export const Dimensions = { + get: () => ({width: 390, height: 844, scale: 3, fontScale: 1}), + addEventListener: () => ({remove() {}}), +} diff --git a/vitest/sentry-stub.ts b/vitest/sentry-stub.ts new file mode 100644 index 0000000000..1519c71028 --- /dev/null +++ b/vitest/sentry-stub.ts @@ -0,0 +1,16 @@ +/* + * Stub for @sentry/react-native. The real package is CJS and requires + * `react-native` internally, which boots the native runtime under node. Many + * test-reachable modules pull it in transitively via #/logger. We only need + * the surface sentryTransport touches (src/logger/transports/sentry.ts). + * + * Aliased in place of @sentry/react-native via resolve.alias in + * vitest.config.ts. Tests that assert on Sentry calls (logger.test.ts) still + * vi.mock it directly, which takes precedence. + */ +import {vi} from 'vitest' + +export const addBreadcrumb = vi.fn() +export const captureException = vi.fn() +export const captureMessage = vi.fn() +export const init = vi.fn() diff --git a/vitest/vitestSetup.ts b/vitest/vitestSetup.ts index 50e23580b5..5e32db31c6 100644 --- a/vitest/vitestSetup.ts +++ b/vitest/vitestSetup.ts @@ -1,16 +1,36 @@ import {vi} from 'vitest' /* - * The only place `react-native` enters the test module graph is a single - * transitive import in src/lib/media/manip.ts (`import {Image} from - * 'react-native'`). react-native's entry is Flow-typed, which esbuild cannot - * parse, so we stub it. manip.ts only touches RNImage.getSize; Platform is - * included for any other incidental use. + * Note: `react-native` itself is stubbed via resolve.alias in + * vitest.config.ts (-> vitest/react-native-stub.ts), not here. */ -vi.mock('react-native', () => ({ - Image: {getSize: vi.fn()}, + +/* + * expo-modules-core reads its native EventEmitter / NativeModule classes off + * globalThis.expo, which only exists in a real Expo runtime. Several utility + * modules pull it in transitively (via expo-application, expo-localization, + * etc). We only need the handful of helpers tests actually touch. + */ +vi.mock('expo-modules-core', () => ({ + requireNativeModule: vi.fn((moduleName: string) => { + if (moduleName === 'ExpoPlatformInfo') { + return {getIsReducedMotionEnabled: () => false} + } + if (moduleName === 'BottomSheet') { + return {dismissAll: () => {}} + } + return {} + }), + requireNativeViewManager: vi.fn(() => () => null), + requireOptionalNativeModule: vi.fn(() => null), + createPermissionHook: () => () => [true], + NativeModule: class {}, + SharedObject: class {}, + SharedRef: class {}, + EventEmitter: class {}, Platform: { OS: 'ios', - select: (o: Record) => o.ios ?? o.default, + isDOMAvailable: false, + select: (o: Record) => o.ios, }, }))