diff --git a/mobile/__mocks__/@testing-library/react-native.tsx b/mobile/__mocks__/@testing-library/react-native.tsx new file mode 100644 index 0000000..754c9f3 --- /dev/null +++ b/mobile/__mocks__/@testing-library/react-native.tsx @@ -0,0 +1,78 @@ +/** + * Lightweight @testing-library/react-native mock for Vitest. + * Uses react-test-renderer under the hood with simple query helpers. + */ +import React from 'react'; +import renderer, { type ReactTestRendererJSON } from 'react-test-renderer'; + +function collectTexts(node: ReactTestRendererJSON | ReactTestRendererJSON[] | string | null): string[] { + if (node === null) return []; + if (typeof node === 'string') return [node]; + if (Array.isArray(node)) return node.flatMap(collectTexts); + + const texts: string[] = []; + if (node.children) { + for (const child of node.children) { + texts.push(...collectTexts(child)); + } + } + return texts; +} + +function collectNodes(node: ReactTestRendererJSON | ReactTestRendererJSON[] | null): ReactTestRendererJSON[] { + if (node === null) return []; + if (Array.isArray(node)) return node.flatMap(collectNodes); + + const nodes: ReactTestRendererJSON[] = [node]; + if (node.children) { + for (const child of node.children) { + if (typeof child !== 'string') { + nodes.push(...collectNodes(child)); + } + } + } + return nodes; +} + +let currentTree: ReturnType | null = null; + +export function render(element: React.ReactElement) { + currentTree = renderer.create(element); + return { unmount: () => currentTree?.unmount() }; +} + +export const screen = { + getByText(text: string) { + const json = currentTree?.toJSON(); + const allTexts = collectTexts(json ?? null); + const found = allTexts.find((t) => t.includes(text)); + if (!found) throw new Error(`Unable to find text: "${text}"`); + return { type: 'Text', children: [found] }; + }, + + getByPlaceholderText(placeholder: string) { + const json = currentTree?.toJSON(); + const allNodes = collectNodes(json ?? null); + const found = allNodes.find( + (n) => n.props?.placeholder === placeholder || n.props?.placeholderTextColor != null && n.props?.placeholder === placeholder, + ); + if (!found) throw new Error(`Unable to find placeholder: "${placeholder}"`); + return found; + }, + + queryByText(text: string) { + try { + return screen.getByText(text); + } catch { + return null; + } + }, +}; + +export function fireEvent(element: ReactTestRendererJSON, event: string, ...args: unknown[]) { + const handler = element.props?.[`on${event.charAt(0).toUpperCase()}${event.slice(1)}`]; + if (typeof handler === 'function') handler(...args); +} + +fireEvent.press = (element: ReactTestRendererJSON) => fireEvent(element, 'press'); +fireEvent.changeText = (element: ReactTestRendererJSON, text: string) => fireEvent(element, 'changeText', text); diff --git a/mobile/__mocks__/expo-constants.ts b/mobile/__mocks__/expo-constants.ts new file mode 100644 index 0000000..68d8e2d --- /dev/null +++ b/mobile/__mocks__/expo-constants.ts @@ -0,0 +1,12 @@ +/** + * Minimal expo-constants mock for Vitest. + */ +export default { + expoConfig: { + name: 'NoteLett', + slug: 'notelett', + version: '0.1.0', + ios: { buildNumber: '1' }, + android: { versionCode: 1 }, + }, +}; diff --git a/mobile/__mocks__/expo-router.ts b/mobile/__mocks__/expo-router.ts new file mode 100644 index 0000000..907f206 --- /dev/null +++ b/mobile/__mocks__/expo-router.ts @@ -0,0 +1,44 @@ +/** + * Minimal expo-router mock for Vitest component tests. + */ +import React from 'react'; + +export const router = { + push: () => {}, + replace: () => {}, + back: () => {}, + canGoBack: () => false, +}; + +export function useRouter() { + return router; +} + +export function useLocalSearchParams() { + return {}; +} + +export function useSegments() { + return []; +} + +export const Link = (props: Record) => + React.createElement('Link', props, props.children as React.ReactNode); + +export const Stack = Object.assign( + (props: Record) => + React.createElement('Stack', props, props.children as React.ReactNode), + { + Screen: (props: Record) => + React.createElement('Stack.Screen', props), + }, +); + +export const Tabs = Object.assign( + (props: Record) => + React.createElement('Tabs', props, props.children as React.ReactNode), + { + Screen: (props: Record) => + React.createElement('Tabs.Screen', props), + }, +); diff --git a/mobile/__mocks__/expo-status-bar.ts b/mobile/__mocks__/expo-status-bar.ts new file mode 100644 index 0000000..72aed89 --- /dev/null +++ b/mobile/__mocks__/expo-status-bar.ts @@ -0,0 +1,8 @@ +/** + * Minimal expo-status-bar mock for Vitest. + */ +import React from 'react'; + +export function StatusBar(_props: Record) { + return React.createElement('StatusBar'); +} diff --git a/mobile/__mocks__/react-native-mmkv.ts b/mobile/__mocks__/react-native-mmkv.ts new file mode 100644 index 0000000..4792d28 --- /dev/null +++ b/mobile/__mocks__/react-native-mmkv.ts @@ -0,0 +1,46 @@ +/** + * Mock for react-native-mmkv in Vitest. + * In-memory storage that mimics MMKV API. + */ +const store = new Map(); + +export class MMKV { + private id: string; + + constructor(config?: { id?: string }) { + this.id = config?.id ?? 'default'; + } + + getString(key: string): string | undefined { + const val = store.get(`${this.id}:${key}`); + return typeof val === 'string' ? val : undefined; + } + + set(key: string, value: string | number | boolean): void { + store.set(`${this.id}:${key}`, value); + } + + delete(key: string): void { + store.delete(`${this.id}:${key}`); + } + + contains(key: string): boolean { + return store.has(`${this.id}:${key}`); + } + + clearAll(): void { + const prefix = `${this.id}:`; + for (const key of store.keys()) { + if (key.startsWith(prefix)) { + store.delete(key); + } + } + } + + getAllKeys(): string[] { + const prefix = `${this.id}:`; + return Array.from(store.keys()) + .filter((k) => k.startsWith(prefix)) + .map((k) => k.slice(prefix.length)); + } +} diff --git a/mobile/__mocks__/react-native.ts b/mobile/__mocks__/react-native.ts new file mode 100644 index 0000000..3e94152 --- /dev/null +++ b/mobile/__mocks__/react-native.ts @@ -0,0 +1,102 @@ +/** + * Minimal react-native mock for Vitest. + * Provides lightweight stubs for components and APIs used in the mobile app. + */ +import React from 'react'; + +function createMockComponent(name: string) { + const Component = (props: Record) => + React.createElement(name, props, props.children as React.ReactNode); + Component.displayName = name; + return Component; +} + +export const View = createMockComponent('View'); +export const Text = createMockComponent('Text'); +export const TextInput = createMockComponent('TextInput'); +export const Pressable = createMockComponent('Pressable'); +export const ScrollView = createMockComponent('ScrollView'); +export const Modal = createMockComponent('Modal'); +export const Image = createMockComponent('Image'); +export const FlatList = createMockComponent('FlatList'); +export const TouchableOpacity = createMockComponent('TouchableOpacity'); +export const SafeAreaView = createMockComponent('SafeAreaView'); +export const ActivityIndicator = createMockComponent('ActivityIndicator'); +export const Alert = { + alert: () => {}, +}; + +export const StyleSheet = { + create: >(styles: T): T => styles, + flatten: (style: unknown) => style, + hairlineWidth: 1, +}; + +export const Platform = { + OS: 'ios' as const, + Version: '17.0', + select: (specifics: Record) => specifics.ios ?? specifics.default, +}; + +export const AppState = { + currentState: 'active', + addEventListener: (_type: string, _handler: unknown) => ({ + remove: () => {}, + }), +}; + +export const Dimensions = { + get: () => ({ width: 375, height: 812, scale: 2, fontScale: 1 }), + addEventListener: () => ({ remove: () => {} }), +}; + +export const Keyboard = { + dismiss: () => {}, + addListener: () => ({ remove: () => {} }), +}; + +export const Linking = { + openURL: async () => {}, + canOpenURL: async () => true, + getInitialURL: async () => null, + addEventListener: () => ({ remove: () => {} }), +}; + +export const Animated = { + View: createMockComponent('Animated.View'), + Text: createMockComponent('Animated.Text'), + Image: createMockComponent('Animated.Image'), + Value: class { + _value: number; + constructor(val: number) { this._value = val; } + setValue(val: number) { this._value = val; } + interpolate() { return this; } + }, + timing: () => ({ start: (cb?: () => void) => cb?.() }), + spring: () => ({ start: (cb?: () => void) => cb?.() }), + parallel: () => ({ start: (cb?: () => void) => cb?.() }), + sequence: () => ({ start: (cb?: () => void) => cb?.() }), + event: () => () => {}, +}; + +export default { + View, + Text, + TextInput, + Pressable, + ScrollView, + Modal, + Image, + FlatList, + TouchableOpacity, + SafeAreaView, + ActivityIndicator, + Alert, + StyleSheet, + Platform, + AppState, + Dimensions, + Keyboard, + Linking, + Animated, +}; diff --git a/mobile/package.json b/mobile/package.json index e528fdb..00cf518 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -21,8 +21,8 @@ "@bytelyst/broadcast-client": "^0.1.0", "@bytelyst/design-tokens": "^0.1.0", "@bytelyst/diagnostics-client": "^0.1.0", - "@bytelyst/feedback-client": "^0.1.0", "@bytelyst/feature-flag-client": "^0.1.0", + "@bytelyst/feedback-client": "^0.1.0", "@bytelyst/kill-switch-client": "^0.1.0", "@bytelyst/offline-queue": "^0.1.0", "@bytelyst/survey-client": "^0.1.0", @@ -42,11 +42,14 @@ "zustand": "^5.0.8" }, "devDependencies": { + "@testing-library/react-native": "^13.2.1", "@types/react": "~19.2.10", + "@types/react-test-renderer": "^19.1.0", "@typescript-eslint/eslint-plugin": "^8.44.0", "@typescript-eslint/parser": "^8.44.0", "eslint": "^9.36.0", "eslint-config-expo": "~10.0.0", + "react-test-renderer": "19.2.0", "typescript": "~5.9.2", "vitest": "^3.2.4" } diff --git a/mobile/src/app/(tabs)/index.test.tsx b/mobile/src/app/(tabs)/index.test.tsx new file mode 100644 index 0000000..3477707 --- /dev/null +++ b/mobile/src/app/(tabs)/index.test.tsx @@ -0,0 +1,47 @@ +import { describe, expect, it, vi } from 'vitest'; +import React, { isValidElement } from 'react'; + +vi.mock('expo-router', () => ({ + router: { push: vi.fn() }, +})); + +vi.mock('../../store/notes-store', () => ({ + useNotesStore: (selector: (state: { + notes: Array<{ id: string; title: string; workspaceName: string; body: string }>; + isLoading: boolean; + }) => unknown) => + selector({ + notes: [ + { id: 'n1', title: 'Test note', workspaceName: 'Work', body: 'Body text' }, + ], + isLoading: false, + }), +})); + +vi.mock('../../store/workspace-store', () => ({ + useWorkspaceStore: (selector: (state: { + workspaces: Array<{ id: string; name: string }>; + activeWorkspaceId: string | null; + setActiveWorkspace: (id: string | null) => void; + }) => unknown) => + selector({ + workspaces: [{ id: 'ws-1', name: 'Work' }], + activeWorkspaceId: null, + setActiveWorkspace: vi.fn(), + }), +})); + +import HomeScreen from './index'; + +describe('HomeScreen', () => { + it('is a valid React component', () => { + expect(typeof HomeScreen).toBe('function'); + const element = React.createElement(HomeScreen); + expect(isValidElement(element)).toBe(true); + }); + + it('exports as default', () => { + expect(HomeScreen).toBeDefined(); + expect(HomeScreen.name).toBe('HomeScreen'); + }); +}); diff --git a/mobile/src/app/auth.test.tsx b/mobile/src/app/auth.test.tsx new file mode 100644 index 0000000..a3c8317 --- /dev/null +++ b/mobile/src/app/auth.test.tsx @@ -0,0 +1,49 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import React, { isValidElement } from 'react'; + +const replaceMock = vi.fn(); +const signInMock = vi.fn(); +const registerMock = vi.fn(); + +vi.mock('expo-router', () => ({ + router: { replace: (...args: unknown[]) => replaceMock(...args) }, +})); + +vi.mock('../store/auth-store', () => ({ + useAuthStore: (selector: (state: { + isAuthenticated: boolean; + signIn: typeof signInMock; + register: typeof registerMock; + isLoading: boolean; + }) => unknown) => + selector({ + isAuthenticated: false, + signIn: signInMock, + register: registerMock, + isLoading: false, + }), +})); + +import AuthScreen from './auth'; + +describe('AuthScreen', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('is a valid React component', () => { + expect(typeof AuthScreen).toBe('function'); + const element = React.createElement(AuthScreen); + expect(isValidElement(element)).toBe(true); + }); + + it('exports as default', () => { + expect(AuthScreen).toBeDefined(); + expect(AuthScreen.name).toBe('AuthScreen'); + }); + + it('does not redirect when not authenticated', () => { + React.createElement(AuthScreen); + expect(replaceMock).not.toHaveBeenCalled(); + }); +}); diff --git a/mobile/vitest.config.ts b/mobile/vitest.config.ts index b33bef1..8768fdd 100644 --- a/mobile/vitest.config.ts +++ b/mobile/vitest.config.ts @@ -1,7 +1,20 @@ import { defineConfig } from 'vitest/config'; +import path from 'path'; export default defineConfig({ + resolve: { + alias: { + 'react-native': path.resolve(__dirname, '__mocks__/react-native.ts'), + 'react-native-mmkv': path.resolve(__dirname, '__mocks__/react-native-mmkv.ts'), + 'expo-router': path.resolve(__dirname, '__mocks__/expo-router.ts'), + 'expo-constants': path.resolve(__dirname, '__mocks__/expo-constants.ts'), + 'expo-status-bar': path.resolve(__dirname, '__mocks__/expo-status-bar.ts'), + '@testing-library/react-native': path.resolve(__dirname, '__mocks__/@testing-library/react-native.tsx'), + }, + }, test: { passWithNoTests: true, + include: ['src/**/*.test.ts', 'src/**/*.test.tsx'], + environment: 'node', }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd71078..507befd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,7 +131,7 @@ importers: version: 18.0.13(expo@55.0.8)(react-native@0.83.2(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)) expo-router: specifier: ~6.0.4 - version: 6.0.23(8f4d8b3ea945913a36065f8394178341) + version: 6.0.23(a351c10dd5cbc088ef68072237cdd807) expo-status-bar: specifier: ~3.0.9 version: 3.0.9(react-native@0.83.2(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -163,9 +163,15 @@ importers: specifier: ^5.0.8 version: 5.0.12(@types/react@19.2.14)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) devDependencies: + '@testing-library/react-native': + specifier: ^13.2.1 + version: 13.3.3(react-native@0.83.2(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react-test-renderer@19.2.0(react@19.2.0))(react@19.2.0) '@types/react': specifier: ~19.2.10 version: 19.2.14 + '@types/react-test-renderer': + specifier: ^19.1.0 + version: 19.1.0 '@typescript-eslint/eslint-plugin': specifier: ^8.44.0 version: 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) @@ -178,6 +184,9 @@ importers: eslint-config-expo: specifier: ~10.0.0 version: 10.0.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + react-test-renderer: + specifier: 19.2.0 + version: 19.2.0(react@19.2.0) typescript: specifier: ~5.9.2 version: 5.9.3 @@ -1656,6 +1665,10 @@ packages: resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/diff-sequences@30.3.0': + resolution: {integrity: sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/environment@29.7.0': resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1664,10 +1677,18 @@ packages: resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/transform@29.7.0': resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2294,6 +2315,9 @@ packages: '@sinclair/typebox@0.27.10': resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + '@sinclair/typebox@0.34.49': + resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} + '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -2402,6 +2426,18 @@ packages: resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/react-native@13.3.3': + resolution: {integrity: sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==} + engines: {node: '>=18'} + peerDependencies: + jest: '>=29.0.0' + react: '>=18.2.0' + react-native: '>=0.71' + react-test-renderer: '>=18.2.0' + peerDependenciesMeta: + jest: + optional: true + '@testing-library/react@16.3.2': resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} engines: {node: '>=18'} @@ -2619,6 +2655,9 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-test-renderer@19.1.0': + resolution: {integrity: sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==} + '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} @@ -2856,6 +2895,7 @@ packages: '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -4296,6 +4336,10 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + jest-diff@30.3.0: + resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-environment-node@29.7.0: resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4308,6 +4352,10 @@ packages: resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-matcher-utils@30.3.0: + resolution: {integrity: sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-message-util@29.7.0: resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5066,6 +5114,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-format@30.3.0: + resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + priorityqueuejs@2.0.0: resolution: {integrity: sha512-19BMarhgpq3x4ccvVi8k2QpJZcymo/iFUcrhPd4V96kYGovOdTsWwy7fxChYi4QY+m2EnGBWSX9Buakz+tWNQQ==} @@ -5301,6 +5353,11 @@ packages: '@types/react': optional: true + react-test-renderer@19.2.0: + resolution: {integrity: sha512-zLCFMHFE9vy/w3AxO0zNxy6aAupnCuLSVOJYDe/Tp+ayGI1f2PLQsFVPANSD42gdSbmYx5oN+1VWDhcXtq7hAQ==} + peerDependencies: + react: ^19.2.0 + react@19.2.0: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} @@ -7399,7 +7456,7 @@ snapshots: ws: 8.20.0 zod: 3.25.76 optionalDependencies: - expo-router: 6.0.23(8f4d8b3ea945913a36065f8394178341) + expo-router: 6.0.23(a351c10dd5cbc088ef68072237cdd807) react-native: 0.83.2(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0) transitivePeerDependencies: - '@expo/dom-webview' @@ -7691,7 +7748,7 @@ snapshots: expo-server: 55.0.6 react: 19.2.0 optionalDependencies: - expo-router: 6.0.23(8f4d8b3ea945913a36065f8394178341) + expo-router: 6.0.23(a351c10dd5cbc088ef68072237cdd807) react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: - supports-color @@ -7874,6 +7931,8 @@ snapshots: dependencies: '@jest/types': 29.6.3 + '@jest/diff-sequences@30.3.0': {} + '@jest/environment@29.7.0': dependencies: '@jest/fake-timers': 29.7.0 @@ -7890,10 +7949,16 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 + '@jest/get-type@30.1.0': {} + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.10 + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.49 + '@jest/transform@29.7.0': dependencies: '@babel/core': 7.29.0 @@ -8412,7 +8477,9 @@ snapshots: metro-runtime: 0.83.5 transitivePeerDependencies: - '@babel/core' + - bufferutil - supports-color + - utf-8-validate '@react-native/normalize-colors@0.83.2': {} @@ -8569,6 +8636,8 @@ snapshots: '@sinclair/typebox@0.27.10': {} + '@sinclair/typebox@0.34.49': {} + '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 @@ -8672,6 +8741,16 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 + '@testing-library/react-native@13.3.3(react-native@0.83.2(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react-test-renderer@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + jest-matcher-utils: 30.3.0 + picocolors: 1.1.1 + pretty-format: 30.3.0 + react: 19.2.0 + react-native: 0.83.2(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0) + react-test-renderer: 19.2.0(react@19.2.0) + redent: 3.0.0 + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.29.2 @@ -8915,6 +8994,10 @@ snapshots: dependencies: '@types/react': 19.2.14 + '@types/react-test-renderer@19.1.0': + dependencies: + '@types/react': 19.2.14 + '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -10017,7 +10100,7 @@ snapshots: eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@2.6.1)) @@ -10050,7 +10133,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -10128,7 +10211,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -10366,7 +10449,7 @@ snapshots: react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0) - expo-router@6.0.23(8f4d8b3ea945913a36065f8394178341): + expo-router@6.0.23(a351c10dd5cbc088ef68072237cdd807): dependencies: '@expo/schema-utils': 0.1.8 '@radix-ui/react-slot': 1.2.0(@types/react@19.2.14)(react@19.2.0) @@ -10398,6 +10481,7 @@ snapshots: use-latest-callback: 0.2.6(react@19.2.0) vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: + '@testing-library/react-native': 13.3.3(react-native@0.83.2(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react-test-renderer@19.2.0(react@19.2.0))(react@19.2.0) react-dom: 19.2.0(react@19.2.0) react-native-gesture-handler: 2.30.0(react-native@0.83.2(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react-native-reanimated: 4.2.1(react-native-worklets@0.8.1(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(react-native@0.83.2(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -10953,6 +11037,13 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + jest-diff@30.3.0: + dependencies: + '@jest/diff-sequences': 30.3.0 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.3.0 + jest-environment-node@29.7.0: dependencies: '@jest/environment': 29.7.0 @@ -10980,6 +11071,13 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + jest-matcher-utils@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + jest-diff: 30.3.0 + pretty-format: 30.3.0 + jest-message-util@29.7.0: dependencies: '@babel/code-frame': 7.29.0 @@ -11958,6 +12056,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + pretty-format@30.3.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + priorityqueuejs@2.0.0: {} proc-log@4.2.0: {} @@ -12274,6 +12378,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + react-test-renderer@19.2.0(react@19.2.0): + dependencies: + react: 19.2.0 + react-is: 19.2.4 + scheduler: 0.27.0 + react@19.2.0: {} real-require@0.2.0: {}