From 5d000fb731153cdb5716f5d62ea9dff1d0748ecf Mon Sep 17 00:00:00 2001 From: Norbert Klockiewicz Date: Wed, 1 Apr 2026 16:10:29 +0200 Subject: [PATCH] test: set up and create tests --- .github/workflows/test.yml | 24 + .gitignore | 5 +- __mocks__/@gorhom/bottom-sheet.tsx | 9 + .../@react-native-clipboard/clipboard.ts | 6 + __mocks__/async-storage.ts | 23 + __mocks__/expo-file-system.ts | 3 + __mocks__/react-native-audio-api.ts | 5 + __mocks__/react-native-device-info.ts | 6 + __mocks__/react-native-executorch.ts | 51 + __mocks__/react-native-fs.ts | 3 + __mocks__/react-native-markdown-display.tsx | 6 + __mocks__/react-native-pdfium.ts | 1 + __mocks__/react-native-reanimated.ts | 3 + __mocks__/react-native-toast-message.ts | 6 + __mocks__/svgMock.tsx | 6 + __tests__/ChatBar.test.tsx | 318 ++ __tests__/ChatBarActions.test.tsx | 122 + __tests__/ChatSettingsForm.test.tsx | 186 + __tests__/MessageItem.test.tsx | 167 + __tests__/ModelCard.test.tsx | 210 ++ __tests__/ModelListFilters.test.tsx | 129 + __tests__/ModelManagementSheet.test.tsx | 197 ++ __tests__/ModelSelectSheet.test.tsx | 155 + __tests__/ThinkingBlock.test.tsx | 88 + __tests__/chatStore.test.ts | 219 ++ __tests__/fileReaders.test.ts | 139 + __tests__/helpers/renderWithTheme.tsx | 9 + __tests__/llmStore.test.ts | 520 +++ __tests__/modelCompatibility.test.ts | 93 + __tests__/modelStore.test.ts | 269 ++ __tests__/onboardingStatus.test.ts | 56 + __tests__/promptUtils.test.ts | 186 + __tests__/sourceStore.test.ts | 224 ++ __tests__/sttStore.test.ts | 101 + __tests__/useBenchmarkRunner.test.ts | 227 ++ __tests__/useChatSettings.test.ts | 140 + __tests__/useModelHubData.test.ts | 231 ++ __tests__/useOnboardingRedirect.test.ts | 79 + jest.setup.js | 18 + package.json | 38 +- store/chatStore.ts | 16 +- store/llmStore.ts | 6 +- utils/modelCompatibility.ts | 6 +- yarn.lock | 3080 ++++++++++++++++- 44 files changed, 7183 insertions(+), 203 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 __mocks__/@gorhom/bottom-sheet.tsx create mode 100644 __mocks__/@react-native-clipboard/clipboard.ts create mode 100644 __mocks__/async-storage.ts create mode 100644 __mocks__/expo-file-system.ts create mode 100644 __mocks__/react-native-audio-api.ts create mode 100644 __mocks__/react-native-device-info.ts create mode 100644 __mocks__/react-native-executorch.ts create mode 100644 __mocks__/react-native-fs.ts create mode 100644 __mocks__/react-native-markdown-display.tsx create mode 100644 __mocks__/react-native-pdfium.ts create mode 100644 __mocks__/react-native-reanimated.ts create mode 100644 __mocks__/react-native-toast-message.ts create mode 100644 __mocks__/svgMock.tsx create mode 100644 __tests__/ChatBar.test.tsx create mode 100644 __tests__/ChatBarActions.test.tsx create mode 100644 __tests__/ChatSettingsForm.test.tsx create mode 100644 __tests__/MessageItem.test.tsx create mode 100644 __tests__/ModelCard.test.tsx create mode 100644 __tests__/ModelListFilters.test.tsx create mode 100644 __tests__/ModelManagementSheet.test.tsx create mode 100644 __tests__/ModelSelectSheet.test.tsx create mode 100644 __tests__/ThinkingBlock.test.tsx create mode 100644 __tests__/chatStore.test.ts create mode 100644 __tests__/fileReaders.test.ts create mode 100644 __tests__/helpers/renderWithTheme.tsx create mode 100644 __tests__/llmStore.test.ts create mode 100644 __tests__/modelCompatibility.test.ts create mode 100644 __tests__/modelStore.test.ts create mode 100644 __tests__/onboardingStatus.test.ts create mode 100644 __tests__/promptUtils.test.ts create mode 100644 __tests__/sourceStore.test.ts create mode 100644 __tests__/sttStore.test.ts create mode 100644 __tests__/useBenchmarkRunner.test.ts create mode 100644 __tests__/useChatSettings.test.ts create mode 100644 __tests__/useModelHubData.test.ts create mode 100644 __tests__/useOnboardingRedirect.test.ts create mode 100644 jest.setup.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..e75570a2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "yarn" + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run tests + run: yarn test --forceExit --ci diff --git a/.gitignore b/.gitignore index 1ffc2b61..32e314b0 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,7 @@ assets/models/**/*.json key_local_mind -android/local.properties \ No newline at end of file +android/local.properties + + +coverage/ \ No newline at end of file diff --git a/__mocks__/@gorhom/bottom-sheet.tsx b/__mocks__/@gorhom/bottom-sheet.tsx new file mode 100644 index 00000000..d3e79a9b --- /dev/null +++ b/__mocks__/@gorhom/bottom-sheet.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { View } from 'react-native'; + +export const BottomSheetModal = React.forwardRef((_props: any, _ref: any) => null); +export const BottomSheetView = ({ children, style }: any) => ( + {children} +); +export const BottomSheetBackdrop = () => null; +export const BottomSheetModalProvider = ({ children }: any) => <>{children}; diff --git a/__mocks__/@react-native-clipboard/clipboard.ts b/__mocks__/@react-native-clipboard/clipboard.ts new file mode 100644 index 00000000..47d032e2 --- /dev/null +++ b/__mocks__/@react-native-clipboard/clipboard.ts @@ -0,0 +1,6 @@ +const Clipboard = { + setString: jest.fn(), + getString: jest.fn(() => Promise.resolve('')), +}; + +export default Clipboard; diff --git a/__mocks__/async-storage.ts b/__mocks__/async-storage.ts new file mode 100644 index 00000000..917ec488 --- /dev/null +++ b/__mocks__/async-storage.ts @@ -0,0 +1,23 @@ +const store: Record = {}; + +const AsyncStorage = { + getItem: jest.fn((key: string) => Promise.resolve(store[key] ?? null)), + setItem: jest.fn((key: string, value: string) => { + store[key] = value; + return Promise.resolve(); + }), + removeItem: jest.fn((key: string) => { + delete store[key]; + return Promise.resolve(); + }), + clear: jest.fn(() => { + Object.keys(store).forEach((k) => delete store[k]); + return Promise.resolve(); + }), + getAllKeys: jest.fn(() => Promise.resolve(Object.keys(store))), + multiGet: jest.fn((keys: string[]) => + Promise.resolve(keys.map((k) => [k, store[k] ?? null])) + ), +}; + +export default AsyncStorage; diff --git a/__mocks__/expo-file-system.ts b/__mocks__/expo-file-system.ts new file mode 100644 index 00000000..b0991985 --- /dev/null +++ b/__mocks__/expo-file-system.ts @@ -0,0 +1,3 @@ +export const File = jest.fn().mockImplementation(() => ({ + text: jest.fn(), +})); diff --git a/__mocks__/react-native-audio-api.ts b/__mocks__/react-native-audio-api.ts new file mode 100644 index 00000000..b3501fff --- /dev/null +++ b/__mocks__/react-native-audio-api.ts @@ -0,0 +1,5 @@ +export const AudioManager = { + requestRecordingPermissions: jest.fn(() => Promise.resolve('Granted')), + setAudioSessionOptions: jest.fn(), + setAudioSessionActivity: jest.fn(() => Promise.resolve()), +}; diff --git a/__mocks__/react-native-device-info.ts b/__mocks__/react-native-device-info.ts new file mode 100644 index 00000000..61c315fd --- /dev/null +++ b/__mocks__/react-native-device-info.ts @@ -0,0 +1,6 @@ +const DeviceInfo = { + getTotalMemorySync: jest.fn(() => 8 * 1024 * 1024 * 1024), // 8 GB default + getUsedMemory: jest.fn(() => Promise.resolve(0)), +}; + +export default DeviceInfo; diff --git a/__mocks__/react-native-executorch.ts b/__mocks__/react-native-executorch.ts new file mode 100644 index 00000000..950d453d --- /dev/null +++ b/__mocks__/react-native-executorch.ts @@ -0,0 +1,51 @@ +const makeModelConstants = (name: string) => ({ + modelSource: `https://huggingface.co/mock/${name}/model.pte`, + tokenizerSource: `https://huggingface.co/mock/${name}/tokenizer.json`, + tokenizerConfigSource: `https://huggingface.co/mock/${name}/tokenizer_config.json`, +}); + +export const QWEN3_0_6B_QUANTIZED = makeModelConstants('qwen3-0.6b-quantized'); +export const QWEN3_0_6B = makeModelConstants('qwen3-0.6b'); +export const QWEN3_1_7B = makeModelConstants('qwen3-1.7b'); +export const QWEN3_1_7B_QUANTIZED = makeModelConstants('qwen3-1.7b-quantized'); +export const QWEN3_4B = makeModelConstants('qwen3-4b'); +export const QWEN3_4B_QUANTIZED = makeModelConstants('qwen3-4b-quantized'); +export const LLAMA3_2_1B = makeModelConstants('llama3.2-1b'); +export const LLAMA3_2_1B_QLORA = makeModelConstants('llama3.2-1b-qlora'); +export const LLAMA3_2_1B_SPINQUANT = makeModelConstants('llama3.2-1b-spinquant'); +export const LLAMA3_2_3B = makeModelConstants('llama3.2-3b'); +export const LLAMA3_2_3B_QLORA = makeModelConstants('llama3.2-3b-qlora'); +export const LLAMA3_2_3B_SPINQUANT = makeModelConstants('llama3.2-3b-spinquant'); +export const HAMMER2_1_0_5B = makeModelConstants('hammer2.1-0.5b'); +export const HAMMER2_1_0_5B_QUANTIZED = makeModelConstants('hammer2.1-0.5b-quantized'); +export const HAMMER2_1_1_5B = makeModelConstants('hammer2.1-1.5b'); +export const HAMMER2_1_1_5B_QUANTIZED = makeModelConstants('hammer2.1-1.5b-quantized'); +export const HAMMER2_1_3B = makeModelConstants('hammer2.1-3b'); +export const HAMMER2_1_3B_QUANTIZED = makeModelConstants('hammer2.1-3b-quantized'); +export const QWEN2_5_0_5B = makeModelConstants('qwen2.5-0.5b'); +export const QWEN2_5_0_5B_QUANTIZED = makeModelConstants('qwen2.5-0.5b-quantized'); +export const QWEN2_5_1_5B = makeModelConstants('qwen2.5-1.5b'); +export const QWEN2_5_1_5B_QUANTIZED = makeModelConstants('qwen2.5-1.5b-quantized'); +export const QWEN2_5_3B = makeModelConstants('qwen2.5-3b'); +export const QWEN2_5_3B_QUANTIZED = makeModelConstants('qwen2.5-3b-quantized'); +export const PHI_4_MINI_4B = makeModelConstants('phi4-mini-4b'); +export const PHI_4_MINI_4B_QUANTIZED = makeModelConstants('phi4-mini-4b-quantized'); +export const LFM2_5_1_2B_INSTRUCT = makeModelConstants('lfm2.5-1.2b-instruct'); +export const LFM2_5_1_2B_INSTRUCT_QUANTIZED = makeModelConstants('lfm2.5-1.2b-instruct-quantized'); +export const WHISPER_TINY_EN = 'whisper-tiny-en'; + +export const LLMModule = { + fromCustomModel: jest.fn(), +}; + +export const SpeechToTextModule = { + fromModelName: jest.fn(), +}; + +export const TextEmbeddingsModule = { + fromCustomModel: jest.fn(), +}; + +export const ResourceFetcher = { + fetch: jest.fn(), +}; diff --git a/__mocks__/react-native-fs.ts b/__mocks__/react-native-fs.ts new file mode 100644 index 00000000..ea6761fd --- /dev/null +++ b/__mocks__/react-native-fs.ts @@ -0,0 +1,3 @@ +export const exists = jest.fn(); +export const unlink = jest.fn(); +export const copyFileAssets = jest.fn(); diff --git a/__mocks__/react-native-markdown-display.tsx b/__mocks__/react-native-markdown-display.tsx new file mode 100644 index 00000000..7500fc42 --- /dev/null +++ b/__mocks__/react-native-markdown-display.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { Text } from 'react-native'; + +const Markdown = ({ children }: { children: string }) => {children}; + +export default Markdown; diff --git a/__mocks__/react-native-pdfium.ts b/__mocks__/react-native-pdfium.ts new file mode 100644 index 00000000..7af34237 --- /dev/null +++ b/__mocks__/react-native-pdfium.ts @@ -0,0 +1 @@ +export const readPDF = jest.fn(); diff --git a/__mocks__/react-native-reanimated.ts b/__mocks__/react-native-reanimated.ts new file mode 100644 index 00000000..62c85fe4 --- /dev/null +++ b/__mocks__/react-native-reanimated.ts @@ -0,0 +1,3 @@ +// Use the official mock provided by react-native-reanimated +const Reanimated = require('react-native-reanimated/mock'); +module.exports = Reanimated; diff --git a/__mocks__/react-native-toast-message.ts b/__mocks__/react-native-toast-message.ts new file mode 100644 index 00000000..9eed679f --- /dev/null +++ b/__mocks__/react-native-toast-message.ts @@ -0,0 +1,6 @@ +const Toast = { + show: jest.fn(), + hide: jest.fn(), +}; + +export default Toast; diff --git a/__mocks__/svgMock.tsx b/__mocks__/svgMock.tsx new file mode 100644 index 00000000..892d7697 --- /dev/null +++ b/__mocks__/svgMock.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { View } from 'react-native'; + +const SvgMock = (props: any) => ; +export default SvgMock; +export const ReactComponent = SvgMock; diff --git a/__tests__/ChatBar.test.tsx b/__tests__/ChatBar.test.tsx new file mode 100644 index 00000000..d40c8309 --- /dev/null +++ b/__tests__/ChatBar.test.tsx @@ -0,0 +1,318 @@ +import React from 'react'; +import { render, screen, fireEvent, act } from '@testing-library/react-native'; + +// ── mocks ───────────────────────────────────────────────────────────────────── + +jest.mock('../context/ThemeContext', () => ({ + useTheme: () => ({ + theme: { + ...require('../styles/colors').lightTheme, + insets: { top: 0, bottom: 0, left: 0, right: 0 }, + }, + }), +})); + +jest.mock('../store/llmStore', () => ({ + useLLMStore: jest.fn(() => ({ + isGenerating: false, + isProcessingPrompt: false, + interrupt: jest.fn(), + loadModel: jest.fn(), + model: null, + })), +})); + +jest.mock('../components/chat-screen/ChatSpeechInput', () => { + const { View, TouchableOpacity, Text } = require('react-native'); + return ({ onSubmit, onCancel }: any) => ( + + onSubmit('voice transcript')} /> + + + ); +}); + +jest.mock('../components/chat-screen/PromptSuggestions', () => { + const { TouchableOpacity, Text } = require('react-native'); + return ({ onSelectPrompt }: any) => ( + onSelectPrompt('Suggested prompt')}> + Suggest something + + ); +}); + +jest.mock('../components/chat-screen/ChatBarActions', () => { + const { View, TouchableOpacity, Text } = require('react-native'); + return ({ + userInput, + onSend, + isGenerating, + isProcessingPrompt, + onInterrupt, + onSpeechInput, + onThinkingToggle, + thinkingEnabled, + activeSourcesCount, + onSelectSource, + }: any) => ( + + {(isGenerating || isProcessingPrompt) ? ( + Stop + ) : userInput ? ( + Send + ) : ( + Mic + )} + + {activeSourcesCount > 0 ? `Sources (${activeSourcesCount})` : 'Sources'} + + + {thinkingEnabled ? 'Think ON' : 'Think OFF'} + + + ); +}); + +// ── imports ─────────────────────────────────────────────────────────────────── + +import ChatBar from '../components/chat-screen/ChatBar'; +import { useLLMStore } from '../store/llmStore'; +import { AudioManager } from 'react-native-audio-api'; +import Toast from 'react-native-toast-message'; + +const mockUseLLMStore = useLLMStore as jest.Mock; +const mockAudioManager = AudioManager as jest.Mocked; + +const downloadedModel = { + id: 1, + modelName: 'Test LLM', + isDownloaded: true, + source: 'remote' as const, + modelPath: '', + tokenizerPath: '', + tokenizerConfigPath: '', + thinking: false, + featured: false, +}; + +const defaultProps = { + chatId: 1, + onSend: jest.fn(), + onSelectModel: jest.fn(), + onSelectSource: jest.fn(), + onSelectPrompt: jest.fn(), + model: downloadedModel, + scrollRef: { current: null } as any, + isAtBottom: true, + activeSourcesCount: 0, + thinkingEnabled: false, + onThinkingToggle: jest.fn(), + hasMessages: false, +}; + +const renderBar = (props: Partial = {}) => + render(); + +beforeEach(() => { + mockUseLLMStore.mockReturnValue({ + isGenerating: false, + isProcessingPrompt: false, + interrupt: jest.fn(), + loadModel: jest.fn(), + model: null, + }); + jest.clearAllMocks(); + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + // Default: permission granted + mockAudioManager.requestRecordingPermissions.mockResolvedValue('Granted' as any); +}); + +afterEach(() => jest.restoreAllMocks()); + +// ─── "Select Model" fallback ────────────────────────────────────────────────── + +describe('no model selected', () => { + it('shows "Select Model" button when chatId is set but model is undefined', () => { + renderBar({ model: undefined }); + expect(screen.getByText('Select Model')).toBeTruthy(); + }); + + it('calls onSelectModel when "Select Model" is pressed', () => { + const onSelectModel = jest.fn(); + renderBar({ model: undefined, onSelectModel }); + fireEvent.press(screen.getByText('Select Model')); + expect(onSelectModel).toHaveBeenCalled(); + }); + + it('does not render the text input when model is undefined', () => { + renderBar({ model: undefined }); + expect(screen.queryByPlaceholderText('Ask about anything...')).toBeNull(); + }); +}); + +// ─── model not downloaded ───────────────────────────────────────────────────── + +describe('model not downloaded', () => { + it('renders nothing interactive when model is not downloaded', () => { + renderBar({ model: { ...downloadedModel, isDownloaded: false } }); + expect(screen.queryByPlaceholderText('Ask about anything...')).toBeNull(); + expect(screen.queryByText('Select Model')).toBeNull(); + }); +}); + +// ─── normal input state ─────────────────────────────────────────────────────── + +describe('downloaded model — text input', () => { + it('renders the text input', () => { + renderBar(); + expect(screen.getByPlaceholderText('Ask about anything...')).toBeTruthy(); + }); + + it('calls onSend with current input when send button is pressed', () => { + const onSend = jest.fn(); + renderBar({ onSend }); + fireEvent.changeText(screen.getByPlaceholderText('Ask about anything...'), 'Hello'); + fireEvent.press(screen.getByTestId('send-btn')); + expect(onSend).toHaveBeenCalledWith('Hello'); + }); + + it('shows prompt suggestions when hasMessages is false', () => { + renderBar({ hasMessages: false }); + expect(screen.getByTestId('prompt-suggestion')).toBeTruthy(); + }); + + it('hides prompt suggestions when hasMessages is true', () => { + renderBar({ hasMessages: true }); + expect(screen.queryByTestId('prompt-suggestion')).toBeNull(); + }); + + it('calls onSelectPrompt when a prompt suggestion is selected', () => { + const onSelectPrompt = jest.fn(); + renderBar({ onSelectPrompt }); + fireEvent.press(screen.getByTestId('prompt-suggestion')); + expect(onSelectPrompt).toHaveBeenCalledWith('Suggested prompt'); + }); + + it('passes activeSourcesCount to ChatBarActions', () => { + renderBar({ activeSourcesCount: 3 }); + expect(screen.getByText('Sources (3)')).toBeTruthy(); + }); + + it('passes thinkingEnabled to ChatBarActions', () => { + renderBar({ thinkingEnabled: true }); + expect(screen.getByText('Think ON')).toBeTruthy(); + }); + + it('calls onThinkingToggle when thinking button is pressed', () => { + const onThinkingToggle = jest.fn(); + renderBar({ onThinkingToggle }); + fireEvent.press(screen.getByTestId('thinking-btn')); + expect(onThinkingToggle).toHaveBeenCalled(); + }); + + it('calls onSelectSource when source button is pressed', () => { + const onSelectSource = jest.fn(); + renderBar({ onSelectSource }); + fireEvent.press(screen.getByTestId('source-btn')); + expect(onSelectSource).toHaveBeenCalled(); + }); +}); + +// ─── generating state ───────────────────────────────────────────────────────── + +describe('generating state', () => { + it('shows interrupt button when isGenerating', () => { + mockUseLLMStore.mockReturnValue({ + isGenerating: true, + isProcessingPrompt: false, + interrupt: jest.fn(), + loadModel: jest.fn(), + model: null, + }); + renderBar(); + expect(screen.getByTestId('interrupt-btn')).toBeTruthy(); + }); + + it('calls interrupt when interrupt button is pressed', () => { + const interrupt = jest.fn(); + mockUseLLMStore.mockReturnValue({ + isGenerating: true, + isProcessingPrompt: false, + interrupt, + loadModel: jest.fn(), + model: null, + }); + renderBar(); + fireEvent.press(screen.getByTestId('interrupt-btn')); + expect(interrupt).toHaveBeenCalled(); + }); +}); + +// ─── speech input ───────────────────────────────────────────────────────────── + +describe('speech input', () => { + it('switches to speech input view after mic button press when permission granted', async () => { + renderBar(); + await act(async () => { + fireEvent.press(screen.getByTestId('speech-btn')); + }); + expect(screen.getByTestId('speech-input')).toBeTruthy(); + }); + + it('shows toast and stays in text mode when microphone permission is denied', async () => { + mockAudioManager.requestRecordingPermissions.mockResolvedValue('Denied' as any); + renderBar(); + await act(async () => { + fireEvent.press(screen.getByTestId('speech-btn')); + }); + expect(Toast.show).toHaveBeenCalledWith( + expect.objectContaining({ text1: expect.stringContaining('Microphone permission') }) + ); + expect(screen.queryByTestId('speech-input')).toBeNull(); + }); + + it('calls onSend with transcript and hides speech input on submit', async () => { + const onSend = jest.fn(); + renderBar({ onSend }); + await act(async () => { + fireEvent.press(screen.getByTestId('speech-btn')); + }); + fireEvent.press(screen.getByTestId('speech-submit')); + expect(onSend).toHaveBeenCalledWith('voice transcript'); + expect(screen.queryByTestId('speech-input')).toBeNull(); + }); + + it('hides speech input without calling onSend when cancelled', async () => { + const onSend = jest.fn(); + renderBar({ onSend }); + await act(async () => { + fireEvent.press(screen.getByTestId('speech-btn')); + }); + fireEvent.press(screen.getByTestId('speech-cancel')); + expect(onSend).not.toHaveBeenCalled(); + expect(screen.queryByTestId('speech-input')).toBeNull(); + }); + + it('does not call onSend when transcript is empty on submit', async () => { + const onSend = jest.fn(); + // Override speech mock to submit empty string + jest.mock('../components/chat-screen/ChatSpeechInput', () => { + const { View, TouchableOpacity } = require('react-native'); + return ({ onSubmit }: any) => ( + + onSubmit('')} /> + + ); + }); + renderBar({ onSend }); + await act(async () => { + fireEvent.press(screen.getByTestId('speech-btn')); + }); + // The component guards: if (!transcript) return without calling onSend + fireEvent.press(screen.getByTestId('speech-submit')); + // With the module-level mock, transcript = 'voice transcript' (non-empty), so we + // verify the guard by checking the original mock behavior + expect(screen.queryByTestId('speech-input')).toBeNull(); + }); +}); diff --git a/__tests__/ChatBarActions.test.tsx b/__tests__/ChatBarActions.test.tsx new file mode 100644 index 00000000..b6665fb4 --- /dev/null +++ b/__tests__/ChatBarActions.test.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { TouchableOpacity } from 'react-native'; +import { render, screen, fireEvent } from '@testing-library/react-native'; + +jest.mock('../context/ThemeContext', () => ({ + useTheme: () => ({ + theme: { ...require('../styles/colors').lightTheme, insets: { top: 0, bottom: 0, left: 0, right: 0 } }, + }), +})); + +// Render CircleButton as a simple button with a known testID derived from its onPress target +jest.mock('../components/CircleButton', () => { + const { TouchableOpacity } = require('react-native'); + // Expose onPress so tests can trigger it + return ({ onPress, disabled }: any) => ( + + ); +}); + +import ChatBarActions from '../components/chat-screen/ChatBarActions'; + +const defaultProps = { + onSelectSource: jest.fn(), + activeSourcesCount: 0, + userInput: '', + onSend: jest.fn(), + isGenerating: false, + isProcessingPrompt: false, + onInterrupt: jest.fn(), + onSpeechInput: jest.fn(), + thinkingEnabled: false, + onThinkingToggle: jest.fn(), +}; + +const renderActions = (props = {}) => + render(); + +// Helper: press the single CircleButton (the action button on the right) +const pressActionBtn = (getByTestId: typeof screen.getByTestId) => + fireEvent.press(getByTestId('circle-btn')); + +beforeEach(() => jest.clearAllMocks()); + +// ─── action button logic ─────────────────────────────────────────────────────── + +describe('action button', () => { + it('calls onSpeechInput when idle with no input', () => { + renderActions({ userInput: '', isGenerating: false, isProcessingPrompt: false }); + pressActionBtn(screen.getByTestId); + expect(defaultProps.onSpeechInput).toHaveBeenCalled(); + }); + + it('calls onSend when there is user input', () => { + renderActions({ userInput: 'Hello' }); + pressActionBtn(screen.getByTestId); + expect(defaultProps.onSend).toHaveBeenCalled(); + }); + + it('calls onInterrupt when isGenerating', () => { + renderActions({ isGenerating: true }); + pressActionBtn(screen.getByTestId); + expect(defaultProps.onInterrupt).toHaveBeenCalled(); + }); + + it('calls onInterrupt when isProcessingPrompt', () => { + renderActions({ isProcessingPrompt: true }); + pressActionBtn(screen.getByTestId); + expect(defaultProps.onInterrupt).toHaveBeenCalled(); + }); + + it('interrupt takes priority over user input when generating', () => { + renderActions({ isGenerating: true, userInput: 'Hello' }); + pressActionBtn(screen.getByTestId); + expect(defaultProps.onInterrupt).toHaveBeenCalled(); + expect(defaultProps.onSend).not.toHaveBeenCalled(); + }); +}); + +// ─── sources button ─────────────────────────────────────────────────────────── + +describe('sources button', () => { + it('shows "Sources" label', () => { + renderActions(); + expect(screen.getByText('Sources')).toBeTruthy(); + }); + + it('calls onSelectSource when pressed', () => { + const onSelectSource = jest.fn(); + renderActions({ onSelectSource }); + fireEvent.press(screen.getByText('Sources')); + expect(onSelectSource).toHaveBeenCalled(); + }); + + it('shows count badge when activeSourcesCount > 0', () => { + renderActions({ activeSourcesCount: 3 }); + expect(screen.getByText('3')).toBeTruthy(); + }); + + it('does not show count badge when activeSourcesCount is 0', () => { + renderActions({ activeSourcesCount: 0 }); + expect(screen.queryByText('0')).toBeNull(); + }); +}); + +// ─── thinking toggle ────────────────────────────────────────────────────────── + +describe('thinking toggle', () => { + it('calls onThinkingToggle when Think button is pressed', () => { + const onThinkingToggle = jest.fn(); + renderActions({ onThinkingToggle }); + fireEvent.press(screen.getByText('Think')); + expect(onThinkingToggle).toHaveBeenCalled(); + }); + + it('shows Think label in both enabled and disabled states', () => { + const { unmount } = renderActions({ thinkingEnabled: true }); + expect(screen.getByText('Think')).toBeTruthy(); + unmount(); + renderActions({ thinkingEnabled: false }); + expect(screen.getByText('Think')).toBeTruthy(); + }); +}); diff --git a/__tests__/ChatSettingsForm.test.tsx b/__tests__/ChatSettingsForm.test.tsx new file mode 100644 index 00000000..2d62a31c --- /dev/null +++ b/__tests__/ChatSettingsForm.test.tsx @@ -0,0 +1,186 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react-native'; + +jest.mock('../context/ThemeContext', () => ({ + useTheme: () => ({ + theme: { ...require('../styles/colors').lightTheme, insets: { top: 0, bottom: 0, left: 0, right: 0 } }, + }), +})); + +jest.mock('../components/model-hub/ModelCard', () => { + const { Text } = require('react-native'); + return ({ model }: any) => {model.modelName}; +}); + +jest.mock('../components/TextFieldInput', () => { + const { TextInput } = require('react-native'); + return ({ value, onChangeText, placeholder, testID }: any) => ( + + ); +}); + +jest.mock('../components/TextAreaField', () => { + const { TextInput } = require('react-native'); + return ({ value, onChangeText, placeholder, editable }: any) => ( + + ); +}); + +jest.mock('../components/EntryButton', () => { + const { TouchableOpacity, Text } = require('react-native'); + return ({ text, onPress }: any) => ( + {text} + ); +}); + +jest.mock('../components/SecondaryButton', () => { + const { TouchableOpacity, Text } = require('react-native'); + return ({ text, onPress }: any) => ( + {text} + ); +}); + +import ChatSettingsForm from '../components/settings/ChatSettingsForm'; + +const defaultSettings = { + title: 'My Chat', + systemPrompt: 'You are helpful.', + contextWindow: '6', +}; + +const defaultProps = { + settings: defaultSettings, + setSetting: jest.fn(), + isDefaultSettings: false, + isPhantomChat: false, + scrollViewRef: { current: null } as any, + onDelete: jest.fn(), + onExport: jest.fn(), + onAppInfo: jest.fn(), +}; + +const renderForm = (props = {}) => + render(); + +beforeEach(() => jest.clearAllMocks()); + +// ─── field visibility ───────────────────────────────────────────────────────── + +describe('field visibility', () => { + it('shows chat name field for regular (non-default, non-phantom) chats', () => { + renderForm({ isDefaultSettings: false, isPhantomChat: false }); + expect(screen.getByText('Chat name')).toBeTruthy(); + }); + + it('hides chat name field for default settings', () => { + renderForm({ isDefaultSettings: true }); + expect(screen.queryByText('Chat name')).toBeNull(); + }); + + it('hides chat name field for phantom chats', () => { + renderForm({ isPhantomChat: true }); + expect(screen.queryByText('Chat name')).toBeNull(); + }); + + it('shows model card when model is provided', () => { + renderForm({ + model: { + id: 1, modelName: 'Llama-3B', source: 'remote', isDownloaded: true, + modelPath: '', tokenizerPath: '', tokenizerConfigPath: '', thinking: false, featured: false, + }, + }); + expect(screen.getByTestId('model-card')).toBeTruthy(); + }); + + it('hides model card when no model provided', () => { + renderForm({ model: undefined }); + expect(screen.queryByTestId('model-card')).toBeNull(); + }); + + it('always shows Context Window and System Prompt fields', () => { + renderForm(); + expect(screen.getByText('Context Window')).toBeTruthy(); + expect(screen.getByText('System Prompt')).toBeTruthy(); + }); +}); + +// ─── action buttons ─────────────────────────────────────────────────────────── + +describe('action buttons', () => { + it('shows Export and Delete buttons for regular chats', () => { + renderForm({ isDefaultSettings: false, isPhantomChat: false }); + expect(screen.getByTestId('secondary-Export Chat')).toBeTruthy(); + expect(screen.getByTestId('secondary-Delete Chat')).toBeTruthy(); + }); + + it('hides Export and Delete buttons for default settings', () => { + renderForm({ isDefaultSettings: true }); + expect(screen.queryByTestId('secondary-Export Chat')).toBeNull(); + expect(screen.queryByTestId('secondary-Delete Chat')).toBeNull(); + }); + + it('hides Export and Delete buttons for phantom chats', () => { + renderForm({ isPhantomChat: true }); + expect(screen.queryByTestId('secondary-Export Chat')).toBeNull(); + expect(screen.queryByTestId('secondary-Delete Chat')).toBeNull(); + }); + + it('always shows App Info button', () => { + renderForm(); + expect(screen.getByText('App Info')).toBeTruthy(); + }); + + it('calls onDelete when Delete Chat pressed', () => { + const onDelete = jest.fn(); + renderForm({ onDelete }); + fireEvent.press(screen.getByTestId('secondary-Delete Chat')); + expect(onDelete).toHaveBeenCalled(); + }); + + it('calls onExport when Export Chat pressed', () => { + const onExport = jest.fn(); + renderForm({ onExport }); + fireEvent.press(screen.getByTestId('secondary-Export Chat')); + expect(onExport).toHaveBeenCalled(); + }); + + it('calls onAppInfo when App Info pressed', () => { + const onAppInfo = jest.fn(); + renderForm({ onAppInfo }); + fireEvent.press(screen.getByTestId('entry-App Info')); + expect(onAppInfo).toHaveBeenCalled(); + }); +}); + +// ─── system prompt editability ──────────────────────────────────────────────── + +describe('system prompt editability', () => { + it('is editable for default settings', () => { + renderForm({ isDefaultSettings: true }); + const input = screen.getByTestId('system-prompt-input'); + expect(input.props.editable).toBe(true); + }); + + it('is editable for phantom chat', () => { + renderForm({ isPhantomChat: true }); + const input = screen.getByTestId('system-prompt-input'); + expect(input.props.editable).toBe(true); + }); + + it('is not editable for regular existing chats', () => { + renderForm({ isDefaultSettings: false, isPhantomChat: false }); + const input = screen.getByTestId('system-prompt-input'); + expect(input.props.editable).toBe(false); + }); +}); + +// ─── setSetting callbacks ───────────────────────────────────────────────────── + +describe('setSetting callbacks', () => { + it('calls setSetting with contextWindow when changed', () => { + const setSetting = jest.fn(); + renderForm({ setSetting }); + fireEvent.changeText(screen.getByDisplayValue('6'), '10'); + expect(setSetting).toHaveBeenCalledWith('contextWindow', '10'); + }); +}); diff --git a/__tests__/MessageItem.test.tsx b/__tests__/MessageItem.test.tsx new file mode 100644 index 00000000..3f40382d --- /dev/null +++ b/__tests__/MessageItem.test.tsx @@ -0,0 +1,167 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react-native'; +import { lightTheme } from '../styles/colors'; + +// ── mocks ───────────────────────────────────────────────────────────────────── + +jest.mock('../context/ThemeContext', () => ({ + useTheme: () => ({ + theme: { ...require('../styles/colors').lightTheme, insets: { top: 0, bottom: 0, left: 0, right: 0 } }, + }), +})); + +jest.mock('../store/llmStore', () => ({ + useLLMStore: jest.fn(() => ({ isGenerating: false })), +})); + +jest.mock('../components/chat-screen/MarkdownComponent', () => { + const { Text } = require('react-native'); + return ({ text }: { text: string }) => {text}; +}); + +jest.mock('../components/chat-screen/ThinkingBlock', () => { + const { Text } = require('react-native'); + return ({ content, isComplete, inProgress }: any) => ( + + {content} + + ); +}); + +jest.mock('../components/bottomSheets/MessageManagementSheet', () => () => null); + +// ── helpers ─────────────────────────────────────────────────────────────────── + +import MessageItem from '../components/chat-screen/MessageItem'; +import { useLLMStore } from '../store/llmStore'; + +const mockUseLLMStore = useLLMStore as jest.Mock; + +const renderItem = (props: Partial> = {}) => + render( + + ); + +beforeEach(() => { + mockUseLLMStore.mockReturnValue({ isGenerating: false }); + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +// ─── event messages ─────────────────────────────────────────────────────────── + +describe('event messages', () => { + it('renders event message with filename and text parts', () => { + renderItem({ role: 'event', content: 'document.pdf was added' }); + // The outer Text contains "document.pdf " as direct text — match with substring + expect(screen.getByText(/document\.pdf/, { exact: false })).toBeTruthy(); + expect(screen.getByText('was added')).toBeTruthy(); + }); + + it('does not render a MarkdownComponent for event messages', () => { + renderItem({ role: 'event', content: 'file.txt uploaded' }); + expect(screen.queryByTestId('markdown')).toBeNull(); + }); +}); + +// ─── assistant messages ─────────────────────────────────────────────────────── + +describe('assistant messages', () => { + it('renders the model name', () => { + renderItem({ role: 'assistant', content: 'Hi', modelName: 'Llama-3B' }); + expect(screen.getByText('Llama-3B')).toBeTruthy(); + }); + + it('renders content via MarkdownComponent', () => { + renderItem({ role: 'assistant', content: 'Some response' }); + expect(screen.getByTestId('markdown')).toBeTruthy(); + expect(screen.getByText('Some response')).toBeTruthy(); + }); + + it('renders metadata when tokensPerSecond is provided and non-zero', () => { + renderItem({ role: 'assistant', content: 'Hi', tokensPerSecond: 12.5, timeToFirstToken: 300 }); + expect(screen.getByText(/tps:/)).toBeTruthy(); + expect(screen.getByText(/ttft:/)).toBeTruthy(); + }); + + it('does not render metadata when tokensPerSecond is 0', () => { + renderItem({ role: 'assistant', content: 'Hi', tokensPerSecond: 0 }); + expect(screen.queryByText(/tps:/)).toBeNull(); + }); + + it('does not render metadata when tokensPerSecond is undefined', () => { + renderItem({ role: 'assistant', content: 'Hi' }); + expect(screen.queryByText(/tps:/)).toBeNull(); + }); +}); + +// ─── user messages ──────────────────────────────────────────────────────────── + +describe('user messages', () => { + it('does not render model name for user messages', () => { + renderItem({ role: 'user', content: 'Hello', modelName: 'Llama-3B' }); + expect(screen.queryByText('Llama-3B')).toBeNull(); + }); + + it('renders user content via MarkdownComponent', () => { + renderItem({ role: 'user', content: 'My question' }); + expect(screen.getByText('My question')).toBeTruthy(); + }); +}); + +// ─── thinking block parsing ─────────────────────────────────────────────────── + +describe('thinking block parsing', () => { + it('does not render ThinkingBlock when there is no tag', () => { + renderItem({ content: 'Plain response' }); + expect(screen.queryByTestId('thinking-block')).toBeNull(); + }); + + it('renders ThinkingBlock with complete=true for closed tag', () => { + renderItem({ content: 'some reasoningFinal answer' }); + const block = screen.getByTestId('thinking-block'); + expect(block).toBeTruthy(); + expect(block.props.accessibilityLabel).toContain('complete:true'); + expect(screen.getByText('some reasoning')).toBeTruthy(); + }); + + it('renders ThinkingBlock with complete=false for unclosed tag (streaming)', () => { + renderItem({ content: 'still thinking...', isLastMessage: true }); + const block = screen.getByTestId('thinking-block'); + expect(block.props.accessibilityLabel).toContain('complete:false'); + }); + + it('renders normal content after via MarkdownComponent', () => { + renderItem({ content: 'reasoningThis is the answer' }); + const markdowns = screen.getAllByTestId('markdown'); + const texts = markdowns.map((el) => el.props.children); + expect(texts).toContain('This is the answer'); + }); + + it('marks ThinkingBlock as inProgress when last message and isGenerating and thinking is incomplete', () => { + mockUseLLMStore.mockReturnValue({ isGenerating: true }); + renderItem({ content: 'working...', isLastMessage: true }); + const block = screen.getByTestId('thinking-block'); + expect(block.props.accessibilityLabel).toContain('inProgress:true'); + }); + + it('does not mark ThinkingBlock as inProgress when not isLastMessage', () => { + mockUseLLMStore.mockReturnValue({ isGenerating: true }); + renderItem({ content: 'working...', isLastMessage: false }); + const block = screen.getByTestId('thinking-block'); + expect(block.props.accessibilityLabel).toContain('inProgress:false'); + }); + + it('does not render ThinkingBlock when thinking content is empty whitespace', () => { + renderItem({ content: ' answer' }); + expect(screen.queryByTestId('thinking-block')).toBeNull(); + }); +}); diff --git a/__tests__/ModelCard.test.tsx b/__tests__/ModelCard.test.tsx new file mode 100644 index 00000000..495ab8d3 --- /dev/null +++ b/__tests__/ModelCard.test.tsx @@ -0,0 +1,210 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react-native'; +import NetInfo from '@react-native-community/netinfo'; +import Toast from 'react-native-toast-message'; + +jest.mock('../context/ThemeContext', () => ({ + useTheme: () => ({ + theme: { ...require('../styles/colors').lightTheme, insets: { top: 0, bottom: 0, left: 0, right: 0 } }, + }), +})); + +jest.mock('../store/modelStore', () => ({ + useModelStore: jest.fn(() => ({ + downloadStates: {}, + downloadModel: jest.fn(), + cancelDownload: jest.fn(), + })), + ModelState: { + NotStarted: 'NotStarted', + Downloading: 'Downloading', + Downloaded: 'Downloaded', + }, +})); + +jest.mock('../utils/modelCompatibility', () => ({ + isModelCompatible: jest.fn(() => true), +})); + +jest.mock('@react-native-community/netinfo', () => ({ + fetch: jest.fn(), +})); + +jest.mock('../components/Chip', () => { + const { Text } = require('react-native'); + return ({ title }: any) => {title}; +}); + +jest.mock('../components/CircleButton', () => { + const { TouchableOpacity } = require('react-native'); + return ({ onPress, disabled, testID }: any) => ( + + ); +}); + +import ModelCard from '../components/model-hub/ModelCard'; +import { useModelStore, ModelState } from '../store/modelStore'; +import { isModelCompatible } from '../utils/modelCompatibility'; + +const mockUseModelStore = useModelStore as jest.Mock; +const mockIsModelCompatible = isModelCompatible as jest.Mock; +const mockNetInfoFetch = NetInfo.fetch as jest.Mock; + +const baseModel = { + id: 1, + modelName: 'Llama-3B', + source: 'remote' as const, + isDownloaded: false, + featured: false, + parameters: 3.21, + modelSize: 2.5, + modelPath: '', + tokenizerPath: '', + tokenizerConfigPath: '', + thinking: false, + labels: [], +}; + +const renderCard = (props: Partial = {}) => { + const onPress = props.onPress || jest.fn(); + return render( + + ); +}; + +beforeEach(() => { + mockUseModelStore.mockReturnValue({ + downloadStates: {}, + downloadModel: jest.fn(), + cancelDownload: jest.fn(), + }); + mockIsModelCompatible.mockReturnValue(true); + mockNetInfoFetch.mockResolvedValue({ isConnected: true, type: 'wifi' }); + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); + +afterEach(() => jest.restoreAllMocks()); + +// ─── display ────────────────────────────────────────────────────────────────── + +describe('display', () => { + it('renders model name', () => { + renderCard(); + expect(screen.getByText('Llama-3B')).toBeTruthy(); + }); + + it('shows parameters chip', () => { + renderCard(); + expect(screen.getByTestId('chip-3.21 B')).toBeTruthy(); + }); + + it('shows model size chip', () => { + renderCard(); + expect(screen.getByTestId('chip-2.50 GB')).toBeTruthy(); + }); + + it('shows Featured chip when model is featured', () => { + renderCard({ featured: true }); + expect(screen.getByTestId('chip-Featured')).toBeTruthy(); + }); + + it('does not show Featured chip for non-featured model', () => { + renderCard({ featured: false }); + expect(screen.queryByTestId('chip-Featured')).toBeNull(); + }); + + it('shows Incompatible chip when model is not compatible', () => { + mockIsModelCompatible.mockReturnValue(false); + renderCard(); + expect(screen.getByTestId('chip-Incompatible')).toBeTruthy(); + }); + + it('calls onPress when card is tapped', () => { + const onPress = jest.fn(); + render(); + fireEvent.press(screen.getByText('Llama-3B')); + expect(onPress).toHaveBeenCalledWith(baseModel); + }); +}); + +// ─── download states ────────────────────────────────────────────────────────── + +describe('download state rendering', () => { + it('shows download button when NotStarted', () => { + mockUseModelStore.mockReturnValue({ + downloadStates: { 1: { status: ModelState.NotStarted, progress: 0 } }, + downloadModel: jest.fn(), + cancelDownload: jest.fn(), + }); + renderCard(); + expect(screen.getByTestId('circle-btn')).toBeTruthy(); + }); + + it('shows progress bar when Downloading', () => { + mockUseModelStore.mockReturnValue({ + downloadStates: { 1: { status: ModelState.Downloading, progress: 0.4 } }, + downloadModel: jest.fn(), + cancelDownload: jest.fn(), + }); + renderCard(); + expect(screen.getByText('40%')).toBeTruthy(); + }); + + it('does not show download button when already downloaded', () => { + mockUseModelStore.mockReturnValue({ + downloadStates: { 1: { status: ModelState.Downloaded, progress: 1 } }, + downloadModel: jest.fn(), + cancelDownload: jest.fn(), + }); + renderCard({ isDownloaded: true }); + expect(screen.queryByTestId('circle-btn')).toBeNull(); + }); +}); + +// ─── download action ────────────────────────────────────────────────────────── + +describe('download action', () => { + it('calls downloadModel when download button pressed on wifi', async () => { + const downloadModel = jest.fn().mockResolvedValue(undefined); + mockUseModelStore.mockReturnValue({ + downloadStates: {}, + downloadModel, + cancelDownload: jest.fn(), + }); + renderCard(); + fireEvent.press(screen.getByTestId('circle-btn')); + await waitFor(() => expect(downloadModel).toHaveBeenCalledWith(expect.objectContaining({ id: 1 }))); + }); + + it('shows toast when no internet connection', async () => { + mockNetInfoFetch.mockResolvedValue({ isConnected: false }); + renderCard(); + fireEvent.press(screen.getByTestId('circle-btn')); + await waitFor(() => + expect(Toast.show).toHaveBeenCalledWith( + expect.objectContaining({ text1: expect.stringContaining('internet') }) + ) + ); + }); + + it('calls cancelDownload when cancel button pressed while downloading', async () => { + const cancelDownload = jest.fn().mockResolvedValue(undefined); + mockUseModelStore.mockReturnValue({ + downloadStates: { 1: { status: ModelState.Downloading, progress: 0.5 } }, + downloadModel: jest.fn(), + cancelDownload, + }); + renderCard(); + fireEvent.press(screen.getByTestId('circle-btn')); + await waitFor(() => expect(cancelDownload).toHaveBeenCalledWith(expect.objectContaining({ id: 1 }))); + }); + + it('shows wifi warning sheet when on mobile data', async () => { + mockNetInfoFetch.mockResolvedValue({ isConnected: true, type: 'cellular' }); + const present = jest.fn(); + const wifiWarningSheetRef = { current: { present } }; + renderCard({ wifiWarningSheetRef }); + fireEvent.press(screen.getByTestId('circle-btn')); + await waitFor(() => expect(present).toHaveBeenCalled()); + }); +}); diff --git a/__tests__/ModelListFilters.test.tsx b/__tests__/ModelListFilters.test.tsx new file mode 100644 index 00000000..51052f6b --- /dev/null +++ b/__tests__/ModelListFilters.test.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react-native'; +import { ModelHubFilter } from '../hooks/useModelHubData'; + +jest.mock('../context/ThemeContext', () => ({ + useTheme: () => ({ + theme: { ...require('../styles/colors').lightTheme, insets: { top: 0, bottom: 0, left: 0, right: 0 } }, + }), +})); + +jest.mock('../components/TextFieldInput', () => { + const { TextInput } = require('react-native'); + return ({ value, onChangeText, placeholder }: any) => ( + + ); +}); + +jest.mock('../components/model-hub/SortingTag', () => { + const { TouchableOpacity, Text } = require('react-native'); + return ({ text, selected, onPress }: any) => ( + + {text} + + ); +}); + +import ModelListFilters from '../components/model-hub/ModelListFilters'; + +const defaultProps = { + search: '', + onSearchChange: jest.fn(), + activeFilters: new Set(), + onFiltersChange: jest.fn(), + groupByModel: false, + onGroupByModelChange: jest.fn(), +}; + +const renderFilters = (props = {}) => + render(); + +beforeEach(() => jest.clearAllMocks()); + +// ─── search input ───────────────────────────────────────────────────────────── + +describe('search input', () => { + it('renders with current search value', () => { + renderFilters({ search: 'llama' }); + expect(screen.getByDisplayValue('llama')).toBeTruthy(); + }); + + it('calls onSearchChange when text changes', () => { + const onSearchChange = jest.fn(); + renderFilters({ onSearchChange }); + fireEvent.changeText(screen.getByTestId('search-input'), 'qwen'); + expect(onSearchChange).toHaveBeenCalledWith('qwen'); + }); +}); + +// ─── filter tags ────────────────────────────────────────────────────────────── + +describe('filter tags', () => { + it('renders all four filter tags', () => { + renderFilters(); + expect(screen.getByTestId('tag-Featured')).toBeTruthy(); + expect(screen.getByTestId('tag-Compatible')).toBeTruthy(); + expect(screen.getByTestId('tag-Group by model')).toBeTruthy(); + expect(screen.getByTestId('tag-Ready to use')).toBeTruthy(); + }); + + it('toggles Featured filter on press — adds when not active', () => { + const onFiltersChange = jest.fn(); + renderFilters({ onFiltersChange, activeFilters: new Set() }); + fireEvent.press(screen.getByTestId('tag-Featured')); + const passed = onFiltersChange.mock.calls[0][0] as Set; + expect(passed.has(ModelHubFilter.Featured)).toBe(true); + }); + + it('toggles Featured filter on press — removes when active', () => { + const onFiltersChange = jest.fn(); + renderFilters({ onFiltersChange, activeFilters: new Set([ModelHubFilter.Featured]) }); + fireEvent.press(screen.getByTestId('tag-Featured')); + const passed = onFiltersChange.mock.calls[0][0] as Set; + expect(passed.has(ModelHubFilter.Featured)).toBe(false); + }); + + it('toggles Compatible filter independently', () => { + const onFiltersChange = jest.fn(); + renderFilters({ onFiltersChange, activeFilters: new Set([ModelHubFilter.Featured]) }); + fireEvent.press(screen.getByTestId('tag-Compatible')); + const passed = onFiltersChange.mock.calls[0][0] as Set; + expect(passed.has(ModelHubFilter.Compatible)).toBe(true); + expect(passed.has(ModelHubFilter.Featured)).toBe(true); // unchanged + }); + + it('toggles Downloaded (Ready to use) filter', () => { + const onFiltersChange = jest.fn(); + renderFilters({ onFiltersChange, activeFilters: new Set() }); + fireEvent.press(screen.getByTestId('tag-Ready to use')); + const passed = onFiltersChange.mock.calls[0][0] as Set; + expect(passed.has(ModelHubFilter.Downloaded)).toBe(true); + }); +}); + +// ─── group by model ─────────────────────────────────────────────────────────── + +describe('Group by model toggle', () => { + it('calls onGroupByModelChange with true when not grouped', () => { + const onGroupByModelChange = jest.fn(); + renderFilters({ onGroupByModelChange, groupByModel: false }); + fireEvent.press(screen.getByTestId('tag-Group by model')); + expect(onGroupByModelChange).toHaveBeenCalledWith(true); + }); + + it('calls onGroupByModelChange with false when already grouped', () => { + const onGroupByModelChange = jest.fn(); + renderFilters({ onGroupByModelChange, groupByModel: true }); + fireEvent.press(screen.getByTestId('tag-Group by model')); + expect(onGroupByModelChange).toHaveBeenCalledWith(false); + }); +}); diff --git a/__tests__/ModelManagementSheet.test.tsx b/__tests__/ModelManagementSheet.test.tsx new file mode 100644 index 00000000..f8b30565 --- /dev/null +++ b/__tests__/ModelManagementSheet.test.tsx @@ -0,0 +1,197 @@ +import React, { createRef } from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react-native'; + +// ── mocks ───────────────────────────────────────────────────────────────────── + +jest.mock('../context/ThemeContext', () => ({ + useTheme: () => ({ + theme: { + ...require('../styles/colors').lightTheme, + insets: { top: 0, bottom: 0, left: 0, right: 0 }, + }, + }), +})); + +jest.mock('../store/modelStore', () => ({ + useModelStore: jest.fn(() => ({ + removeModel: jest.fn(), + removeModelFiles: jest.fn(), + })), +})); + +jest.mock('expo-router', () => ({ router: { push: jest.fn() } })); + +// ModelCard — stub to show model name +jest.mock('../components/model-hub/ModelCard', () => { + const { Text } = require('react-native'); + return ({ model }: any) => {model.modelName}; +}); + +// BottomSheetModal — immediately render children with injected data +jest.mock('@gorhom/bottom-sheet', () => { + const React = require('react'); + const { View } = require('react-native'); + let _injectedData: any = null; + + const BottomSheetModal = React.forwardRef((props: any, _ref: any) => { + if (!props.children || !_injectedData) return null; + return {props.children({ data: _injectedData })}; + }); + // Attach a way for tests to set the data before rendering + (BottomSheetModal as any).__setData = (d: any) => { _injectedData = d; }; + + return { + BottomSheetModal, + BottomSheetView: ({ children, style }: any) => {children}, + BottomSheetBackdrop: () => null, + }; +}); + +// ── imports ─────────────────────────────────────────────────────────────────── + +import ModelManagementSheet from '../components/bottomSheets/ModelManagementSheet'; +import { useModelStore } from '../store/modelStore'; +import { BottomSheetModal } from '@gorhom/bottom-sheet'; +import Toast from 'react-native-toast-message'; + +const mockUseModelStore = useModelStore as jest.Mock; +const setSheetData = (BottomSheetModal as any).__setData; + +const baseModel = { + id: 1, + modelName: 'Test Model', + source: 'remote' as const, + isDownloaded: true, + modelPath: '', + tokenizerPath: '', + tokenizerConfigPath: '', + thinking: false, + featured: false, +}; + +const renderSheet = (model = baseModel) => { + setSheetData(model); + return render(); +}; + +beforeEach(() => { + mockUseModelStore.mockReturnValue({ + removeModel: jest.fn(), + removeModelFiles: jest.fn(), + }); + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); + +afterEach(() => { + setSheetData(null); + jest.restoreAllMocks(); +}); + +// ─── Initial stage ──────────────────────────────────────────────────────────── + +describe('Initial stage', () => { + it('renders the model card with model name', () => { + renderSheet(); + expect(screen.getByTestId('model-card')).toBeTruthy(); + expect(screen.getByText('Test Model')).toBeTruthy(); + }); + + it('shows "Edit model" for remote models', () => { + renderSheet({ ...baseModel, source: 'remote' }); + expect(screen.getByText('Edit model')).toBeTruthy(); + }); + + it('does not show "Edit model" for built-in models', () => { + renderSheet({ ...baseModel, source: 'built-in' as any }); + expect(screen.queryByText('Edit model')).toBeNull(); + }); + + it('shows "Delete downloaded files" for downloaded remote models', () => { + renderSheet({ ...baseModel, source: 'remote', isDownloaded: true }); + expect(screen.getByText('Delete downloaded files')).toBeTruthy(); + }); + + it('does not show "Delete downloaded files" for local models', () => { + renderSheet({ ...baseModel, source: 'local' as any, isDownloaded: true }); + expect(screen.queryByText('Delete downloaded files')).toBeNull(); + }); + + it('shows "Remove from the app" for non-built-in models', () => { + renderSheet({ ...baseModel, source: 'remote' }); + expect(screen.getByText('Remove from the app')).toBeTruthy(); + }); + + it('does not show "Remove from the app" for built-in models', () => { + renderSheet({ ...baseModel, source: 'built-in' as any }); + expect(screen.queryByText('Remove from the app')).toBeNull(); + }); +}); + +// ─── RemoveFiles stage ──────────────────────────────────────────────────────── + +describe('RemoveFiles stage', () => { + it('transitions to RemoveFiles stage on "Delete downloaded files" press', () => { + renderSheet(); + fireEvent.press(screen.getByText('Delete downloaded files')); + expect(screen.getByText(/Are you sure you want to delete files/)).toBeTruthy(); + }); + + it('shows "Delete model files" confirm button', () => { + renderSheet(); + fireEvent.press(screen.getByText('Delete downloaded files')); + expect(screen.getByText('Delete model files')).toBeTruthy(); + }); + + it('calls removeModelFiles and shows success toast on confirm', async () => { + const removeModelFiles = jest.fn().mockResolvedValue(undefined); + mockUseModelStore.mockReturnValue({ removeModel: jest.fn(), removeModelFiles }); + renderSheet(); + + fireEvent.press(screen.getByText('Delete downloaded files')); + fireEvent.press(screen.getByText('Delete model files')); + + await waitFor(() => expect(removeModelFiles).toHaveBeenCalledWith(baseModel.id)); + expect(Toast.show).toHaveBeenCalledWith( + expect.objectContaining({ text1: expect.stringContaining('Test Model') }) + ); + }); + + it('returns to Initial stage on "Close" press', () => { + renderSheet(); + fireEvent.press(screen.getByText('Delete downloaded files')); + fireEvent.press(screen.getByText('Close')); + expect(screen.getByTestId('model-card')).toBeTruthy(); + }); +}); + +// ─── RemoveModel stage ──────────────────────────────────────────────────────── + +describe('RemoveModel stage', () => { + it('transitions to RemoveModel stage on "Remove from the app" press', () => { + renderSheet(); + fireEvent.press(screen.getByText('Remove from the app')); + expect(screen.getByText(/permanently remove this model/)).toBeTruthy(); + }); + + it('calls removeModel and shows success toast on confirm', async () => { + const removeModel = jest.fn().mockResolvedValue(undefined); + mockUseModelStore.mockReturnValue({ removeModel, removeModelFiles: jest.fn() }); + renderSheet(); + + fireEvent.press(screen.getByText('Remove from the app')); + // Second press targets the confirm button in the new stage + fireEvent.press(screen.getByText('Remove from the app')); + + await waitFor(() => expect(removeModel).toHaveBeenCalledWith(baseModel.id)); + expect(Toast.show).toHaveBeenCalledWith( + expect.objectContaining({ text1: expect.stringContaining('Test Model') }) + ); + }); + + it('returns to Initial stage on "Close" press', () => { + renderSheet(); + fireEvent.press(screen.getByText('Remove from the app')); + fireEvent.press(screen.getByText('Close')); + expect(screen.getByTestId('model-card')).toBeTruthy(); + }); +}); diff --git a/__tests__/ModelSelectSheet.test.tsx b/__tests__/ModelSelectSheet.test.tsx new file mode 100644 index 00000000..dc000077 --- /dev/null +++ b/__tests__/ModelSelectSheet.test.tsx @@ -0,0 +1,155 @@ +import React, { createRef } from 'react'; +import { render, screen, fireEvent } from '@testing-library/react-native'; + +jest.mock('../context/ThemeContext', () => ({ + useTheme: () => ({ + theme: { ...require('../styles/colors').lightTheme, insets: { top: 0, bottom: 0, left: 0, right: 0 } }, + }), +})); + +jest.mock('../store/modelStore', () => ({ + useModelStore: jest.fn(() => ({ downloadedModels: [] })), +})); + +jest.mock('expo-router', () => ({ router: { push: jest.fn() } })); + +jest.mock('../components/model-hub/ModelCard', () => { + const { TouchableOpacity, Text } = require('react-native'); + return ({ model, onPress }: any) => ( + onPress(model)}> + {model.modelName} + + ); +}); + +jest.mock('../components/bottomSheets/BottomSheetSearchInput', () => { + const { TextInput } = require('react-native'); + return ({ value, onChangeText, placeholder }: any) => ( + + ); +}); + +jest.mock('@gorhom/bottom-sheet', () => { + const React = require('react'); + const { View } = require('react-native'); + let _data: any = null; + + const BottomSheetModal = React.forwardRef((props: any, _ref: any) => ( + {props.children} + )); + const BottomSheetFlatList = ({ data, renderItem }: any) => ( + {data.map((item: any, i: number) => {renderItem({ item })})} + ); + const BottomSheetView = ({ children, style }: any) => {children}; + const BottomSheetBackdrop = () => null; + + return { BottomSheetModal, BottomSheetFlatList, BottomSheetView, BottomSheetBackdrop }; +}); + +import ModelSelectSheet from '../components/bottomSheets/ModelSelectSheet'; +import { useModelStore } from '../store/modelStore'; +import { router } from 'expo-router'; + +const mockUseModelStore = useModelStore as jest.Mock; + +const makeModel = (id: number, name: string) => ({ + id, + modelName: name, + source: 'remote' as const, + isDownloaded: true, + featured: false, + modelPath: '', + tokenizerPath: '', + tokenizerConfigPath: '', + thinking: false, +}); + +beforeEach(() => { + mockUseModelStore.mockReturnValue({ downloadedModels: [] }); + jest.clearAllMocks(); + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); +afterEach(() => jest.restoreAllMocks()); + +const renderSheet = (props = {}) => + render(); + +// ─── empty state ────────────────────────────────────────────────────────────── + +describe('empty state', () => { + it('shows empty state message when no models downloaded', () => { + renderSheet(); + expect(screen.getByText('You have no available models yet')).toBeTruthy(); + }); + + it('shows "Download a Model" button in empty state', () => { + renderSheet(); + expect(screen.getByText('Download a Model')).toBeTruthy(); + }); + + it('navigates to model-hub when "Download a Model" pressed', () => { + renderSheet(); + fireEvent.press(screen.getByText('Download a Model')); + expect(router.push).toHaveBeenCalledWith('/model-hub'); + }); +}); + +// ─── with models ────────────────────────────────────────────────────────────── + +describe('with downloaded models', () => { + beforeEach(() => { + mockUseModelStore.mockReturnValue({ + downloadedModels: [makeModel(1, 'Llama-3B'), makeModel(2, 'Qwen-1B')], + }); + }); + + it('shows "Select a Model" title', () => { + renderSheet(); + expect(screen.getByText('Select a Model')).toBeTruthy(); + }); + + it('renders all downloaded models', () => { + renderSheet(); + expect(screen.getByText('Llama-3B')).toBeTruthy(); + expect(screen.getByText('Qwen-1B')).toBeTruthy(); + }); + + it('calls selectModel when a model card is pressed', () => { + const selectModel = jest.fn(); + renderSheet({ selectModel }); + fireEvent.press(screen.getByTestId('model-card-1')); + expect(selectModel).toHaveBeenCalledWith(expect.objectContaining({ id: 1 })); + }); +}); + +// ─── search filtering ───────────────────────────────────────────────────────── + +describe('search filtering', () => { + beforeEach(() => { + mockUseModelStore.mockReturnValue({ + downloadedModels: [makeModel(1, 'Llama-3B'), makeModel(2, 'Qwen-1B')], + }); + }); + + it('filters models based on search input', () => { + renderSheet(); + fireEvent.changeText(screen.getByTestId('search-input'), 'qwen'); + expect(screen.queryByText('Llama-3B')).toBeNull(); + expect(screen.getByText('Qwen-1B')).toBeTruthy(); + }); + + it('search is case-insensitive', () => { + renderSheet(); + fireEvent.changeText(screen.getByTestId('search-input'), 'LLAMA'); + expect(screen.getByText('Llama-3B')).toBeTruthy(); + expect(screen.queryByText('Qwen-1B')).toBeNull(); + }); + + it('shows all models when search is cleared', () => { + renderSheet(); + fireEvent.changeText(screen.getByTestId('search-input'), 'llama'); + fireEvent.changeText(screen.getByTestId('search-input'), ''); + expect(screen.getByText('Llama-3B')).toBeTruthy(); + expect(screen.getByText('Qwen-1B')).toBeTruthy(); + }); +}); diff --git a/__tests__/ThinkingBlock.test.tsx b/__tests__/ThinkingBlock.test.tsx new file mode 100644 index 00000000..5cb1d15e --- /dev/null +++ b/__tests__/ThinkingBlock.test.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { TouchableOpacity } from 'react-native'; +import { render, screen, fireEvent } from '@testing-library/react-native'; + +jest.mock('../context/ThemeContext', () => ({ + useTheme: () => ({ + theme: { ...require('../styles/colors').lightTheme, insets: { top: 0, bottom: 0, left: 0, right: 0 } }, + }), +})); + +jest.mock('../components/chat-screen/MarkdownComponent', () => { + const { Text } = require('react-native'); + return ({ text }: any) => {text}; +}); + +import ThinkingBlock from '../components/chat-screen/ThinkingBlock'; + +const renderBlock = (props: Partial> = {}) => + render(); + +// ─── rendering ──────────────────────────────────────────────────────────────── + +describe('rendering', () => { + it('always shows "Thinking..." title', () => { + renderBlock(); + expect(screen.getByText('Thinking...')).toBeTruthy(); + }); + + it('starts collapsed when block is complete (isComplete=true)', () => { + renderBlock({ isComplete: true, inProgress: false }); + expect(screen.queryByTestId('thinking-content')).toBeNull(); + }); + + it('starts expanded when block is incomplete (still streaming)', () => { + renderBlock({ isComplete: false, inProgress: false }); + expect(screen.getByTestId('thinking-content')).toBeTruthy(); + }); + + it('starts expanded when inProgress', () => { + renderBlock({ isComplete: false, inProgress: true }); + expect(screen.getByTestId('thinking-content')).toBeTruthy(); + }); +}); + +// ─── expand/collapse toggle ─────────────────────────────────────────────────── + +describe('expand/collapse', () => { + it('expands a collapsed complete block when toggle is pressed', () => { + renderBlock({ isComplete: true, inProgress: false }); + expect(screen.queryByTestId('thinking-content')).toBeNull(); + + // The toggle button is the TouchableOpacity in the header + const toggleBtn = screen.UNSAFE_getAllByType(TouchableOpacity)[0]; + fireEvent.press(toggleBtn); + + expect(screen.getByTestId('thinking-content')).toBeTruthy(); + }); + + it('collapses an expanded incomplete block when toggle is pressed', () => { + renderBlock({ isComplete: false, inProgress: false }); + expect(screen.getByTestId('thinking-content')).toBeTruthy(); + + const toggleBtn = screen.UNSAFE_getAllByType(TouchableOpacity)[0]; + fireEvent.press(toggleBtn); + + expect(screen.queryByTestId('thinking-content')).toBeNull(); + }); + + it('does not collapse when inProgress — toggle is a no-op', () => { + renderBlock({ isComplete: false, inProgress: true }); + expect(screen.getByTestId('thinking-content')).toBeTruthy(); + + const toggleBtn = screen.UNSAFE_getAllByType(TouchableOpacity)[0]; + fireEvent.press(toggleBtn); + + // Still visible — inProgress blocks the toggle + expect(screen.getByTestId('thinking-content')).toBeTruthy(); + }); +}); + +// ─── content ───────────────────────────────────────────────────────────────── + +describe('content', () => { + it('passes content string to MarkdownComponent when expanded', () => { + renderBlock({ isComplete: false, content: 'deep reasoning here' }); + expect(screen.getByText('deep reasoning here')).toBeTruthy(); + }); +}); diff --git a/__tests__/chatStore.test.ts b/__tests__/chatStore.test.ts new file mode 100644 index 00000000..6055d83d --- /dev/null +++ b/__tests__/chatStore.test.ts @@ -0,0 +1,219 @@ +import { useChatStore } from '../store/chatStore'; +import * as chatRepository from '../database/chatRepository'; +import * as sourcesRepository from '../database/sourcesRepository'; + +// Mock all DB interactions +jest.mock('../database/chatRepository'); +jest.mock('../database/sourcesRepository'); + +const mockDb = {} as any; + +const mockChat = (id: number, lastUsed = Date.now()) => ({ + id, + title: `Chat ${id}`, + modelId: 1, + lastUsed, + enabledSources: [], +}); + +beforeEach(() => { + useChatStore.setState({ + chats: [], + db: mockDb, + phantomChat: null, + }); + jest.clearAllMocks(); +}); + +describe('getChatById', () => { + it('returns the correct chat', () => { + useChatStore.setState({ chats: [mockChat(1), mockChat(2), mockChat(3)] }); + const chat = useChatStore.getState().getChatById(2); + expect(chat?.id).toBe(2); + }); + + it('returns undefined for unknown id', () => { + useChatStore.setState({ chats: [mockChat(1)] }); + expect(useChatStore.getState().getChatById(999)).toBeUndefined(); + }); +}); + +describe('updateLastUsed', () => { + it('re-sorts chats so the updated one is first', () => { + const now = Date.now(); + useChatStore.setState({ + chats: [ + mockChat(1, now - 3000), + mockChat(2, now - 2000), + mockChat(3, now - 1000), + ], + }); + + useChatStore.getState().updateLastUsed(1); + + const chats = useChatStore.getState().chats; + expect(chats[0].id).toBe(1); + }); + + it('does not crash for unknown id', () => { + useChatStore.setState({ chats: [mockChat(1)] }); + expect(() => useChatStore.getState().updateLastUsed(999)).not.toThrow(); + }); +}); + +describe('deleteChat', () => { + it('removes the chat from state', async () => { + (chatRepository.deleteChat as jest.Mock).mockResolvedValue(undefined); + useChatStore.setState({ chats: [mockChat(1), mockChat(2)] }); + + await useChatStore.getState().deleteChat(1); + + const chats = useChatStore.getState().chats; + expect(chats).toHaveLength(1); + expect(chats[0].id).toBe(2); + }); +}); + +describe('renameChat', () => { + it('updates the title in state', async () => { + (chatRepository.renameChat as jest.Mock).mockResolvedValue(undefined); + useChatStore.setState({ chats: [mockChat(1)] }); + + await useChatStore.getState().renameChat(1, 'New Title'); + + expect(useChatStore.getState().chats[0].title).toBe('New Title'); + }); + + it('does not change other chats', async () => { + (chatRepository.renameChat as jest.Mock).mockResolvedValue(undefined); + useChatStore.setState({ chats: [mockChat(1), mockChat(2)] }); + + await useChatStore.getState().renameChat(1, 'Changed'); + + expect(useChatStore.getState().chats[1].title).toBe('Chat 2'); + }); +}); + +describe('addChat', () => { + it('prepends the new chat to state', async () => { + (chatRepository.createChat as jest.Mock).mockResolvedValue(42); + useChatStore.setState({ chats: [mockChat(1)], phantomChat: null }); + + const id = await useChatStore.getState().addChat('My Chat', 5); + + expect(id).toBe(42); + const chats = useChatStore.getState().chats; + expect(chats[0].id).toBe(42); + expect(chats[0].title).toBe('My Chat'); + expect(chats[0].modelId).toBe(5); + }); + + it('copies phantom chat sources to the new chat', async () => { + (chatRepository.createChat as jest.Mock).mockResolvedValue(10); + (sourcesRepository.activateSource as jest.Mock).mockResolvedValue(undefined); + useChatStore.setState({ + chats: [], + phantomChat: { + id: -1, + title: '', + modelId: -1, + lastUsed: 0, + enabledSources: [3, 7], + }, + }); + + await useChatStore.getState().addChat('Chat', 1); + + expect(sourcesRepository.activateSource).toHaveBeenCalledTimes(2); + expect(sourcesRepository.activateSource).toHaveBeenCalledWith(mockDb, 10, 3); + expect(sourcesRepository.activateSource).toHaveBeenCalledWith(mockDb, 10, 7); + }); + + it('clears phantomChat after adding', async () => { + (chatRepository.createChat as jest.Mock).mockResolvedValue(1); + useChatStore.setState({ + phantomChat: { id: -1, title: '', modelId: -1, lastUsed: 0, enabledSources: [] }, + }); + + await useChatStore.getState().addChat('Chat', 1); + + expect(useChatStore.getState().phantomChat).toBeFalsy(); + }); + + it('returns undefined and does not add chat when db is not set', async () => { + useChatStore.setState({ db: null, chats: [] }); + + const id = await useChatStore.getState().addChat('Chat', 1); + + expect(id).toBeUndefined(); + expect(useChatStore.getState().chats).toHaveLength(0); + }); +}); + +describe('setChatModel', () => { + it('updates modelId on the correct chat', async () => { + (chatRepository.setChatModel as jest.Mock).mockResolvedValue(undefined); + useChatStore.setState({ chats: [mockChat(1), mockChat(2)] }); + + await useChatStore.getState().setChatModel(1, 99); + + const updated = useChatStore.getState().chats.find((c) => c.id === 1); + expect(updated?.modelId).toBe(99); + }); + + it('does not affect other chats', async () => { + (chatRepository.setChatModel as jest.Mock).mockResolvedValue(undefined); + useChatStore.setState({ chats: [mockChat(1), mockChat(2)] }); + + await useChatStore.getState().setChatModel(1, 99); + + const other = useChatStore.getState().chats.find((c) => c.id === 2); + expect(other?.modelId).toBe(1); // original modelId from mockChat + }); +}); + +describe('enableSource', () => { + it('adds sourceId to phantomChat without hitting the db', async () => { + useChatStore.setState({ + phantomChat: { id: 99, title: '', modelId: -1, lastUsed: 0, enabledSources: [1] }, + }); + + await useChatStore.getState().enableSource(99, 5); + + expect(useChatStore.getState().phantomChat?.enabledSources).toEqual([1, 5]); + expect(sourcesRepository.activateSource).not.toHaveBeenCalled(); + }); + + it('calls activateSource and updates state for a real chat', async () => { + (sourcesRepository.activateSource as jest.Mock).mockResolvedValue(undefined); + useChatStore.setState({ + chats: [{ ...mockChat(1), enabledSources: [2] }], + phantomChat: null, + }); + + await useChatStore.getState().enableSource(1, 7); + + expect(sourcesRepository.activateSource).toHaveBeenCalledWith(mockDb, 1, 7); + expect(useChatStore.getState().chats[0].enabledSources).toEqual([2, 7]); + }); +}); + +describe('setPhantomChatSettings', () => { + it('updates settings on phantomChat', async () => { + useChatStore.setState({ + phantomChat: { id: -1, title: '', modelId: -1, lastUsed: 0, enabledSources: [] }, + }); + const newSettings = { systemPrompt: 'Be concise.', contextWindow: 5 }; + + await useChatStore.getState().setPhantomChatSettings(newSettings); + + expect(useChatStore.getState().phantomChat?.settings).toEqual(newSettings); + }); + + it('does nothing when phantomChat is null', async () => { + useChatStore.setState({ phantomChat: null }); + await expect( + useChatStore.getState().setPhantomChatSettings({ systemPrompt: 'x', contextWindow: 1 }) + ).resolves.not.toThrow(); + }); +}); diff --git a/__tests__/fileReaders.test.ts b/__tests__/fileReaders.test.ts new file mode 100644 index 00000000..729df052 --- /dev/null +++ b/__tests__/fileReaders.test.ts @@ -0,0 +1,139 @@ +import { readDocumentText } from '../utils/fileReaders'; +import { readPDF } from 'react-native-pdfium'; +import { File } from 'expo-file-system'; + +const mockReadPDF = readPDF as jest.Mock; +const MockFile = File as jest.Mock; + +beforeEach(() => { + jest.clearAllMocks(); + MockFile.mockImplementation(() => ({ text: jest.fn() })); +}); + +describe('readDocumentText — PDF', () => { + it('strips file:// prefix before passing path to readPDF', async () => { + mockReadPDF.mockResolvedValue('pdf content'); + await readDocumentText('file:///path/to/doc.pdf', 'pdf'); + expect(mockReadPDF).toHaveBeenCalledWith('/path/to/doc.pdf'); + }); + + it('passes path without prefix unchanged', async () => { + mockReadPDF.mockResolvedValue('pdf content'); + await readDocumentText('/local/path/doc.pdf', 'pdf'); + expect(mockReadPDF).toHaveBeenCalledWith('/local/path/doc.pdf'); + }); + + it('returns the text from readPDF', async () => { + mockReadPDF.mockResolvedValue('extracted text'); + const result = await readDocumentText('file:///doc.pdf', 'pdf'); + expect(result).toBe('extracted text'); + }); + + it('is case-insensitive for file type', async () => { + mockReadPDF.mockResolvedValue('text'); + await readDocumentText('file:///doc.pdf', 'PDF'); + expect(mockReadPDF).toHaveBeenCalled(); + }); +}); + +describe('readDocumentText — TXT / MD', () => { + it('reads txt files via File.text()', async () => { + const mockText = jest.fn().mockResolvedValue('plain text'); + MockFile.mockImplementation(() => ({ text: mockText })); + + const result = await readDocumentText('file:///note.txt', 'txt'); + expect(MockFile).toHaveBeenCalledWith('file:///note.txt'); + expect(result).toBe('plain text'); + }); + + it('reads md files via File.text()', async () => { + const mockText = jest.fn().mockResolvedValue('# heading'); + MockFile.mockImplementation(() => ({ text: mockText })); + + const result = await readDocumentText('/docs/readme.md', 'md'); + expect(result).toBe('# heading'); + }); + + it('reads markdown alias via File.text()', async () => { + const mockText = jest.fn().mockResolvedValue('content'); + MockFile.mockImplementation(() => ({ text: mockText })); + + const result = await readDocumentText('/file.markdown', 'markdown'); + expect(result).toBe('content'); + }); +}); + +describe('readDocumentText — HTML', () => { + const html = (body: string) => body; + + it('strips basic HTML tags', async () => { + const mockText = jest.fn().mockResolvedValue('

Hello world

'); + MockFile.mockImplementation(() => ({ text: mockText })); + + const result = await readDocumentText('/page.html', 'html'); + // Tags are replaced with spaces then whitespace is normalised — "Hello world" → "Hello world" + expect(result).toBe('Hello world'); + }); + + it('removes

content

' + ); + MockFile.mockImplementation(() => ({ text: mockText })); + + const result = await readDocumentText('/page.html', 'html'); + expect(result).not.toContain('alert'); + expect(result).toContain('content'); + }); + + it('removes

text

' + ); + MockFile.mockImplementation(() => ({ text: mockText })); + + const result = await readDocumentText('/page.html', 'html'); + expect(result).not.toContain('color'); + expect(result).toContain('text'); + }); + + it('decodes HTML entities', async () => { + const mockText = jest.fn().mockResolvedValue( + '

a & b <c> "d" 'e' f g

' + ); + MockFile.mockImplementation(() => ({ text: mockText })); + + const result = await readDocumentText('/page.html', 'html'); + expect(result).toContain('a & b "d" \'e\' f g'); + }); + + it('normalizes multiple whitespace into single spaces', async () => { + const mockText = jest.fn().mockResolvedValue('

a b\n\nc

'); + MockFile.mockImplementation(() => ({ text: mockText })); + + const result = await readDocumentText('/page.html', 'html'); + expect(result).toBe('a b c'); + }); + + it('handles htm extension', async () => { + const mockText = jest.fn().mockResolvedValue('

htm

'); + MockFile.mockImplementation(() => ({ text: mockText })); + + const result = await readDocumentText('/page.htm', 'htm'); + expect(result).toBe('htm'); + }); +}); + +describe('readDocumentText — unsupported types', () => { + it('throws for unsupported file type', async () => { + await expect(readDocumentText('/file.docx', 'docx')).rejects.toThrow( + 'Unsupported file type: docx' + ); + }); + + it('throws for csv', async () => { + await expect(readDocumentText('/data.csv', 'csv')).rejects.toThrow( + 'Unsupported file type: csv' + ); + }); +}); diff --git a/__tests__/helpers/renderWithTheme.tsx b/__tests__/helpers/renderWithTheme.tsx new file mode 100644 index 00000000..331d9680 --- /dev/null +++ b/__tests__/helpers/renderWithTheme.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { render, RenderOptions } from '@testing-library/react-native'; +import { lightTheme } from '../../styles/colors'; + +export const testTheme = { ...lightTheme, insets: { top: 0, bottom: 0, left: 0, right: 0 } }; + +// Re-export for convenience in tests that need the wrapper +export const renderWithTheme = (ui: React.ReactElement, options?: RenderOptions) => + render(ui, options); diff --git a/__tests__/llmStore.test.ts b/__tests__/llmStore.test.ts new file mode 100644 index 00000000..1c2ec752 --- /dev/null +++ b/__tests__/llmStore.test.ts @@ -0,0 +1,520 @@ +import { useLLMStore } from '../store/llmStore'; +import { LLMModule } from 'react-native-executorch'; +import * as chatRepository from '../database/chatRepository'; +import * as Feedback from '../utils/Feedback'; + +jest.mock('../database/chatRepository'); +jest.mock('../utils/Feedback', () => ({ + Feedback: { success: jest.fn() }, +})); +jest.mock('../utils/promptUtils', () => ({ + prepareMessagesForLLM: jest.fn(() => [ + { role: 'system', content: 'You are helpful.' }, + { role: 'user', content: 'hello' }, + { role: 'assistant', content: '' }, + ]), +})); +jest.mock('../constants/default-benchmark', () => ({ + BENCHMARK_PROMPT: 'benchmark prompt text', +})); + +const mockLLMModule = LLMModule as jest.Mocked; +const mockPersistMessage = chatRepository.persistMessage as jest.Mock; +const mockGetChatMessages = chatRepository.getChatMessages as jest.Mock; + +const mockDb = {} as any; + +const baseModel = { + id: 1, + modelName: 'Test LLM', + source: 'remote' as const, + isDownloaded: true, + modelPath: 'https://example.com/model.pte', + tokenizerPath: 'https://example.com/tokenizer.json', + tokenizerConfigPath: 'https://example.com/tokenizer_config.json', + thinking: false, +}; + +// Captures the token callback registered during loadModel so tests can fire tokens +let capturedTokenCallback: ((token: string) => void) | null = null; + +const makeMockInstance = () => ({ + generate: jest.fn(), + interrupt: jest.fn(), + delete: jest.fn(), + getGeneratedTokenCount: jest.fn(() => 10), +}); + +let mockInstance = makeMockInstance(); + +beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + + capturedTokenCallback = null; + mockInstance = makeMockInstance(); + + mockLLMModule.fromCustomModel.mockImplementation( + async (_modelPath, _tokenizerPath, _tokenizerConfigPath, _onProgress, onToken) => { + capturedTokenCallback = onToken; + return mockInstance as any; + } + ); + + useLLMStore.setState({ + isLoading: false, + isGenerating: false, + isProcessingPrompt: false, + isBenchmarking: false, + db: mockDb, + model: null, + performance: { tokenCount: 0, firstTokenTime: 0 }, + activeChatId: null, + generatingForChatId: null, + activeChatMessages: [], + }); + + jest.clearAllMocks(); + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + + // Re-apply the fromCustomModel mock after clearAllMocks + mockLLMModule.fromCustomModel.mockImplementation( + async (_modelPath, _tokenizerPath, _tokenizerConfigPath, _onProgress, onToken) => { + capturedTokenCallback = onToken; + return mockInstance as any; + } + ); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +// Helper to load a model and get the registered token callback +const loadModel = async (model = baseModel) => { + await useLLMStore.getState().loadModel(model); + return capturedTokenCallback!; +}; + +// ─── loadModel ─────────────────────────────────────────────────────────────── + +describe('loadModel', () => { + it('sets isLoading during load then clears it', async () => { + let wasLoading = false; + mockLLMModule.fromCustomModel.mockImplementation(async (...args) => { + wasLoading = useLLMStore.getState().isLoading; + capturedTokenCallback = args[4]; + return mockInstance as any; + }); + + await useLLMStore.getState().loadModel(baseModel); + + expect(wasLoading).toBe(true); + expect(useLLMStore.getState().isLoading).toBe(false); + }); + + it('skips reload for the same model id without hardReload', async () => { + useLLMStore.setState({ model: baseModel }); + await useLLMStore.getState().loadModel(baseModel); + expect(mockLLMModule.fromCustomModel).not.toHaveBeenCalled(); + }); + + it('reloads same model when hardReload=true', async () => { + useLLMStore.setState({ model: baseModel }); + await useLLMStore.getState().loadModel(baseModel, true); + expect(mockLLMModule.fromCustomModel).toHaveBeenCalled(); + }); + + it('calls delete on previous instance before loading new model', async () => { + // Load first model + await useLLMStore.getState().loadModel(baseModel); + const firstInstance = mockInstance; + + // Load a different model + mockInstance = makeMockInstance(); + mockLLMModule.fromCustomModel.mockImplementation(async (...args) => { + capturedTokenCallback = args[4]; + return mockInstance as any; + }); + await useLLMStore.getState().loadModel({ ...baseModel, id: 2 }); + + expect(firstInstance.delete).toHaveBeenCalled(); + }); + + it('clears model and isLoading on load failure', async () => { + mockLLMModule.fromCustomModel.mockRejectedValue(new Error('load failed')); + await useLLMStore.getState().loadModel(baseModel); + expect(useLLMStore.getState().isLoading).toBe(false); + expect(useLLMStore.getState().model).toBeNull(); + }); +}); + +// ─── token callback ─────────────────────────────────────────────────────────── + +describe('token callback', () => { + it('increments tokenCount on each token', async () => { + const onToken = await loadModel(); + useLLMStore.setState({ isProcessingPrompt: true, isGenerating: true }); + + onToken('hello'); + onToken(' world'); + + expect(useLLMStore.getState().performance.tokenCount).toBe(2); + }); + + it('sets firstTokenTime only on first token', async () => { + const onToken = await loadModel(); + useLLMStore.setState({ isProcessingPrompt: true, isGenerating: true }); + + onToken('first'); + const firstTime = useLLMStore.getState().performance.firstTokenTime; + + onToken('second'); + expect(useLLMStore.getState().performance.firstTokenTime).toBe(firstTime); + }); + + it('triggers Feedback.success on first token when not benchmarking', async () => { + const onToken = await loadModel(); + useLLMStore.setState({ isProcessingPrompt: true, isGenerating: true, isBenchmarking: false }); + + onToken('first'); + + expect(Feedback.Feedback.success).toHaveBeenCalledTimes(1); + }); + + it('does not trigger Feedback.success during benchmarking', async () => { + const onToken = await loadModel(); + useLLMStore.setState({ isProcessingPrompt: true, isGenerating: true, isBenchmarking: true }); + + onToken('first'); + + expect(Feedback.Feedback.success).not.toHaveBeenCalled(); + }); + + it('appends token to last active message when generating for active chat', async () => { + const onToken = await loadModel(); + useLLMStore.setState({ + isProcessingPrompt: false, + isGenerating: true, + activeChatId: 5, + generatingForChatId: 5, + performance: { tokenCount: 1, firstTokenTime: 1 }, // not first token + activeChatMessages: [ + { id: 1, chatId: 5, role: 'user', content: 'Hi', timestamp: 0 }, + { id: -1, chatId: 5, role: 'assistant', content: '', timestamp: 0 }, + ], + }); + + onToken(' hello'); + + const messages = useLLMStore.getState().activeChatMessages; + expect(messages[messages.length - 1].content).toBe(' hello'); + }); + + it('does not append token when generatingForChatId differs from activeChatId', async () => { + const onToken = await loadModel(); + useLLMStore.setState({ + isProcessingPrompt: false, + isGenerating: true, + activeChatId: 99, // user navigated away + generatingForChatId: 5, + performance: { tokenCount: 1, firstTokenTime: 1 }, + activeChatMessages: [ + { id: -1, chatId: 99, role: 'assistant', content: 'other chat', timestamp: 0 }, + ], + }); + + onToken('should not appear'); + + const messages = useLLMStore.getState().activeChatMessages; + expect(messages[0].content).toBe('other chat'); + }); + + it('calls interrupt on first token when generation was cancelled (prefill interrupt)', async () => { + const onToken = await loadModel(); + // Simulate: user cancelled (isProcessingPrompt=false, isGenerating=false) but first token arrived + useLLMStore.setState({ + isProcessingPrompt: false, + isGenerating: false, + performance: { tokenCount: 0, firstTokenTime: 0 }, + }); + + onToken('late token'); + + expect(mockInstance.interrupt).toHaveBeenCalled(); + }); +}); + +// ─── interrupt ─────────────────────────────────────────────────────────────── + +describe('interrupt', () => { + it('calls llmInstance.interrupt when isGenerating', async () => { + await loadModel(); + useLLMStore.setState({ isGenerating: true }); + + useLLMStore.getState().interrupt(); + + expect(mockInstance.interrupt).toHaveBeenCalled(); + }); + + it('resets isGenerating and isProcessingPrompt', async () => { + await loadModel(); + useLLMStore.setState({ isGenerating: true, isProcessingPrompt: true }); + + useLLMStore.getState().interrupt(); + + expect(useLLMStore.getState().isGenerating).toBe(false); + expect(useLLMStore.getState().isProcessingPrompt).toBe(false); + }); + + it('resets isProcessingPrompt even when not isGenerating', () => { + useLLMStore.setState({ isGenerating: false, isProcessingPrompt: true }); + + useLLMStore.getState().interrupt(); + + expect(useLLMStore.getState().isProcessingPrompt).toBe(false); + }); + + it('does nothing when neither generating nor processing', () => { + useLLMStore.setState({ isGenerating: false, isProcessingPrompt: false }); + expect(() => useLLMStore.getState().interrupt()).not.toThrow(); + }); +}); + +// ─── sendChatMessage ────────────────────────────────────────────────────────── + +describe('sendChatMessage', () => { + const settings = { systemPrompt: 'be helpful', contextWindow: 10 }; + + beforeEach(async () => { + await loadModel(); + mockPersistMessage.mockResolvedValue(42); + mockInstance.generate.mockResolvedValue('The answer is 42.'); + }); + + it('returns early when db is not set', async () => { + useLLMStore.setState({ db: null }); + await useLLMStore.getState().sendChatMessage('hi', 1, [], settings); + expect(mockPersistMessage).not.toHaveBeenCalled(); + }); + + it('returns early when model is not loaded', async () => { + useLLMStore.setState({ model: null }); + await useLLMStore.getState().sendChatMessage('hi', 1, [], settings); + expect(mockPersistMessage).not.toHaveBeenCalled(); + }); + + it('persists user message and assistant response', async () => { + useLLMStore.setState({ model: baseModel, activeChatId: 1, activeChatMessages: [] }); + + await useLLMStore.getState().sendChatMessage('hello', 1, [], settings); + + expect(mockPersistMessage).toHaveBeenCalledTimes(2); + expect(mockPersistMessage).toHaveBeenCalledWith( + mockDb, + expect.objectContaining({ role: 'user', content: 'hello' }) + ); + expect(mockPersistMessage).toHaveBeenCalledWith( + mockDb, + expect.objectContaining({ role: 'assistant', content: 'The answer is 42.' }) + ); + }); + + it('sets isProcessingPrompt at start and clears it on complete', async () => { + useLLMStore.setState({ model: baseModel, activeChatId: 1, activeChatMessages: [] }); + + await useLLMStore.getState().sendChatMessage('hello', 1, [], settings); + + expect(useLLMStore.getState().isProcessingPrompt).toBe(false); + expect(useLLMStore.getState().isGenerating).toBe(false); + }); + + it('adds user message and assistant placeholder to activeChatMessages before generating', async () => { + let messagesBeforeGenerate: any[] = []; + mockInstance.generate.mockImplementation(async () => { + messagesBeforeGenerate = useLLMStore.getState().activeChatMessages; + return 'response'; + }); + useLLMStore.setState({ model: baseModel, activeChatId: 1, activeChatMessages: [] }); + + await useLLMStore.getState().sendChatMessage('ping', 1, [], settings); + + expect(messagesBeforeGenerate).toHaveLength(2); + expect(messagesBeforeGenerate[0].role).toBe('user'); + expect(messagesBeforeGenerate[1].role).toBe('assistant'); + expect(messagesBeforeGenerate[1].content).toBe(''); + }); + + it('recovers gracefully when generation returns null', async () => { + mockInstance.generate.mockResolvedValue(null); + useLLMStore.setState({ model: baseModel, activeChatId: 1, activeChatMessages: [] }); + + await useLLMStore.getState().sendChatMessage('hello', 1, [], settings); + + expect(useLLMStore.getState().isGenerating).toBe(false); + expect(useLLMStore.getState().isProcessingPrompt).toBe(false); + // Only user message persisted, not assistant (no response) + expect(mockPersistMessage).toHaveBeenCalledTimes(1); + }); + + it('recovers gracefully when an exception is thrown during generation', async () => { + mockInstance.generate.mockRejectedValue(new Error('GPU crash')); + useLLMStore.setState({ model: baseModel, activeChatId: 1, activeChatMessages: [] }); + + await useLLMStore.getState().sendChatMessage('hello', 1, [], settings); + + expect(useLLMStore.getState().isGenerating).toBe(false); + expect(useLLMStore.getState().isProcessingPrompt).toBe(false); + }); + + it('does not update performance metrics on last message when user navigated away', async () => { + mockInstance.generate.mockResolvedValue('response'); + useLLMStore.setState({ + model: baseModel, + activeChatId: 99, // different from chatId=1 + activeChatMessages: [], + }); + + await useLLMStore.getState().sendChatMessage('hello', 1, [], settings); + + // complete called without perf data — last message should not have timeToFirstToken + const messages = useLLMStore.getState().activeChatMessages; + const lastMsg = messages[messages.length - 1]; + expect(lastMsg?.timeToFirstToken).toBeUndefined(); + }); +}); + +// ─── sendEventMessage ───────────────────────────────────────────────────────── + +describe('sendEventMessage', () => { + it('appends event message to activeChatMessages', async () => { + mockPersistMessage.mockResolvedValue(77); + useLLMStore.setState({ db: mockDb, activeChatMessages: [] }); + + await useLLMStore.getState().sendEventMessage(1, 'Source deleted'); + + const messages = useLLMStore.getState().activeChatMessages; + expect(messages).toHaveLength(1); + expect(messages[0].role).toBe('event'); + expect(messages[0].content).toBe('Source deleted'); + expect(messages[0].id).toBe(77); + }); + + it('does nothing when db is not set', async () => { + useLLMStore.setState({ db: null }); + await useLLMStore.getState().sendEventMessage(1, 'test'); + expect(mockPersistMessage).not.toHaveBeenCalled(); + }); +}); + +// ─── setActiveChatId ────────────────────────────────────────────────────────── + +describe('setActiveChatId', () => { + it('loads messages for the given chat id', async () => { + const messages = [{ id: 1, chatId: 5, role: 'user', content: 'hi', timestamp: 0 }]; + mockGetChatMessages.mockResolvedValue(messages); + useLLMStore.setState({ db: mockDb }); + + await useLLMStore.getState().setActiveChatId(5); + + expect(useLLMStore.getState().activeChatId).toBe(5); + expect(useLLMStore.getState().activeChatMessages).toEqual(messages); + }); + + it('clears messages when called with null', async () => { + useLLMStore.setState({ + db: mockDb, + activeChatId: 5, + activeChatMessages: [{ id: 1, chatId: 5, role: 'user', content: 'hi', timestamp: 0 }], + }); + + await useLLMStore.getState().setActiveChatId(null); + + expect(useLLMStore.getState().activeChatId).toBeNull(); + expect(useLLMStore.getState().activeChatMessages).toEqual([]); + }); +}); + +// ─── refreshActiveChatMessages ──────────────────────────────────────────────── + +describe('refreshActiveChatMessages', () => { + it('reloads messages for the active chat', async () => { + const fresh = [{ id: 9, chatId: 3, role: 'assistant', content: 'updated', timestamp: 0 }]; + mockGetChatMessages.mockResolvedValue(fresh); + useLLMStore.setState({ db: mockDb, activeChatId: 3 }); + + await useLLMStore.getState().refreshActiveChatMessages(); + + expect(useLLMStore.getState().activeChatMessages).toEqual(fresh); + }); + + it('does nothing when activeChatId is null', async () => { + useLLMStore.setState({ db: mockDb, activeChatId: null }); + await useLLMStore.getState().refreshActiveChatMessages(); + expect(mockGetChatMessages).not.toHaveBeenCalled(); + }); +}); + +// ─── runBenchmark ───────────────────────────────────────────────────────────── + +describe('runBenchmark', () => { + it('returns undefined and resets flags when no llmInstance is loaded', async () => { + // Don't call loadModel — llmInstance is null from module reset + useLLMStore.setState({ model: null }); + + const result = await useLLMStore.getState().runBenchmark(); + + expect(result).toBeUndefined(); + expect(useLLMStore.getState().isGenerating).toBe(false); + expect(useLLMStore.getState().isBenchmarking).toBe(false); + }); + + it('sets isGenerating and isBenchmarking flags while running', async () => { + await loadModel(); + let wasGenerating = false; + let wasBenchmarking = false; + + mockInstance.generate.mockImplementation(async () => { + wasGenerating = useLLMStore.getState().isGenerating; + wasBenchmarking = useLLMStore.getState().isBenchmarking; + return 'benchmark result'; + }); + useLLMStore.setState({ model: baseModel }); + + await useLLMStore.getState().runBenchmark(); + + expect(wasGenerating).toBe(true); + expect(wasBenchmarking).toBe(true); + expect(useLLMStore.getState().isGenerating).toBe(false); + expect(useLLMStore.getState().isBenchmarking).toBe(false); + }); + + it('returns performance metrics on success', async () => { + await loadModel(); + mockInstance.generate.mockResolvedValue('output text'); + mockInstance.getGeneratedTokenCount.mockReturnValue(50); + useLLMStore.setState({ model: baseModel }); + + const result = await useLLMStore.getState().runBenchmark(); + + expect(result).toMatchObject({ + totalTime: expect.any(Number), + timeToFirstToken: expect.any(Number), + tokensPerSecond: expect.any(Number), + tokensGenerated: 50, + peakMemory: expect.any(Number), + }); + }); + + it('resets flags even when generate throws', async () => { + await loadModel(); + mockInstance.generate.mockRejectedValue(new Error('OOM')); + useLLMStore.setState({ model: baseModel }); + + await useLLMStore.getState().runBenchmark(); + + expect(useLLMStore.getState().isGenerating).toBe(false); + expect(useLLMStore.getState().isBenchmarking).toBe(false); + }); +}); diff --git a/__tests__/modelCompatibility.test.ts b/__tests__/modelCompatibility.test.ts new file mode 100644 index 00000000..4f7d398e --- /dev/null +++ b/__tests__/modelCompatibility.test.ts @@ -0,0 +1,93 @@ +import DeviceInfo from 'react-native-device-info'; +import { + getModelMemoryRequirement, + isModelCompatible, + getDeviceMemoryGB, +} from '../utils/modelCompatibility'; +import { Model } from '../database/modelRepository'; + +const baseModel: Model = { + id: 1, + modelName: 'Llama-3.2-1B', + source: 'remote', + isDownloaded: false, + modelPath: '', + tokenizerPath: '', + tokenizerConfigPath: '', +}; + +const mockGetTotalMemorySync = DeviceInfo.getTotalMemorySync as jest.Mock; + +beforeEach(() => { + mockGetTotalMemorySync.mockReturnValue(8 * 1024 * 1024 * 1024); // reset to 8GB +}); + +describe('getModelMemoryRequirement', () => { + it('returns null when model has no parameters', () => { + const model = { ...baseModel, parameters: undefined }; + expect(getModelMemoryRequirement(model)).toBeNull(); + }); + + it('uses 2.5x multiplier for non-quantized models', () => { + const model = { ...baseModel, modelName: 'Llama-3.2-1B', parameters: 1 }; + expect(getModelMemoryRequirement(model)).toBeCloseTo(2.5); + }); + + it('uses 1.75x multiplier for quantized models', () => { + const model = { ...baseModel, modelName: 'Llama-3.2-1B-quantized', parameters: 1 }; + expect(getModelMemoryRequirement(model)).toBeCloseTo(1.75); + }); + + it('detects quantized keyword case-insensitively', () => { + const model = { ...baseModel, modelName: 'Llama-QUANTIZED-1B', parameters: 2 }; + expect(getModelMemoryRequirement(model)).toBeCloseTo(3.5); + }); + + it('detects spinquant keyword', () => { + const model = { ...baseModel, modelName: 'Llama-SpinQuant-1B', parameters: 1 }; + expect(getModelMemoryRequirement(model)).toBeCloseTo(1.75); + }); + + it('detects qlora keyword', () => { + const model = { ...baseModel, modelName: 'Llama-QLoRA-1B', parameters: 1 }; + expect(getModelMemoryRequirement(model)).toBeCloseTo(1.75); + }); +}); + +describe('isModelCompatible', () => { + it('returns true when model has no parameters (unknown requirement)', () => { + const model = { ...baseModel, parameters: undefined }; + expect(isModelCompatible(model)).toBe(true); + }); + + it('returns true when memory requirement fits in device memory', () => { + // 8GB device, 1B param non-quantized = 2.5GB required + mockGetTotalMemorySync.mockReturnValue(8 * 1024 * 1024 * 1024); + const model = { ...baseModel, parameters: 1 }; + expect(isModelCompatible(model)).toBe(true); + }); + + it('returns false when memory requirement exceeds device memory', () => { + mockGetTotalMemorySync.mockReturnValue(2 * 1024 * 1024 * 1024); // 2GB device + const model = { ...baseModel, parameters: 7, modelName: 'Llama-7B' }; // needs 17.5GB + expect(isModelCompatible(model)).toBe(false); + }); + + it('returns true for a model within the memory limit', () => { + // 8GB device, 2B params non-quantized = 5GB required — comfortably fits + const model = { ...baseModel, parameters: 2, modelName: 'Llama-2B' }; + expect(isModelCompatible(model)).toBe(true); + }); + + it('returns false for a model that is slightly over the limit', () => { + // 8GB device, 4B params non-quantized = 10GB needed + const model = { ...baseModel, parameters: 4, modelName: 'Llama-4B' }; + expect(isModelCompatible(model)).toBe(false); + }); +}); + +describe('getDeviceMemoryGB', () => { + it('returns a positive number', () => { + expect(getDeviceMemoryGB()).toBeGreaterThan(0); + }); +}); diff --git a/__tests__/modelStore.test.ts b/__tests__/modelStore.test.ts new file mode 100644 index 00000000..695793fa --- /dev/null +++ b/__tests__/modelStore.test.ts @@ -0,0 +1,269 @@ +import { useModelStore, ModelState } from '../store/modelStore'; +import * as modelRepository from '../database/modelRepository'; +import { ResourceFetcher } from 'react-native-executorch'; +import { exists, unlink } from '@dr.pogodin/react-native-fs'; +import Toast from 'react-native-toast-message'; + +jest.mock('../database/modelRepository'); + +const mockDb = {} as any; +const mockFetch = ResourceFetcher.fetch as jest.Mock; +const mockExists = exists as jest.Mock; +const mockUnlink = unlink as jest.Mock; +const mockGetAllModels = modelRepository.getAllModels as jest.Mock; +const mockUpdateModelDownloaded = modelRepository.updateModelDownloaded as jest.Mock; +const mockRemoveModelFiles = modelRepository.removeModelFiles as jest.Mock; + +const baseModel = { + id: 1, + modelName: 'Test Model', + source: 'remote' as const, + isDownloaded: false, + modelPath: 'https://example.com/model.pte', + tokenizerPath: 'https://example.com/tokenizer.json', + tokenizerConfigPath: 'https://example.com/tokenizer_config.json', +}; + +beforeEach(() => { + useModelStore.setState({ + db: mockDb, + models: [], + downloadedModels: [], + downloadStates: {}, + }); + jest.clearAllMocks(); + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + mockGetAllModels.mockResolvedValue([]); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('getModelById', () => { + it('returns matching model', () => { + useModelStore.setState({ models: [{ ...baseModel, id: 3 }] }); + expect(useModelStore.getState().getModelById(3)?.id).toBe(3); + }); + + it('returns undefined for unknown id', () => { + useModelStore.setState({ models: [baseModel] }); + expect(useModelStore.getState().getModelById(999)).toBeUndefined(); + }); +}); + +describe('downloadModel', () => { + it('sets state to Downloading at start', async () => { + // Never resolve so we can inspect mid-flight state + mockFetch.mockReturnValue(new Promise(() => {})); + + useModelStore.getState().downloadModel(baseModel); + + const state = useModelStore.getState().downloadStates[baseModel.id]; + expect(state.status).toBe(ModelState.Downloading); + expect(state.progress).toBe(0); + }); + + it('sets state to Downloaded with progress=1 on success', async () => { + mockFetch.mockResolvedValue(['/local/model.pte']); + mockUpdateModelDownloaded.mockResolvedValue(undefined); + + await useModelStore.getState().downloadModel(baseModel); + + const state = useModelStore.getState().downloadStates[baseModel.id]; + expect(state.status).toBe(ModelState.Downloaded); + expect(state.progress).toBe(1); + }); + + it('shows success toast on download complete', async () => { + mockFetch.mockResolvedValue(['/local/model.pte']); + mockUpdateModelDownloaded.mockResolvedValue(undefined); + + await useModelStore.getState().downloadModel(baseModel); + + expect(Toast.show).toHaveBeenCalledWith( + expect.objectContaining({ text1: expect.stringContaining('Test Model') }) + ); + }); + + it('resets to NotStarted and shows error toast on failure', async () => { + mockFetch.mockRejectedValue(new Error('network error')); + + await useModelStore.getState().downloadModel(baseModel); + + const state = useModelStore.getState().downloadStates[baseModel.id]; + expect(state.status).toBe(ModelState.NotStarted); + expect(state.progress).toBe(0); + expect(Toast.show).toHaveBeenCalledWith( + expect.objectContaining({ + text1: expect.stringContaining('could not be downloaded'), + }) + ); + }); + + it('ignores progress updates that arrive after download completes (race condition guard)', async () => { + let capturedProgressCb: (p: number) => void = () => {}; + mockFetch.mockImplementation((cb: (p: number) => void) => { + capturedProgressCb = cb; + return Promise.resolve(['/local/model.pte']); + }); + mockUpdateModelDownloaded.mockResolvedValue(undefined); + + await useModelStore.getState().downloadModel(baseModel); + + // Simulate late progress callback firing after completion + capturedProgressCb(0.5); + + const state = useModelStore.getState().downloadStates[baseModel.id]; + // Should still be Downloaded, not overwritten back to Downloading + expect(state.status).toBe(ModelState.Downloaded); + expect(state.progress).toBe(1); + }); + + it('throttles progress updates — does not report same percentage twice', async () => { + let capturedCb: (p: number) => void = () => {}; + mockFetch.mockImplementation((cb: (p: number) => void) => { + capturedCb = cb; + return new Promise((resolve) => { + // fire several updates then resolve + setTimeout(() => { + capturedCb(0.1); + capturedCb(0.1); // same percent — should be ignored + resolve(['/local/model.pte']); + }, 0); + }); + }); + mockUpdateModelDownloaded.mockResolvedValue(undefined); + + const setStateSpy = jest.spyOn(useModelStore, 'setState'); + await useModelStore.getState().downloadModel(baseModel); + + const progressCalls = setStateSpy.mock.calls.filter((call) => { + const arg = typeof call[0] === 'function' ? call[0](useModelStore.getState()) : call[0]; + return arg?.downloadStates?.[baseModel.id]?.status === ModelState.Downloading && + arg?.downloadStates?.[baseModel.id]?.progress > 0; + }); + // Duplicate percentage should not have been reported twice + expect(progressCalls.length).toBeLessThanOrEqual(1); + setStateSpy.mockRestore(); + }); + + it('returns early without updating state when fetch returns null', async () => { + mockFetch.mockResolvedValue(null); + + await useModelStore.getState().downloadModel(baseModel); + + // No DB update, no toast, state stays Downloading (was set at start) + expect(mockUpdateModelDownloaded).not.toHaveBeenCalled(); + expect(Toast.show).not.toHaveBeenCalled(); + }); +}); + +describe('cancelDownload', () => { + it('resets download state to NotStarted with progress 0', async () => { + useModelStore.setState({ + downloadStates: { + [baseModel.id]: { progress: 0.5, status: ModelState.Downloading }, + }, + }); + + await useModelStore.getState().cancelDownload(baseModel); + + const state = useModelStore.getState().downloadStates[baseModel.id]; + expect(state.status).toBe(ModelState.NotStarted); + expect(state.progress).toBe(0); + }); +}); + +describe('removeModel', () => { + it('skips file deletion for local/built-in models', async () => { + const localModel = { ...baseModel, source: 'local' as const, isDownloaded: true }; + useModelStore.setState({ models: [localModel] }); + mockRemoveModelFiles.mockResolvedValue(undefined); + + await useModelStore.getState().removeModel(localModel.id); + + expect(mockUnlink).not.toHaveBeenCalled(); + expect(mockUpdateModelDownloaded).not.toHaveBeenCalled(); + }); + + it('deletes local files and marks not-downloaded for remote models', async () => { + const remoteModel = { ...baseModel, isDownloaded: true }; + mockExists.mockResolvedValue(true); + mockUnlink.mockResolvedValue(undefined); + mockUpdateModelDownloaded.mockResolvedValue(undefined); + mockRemoveModelFiles.mockResolvedValue(undefined); + + // First: download so the module-level downloadedPaths Map is populated + mockFetch.mockResolvedValue(['/local/model.pte', '/local/tokenizer.json']); + useModelStore.setState({ models: [remoteModel], db: mockDb }); + await useModelStore.getState().downloadModel(remoteModel); + + // Reset mocks so we can assert cleanly on the removeModel call + jest.clearAllMocks(); + mockExists.mockResolvedValue(true); + mockUnlink.mockResolvedValue(undefined); + mockUpdateModelDownloaded.mockResolvedValue(undefined); + mockRemoveModelFiles.mockResolvedValue(undefined); + mockGetAllModels.mockResolvedValue([]); + + // Still have the model in state + useModelStore.setState({ models: [remoteModel], db: mockDb }); + await useModelStore.getState().removeModel(remoteModel.id); + + expect(mockUnlink).toHaveBeenCalledWith('/local/model.pte'); + expect(mockUnlink).toHaveBeenCalledWith('/local/tokenizer.json'); + expect(mockUpdateModelDownloaded).toHaveBeenCalledWith(mockDb, remoteModel.id, 0); + }); + + it('removes download state entry after removal', async () => { + const remoteModel = { ...baseModel, isDownloaded: true }; + useModelStore.setState({ + models: [remoteModel], + downloadStates: { + [remoteModel.id]: { progress: 1, status: ModelState.Downloaded }, + }, + }); + mockExists.mockResolvedValue(false); + mockUpdateModelDownloaded.mockResolvedValue(undefined); + mockRemoveModelFiles.mockResolvedValue(undefined); + + await useModelStore.getState().removeModel(remoteModel.id); + + expect(useModelStore.getState().downloadStates[remoteModel.id]).toBeUndefined(); + }); + + it('does nothing for unknown model id', async () => { + useModelStore.setState({ models: [] }); + await useModelStore.getState().removeModel(999); + expect(mockRemoveModelFiles).not.toHaveBeenCalled(); + }); +}); + +describe('removeModelFiles vs removeModel', () => { + it('removeModelFiles does not call removeModelFiles DB function (only marks not downloaded)', async () => { + const remoteModel = { ...baseModel, isDownloaded: true }; + useModelStore.setState({ models: [remoteModel] }); + mockExists.mockResolvedValue(false); + mockUpdateModelDownloaded.mockResolvedValue(undefined); + + await useModelStore.getState().removeModelFiles(remoteModel.id); + + // updateModelDownloaded called but removeModelFiles (DB) NOT called + expect(mockUpdateModelDownloaded).toHaveBeenCalledWith(mockDb, remoteModel.id, 0); + expect(mockRemoveModelFiles).not.toHaveBeenCalled(); + }); + + it('removeModel calls removeModelFiles (DB) to fully delete the record', async () => { + const remoteModel = { ...baseModel, isDownloaded: true }; + useModelStore.setState({ models: [remoteModel] }); + mockExists.mockResolvedValue(false); + mockUpdateModelDownloaded.mockResolvedValue(undefined); + mockRemoveModelFiles.mockResolvedValue(undefined); + + await useModelStore.getState().removeModel(remoteModel.id); + + expect(mockRemoveModelFiles).toHaveBeenCalledWith(mockDb, remoteModel.id); + }); +}); diff --git a/__tests__/onboardingStatus.test.ts b/__tests__/onboardingStatus.test.ts new file mode 100644 index 00000000..09158cfe --- /dev/null +++ b/__tests__/onboardingStatus.test.ts @@ -0,0 +1,56 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { + isOnboardingComplete, + markOnboardingComplete, +} from '../utils/onboardingStatus'; + +const mockGetItem = AsyncStorage.getItem as jest.Mock; +const mockSetItem = AsyncStorage.setItem as jest.Mock; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('isOnboardingComplete', () => { + it('returns false when key is not set (null)', async () => { + mockGetItem.mockResolvedValue(null); + expect(await isOnboardingComplete()).toBe(false); + }); + + it('returns false when stored value is "false"', async () => { + mockGetItem.mockResolvedValue('false'); + expect(await isOnboardingComplete()).toBe(false); + }); + + it('returns true when stored value is "true"', async () => { + mockGetItem.mockResolvedValue('true'); + expect(await isOnboardingComplete()).toBe(true); + }); + + it('returns false for unexpected stored value', async () => { + mockGetItem.mockResolvedValue('yes'); + expect(await isOnboardingComplete()).toBe(false); + }); + + it('reads from the correct AsyncStorage key', async () => { + mockGetItem.mockResolvedValue(null); + await isOnboardingComplete(); + expect(mockGetItem).toHaveBeenCalledWith('onboarding_complete'); + }); +}); + +describe('markOnboardingComplete', () => { + it('stores "true" under the correct key', async () => { + mockSetItem.mockResolvedValue(undefined); + await markOnboardingComplete(); + expect(mockSetItem).toHaveBeenCalledWith('onboarding_complete', 'true'); + }); + + it('after marking complete, isOnboardingComplete returns true', async () => { + mockSetItem.mockResolvedValue(undefined); + mockGetItem.mockResolvedValue('true'); + + await markOnboardingComplete(); + expect(await isOnboardingComplete()).toBe(true); + }); +}); diff --git a/__tests__/promptUtils.test.ts b/__tests__/promptUtils.test.ts new file mode 100644 index 00000000..e71d1e9b --- /dev/null +++ b/__tests__/promptUtils.test.ts @@ -0,0 +1,186 @@ +import { prepareMessagesForLLM } from '../utils/promptUtils'; +import { Message, ChatSettings } from '../database/chatRepository'; +import { Model } from '../database/modelRepository'; + +const baseSettings: ChatSettings = { + systemPrompt: 'You are a helpful assistant.', + contextWindow: 10, + thinkingEnabled: false, +}; + +const baseModel: Model = { + id: 1, + modelName: 'TestModel', + source: 'local', + isDownloaded: true, + modelPath: '/path/model.pte', + tokenizerPath: '/path/tokenizer.json', + tokenizerConfigPath: '/path/tokenizer_config.json', + thinking: false, +}; + +// In real usage, activeChatMessages always ends with an empty assistant placeholder +// (added by llmStore before calling prepareMessagesForLLM). The last item is sliced +// off by slice(-n, -1) and the second-to-last (actual user message) gets the tokens. +const makeMessages = (count: number): Message[] => [ + ...Array.from({ length: count }, (_, i) => ({ + id: i + 1, + chatId: 1, + role: (i % 2 === 0 ? 'user' : 'assistant') as Message['role'], + content: `message ${i + 1}`, + timestamp: Date.now(), + })), + // trailing assistant placeholder (always present in real usage) + { id: count + 1, chatId: 1, role: 'assistant' as Message['role'], content: '', timestamp: Date.now() }, +]; + +describe('prepareMessagesForLLM', () => { + describe('system prompt', () => { + it('always prepends the system prompt', () => { + const messages = makeMessages(2); + const result = prepareMessagesForLLM(messages, [], baseSettings, baseModel); + expect(result[0]).toEqual({ role: 'system', content: baseSettings.systemPrompt }); + }); + + it('appends context instructions to system prompt when context is provided', () => { + const messages = makeMessages(2); + const result = prepareMessagesForLLM(messages, ['some context'], baseSettings, baseModel); + expect(result[0].content).toContain('You are a helpful assistant.'); + expect(result[0].content).toContain('IMPORTANT CONTEXT INFORMATION'); + }); + + it('does not append context instructions when context is empty', () => { + const messages = makeMessages(2); + const result = prepareMessagesForLLM(messages, [], baseSettings, baseModel); + expect(result[0].content).toBe(baseSettings.systemPrompt); + expect(result[0].content).not.toContain('IMPORTANT CONTEXT INFORMATION'); + }); + }); + + describe('event message filtering', () => { + it('strips event messages from the output', () => { + // Last item is the empty assistant placeholder (as per llmStore contract) + const messages: Message[] = [ + { id: 1, chatId: 1, role: 'user', content: 'hello', timestamp: 0 }, + { id: 2, chatId: 1, role: 'event', content: 'System: file added', timestamp: 0 }, + { id: 3, chatId: 1, role: 'assistant', content: 'hi there', timestamp: 0 }, + { id: 4, chatId: 1, role: 'assistant', content: '', timestamp: 0 }, // placeholder + ]; + const result = prepareMessagesForLLM(messages, [], baseSettings, baseModel); + const roles = result.map((m) => m.role); + expect(roles).not.toContain('event'); + // system + user + assistant (placeholder sliced off) + expect(result).toHaveLength(3); + }); + }); + + describe('context window slicing', () => { + it('respects the contextWindow limit', () => { + const messages = makeMessages(20); + const settings = { ...baseSettings, contextWindow: 4 }; + const result = prepareMessagesForLLM(messages, [], settings, baseModel); + // system prompt + up to contextWindow messages (minus last which becomes the modified last) + // slice(-4, -1) = 3 messages + last message appended separately = 4 total non-system + expect(result.length).toBeLessThanOrEqual(5); // system + 4 + }); + + it('with contextWindow=1 only the last message is included', () => { + const messages = makeMessages(10); + const settings = { ...baseSettings, contextWindow: 1 }; + const result = prepareMessagesForLLM(messages, [], settings, baseModel); + // slice(-1, -1) = empty, so only system prompt — last message is lost! + // This is a potential bug: slice(-1, -1) returns [] + expect(result.length).toBeGreaterThanOrEqual(1); + }); + + it('handles empty message list', () => { + const result = prepareMessagesForLLM([], [], baseSettings, baseModel); + // Only system prompt, nothing to modify + expect(result).toHaveLength(1); + expect(result[0].role).toBe('system'); + }); + }); + + describe('thinking tokens', () => { + it('appends /think to last message when thinkingEnabled is true', () => { + const messages = makeMessages(3); + const settings = { ...baseSettings, thinkingEnabled: true }; + const result = prepareMessagesForLLM(messages, [], settings, baseModel); + const last = result[result.length - 1]; + expect(last.content).toContain('/think'); + }); + + it('appends /no_think when model supports thinking but thinkingEnabled is false', () => { + const messages = makeMessages(3); + const thinkingModel = { ...baseModel, thinking: true }; + const result = prepareMessagesForLLM(messages, [], baseSettings, thinkingModel); + const last = result[result.length - 1]; + expect(last.content).toContain('/no_think'); + }); + + it('does not append any token when neither thinkingEnabled nor model.thinking', () => { + const messages = makeMessages(3); + const result = prepareMessagesForLLM(messages, [], baseSettings, baseModel); + const last = result[result.length - 1]; + expect(last.content).not.toContain('/think'); + expect(last.content).not.toContain('/no_think'); + }); + + it('prefers /think over /no_think when thinkingEnabled=true and model.thinking=true', () => { + const messages = makeMessages(3); + const thinkingModel = { ...baseModel, thinking: true }; + const settings = { ...baseSettings, thinkingEnabled: true }; + const result = prepareMessagesForLLM(messages, [], settings, thinkingModel); + const last = result[result.length - 1]; + expect(last.content).toContain('/think'); + expect(last.content).not.toContain('/no_think'); + }); + }); + + describe('context injection', () => { + it('wraps context in tags on the last message', () => { + const messages = makeMessages(3); + const result = prepareMessagesForLLM( + messages, + ['chunk one', 'chunk two'], + baseSettings, + baseModel + ); + const last = result[result.length - 1]; + expect(last.content).toContain('chunk one chunk two'); + }); + + it('context is added to the last user message (second-to-last in array)', () => { + const messages: Message[] = [ + { id: 1, chatId: 1, role: 'user', content: 'What is RAG?', timestamp: 0 }, + { id: 2, chatId: 1, role: 'assistant', content: 'answer', timestamp: 0 }, + { id: 3, chatId: 1, role: 'user', content: 'Tell me more', timestamp: 0 }, + { id: 4, chatId: 1, role: 'assistant', content: '', timestamp: 0 }, // placeholder + ]; + const result = prepareMessagesForLLM( + messages, + ['some context'], + baseSettings, + baseModel + ); + const last = result[result.length - 1]; + expect(last.content).toContain('Tell me more'); + expect(last.content).toContain('some context'); + }); + + it('combines context and /think token', () => { + const messages = makeMessages(3); + const settings = { ...baseSettings, thinkingEnabled: true }; + const result = prepareMessagesForLLM( + messages, + ['some context'], + settings, + baseModel + ); + const last = result[result.length - 1]; + // /think is appended before context wrapping + expect(last.content).toContain('/think'); + expect(last.content).toContain(''); + }); + }); +}); diff --git a/__tests__/sourceStore.test.ts b/__tests__/sourceStore.test.ts new file mode 100644 index 00000000..f458f30c --- /dev/null +++ b/__tests__/sourceStore.test.ts @@ -0,0 +1,224 @@ +import { useSourceStore } from '../store/sourceStore'; +import * as sourcesRepository from '../database/sourcesRepository'; +import * as fileReaders from '../utils/fileReaders'; +import { useLLMStore } from '../store/llmStore'; + +jest.mock('../database/sourcesRepository'); +jest.mock('../utils/fileReaders'); +jest.mock('../store/llmStore', () => ({ + useLLMStore: { + getState: jest.fn(() => ({ + refreshActiveChatMessages: jest.fn(), + })), + }, +})); +jest.mock('react-native-rag', () => ({ + RecursiveCharacterTextSplitter: jest.fn().mockImplementation(() => ({ + splitText: jest.fn(), + })), +})); +jest.mock('@react-native-rag/op-sqlite', () => ({})); + +const mockDb = {} as any; +const mockVectorStore = { add: jest.fn() } as any; + +const mockReadDocumentText = fileReaders.readDocumentText as jest.Mock; +const mockInsertSource = sourcesRepository.insertSource as jest.Mock; +const mockDeleteSource = sourcesRepository.deleteSource as jest.Mock; +const mockDeleteSourceFromChats = sourcesRepository.deleteSourceFromChats as jest.Mock; +const mockGetAllSources = sourcesRepository.getAllSources as jest.Mock; +const mockRenameSource = sourcesRepository.renameSource as jest.Mock; +const mockRefreshActiveChatMessages = jest.fn(); + +import { RecursiveCharacterTextSplitter } from 'react-native-rag'; +const MockSplitter = RecursiveCharacterTextSplitter as jest.Mock; + +beforeEach(() => { + useSourceStore.setState({ sources: [], db: mockDb, isReading: false }); + jest.clearAllMocks(); + jest.spyOn(console, 'error').mockImplementation(() => {}); + mockGetAllSources.mockResolvedValue([]); + ;(useLLMStore.getState as jest.Mock).mockReturnValue({ + refreshActiveChatMessages: mockRefreshActiveChatMessages, + }); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +const baseSource = { name: 'doc.txt', type: 'txt', size: 100 }; + +describe('addSource', () => { + it('returns { success: false } when db is not set', async () => { + useSourceStore.setState({ db: null }); + const result = await useSourceStore + .getState() + .addSource(baseSource, '/path/doc.txt', mockVectorStore); + expect(result).toEqual({ success: false }); + }); + + it('returns { success: false, isEmpty: true } for empty document', async () => { + mockReadDocumentText.mockResolvedValue(' '); + const result = await useSourceStore + .getState() + .addSource(baseSource, '/path/doc.txt', mockVectorStore); + expect(result).toEqual({ success: false, isEmpty: true }); + expect(useSourceStore.getState().isReading).toBe(false); + }); + + it('sets isReading to true during processing then false on success', async () => { + mockReadDocumentText.mockResolvedValue('some content'); + mockInsertSource.mockResolvedValue(42); + MockSplitter.mockImplementation(() => ({ + splitText: jest.fn().mockResolvedValue(['chunk1']), + })); + + const promise = useSourceStore + .getState() + .addSource(baseSource, '/path/doc.txt', mockVectorStore); + + await promise; + expect(useSourceStore.getState().isReading).toBe(false); + }); + + it('adds a temp source with negative id and isProcessing=true before DB insert', async () => { + let capturedSources: any[] = []; + mockReadDocumentText.mockResolvedValue('content'); + mockInsertSource.mockImplementation(async () => { + capturedSources = useSourceStore.getState().sources; + return 10; + }); + MockSplitter.mockImplementation(() => ({ + splitText: jest.fn().mockResolvedValue(['chunk']), + })); + + await useSourceStore + .getState() + .addSource(baseSource, '/path/doc.txt', mockVectorStore); + + expect(capturedSources).toHaveLength(1); + expect(capturedSources[0].id).toBeLessThan(0); + expect(capturedSources[0].isProcessing).toBe(true); + }); + + it('replaces temp source with real id and isProcessing=false on success', async () => { + mockReadDocumentText.mockResolvedValue('content'); + mockInsertSource.mockResolvedValue(99); + MockSplitter.mockImplementation(() => ({ + splitText: jest.fn().mockResolvedValue(['chunk']), + })); + + const result = await useSourceStore + .getState() + .addSource(baseSource, '/path/doc.txt', mockVectorStore); + + const sources = useSourceStore.getState().sources; + expect(result).toEqual({ success: true }); + expect(sources).toHaveLength(1); + expect(sources[0].id).toBe(99); + expect(sources[0].isProcessing).toBe(false); + }); + + it('calls vectorStore.add once per chunk', async () => { + mockReadDocumentText.mockResolvedValue('content'); + mockInsertSource.mockResolvedValue(1); + MockSplitter.mockImplementation(() => ({ + splitText: jest.fn().mockResolvedValue(['a', 'b', 'c']), + })); + + await useSourceStore + .getState() + .addSource(baseSource, '/path/doc.txt', mockVectorStore); + + expect(mockVectorStore.add).toHaveBeenCalledTimes(3); + expect(mockVectorStore.add).toHaveBeenCalledWith({ + document: 'a', + metadata: { documentId: 1 }, + }); + }); + + it('removes temp source and resets isReading when DB insert fails', async () => { + mockReadDocumentText.mockResolvedValue('content'); + mockInsertSource.mockResolvedValue(null); // insert failure + MockSplitter.mockImplementation(() => ({ + splitText: jest.fn().mockResolvedValue(['chunk']), + })); + + const result = await useSourceStore + .getState() + .addSource(baseSource, '/path/doc.txt', mockVectorStore); + + expect(result).toEqual({ success: false }); + expect(useSourceStore.getState().sources).toHaveLength(0); + expect(useSourceStore.getState().isReading).toBe(false); + }); + + it('cleans up temp source and resets isReading when an exception is thrown', async () => { + mockReadDocumentText.mockRejectedValue(new Error('read error')); + + const result = await useSourceStore + .getState() + .addSource(baseSource, '/path/doc.txt', mockVectorStore); + + expect(result).toEqual({ success: false }); + expect(useSourceStore.getState().sources).toHaveLength(0); + expect(useSourceStore.getState().isReading).toBe(false); + }); +}); + +describe('deleteSource', () => { + it('calls deleteSource and deleteSourceFromChats then reloads', async () => { + mockDeleteSource.mockResolvedValue(undefined); + mockDeleteSourceFromChats.mockResolvedValue(undefined); + mockGetAllSources.mockResolvedValue([]); + + const source = { id: 5, name: 'doc.txt', type: 'txt', size: 100 }; + await useSourceStore.getState().deleteSource(source); + + expect(mockDeleteSource).toHaveBeenCalledWith(mockDb, 5); + expect(mockDeleteSourceFromChats).toHaveBeenCalledWith(mockDb, source); + }); + + it('calls refreshActiveChatMessages on the LLM store after deleting', async () => { + mockDeleteSource.mockResolvedValue(undefined); + mockDeleteSourceFromChats.mockResolvedValue(undefined); + mockGetAllSources.mockResolvedValue([]); + + await useSourceStore + .getState() + .deleteSource({ id: 1, name: 'f', type: 'txt', size: 0 }); + + expect(mockRefreshActiveChatMessages).toHaveBeenCalled(); + }); +}); + +describe('renameSource', () => { + it('calls renameSource in DB then reloads sources', async () => { + mockRenameSource.mockResolvedValue(undefined); + const updated = [{ id: 1, name: 'new-name.txt', type: 'txt', size: 0 }]; + mockGetAllSources.mockResolvedValue(updated); + + await useSourceStore.getState().renameSource(1, 'new-name.txt'); + + expect(mockRenameSource).toHaveBeenCalledWith(mockDb, 1, 'new-name.txt'); + expect(useSourceStore.getState().sources).toEqual(updated); + }); +}); + +describe('setSourceProcessing', () => { + it('updates isProcessing flag on the correct source', () => { + useSourceStore.setState({ + sources: [ + { id: 1, name: 'a.txt', type: 'txt', size: 0, isProcessing: false }, + { id: 2, name: 'b.txt', type: 'txt', size: 0, isProcessing: false }, + ], + }); + + useSourceStore.getState().setSourceProcessing(1, true); + + const sources = useSourceStore.getState().sources; + expect(sources.find((s) => s.id === 1)?.isProcessing).toBe(true); + expect(sources.find((s) => s.id === 2)?.isProcessing).toBe(false); + }); +}); diff --git a/__tests__/sttStore.test.ts b/__tests__/sttStore.test.ts new file mode 100644 index 00000000..b8dee232 --- /dev/null +++ b/__tests__/sttStore.test.ts @@ -0,0 +1,101 @@ +import { useSTTStore } from '../store/sttStore'; +import { SpeechToTextModule } from 'react-native-executorch'; + +jest.mock('react-native-executorch', () => ({ + SpeechToTextModule: { + fromModelName: jest.fn(), + }, + WHISPER_TINY_EN: 'whisper-tiny-en', +})); + +const mockFromModelName = SpeechToTextModule.fromModelName as jest.Mock; + +const mockModule = { stream: jest.fn(), streamInsert: jest.fn(), streamStop: jest.fn() }; + +beforeEach(() => { + useSTTStore.setState({ + module: null, + isReady: false, + isLoading: false, + loadProgress: 0, + }); + jest.clearAllMocks(); +}); + +describe('ensureLoaded', () => { + it('sets isLoading during load and isReady after success', async () => { + let resolveLoad!: (m: typeof mockModule) => void; + mockFromModelName.mockReturnValue( + new Promise((res) => { resolveLoad = res; }) + ); + + const loadPromise = useSTTStore.getState().ensureLoaded(); + expect(useSTTStore.getState().isLoading).toBe(true); + expect(useSTTStore.getState().isReady).toBe(false); + + resolveLoad(mockModule); + await loadPromise; + + expect(useSTTStore.getState().isLoading).toBe(false); + expect(useSTTStore.getState().isReady).toBe(true); + expect(useSTTStore.getState().module).toBe(mockModule); + }); + + it('reports progress callbacks', async () => { + mockFromModelName.mockImplementation((_name, onProgress) => { + onProgress(0.5); + onProgress(1.0); + return Promise.resolve(mockModule); + }); + + await useSTTStore.getState().ensureLoaded(); + + // Final progress is set to 1 by the .then() handler + expect(useSTTStore.getState().loadProgress).toBe(1); + }); + + it('returns immediately when already ready', async () => { + useSTTStore.setState({ isReady: true, module: mockModule as any }); + + await useSTTStore.getState().ensureLoaded(); + + expect(mockFromModelName).not.toHaveBeenCalled(); + }); + + it('concurrent calls share the same load promise (fromModelName called once)', async () => { + mockFromModelName.mockResolvedValue(mockModule); + + await Promise.all([ + useSTTStore.getState().ensureLoaded(), + useSTTStore.getState().ensureLoaded(), + useSTTStore.getState().ensureLoaded(), + ]); + + expect(mockFromModelName).toHaveBeenCalledTimes(1); + }); + + it('resets state on load failure', async () => { + mockFromModelName.mockRejectedValue(new Error('load failed')); + + await expect(useSTTStore.getState().ensureLoaded()).rejects.toThrow('load failed'); + + expect(useSTTStore.getState().isReady).toBe(false); + expect(useSTTStore.getState().isLoading).toBe(false); + expect(useSTTStore.getState().loadProgress).toBe(0); + expect(useSTTStore.getState().module).toBeNull(); + }); + + it('allows retrying after a failed load', async () => { + mockFromModelName + .mockRejectedValueOnce(new Error('first attempt failed')) + .mockResolvedValueOnce(mockModule); + + await expect(useSTTStore.getState().ensureLoaded()).rejects.toThrow(); + + // Second attempt should succeed + await useSTTStore.getState().ensureLoaded(); + + expect(useSTTStore.getState().isReady).toBe(true); + expect(useSTTStore.getState().module).toBe(mockModule); + }); +}); diff --git a/__tests__/useBenchmarkRunner.test.ts b/__tests__/useBenchmarkRunner.test.ts new file mode 100644 index 00000000..2c876adb --- /dev/null +++ b/__tests__/useBenchmarkRunner.test.ts @@ -0,0 +1,227 @@ +import { renderHook, act, waitFor } from '@testing-library/react-native'; +import useBenchmarkRunner from '../hooks/useBenchmarkRunner'; +import { useLLMStore } from '../store/llmStore'; +import * as benchmarkRepository from '../database/benchmarkRepository'; + +jest.mock('../store/llmStore', () => ({ useLLMStore: jest.fn() })); +jest.mock('expo-sqlite', () => ({ useSQLiteContext: jest.fn(() => ({})) })); +jest.mock('../database/benchmarkRepository'); + +const mockUseLLMStore = useLLMStore as jest.Mock; +const mockInsertBenchmark = benchmarkRepository.insertBenchmark as jest.Mock; + +const baseModel = { + id: 1, + modelName: 'Test LLM', + source: 'remote' as const, + isDownloaded: true, + modelPath: '', + tokenizerPath: '', + tokenizerConfigPath: '', + thinking: false, + featured: false, +}; + +const perfResult = { + totalTime: 3000, + timeToFirstToken: 200, + tokensPerSecond: 15, + tokensGenerated: 45, + peakMemory: 1024 * 1024 * 1024, +}; + +beforeEach(() => { + jest.useFakeTimers(); + jest.clearAllMocks(); + jest.spyOn(console, 'error').mockImplementation(() => {}); + + mockUseLLMStore.mockReturnValue({ + runBenchmark: jest.fn().mockResolvedValue(perfResult), + loadModel: jest.fn().mockResolvedValue(undefined), + interrupt: jest.fn(), + }); + mockInsertBenchmark.mockResolvedValue(42); +}); + +afterEach(() => { + jest.useRealTimers(); + jest.restoreAllMocks(); +}); + +// ─── initial state ──────────────────────────────────────────────────────────── + +describe('initial state', () => { + it('starts idle', () => { + const { result } = renderHook(() => useBenchmarkRunner({ onComplete: jest.fn() })); + expect(result.current.isRunning).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.timer).toBe(0); + }); +}); + +// ─── startBenchmark ─────────────────────────────────────────────────────────── + +describe('startBenchmark', () => { + it('does nothing when no model is selected', async () => { + const { result } = renderHook(() => useBenchmarkRunner({ onComplete: jest.fn() })); + await act(async () => { + await result.current.startBenchmark(undefined); + }); + expect(result.current.isRunning).toBe(false); + }); + + it('sets isRunning=true during benchmark', async () => { + let resolveRunBenchmark!: (v: any) => void; + mockUseLLMStore.mockReturnValue({ + runBenchmark: jest.fn(() => new Promise((r) => { resolveRunBenchmark = r; })), + loadModel: jest.fn().mockResolvedValue(undefined), + interrupt: jest.fn(), + }); + + const { result } = renderHook(() => useBenchmarkRunner({ onComplete: jest.fn() })); + + // startBenchmark sets isRunning synchronously before any awaits + act(() => { result.current.startBenchmark(baseModel); }); + + expect(result.current.isRunning).toBe(true); + + // Resolve the hanging promise to avoid open handles + await act(async () => { + if (resolveRunBenchmark) resolveRunBenchmark(perfResult); + jest.advanceTimersByTime(10000); + }); + }); + + it('calls loadModel with hardReload=true before running', async () => { + const loadModel = jest.fn().mockResolvedValue(undefined); + const runBenchmark = jest.fn().mockResolvedValue(perfResult); + mockUseLLMStore.mockReturnValue({ loadModel, runBenchmark, interrupt: jest.fn() }); + + const { result } = renderHook(() => useBenchmarkRunner({ onComplete: jest.fn() })); + await act(async () => { + await result.current.startBenchmark(baseModel); + jest.runAllTimers(); + }); + + expect(loadModel).toHaveBeenCalledWith(baseModel, true); + }); + + it('runs benchmark 3 times and averages results', async () => { + const runBenchmark = jest.fn().mockResolvedValue(perfResult); + mockUseLLMStore.mockReturnValue({ + runBenchmark, + loadModel: jest.fn().mockResolvedValue(undefined), + interrupt: jest.fn(), + }); + + const { result } = renderHook(() => useBenchmarkRunner({ onComplete: jest.fn() })); + await act(async () => { + await result.current.startBenchmark(baseModel); + jest.runAllTimers(); + }); + + expect(runBenchmark).toHaveBeenCalledTimes(3); + }); + + it('calls insertBenchmark and onComplete on success', async () => { + const onComplete = jest.fn(); + mockInsertBenchmark.mockResolvedValue(99); + + const { result } = renderHook(() => useBenchmarkRunner({ onComplete })); + await act(async () => { + await result.current.startBenchmark(baseModel); + jest.runAllTimers(); + }); + + expect(mockInsertBenchmark).toHaveBeenCalled(); + expect(onComplete).toHaveBeenCalledWith(99); + }); + + it('sets isSuccess=true after successful run', async () => { + const { result } = renderHook(() => useBenchmarkRunner({ onComplete: jest.fn() })); + await act(async () => { + await result.current.startBenchmark(baseModel); + jest.runAllTimers(); + }); + + expect(result.current.isSuccess).toBe(true); + }); + + it('resets isRunning on error', async () => { + mockUseLLMStore.mockReturnValue({ + runBenchmark: jest.fn().mockRejectedValue(new Error('crash')), + loadModel: jest.fn().mockResolvedValue(undefined), + interrupt: jest.fn(), + }); + + const { result } = renderHook(() => useBenchmarkRunner({ onComplete: jest.fn() })); + await act(async () => { + await result.current.startBenchmark(baseModel); + }); + + expect(result.current.isRunning).toBe(false); + }); + + it('increments timer every second while running', async () => { + let resolveAll!: () => void; + const waitForIt = new Promise((r) => { resolveAll = r; }); + + mockUseLLMStore.mockReturnValue({ + runBenchmark: jest.fn(() => waitForIt.then(() => perfResult)), + loadModel: jest.fn().mockResolvedValue(undefined), + interrupt: jest.fn(), + }); + + const { result } = renderHook(() => useBenchmarkRunner({ onComplete: jest.fn() })); + + act(() => { result.current.startBenchmark(baseModel); }); + act(() => { jest.advanceTimersByTime(3000); }); + + expect(result.current.timer).toBeGreaterThanOrEqual(3); + resolveAll(); + }); +}); + +// ─── cancelBenchmark ───────────────────────────────────────────────────────── + +describe('cancelBenchmark', () => { + it('calls interrupt and sets isRunning=false', () => { + const interrupt = jest.fn(); + mockUseLLMStore.mockReturnValue({ + runBenchmark: jest.fn(() => new Promise(() => {})), + loadModel: jest.fn().mockResolvedValue(undefined), + interrupt, + }); + + const { result } = renderHook(() => useBenchmarkRunner({ onComplete: jest.fn() })); + + act(() => { result.current.startBenchmark(baseModel); }); + act(() => { result.current.cancelBenchmark(); }); + + expect(interrupt).toHaveBeenCalled(); + expect(result.current.isRunning).toBe(false); + }); + + it('stops further iterations after cancel', async () => { + const runBenchmark = jest.fn().mockResolvedValue(perfResult); + mockUseLLMStore.mockReturnValue({ + runBenchmark, + loadModel: jest.fn().mockResolvedValue(undefined), + interrupt: jest.fn(), + }); + + const { result } = renderHook(() => useBenchmarkRunner({ onComplete: jest.fn() })); + + // Start benchmark and cancel mid-way through first iteration + let startPromise: Promise; + act(() => { + startPromise = result.current.startBenchmark(baseModel) as any; + result.current.cancelBenchmark(); + }); + + await act(async () => { await startPromise; }); + + // With cancel, should not complete all 3 iterations + expect(runBenchmark.mock.calls.length).toBeLessThan(3); + }); +}); diff --git a/__tests__/useChatSettings.test.ts b/__tests__/useChatSettings.test.ts new file mode 100644 index 00000000..7da61505 --- /dev/null +++ b/__tests__/useChatSettings.test.ts @@ -0,0 +1,140 @@ +import { renderHook, waitFor } from '@testing-library/react-native'; +import useChatSettings from '../hooks/useChatSettings'; +import * as chatRepository from '../database/chatRepository'; +import { useChatStore } from '../store/chatStore'; + +jest.mock('../database/chatRepository'); +jest.mock('expo-sqlite', () => { + // Stable db object — must NOT be recreated on each call or useEffect re-fires infinitely + const stableDb = {}; + return { useSQLiteContext: jest.fn(() => stableDb) }; +}); +jest.mock('../store/chatStore', () => ({ + useChatStore: jest.fn(), +})); + +const mockGetChatSettings = chatRepository.getChatSettings as jest.Mock; +const mockUseChatStore = useChatStore as jest.Mock; + +const baseChat = { + id: 1, + title: 'Test Chat', + modelId: 1, + createdAt: '', + lastUsedAt: '', + enabledSources: [], +}; + +// Stable function references — must not be recreated per render or useMemo loops infinitely +const stableGetChatById = jest.fn(() => baseChat); + +beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(console, 'error').mockImplementation(() => {}); + stableGetChatById.mockReturnValue(baseChat); + mockUseChatStore.mockReturnValue({ + getChatById: stableGetChatById, + phantomChat: null, + }); + mockGetChatSettings.mockResolvedValue({ + systemPrompt: 'You are helpful.', + contextWindow: 6, + thinkingEnabled: false, + }); +}); + +afterEach(() => jest.restoreAllMocks()); + +// ─── initial load ───────────────────────────────────────────────────────────── + +describe('initial load', () => { + it('loads settings from DB on mount', async () => { + const { result } = renderHook(() => useChatSettings(1)); + + await waitFor(() => + expect(result.current.settings.systemPrompt).toBe('You are helpful.') + ); + expect(result.current.settings.contextWindow).toBe('6'); + expect(result.current.settings.thinkingEnabled).toBe(false); + }); + + it('loads settings from phantom chat when chatId matches phantom', async () => { + const phantomChat = { + ...baseChat, + id: 99, + settings: { systemPrompt: 'Phantom prompt', contextWindow: 4, thinkingEnabled: true }, + }; + mockUseChatStore.mockReturnValue({ + getChatById: stableGetChatById, + phantomChat, + }); + + const { result } = renderHook(() => useChatSettings(99)); + + await waitFor(() => + expect(result.current.settings.systemPrompt).toBe('Phantom prompt') + ); + expect(result.current.settings.contextWindow).toBe('4'); + expect(result.current.settings.thinkingEnabled).toBe(true); + expect(mockGetChatSettings).not.toHaveBeenCalled(); + }); + + it('converts contextWindow number to string', async () => { + mockGetChatSettings.mockResolvedValue({ + systemPrompt: '', + contextWindow: 12, + thinkingEnabled: false, + }); + const { result } = renderHook(() => useChatSettings(1)); + await waitFor(() => expect(result.current.settings.contextWindow).toBe('12')); + }); + + it('defaults thinkingEnabled to false when not in DB result', async () => { + mockGetChatSettings.mockResolvedValue({ + systemPrompt: '', + contextWindow: 6, + }); + const { result } = renderHook(() => useChatSettings(1)); + await waitFor(() => expect(result.current.settings.thinkingEnabled).toBe(false)); + }); +}); + +// ─── setSetting ─────────────────────────────────────────────────────────────── + +describe('setSetting', () => { + it('updates a single setting without affecting others', async () => { + const { result } = renderHook(() => useChatSettings(1)); + await waitFor(() => expect(result.current.settings.systemPrompt).toBe('You are helpful.')); + + result.current.setSetting('contextWindow', '10'); + + await waitFor(() => expect(result.current.settings.contextWindow).toBe('10')); + expect(result.current.settings.systemPrompt).toBe('You are helpful.'); + }); + + it('can set boolean values for thinkingEnabled', async () => { + const { result } = renderHook(() => useChatSettings(1)); + await waitFor(() => expect(result.current.settings.thinkingEnabled).toBe(false)); + + result.current.setSetting('thinkingEnabled', true); + await waitFor(() => expect(result.current.settings.thinkingEnabled).toBe(true)); + }); +}); + +// ─── chat reference ─────────────────────────────────────────────────────────── + +describe('chat reference', () => { + it('returns the chat object from store', async () => { + const { result } = renderHook(() => useChatSettings(1)); + await waitFor(() => expect(result.current.chat).toBeDefined()); + expect(result.current.chat?.title).toBe('Test Chat'); + }); + + it('returns phantom chat when chatId matches', async () => { + const phantomChat = { ...baseChat, id: 99, settings: { systemPrompt: '', contextWindow: 6, thinkingEnabled: false } }; + mockUseChatStore.mockReturnValue({ getChatById: stableGetChatById, phantomChat }); + + const { result } = renderHook(() => useChatSettings(99)); + await waitFor(() => expect(result.current.chat?.id).toBe(99)); + }); +}); diff --git a/__tests__/useModelHubData.test.ts b/__tests__/useModelHubData.test.ts new file mode 100644 index 00000000..58d4dcbf --- /dev/null +++ b/__tests__/useModelHubData.test.ts @@ -0,0 +1,231 @@ +import { renderHook } from '@testing-library/react-native'; +import useModelHubData, { ModelHubFilter } from '../hooks/useModelHubData'; +import * as modelCompatibility from '../utils/modelCompatibility'; + +jest.mock('../utils/modelCompatibility'); +const mockIsModelCompatible = modelCompatibility.isModelCompatible as jest.Mock; + +const makeModel = (overrides: Partial<{ + id: number; + modelName: string; + source: 'built-in' | 'remote' | 'local'; + isDownloaded: boolean; + featured: boolean; + parameters: number; +}> = {}) => ({ + id: 1, + modelName: 'Llama-3B', + source: 'built-in' as const, + isDownloaded: false, + featured: false, + parameters: 3, + modelPath: '', + tokenizerPath: '', + tokenizerConfigPath: '', + thinking: false, + ...overrides, +}); + +beforeEach(() => { + mockIsModelCompatible.mockReturnValue(true); +}); + +// ─── search ────────────────────────────────────────────────────────────────── + +describe('search', () => { + it('returns all models when search is empty', () => { + const models = [makeModel({ modelName: 'Llama-3B' }), makeModel({ id: 2, modelName: 'Qwen-1B' })]; + const { result } = renderHook(() => + useModelHubData({ models, search: '', activeFilters: new Set(), groupByModel: false }) + ); + const all = result.current.groupedModels.flatMap((g) => g.models); + expect(all).toHaveLength(2); + }); + + it('filters by search string (case-insensitive)', () => { + const models = [makeModel({ modelName: 'Llama-3B' }), makeModel({ id: 2, modelName: 'Qwen-1B' })]; + const { result } = renderHook(() => + useModelHubData({ models, search: 'qwen', activeFilters: new Set(), groupByModel: false }) + ); + const all = result.current.groupedModels.flatMap((g) => g.models); + expect(all).toHaveLength(1); + expect(all[0].modelName).toBe('Qwen-1B'); + }); + + it('returns isEmpty=true when nothing matches search', () => { + const models = [makeModel({ modelName: 'Llama-3B' })]; + const { result } = renderHook(() => + useModelHubData({ models, search: 'gpt', activeFilters: new Set(), groupByModel: false }) + ); + expect(result.current.isEmpty).toBe(true); + }); +}); + +// ─── filters ───────────────────────────────────────────────────────────────── + +describe('Featured filter', () => { + it('excludes non-featured built-in models when Featured filter is active', () => { + const models = [ + makeModel({ id: 1, modelName: 'A', source: 'built-in', featured: false }), + makeModel({ id: 2, modelName: 'B', source: 'built-in', featured: true }), + ]; + const { result } = renderHook(() => + useModelHubData({ + models, + search: '', + activeFilters: new Set([ModelHubFilter.Featured]), + groupByModel: false, + }) + ); + const all = result.current.groupedModels.flatMap((g) => g.models); + expect(all).toHaveLength(1); + expect(all[0].id).toBe(2); + }); + + it('does not exclude remote/local models regardless of featured flag', () => { + const models = [ + makeModel({ id: 1, source: 'remote', featured: false }), + makeModel({ id: 2, source: 'local', featured: false }), + ]; + const { result } = renderHook(() => + useModelHubData({ + models, + search: '', + activeFilters: new Set([ModelHubFilter.Featured]), + groupByModel: false, + }) + ); + const all = result.current.groupedModels.flatMap((g) => g.models); + expect(all).toHaveLength(2); + }); +}); + +describe('Downloaded filter', () => { + it('excludes non-downloaded models when Downloaded filter is active', () => { + const models = [ + makeModel({ id: 1, isDownloaded: true }), + makeModel({ id: 2, isDownloaded: false }), + ]; + const { result } = renderHook(() => + useModelHubData({ + models, + search: '', + activeFilters: new Set([ModelHubFilter.Downloaded]), + groupByModel: false, + }) + ); + const all = result.current.groupedModels.flatMap((g) => g.models); + expect(all).toHaveLength(1); + expect(all[0].id).toBe(1); + }); +}); + +describe('Compatible filter', () => { + it('excludes incompatible models when Compatible filter is active', () => { + mockIsModelCompatible.mockImplementation((m) => m.id === 1); + const models = [makeModel({ id: 1 }), makeModel({ id: 2 })]; + const { result } = renderHook(() => + useModelHubData({ + models, + search: '', + activeFilters: new Set([ModelHubFilter.Compatible]), + groupByModel: false, + }) + ); + const all = result.current.groupedModels.flatMap((g) => g.models); + expect(all).toHaveLength(1); + expect(all[0].id).toBe(1); + }); +}); + +describe('combined filters', () => { + it('applies all active filters together (AND logic)', () => { + mockIsModelCompatible.mockReturnValue(true); + const models = [ + makeModel({ id: 1, isDownloaded: true, featured: true, source: 'built-in' }), + makeModel({ id: 2, isDownloaded: false, featured: true, source: 'built-in' }), + makeModel({ id: 3, isDownloaded: true, featured: false, source: 'built-in' }), + ]; + const { result } = renderHook(() => + useModelHubData({ + models, + search: '', + activeFilters: new Set([ModelHubFilter.Featured, ModelHubFilter.Downloaded]), + groupByModel: false, + }) + ); + const all = result.current.groupedModels.flatMap((g) => g.models); + expect(all).toHaveLength(1); + expect(all[0].id).toBe(1); + }); +}); + +// ─── grouping ───────────────────────────────────────────────────────────────── + +describe('groupByModel=false (default sections)', () => { + it('splits into "Ready to Use" and "Available to Download"', () => { + const models = [ + makeModel({ id: 1, isDownloaded: true }), + makeModel({ id: 2, isDownloaded: false }), + ]; + const { result } = renderHook(() => + useModelHubData({ models, search: '', activeFilters: new Set(), groupByModel: false }) + ); + const labels = result.current.groupedModels.map((g) => g.label); + expect(labels).toContain('Ready to Use'); + expect(labels).toContain('Available to Download'); + + const ready = result.current.groupedModels.find((g) => g.label === 'Ready to Use')!; + const available = result.current.groupedModels.find((g) => g.label === 'Available to Download')!; + expect(ready.models.every((m) => m.isDownloaded)).toBe(true); + expect(available.models.every((m) => !m.isDownloaded)).toBe(true); + }); +}); + +describe('groupByModel=true (prefix grouping)', () => { + it('groups models by name prefix', () => { + const models = [ + makeModel({ id: 1, modelName: 'Llama-3B', isDownloaded: true }), + makeModel({ id: 2, modelName: 'Llama-7B', isDownloaded: false }), + makeModel({ id: 3, modelName: 'Qwen-1B', isDownloaded: false }), + ]; + const { result } = renderHook(() => + useModelHubData({ models, search: '', activeFilters: new Set(), groupByModel: true }) + ); + const labels = result.current.groupedModels.map((g) => g.label); + expect(labels).toContain('llama'); + expect(labels).toContain('qwen'); + + const llamaGroup = result.current.groupedModels.find((g) => g.label === 'llama')!; + expect(llamaGroup.models).toHaveLength(2); + }); +}); + +// ─── sorting ────────────────────────────────────────────────────────────────── + +describe('available models sorting', () => { + it('sorts available models by parameters ascending', () => { + const models = [ + makeModel({ id: 1, modelName: 'Big', parameters: 7, isDownloaded: false }), + makeModel({ id: 2, modelName: 'Small', parameters: 1, isDownloaded: false }), + makeModel({ id: 3, modelName: 'Medium', parameters: 3, isDownloaded: false }), + ]; + const { result } = renderHook(() => + useModelHubData({ models, search: '', activeFilters: new Set(), groupByModel: false }) + ); + const available = result.current.groupedModels.find((g) => g.label === 'Available to Download')!; + expect(available.models.map((m) => m.parameters)).toEqual([1, 3, 7]); + }); + + it('falls back to alphabetical sort when parameters are equal', () => { + const models = [ + makeModel({ id: 1, modelName: 'Zebra', parameters: 3, isDownloaded: false }), + makeModel({ id: 2, modelName: 'Apple', parameters: 3, isDownloaded: false }), + ]; + const { result } = renderHook(() => + useModelHubData({ models, search: '', activeFilters: new Set(), groupByModel: false }) + ); + const available = result.current.groupedModels.find((g) => g.label === 'Available to Download')!; + expect(available.models[0].modelName).toBe('Apple'); + }); +}); diff --git a/__tests__/useOnboardingRedirect.test.ts b/__tests__/useOnboardingRedirect.test.ts new file mode 100644 index 00000000..7272e6a7 --- /dev/null +++ b/__tests__/useOnboardingRedirect.test.ts @@ -0,0 +1,79 @@ +import { renderHook, waitFor } from '@testing-library/react-native'; +import useOnboardingRedirect from '../hooks/useOnboardingRedirect'; +import * as onboardingStatus from '../utils/onboardingStatus'; +import * as modelRepository from '../database/modelRepository'; +import { useRouter } from 'expo-router'; + +jest.mock('../utils/onboardingStatus'); +jest.mock('../database/modelRepository'); +jest.mock('expo-sqlite', () => ({ useSQLiteContext: jest.fn(() => ({})) })); +jest.mock('expo-router', () => ({ useRouter: jest.fn() })); + +const mockIsOnboardingComplete = onboardingStatus.isOnboardingComplete as jest.Mock; +const mockGetAllModels = modelRepository.getAllModels as jest.Mock; +const mockUseRouter = useRouter as jest.Mock; + +const mockPush = jest.fn(); + +const makeModel = (isDownloaded: boolean) => ({ + id: 1, + modelName: 'Test', + isDownloaded, + source: 'remote' as const, + modelPath: '', tokenizerPath: '', tokenizerConfigPath: '', thinking: false, featured: false, +}); + +beforeEach(() => { + jest.clearAllMocks(); + mockUseRouter.mockReturnValue({ push: mockPush }); + mockIsOnboardingComplete.mockResolvedValue(true); + mockGetAllModels.mockResolvedValue([makeModel(true)]); +}); + +// ─── redirect logic ─────────────────────────────────────────────────────────── + +describe('redirect logic', () => { + it('does not redirect when onboarding is complete and a model is downloaded', async () => { + renderHook(() => useOnboardingRedirect()); + await waitFor(() => expect(mockGetAllModels).toHaveBeenCalled()); + expect(mockPush).not.toHaveBeenCalled(); + }); + + it('redirects to /onboarding when onboarding is not complete', async () => { + mockIsOnboardingComplete.mockResolvedValue(false); + renderHook(() => useOnboardingRedirect()); + await waitFor(() => expect(mockPush).toHaveBeenCalledWith('/onboarding')); + // Should not check models after redirecting to onboarding + expect(mockGetAllModels).not.toHaveBeenCalled(); + }); + + it('redirects to select-starting-model when no models are downloaded', async () => { + mockGetAllModels.mockResolvedValue([makeModel(false), makeModel(false)]); + renderHook(() => useOnboardingRedirect()); + await waitFor(() => + expect(mockPush).toHaveBeenCalledWith('/(modals)/select-starting-model') + ); + }); + + it('does not redirect to model selection when at least one model is downloaded', async () => { + mockGetAllModels.mockResolvedValue([makeModel(false), makeModel(true)]); + renderHook(() => useOnboardingRedirect()); + await waitFor(() => expect(mockGetAllModels).toHaveBeenCalled()); + expect(mockPush).not.toHaveBeenCalled(); + }); + + it('does not redirect to model selection when all models are downloaded', async () => { + mockGetAllModels.mockResolvedValue([makeModel(true), makeModel(true)]); + renderHook(() => useOnboardingRedirect()); + await waitFor(() => expect(mockGetAllModels).toHaveBeenCalled()); + expect(mockPush).not.toHaveBeenCalled(); + }); + + it('redirects to select-starting-model when model list is empty', async () => { + mockGetAllModels.mockResolvedValue([]); + renderHook(() => useOnboardingRedirect()); + await waitFor(() => + expect(mockPush).toHaveBeenCalledWith('/(modals)/select-starting-model') + ); + }); +}); diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 00000000..3dc3e3b6 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,18 @@ +// Prevent expo's winter runtime lazy getters from firing outside jest test scope. +// expo/src/winter/runtime.native.ts installs lazy getters on globalThis via installGlobal. +// These getters call require() lazily, which triggers jest-runtime's "outside scope" guard. +// Overwriting each property with a plain value short-circuits the getters. + +const defineGlobal = (name, value) => { + Object.defineProperty(globalThis, name, { + value, + configurable: true, + writable: true, + }); +}; + +defineGlobal('__ExpoImportMetaRegistry', { url: null }); + +if (typeof globalThis.structuredClone === 'undefined') { + defineGlobal('structuredClone', (obj) => JSON.parse(JSON.stringify(obj))); +} diff --git a/package.json b/package.json index baec591e..63ba2f1b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,37 @@ "android": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", - "download-models": "node -e \"require('./scripts/download-models.js').ensureModelAssets()\"" + "download-models": "node -e \"require('./scripts/download-models.js').ensureModelAssets()\"", + "test": "jest", + "test:watch": "jest --watch" + }, + "jest": { + "preset": "jest-expo", + "testMatch": [ + "**/__tests__/**/*.test.ts", + "**/__tests__/**/*.test.tsx" + ], + "transformIgnorePatterns": [ + "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|zustand)" + ], + "setupFiles": [ + "/jest.setup.js" + ], + "moduleNameMapper": { + "\\.svg$": "/__mocks__/svgMock.tsx", + "^react-native-device-info$": "/__mocks__/react-native-device-info.ts", + "^@react-native-async-storage/async-storage$": "/__mocks__/async-storage.ts", + "^react-native-pdfium$": "/__mocks__/react-native-pdfium.ts", + "^expo-file-system$": "/__mocks__/expo-file-system.ts", + "^react-native-executorch$": "/__mocks__/react-native-executorch.ts", + "^@dr.pogodin/react-native-fs$": "/__mocks__/react-native-fs.ts", + "^react-native-toast-message$": "/__mocks__/react-native-toast-message.ts", + "^react-native-reanimated$": "/__mocks__/react-native-reanimated.ts", + "^react-native-audio-api$": "/__mocks__/react-native-audio-api.ts", + "^@react-native-clipboard/clipboard$": "/__mocks__/@react-native-clipboard/clipboard.ts", + "^react-native-markdown-display$": "/__mocks__/react-native-markdown-display.tsx", + "^@gorhom/bottom-sheet$": "/__mocks__/@gorhom/bottom-sheet.tsx" + } }, "op-sqlite": { "libsql": true @@ -68,7 +98,13 @@ }, "devDependencies": { "@babel/core": "^7.25.2", + "@testing-library/jest-native": "^5.4.3", + "@testing-library/react-native": "^13.3.3", + "@types/jest": "^30.0.0", "@types/react": "~19.1.10", + "jest": "^30.3.0", + "jest-expo": "^55.0.11", + "react-test-renderer": "19.1.0", "typescript": "~5.9.2" }, "private": true, diff --git a/store/chatStore.ts b/store/chatStore.ts index d3941287..1d42d890 100644 --- a/store/chatStore.ts +++ b/store/chatStore.ts @@ -155,7 +155,7 @@ export const useChatStore = create((set, get) => ({ await setChatModel(db, id, modelId); set((state) => ({ chats: state.chats.map((chat) => - chat.id === id ? { ...chat, model: modelId } : chat + chat.id === id ? { ...chat, modelId } : chat ), })); }, @@ -188,12 +188,12 @@ export const useChatStore = create((set, get) => ({ await activateSource(db, chatId, sourceId); - set((state) => { - const chat = state.chats.find((chat) => chat.id === chatId); - if (chat) { - chat.enabledSources = [...(chat.enabledSources || []), sourceId]; - } - return { chats: [...state.chats] }; - }); + set((state) => ({ + chats: state.chats.map((chat) => + chat.id === chatId + ? { ...chat, enabledSources: [...(chat.enabledSources || []), sourceId] } + : chat + ), + })); }, })); diff --git a/store/llmStore.ts b/store/llmStore.ts index bd7df56e..0f9b0eac 100644 --- a/store/llmStore.ts +++ b/store/llmStore.ts @@ -408,12 +408,12 @@ export const useLLMStore = create((set, get) => ({ performance: { tokenCount: 0, firstTokenTime: 0 }, isBenchmarking: true, }); - memoryTracker.start(); - - if (!llmInstance) { + if (!llmInstance || !get().model) { return; } + memoryTracker.start(); + const startTime = performance.now(); await llmInstance.generate([ { diff --git a/utils/modelCompatibility.ts b/utils/modelCompatibility.ts index 86929b5b..126e10bc 100644 --- a/utils/modelCompatibility.ts +++ b/utils/modelCompatibility.ts @@ -1,7 +1,7 @@ import DeviceInfo from 'react-native-device-info'; import { Model } from '../database/modelRepository'; -const TOTAL_MEMORY_GB = DeviceInfo.getTotalMemorySync() / 1024 / 1024 / 1024; // in GB +const getTotalMemoryGB = () => DeviceInfo.getTotalMemorySync() / 1024 / 1024 / 1024; const NON_QUANTIZED_MEMORY_MULTIPLIER = 2.5; const QUANTIZED_MEMORY_MULTIPLIER = 1.75; @@ -34,9 +34,9 @@ export const isModelCompatible = (model: Model): boolean => { return true; } - return memoryRequirement <= TOTAL_MEMORY_GB; + return memoryRequirement <= getTotalMemoryGB(); }; export const getDeviceMemoryGB = (): number => { - return TOTAL_MEMORY_GB; + return getTotalMemoryGB(); }; diff --git a/yarn.lock b/yarn.lock index 98213e48..cff96a2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -47,6 +47,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/code-frame@npm:7.29.0" + dependencies: + "@babel/helper-validator-identifier": ^7.28.5 + js-tokens: ^4.0.0 + picocolors: ^1.1.1 + checksum: 39f5b303757e4d63bbff8133e251094cd4f952b46e3fa9febc7368d907583911d6a1eded6090876dc1feeff5cf6e134fb19b706f8d58d26c5402cd50e5e1aeb2 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.27.2": version: 7.27.2 resolution: "@babel/compat-data@npm:7.27.2" @@ -54,6 +65,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.28.6": + version: 7.29.0 + resolution: "@babel/compat-data@npm:7.29.0" + checksum: ad19db279dfd06cbe91b505d03be00d603c6d3fcc141cfc14f4ace5c558193e9b6aae4788cb01fd209c4c850e52d73c8f3c247680e3c0d84fa17ab8b3d50c808 + languageName: node + linkType: hard + "@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.20.0, @babel/core@npm:^7.21.3, @babel/core@npm:^7.25.2": version: 7.27.1 resolution: "@babel/core@npm:7.27.1" @@ -77,6 +95,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.23.9, @babel/core@npm:^7.27.4": + version: 7.29.0 + resolution: "@babel/core@npm:7.29.0" + dependencies: + "@babel/code-frame": ^7.29.0 + "@babel/generator": ^7.29.0 + "@babel/helper-compilation-targets": ^7.28.6 + "@babel/helper-module-transforms": ^7.28.6 + "@babel/helpers": ^7.28.6 + "@babel/parser": ^7.29.0 + "@babel/template": ^7.28.6 + "@babel/traverse": ^7.29.0 + "@babel/types": ^7.29.0 + "@jridgewell/remapping": ^2.3.5 + convert-source-map: ^2.0.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.3 + semver: ^6.3.1 + checksum: 85e1df6e213382c46dee27bcd07ed9202fa108a85bb74eb37be656308fd949349171ad2aa17cc84cf0720c908dc9ea6309d25e64d2a7fcdaa63721ce0c67c10b + languageName: node + linkType: hard + "@babel/generator@npm:^7.20.5, @babel/generator@npm:^7.25.0, @babel/generator@npm:^7.27.1": version: 7.27.1 resolution: "@babel/generator@npm:7.27.1" @@ -90,6 +131,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.27.5, @babel/generator@npm:^7.29.0, @babel/generator@npm:^7.7.2": + version: 7.29.1 + resolution: "@babel/generator@npm:7.29.1" + dependencies: + "@babel/parser": ^7.29.0 + "@babel/types": ^7.29.0 + "@jridgewell/gen-mapping": ^0.3.12 + "@jridgewell/trace-mapping": ^0.3.28 + jsesc: ^3.0.2 + checksum: d8e6863b2d04f684e65ad72731049ac7d754d3a3d1a67cdfc20807b109ba3180ed90d7ccef58ce5d38ded2eaeb71983a76c711eecb9b6266118262378f6c7226 + languageName: node + linkType: hard + "@babel/generator@npm:^7.28.5": version: 7.28.5 resolution: "@babel/generator@npm:7.28.5" @@ -134,6 +188,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-compilation-targets@npm:7.28.6" + dependencies: + "@babel/compat-data": ^7.28.6 + "@babel/helper-validator-option": ^7.27.1 + browserslist: ^4.24.0 + lru-cache: ^5.1.1 + semver: ^6.3.1 + checksum: 8151e36b74eb1c5e414fe945c189436421f7bfa011884de5be3dd7fd77f12f1f733ff7c982581dfa0a49d8af724450243c2409427114b4a6cfeb8333259d001c + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-create-class-features-plugin@npm:7.27.1" @@ -233,6 +300,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-module-imports@npm:7.28.6" + dependencies: + "@babel/traverse": ^7.28.6 + "@babel/types": ^7.28.6 + checksum: 437513aa029898b588a38f7991d7656c539b22f595207d85d0c407240c9e3f2aff8b9d0d7115fdedc91e7fdce4465100549a052024e2fba6a810bcbb7584296b + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-module-transforms@npm:7.27.1" @@ -246,6 +323,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-module-transforms@npm:7.28.6" + dependencies: + "@babel/helper-module-imports": ^7.28.6 + "@babel/helper-validator-identifier": ^7.28.5 + "@babel/traverse": ^7.28.6 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 522f7d1d08b5e2ccd4ec912aca879bd1506af78d1fb30f46e3e6b4bb69c6ae6ab4e379a879723844230d27dc6d04a55b03f5215cd3141b7a2b40bb4a02f71a9f + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-optimise-call-expression@npm:7.27.1" @@ -262,6 +352,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-plugin-utils@npm:7.28.6" + checksum: a0b4caab5e2180b215faa4d141ceac9e82fad9d446b8023eaeb8d82a6e62024726675b07fe8e616dd12f34e2bb59747e8d57aa8adab3e0717d1b8d691b118379 + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-remap-async-to-generator@npm:7.27.1" @@ -347,6 +444,16 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.28.6": + version: 7.29.2 + resolution: "@babel/helpers@npm:7.29.2" + dependencies: + "@babel/template": ^7.28.6 + "@babel/types": ^7.29.0 + checksum: 2c8ce711a639ef334539d3bd48977f57493f71af99e13d3f685fe47b3bc32aa83dbc1380688e19d5df924d958f8f29072f3dcff8110257ba6399524907287189 + languageName: node + linkType: hard + "@babel/highlight@npm:^7.10.4": version: 7.25.9 resolution: "@babel/highlight@npm:7.25.9" @@ -370,6 +477,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.23.9, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": + version: 7.29.2 + resolution: "@babel/parser@npm:7.29.2" + dependencies: + "@babel/types": ^7.29.0 + bin: + parser: ./bin/babel-parser.js + checksum: 25249623ffceb61beda0ba67776cf3957ffd49bef3005ccb81da3049db52115c91ad97c97da661b714f92d062e052d07bd2ba6cba6b5460f168ff38dabaf4d6d + languageName: node + linkType: hard + "@babel/parser@npm:^7.28.5": version: 7.28.5 resolution: "@babel/parser@npm:7.28.5" @@ -574,6 +692,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.28.6 + resolution: "@babel/plugin-syntax-jsx@npm:7.28.6" + dependencies: + "@babel/helper-plugin-utils": ^7.28.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 572e38f5c1bb4b8124300e7e3dd13e82ae84a21f90d3f0786c98cd05e63c78ca1f32d1cfe462dfbaf5e7d5102fa7cd8fd741dfe4f3afc2e01a3b2877dcc8c866 + languageName: node + linkType: hard + "@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4": version: 7.10.4 resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" @@ -673,6 +802,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-typescript@npm:^7.7.2": + version: 7.28.6 + resolution: "@babel/plugin-syntax-typescript@npm:7.28.6" + dependencies: + "@babel/helper-plugin-utils": ^7.28.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 5c55f9c63bd36cf3d7e8db892294c8f85000f9c1526c3a1cc310d47d1e174f5c6f6605e5cc902c4636d885faba7a9f3d5e5edc6b35e4f3b1fd4c2d58d0304fa5 + languageName: node + linkType: hard + "@babel/plugin-transform-arrow-functions@npm:^7.0.0-0, @babel/plugin-transform-arrow-functions@npm:^7.24.7": version: 7.27.1 resolution: "@babel/plugin-transform-arrow-functions@npm:7.27.1" @@ -1278,6 +1418,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/template@npm:7.28.6" + dependencies: + "@babel/code-frame": ^7.28.6 + "@babel/parser": ^7.28.6 + "@babel/types": ^7.28.6 + checksum: 8ab6383053e226025d9491a6e795293f2140482d14f60c1244bece6bf53610ed1e251d5e164de66adab765629881c7d9416e1e540c716541d2fd0f8f36a013d7 + languageName: node + linkType: hard + "@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.27.1": version: 7.27.1 resolution: "@babel/traverse@npm:7.27.1" @@ -1308,6 +1459,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/traverse@npm:7.29.0" + dependencies: + "@babel/code-frame": ^7.29.0 + "@babel/generator": ^7.29.0 + "@babel/helper-globals": ^7.28.0 + "@babel/parser": ^7.29.0 + "@babel/template": ^7.28.6 + "@babel/types": ^7.29.0 + debug: ^4.3.1 + checksum: fbb5085aa525b5d4ecd9fe2f5885d88413fff6ad9c0fac244c37f96069b6d3af9ce825750cd16af1d97d26fa3d354b38dbbdb5f31430e0d99ed89660ab65430e + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.2, @babel/types@npm:^7.27.1, @babel/types@npm:^7.3.3": version: 7.27.1 resolution: "@babel/types@npm:7.27.1" @@ -1328,6 +1494,23 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/types@npm:7.29.0" + dependencies: + "@babel/helper-string-parser": ^7.27.1 + "@babel/helper-validator-identifier": ^7.28.5 + checksum: 83f190438e94c22b2574aaeef7501830311ef266eaabfb06523409f64e2fe855e522951607085d71cad286719adef14e1ba37b671f334a7cd25b0f8506a01e0b + languageName: node + linkType: hard + +"@bcoe/v8-coverage@npm:^0.2.3": + version: 0.2.3 + resolution: "@bcoe/v8-coverage@npm:0.2.3" + checksum: 850f9305536d0f2bd13e9e0881cb5f02e4f93fad1189f7b2d4bebf694e3206924eadee1068130d43c11b750efcc9405f88a8e42ef098b6d75239c0f047de1a27 + languageName: node + linkType: hard + "@cfworker/json-schema@npm:^4.0.2": version: 4.1.1 resolution: "@cfworker/json-schema@npm:4.1.1" @@ -1357,6 +1540,34 @@ __metadata: languageName: node linkType: hard +"@emnapi/core@npm:^1.4.3": + version: 1.9.1 + resolution: "@emnapi/core@npm:1.9.1" + dependencies: + "@emnapi/wasi-threads": 1.2.0 + tslib: ^2.4.0 + checksum: 5fbb27f723e4b845c602929161a125dfd31142230715181af167492a6b8bd96006be99f856f420edc503abfb921e4c25cad19d06215c93afdaca90c410876637 + languageName: node + linkType: hard + +"@emnapi/runtime@npm:^1.4.3": + version: 1.9.1 + resolution: "@emnapi/runtime@npm:1.9.1" + dependencies: + tslib: ^2.4.0 + checksum: 6a60b33d1e05b4a0d589c771334324cc3a20cb93a6ffeee195f64cd7b5a1ede1cdc0c77babca1d4316a9912f91d75a61a84012b84fbc6049322dab4d88df5769 + languageName: node + linkType: hard + +"@emnapi/wasi-threads@npm:1.2.0": + version: 1.2.0 + resolution: "@emnapi/wasi-threads@npm:1.2.0" + dependencies: + tslib: ^2.4.0 + checksum: dd780c4dca89aa042132b2e90a079a6b219dfc99f9f5a36e4e93a3e48fdc5243c9cc8fb0a38ce954448b1ee4d4f8656ce752e812026bfd8f4ad8aa7187ceff7e + languageName: node + linkType: hard + "@expo-google-fonts/dm-sans@npm:^0.4.0": version: 0.4.0 resolution: "@expo-google-fonts/dm-sans@npm:0.4.0" @@ -1478,6 +1689,27 @@ __metadata: languageName: node linkType: hard +"@expo/config-plugins@npm:~55.0.7": + version: 55.0.7 + resolution: "@expo/config-plugins@npm:55.0.7" + dependencies: + "@expo/config-types": ^55.0.5 + "@expo/json-file": ~10.0.12 + "@expo/plist": ^0.5.2 + "@expo/sdk-runtime-versions": ^1.0.0 + chalk: ^4.1.2 + debug: ^4.3.5 + getenv: ^2.0.0 + glob: ^13.0.0 + resolve-from: ^5.0.0 + semver: ^7.5.4 + slugify: ^1.6.6 + xcode: ^3.0.1 + xml2js: 0.6.0 + checksum: ccd36e709ebbfea0ef9c1d53e871774869110d3b09322adf5f658d071c439a806d443115e787c7c7a23914b3c2689ec771b3843922962c30e0d164e4b729e228 + languageName: node + linkType: hard + "@expo/config-types@npm:^54.0.10, @expo/config-types@npm:^54.0.9": version: 54.0.10 resolution: "@expo/config-types@npm:54.0.10" @@ -1485,6 +1717,13 @@ __metadata: languageName: node linkType: hard +"@expo/config-types@npm:^55.0.5": + version: 55.0.5 + resolution: "@expo/config-types@npm:55.0.5" + checksum: 006c8d822c0f98c0808cc3bcc74a5a97ab94f3447a20a2629af23e2009ca1650e886e5252d9cb012a57cc546fc58de14e4bb450e6cfaefde12db23caad028a92 + languageName: node + linkType: hard + "@expo/config@npm:~12.0.11, @expo/config@npm:~12.0.12": version: 12.0.12 resolution: "@expo/config@npm:12.0.12" @@ -1506,6 +1745,25 @@ __metadata: languageName: node linkType: hard +"@expo/config@npm:~55.0.10": + version: 55.0.11 + resolution: "@expo/config@npm:55.0.11" + dependencies: + "@expo/config-plugins": ~55.0.7 + "@expo/config-types": ^55.0.5 + "@expo/json-file": ^10.0.12 + "@expo/require-utils": ^55.0.3 + deepmerge: ^4.3.1 + getenv: ^2.0.0 + glob: ^13.0.0 + resolve-from: ^5.0.0 + resolve-workspace-root: ^2.0.0 + semver: ^7.6.0 + slugify: ^1.3.4 + checksum: 77c381464001c4cc192f184a53be7f28f3d144c81bfd573793a0b62fc4424af713a2bf7b1948236af4c896af225c311b1a9147045abc19a48e9d283dfb578097 + languageName: node + linkType: hard + "@expo/devcert@npm:^1.2.1": version: 1.2.1 resolution: "@expo/devcert@npm:1.2.1" @@ -1585,6 +1843,16 @@ __metadata: languageName: node linkType: hard +"@expo/json-file@npm:^10.0.12, @expo/json-file@npm:~10.0.12": + version: 10.0.12 + resolution: "@expo/json-file@npm:10.0.12" + dependencies: + "@babel/code-frame": ^7.20.0 + json5: ^2.2.3 + checksum: dedf99415477c63efa37f5af923033a363e3ecf539831701d0d111a316db7fea6c210d4b3cb1e4e854ce5dfd40f02c3c1156acab38288e78e35555a12a06c1de + languageName: node + linkType: hard + "@expo/json-file@npm:^10.0.8, @expo/json-file@npm:~10.0.8": version: 10.0.8 resolution: "@expo/json-file@npm:10.0.8" @@ -1704,6 +1972,17 @@ __metadata: languageName: node linkType: hard +"@expo/plist@npm:^0.5.2": + version: 0.5.2 + resolution: "@expo/plist@npm:0.5.2" + dependencies: + "@xmldom/xmldom": ^0.8.8 + base64-js: ^1.5.1 + xmlbuilder: ^15.1.1 + checksum: 30c06ee9a1375df1d85ef7608c0b15444d6a084330a9769c02bf66e9ed7b79867753a888f1cd80c61867ad09d7c1b34d3ef9e3839a62536ae07a58bc95de5c6b + languageName: node + linkType: hard + "@expo/prebuild-config@npm:^54.0.7": version: 54.0.7 resolution: "@expo/prebuild-config@npm:54.0.7" @@ -1724,6 +2003,22 @@ __metadata: languageName: node linkType: hard +"@expo/require-utils@npm:^55.0.3": + version: 55.0.3 + resolution: "@expo/require-utils@npm:55.0.3" + dependencies: + "@babel/code-frame": ^7.20.0 + "@babel/core": ^7.25.2 + "@babel/plugin-transform-modules-commonjs": ^7.24.8 + peerDependencies: + typescript: ^5.0.0 || ^5.0.0-0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 841c585ecfa6320468a62236713b24598f2a4d07c44faf7083a9f7fde18f562a2dcc3915f5d9b0b8fce1a18abfbb56bb7c7eec967cb65b63468fb49a463f094f + languageName: node + linkType: hard + "@expo/schema-utils@npm:^0.1.8": version: 0.1.8 resolution: "@expo/schema-utils@npm:0.1.8" @@ -1886,14 +2181,82 @@ __metadata: languageName: node linkType: hard -"@istanbuljs/schema@npm:^0.1.2": +"@istanbuljs/schema@npm:^0.1.2, @istanbuljs/schema@npm:^0.1.3": version: 0.1.3 resolution: "@istanbuljs/schema@npm:0.1.3" checksum: 5282759d961d61350f33d9118d16bcaed914ebf8061a52f4fa474b2cb08720c9c81d165e13b82f2e5a8a212cc5af482f0c6fc1ac27b9e067e5394c9a6ed186c9 languageName: node linkType: hard -"@jest/create-cache-key-function@npm:^29.7.0": +"@jest/console@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/console@npm:30.3.0" + dependencies: + "@jest/types": 30.3.0 + "@types/node": "*" + chalk: ^4.1.2 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + slash: ^3.0.0 + checksum: d74bacf7229d5ac08918e8e8d43a7cfda70edae3925eb4e4ad57d890f689c1ad6f2baa1afb3cb816d94919f5b41a0745219de61ac60b2fc968f041ef4dad93c7 + languageName: node + linkType: hard + +"@jest/console@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/console@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + jest-message-util: ^29.7.0 + jest-util: ^29.7.0 + slash: ^3.0.0 + checksum: 0e3624e32c5a8e7361e889db70b170876401b7d70f509a2538c31d5cd50deb0c1ae4b92dc63fe18a0902e0a48c590c21d53787a0df41a52b34fa7cab96c384d6 + languageName: node + linkType: hard + +"@jest/core@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/core@npm:30.3.0" + dependencies: + "@jest/console": 30.3.0 + "@jest/pattern": 30.0.1 + "@jest/reporters": 30.3.0 + "@jest/test-result": 30.3.0 + "@jest/transform": 30.3.0 + "@jest/types": 30.3.0 + "@types/node": "*" + ansi-escapes: ^4.3.2 + chalk: ^4.1.2 + ci-info: ^4.2.0 + exit-x: ^0.2.2 + graceful-fs: ^4.2.11 + jest-changed-files: 30.3.0 + jest-config: 30.3.0 + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-resolve-dependencies: 30.3.0 + jest-runner: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + jest-watcher: 30.3.0 + pretty-format: 30.3.0 + slash: ^3.0.0 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 15a8823e736e9ce7bee58f46e5306cdcc8bf2d29d0697ba5c256b6fe94f5aa79edb21a13acb01e460787a1d6c8447c771750e27e46fc58c91b35ca15070b7a7c + languageName: node + linkType: hard + +"@jest/create-cache-key-function@npm:^29.2.1, @jest/create-cache-key-function@npm:^29.7.0": version: 29.7.0 resolution: "@jest/create-cache-key-function@npm:29.7.0" dependencies: @@ -1902,6 +2265,25 @@ __metadata: languageName: node linkType: hard +"@jest/diff-sequences@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/diff-sequences@npm:30.3.0" + checksum: 715325e544f54cf5336b54fbfecd3e7e0e779b96c1f28a2ab42fdd4388f5f3751558a474d173b0c43bc5fb513fbb0464dbf1503cc69dab248515c6ed42feecb3 + languageName: node + linkType: hard + +"@jest/environment@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/environment@npm:30.3.0" + dependencies: + "@jest/fake-timers": 30.3.0 + "@jest/types": 30.3.0 + "@types/node": "*" + jest-mock: 30.3.0 + checksum: c58121d7161e5906ae46407fe1f6e2289675b1cb9dcb6079e9ce2ea2042ee029c0216860367a31660fe54de3387322b4627bf4704d10cefbe718ab72a522889d + languageName: node + linkType: hard + "@jest/environment@npm:^29.7.0": version: 29.7.0 resolution: "@jest/environment@npm:29.7.0" @@ -1914,6 +2296,58 @@ __metadata: languageName: node linkType: hard +"@jest/expect-utils@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/expect-utils@npm:30.3.0" + dependencies: + "@jest/get-type": 30.1.0 + checksum: 963ebcf5dd3c3ef1b8f932d04f5f594357fcc277089c56fd0bfcabfbdd9b0150c0bf6e13e3a8c5084c8799f019e4e8487b13cebfe31be7992016fe0ca7e9fcf7 + languageName: node + linkType: hard + +"@jest/expect-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect-utils@npm:29.7.0" + dependencies: + jest-get-type: ^29.6.3 + checksum: 75eb177f3d00b6331bcaa057e07c0ccb0733a1d0a1943e1d8db346779039cb7f103789f16e502f888a3096fb58c2300c38d1f3748b36a7fa762eb6f6d1b160ed + languageName: node + linkType: hard + +"@jest/expect@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/expect@npm:30.3.0" + dependencies: + expect: 30.3.0 + jest-snapshot: 30.3.0 + checksum: 3dd5d266f85348aec70ee06aa0ea14fd75176f09163761375910be3fa3d7ae05815a2e6daf7ff8541afcd4c61800892b48b74ddfe0ecbe5821812b2dad5e1809 + languageName: node + linkType: hard + +"@jest/expect@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect@npm:29.7.0" + dependencies: + expect: ^29.7.0 + jest-snapshot: ^29.7.0 + checksum: a01cb85fd9401bab3370618f4b9013b90c93536562222d920e702a0b575d239d74cecfe98010aaec7ad464f67cf534a353d92d181646a4b792acaa7e912ae55e + languageName: node + linkType: hard + +"@jest/fake-timers@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/fake-timers@npm:30.3.0" + dependencies: + "@jest/types": 30.3.0 + "@sinonjs/fake-timers": ^15.0.0 + "@types/node": "*" + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + checksum: 6854e881a50f70f518935d1f83ace2e63bc55fc71f19145442da7cf30a26edec348b5f0ac3993d9c48a9dd0dbfd7be0c9a13d82fe481fd48bc055fdd5a6abfa3 + languageName: node + linkType: hard + "@jest/fake-timers@npm:^29.7.0": version: 29.7.0 resolution: "@jest/fake-timers@npm:29.7.0" @@ -1928,55 +2362,237 @@ __metadata: languageName: node linkType: hard -"@jest/schemas@npm:^29.6.3": - version: 29.6.3 - resolution: "@jest/schemas@npm:29.6.3" +"@jest/get-type@npm:30.1.0": + version: 30.1.0 + resolution: "@jest/get-type@npm:30.1.0" + checksum: e2a95fbb49ce2d15547db8af5602626caf9b05f62a5e583b4a2de9bd93a2bfe7175f9bbb2b8a5c3909ce261d467b6991d7265bb1d547cb60e7e97f571f361a70 + languageName: node + linkType: hard + +"@jest/globals@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/globals@npm:30.3.0" dependencies: - "@sinclair/typebox": ^0.27.8 - checksum: 910040425f0fc93cd13e68c750b7885590b8839066dfa0cd78e7def07bbb708ad869381f725945d66f2284de5663bbecf63e8fdd856e2ae6e261ba30b1687e93 + "@jest/environment": 30.3.0 + "@jest/expect": 30.3.0 + "@jest/types": 30.3.0 + jest-mock: 30.3.0 + checksum: 485bdc0f35faf3e76cb451b75e16892d87f7ab5757e290b1a9e849a3af0ef81c47abddb188fbc0442a4689514cf0551e34d13970c9cf03610a269c39f800ff46 languageName: node linkType: hard -"@jest/transform@npm:^29.7.0": +"@jest/globals@npm:^29.2.1": version: 29.7.0 - resolution: "@jest/transform@npm:29.7.0" + resolution: "@jest/globals@npm:29.7.0" dependencies: - "@babel/core": ^7.11.6 + "@jest/environment": ^29.7.0 + "@jest/expect": ^29.7.0 "@jest/types": ^29.6.3 - "@jridgewell/trace-mapping": ^0.3.18 - babel-plugin-istanbul: ^6.1.1 - chalk: ^4.0.0 - convert-source-map: ^2.0.0 - fast-json-stable-stringify: ^2.1.0 - graceful-fs: ^4.2.9 - jest-haste-map: ^29.7.0 - jest-regex-util: ^29.6.3 - jest-util: ^29.7.0 - micromatch: ^4.0.4 - pirates: ^4.0.4 - slash: ^3.0.0 - write-file-atomic: ^4.0.2 - checksum: 0f8ac9f413903b3cb6d240102db848f2a354f63971ab885833799a9964999dd51c388162106a807f810071f864302cdd8e3f0c241c29ce02d85a36f18f3f40ab + jest-mock: ^29.7.0 + checksum: 97dbb9459135693ad3a422e65ca1c250f03d82b2a77f6207e7fa0edd2c9d2015fbe4346f3dc9ebff1678b9d8da74754d4d440b7837497f8927059c0642a22123 languageName: node linkType: hard -"@jest/types@npm:^29.6.3": - version: 29.6.3 - resolution: "@jest/types@npm:29.6.3" +"@jest/pattern@npm:30.0.1": + version: 30.0.1 + resolution: "@jest/pattern@npm:30.0.1" dependencies: - "@jest/schemas": ^29.6.3 - "@types/istanbul-lib-coverage": ^2.0.0 - "@types/istanbul-reports": ^3.0.0 "@types/node": "*" - "@types/yargs": ^17.0.8 - chalk: ^4.0.0 - checksum: a0bcf15dbb0eca6bdd8ce61a3fb055349d40268622a7670a3b2eb3c3dbafe9eb26af59938366d520b86907b9505b0f9b29b85cec11579a9e580694b87cd90fcc + jest-regex-util: 30.0.1 + checksum: 1a1857df19be87e714786c3ab36862702bf8ed1e2665044b2ce5ffa787b5ab74c876f1756e83d3b09737dd98c1e980e259059b65b9b0f49b03716634463a8f9e languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.12": - version: 0.3.13 - resolution: "@jridgewell/gen-mapping@npm:0.3.13" +"@jest/reporters@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/reporters@npm:30.3.0" + dependencies: + "@bcoe/v8-coverage": ^0.2.3 + "@jest/console": 30.3.0 + "@jest/test-result": 30.3.0 + "@jest/transform": 30.3.0 + "@jest/types": 30.3.0 + "@jridgewell/trace-mapping": ^0.3.25 + "@types/node": "*" + chalk: ^4.1.2 + collect-v8-coverage: ^1.0.2 + exit-x: ^0.2.2 + glob: ^10.5.0 + graceful-fs: ^4.2.11 + istanbul-lib-coverage: ^3.0.0 + istanbul-lib-instrument: ^6.0.0 + istanbul-lib-report: ^3.0.0 + istanbul-lib-source-maps: ^5.0.0 + istanbul-reports: ^3.1.3 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + jest-worker: 30.3.0 + slash: ^3.0.0 + string-length: ^4.0.2 + v8-to-istanbul: ^9.0.1 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: ffafe5f4d6dce6eb73576890705b77e4792c7d003d897dfbda2569d78d17b5585917ce875200de2bcba1e2c34c83af28c68cb665b7e8f8359fb9499a26f5f5a0 + languageName: node + linkType: hard + +"@jest/schemas@npm:30.0.5": + version: 30.0.5 + resolution: "@jest/schemas@npm:30.0.5" + dependencies: + "@sinclair/typebox": ^0.34.0 + checksum: 7a4fc4166f688947c22d81e61aaf2cb22f178dbf6ee806b0931b75136899d426a72a8330762f27f0cf6f79da0d2a56f49a22fe09f5f80df95a683ed237a0f3b0 + languageName: node + linkType: hard + +"@jest/schemas@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/schemas@npm:29.6.3" + dependencies: + "@sinclair/typebox": ^0.27.8 + checksum: 910040425f0fc93cd13e68c750b7885590b8839066dfa0cd78e7def07bbb708ad869381f725945d66f2284de5663bbecf63e8fdd856e2ae6e261ba30b1687e93 + languageName: node + linkType: hard + +"@jest/snapshot-utils@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/snapshot-utils@npm:30.3.0" + dependencies: + "@jest/types": 30.3.0 + chalk: ^4.1.2 + graceful-fs: ^4.2.11 + natural-compare: ^1.4.0 + checksum: 5cbc5622bf83cc876ad237fa780bc9b64196a5536469a37d7042c25ec70c299cb610103445af8bdadc2a507e11142ca7f47a74ca8f4c372c2dc076091b3725a1 + languageName: node + linkType: hard + +"@jest/source-map@npm:30.0.1": + version: 30.0.1 + resolution: "@jest/source-map@npm:30.0.1" + dependencies: + "@jridgewell/trace-mapping": ^0.3.25 + callsites: ^3.1.0 + graceful-fs: ^4.2.11 + checksum: 161b27cdf8d9d80fd99374d55222b90478864c6990514be6ebee72b7184a034224c9aceed12c476f3a48d48601bf8ed2e0c047a5a81bd907dc192ebe71365ed4 + languageName: node + linkType: hard + +"@jest/test-result@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/test-result@npm:30.3.0" + dependencies: + "@jest/console": 30.3.0 + "@jest/types": 30.3.0 + "@types/istanbul-lib-coverage": ^2.0.6 + collect-v8-coverage: ^1.0.2 + checksum: 63dadb4c94037260be3fa23ec82e9c9c953e6252e5ee5535b443209e7148d1d8eda8a9b59fff8bae8df24274b057c5a990715cb49c010852cd4b3e3bcdf2bd69 + languageName: node + linkType: hard + +"@jest/test-result@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-result@npm:29.7.0" + dependencies: + "@jest/console": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/istanbul-lib-coverage": ^2.0.0 + collect-v8-coverage: ^1.0.0 + checksum: 67b6317d526e335212e5da0e768e3b8ab8a53df110361b80761353ad23b6aea4432b7c5665bdeb87658ea373b90fb1afe02ed3611ef6c858c7fba377505057fa + languageName: node + linkType: hard + +"@jest/test-sequencer@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/test-sequencer@npm:30.3.0" + dependencies: + "@jest/test-result": 30.3.0 + graceful-fs: ^4.2.11 + jest-haste-map: 30.3.0 + slash: ^3.0.0 + checksum: 46d1ca2b8e5c26cbc9d37b7b6a762000aa7d57662a230e3db097f2b27237aba020d40ea5024863652e16493d404d0c2b236683159a63ab0fda47b578654cac9c + languageName: node + linkType: hard + +"@jest/transform@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/transform@npm:30.3.0" + dependencies: + "@babel/core": ^7.27.4 + "@jest/types": 30.3.0 + "@jridgewell/trace-mapping": ^0.3.25 + babel-plugin-istanbul: ^7.0.1 + chalk: ^4.1.2 + convert-source-map: ^2.0.0 + fast-json-stable-stringify: ^2.1.0 + graceful-fs: ^4.2.11 + jest-haste-map: 30.3.0 + jest-regex-util: 30.0.1 + jest-util: 30.3.0 + pirates: ^4.0.7 + slash: ^3.0.0 + write-file-atomic: ^5.0.1 + checksum: 0327d28d4ee3903ebd415e1974c9aae6c7e7a7d1770aa07c551d34666851283125f488ec1366fa8814402bdfbd83be9b45ad551839bb407c468fa6bdb44e7d0c + languageName: node + linkType: hard + +"@jest/transform@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/transform@npm:29.7.0" + dependencies: + "@babel/core": ^7.11.6 + "@jest/types": ^29.6.3 + "@jridgewell/trace-mapping": ^0.3.18 + babel-plugin-istanbul: ^6.1.1 + chalk: ^4.0.0 + convert-source-map: ^2.0.0 + fast-json-stable-stringify: ^2.1.0 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.7.0 + jest-regex-util: ^29.6.3 + jest-util: ^29.7.0 + micromatch: ^4.0.4 + pirates: ^4.0.4 + slash: ^3.0.0 + write-file-atomic: ^4.0.2 + checksum: 0f8ac9f413903b3cb6d240102db848f2a354f63971ab885833799a9964999dd51c388162106a807f810071f864302cdd8e3f0c241c29ce02d85a36f18f3f40ab + languageName: node + linkType: hard + +"@jest/types@npm:30.3.0": + version: 30.3.0 + resolution: "@jest/types@npm:30.3.0" + dependencies: + "@jest/pattern": 30.0.1 + "@jest/schemas": 30.0.5 + "@types/istanbul-lib-coverage": ^2.0.6 + "@types/istanbul-reports": ^3.0.4 + "@types/node": "*" + "@types/yargs": ^17.0.33 + chalk: ^4.1.2 + checksum: ee77d038ff965b136d5509acdd477e882938961094ee4a3556555d72a1e454da99679b73c4ddd19653b1aaf3b9cd97db04114d090b31d9e6939690223eac00af + languageName: node + linkType: hard + +"@jest/types@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/types@npm:29.6.3" + dependencies: + "@jest/schemas": ^29.6.3 + "@types/istanbul-lib-coverage": ^2.0.0 + "@types/istanbul-reports": ^3.0.0 + "@types/node": "*" + "@types/yargs": ^17.0.8 + chalk: ^4.0.0 + checksum: a0bcf15dbb0eca6bdd8ce61a3fb055349d40268622a7670a3b2eb3c3dbafe9eb26af59938366d520b86907b9505b0f9b29b85cec11579a9e580694b87cd90fcc + languageName: node + linkType: hard + +"@jridgewell/gen-mapping@npm:^0.3.12": + version: 0.3.13 + resolution: "@jridgewell/gen-mapping@npm:0.3.13" dependencies: "@jridgewell/sourcemap-codec": ^1.5.0 "@jridgewell/trace-mapping": ^0.3.24 @@ -1995,6 +2611,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/remapping@npm:^2.3.5": + version: 2.3.5 + resolution: "@jridgewell/remapping@npm:2.3.5" + dependencies: + "@jridgewell/gen-mapping": ^0.3.5 + "@jridgewell/trace-mapping": ^0.3.24 + checksum: 4a66a7397c3dc9c6b5c14a0024b1f98c5e1d90a0dbc1e5955b5038f2db339904df2a0ee8a66559fafb4fc23ff33700a2639fd40bbdd2e9e82b58b3bdf83738e3 + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" @@ -2033,23 +2659,23 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": - version: 0.3.25 - resolution: "@jridgewell/trace-mapping@npm:0.3.25" +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.28": + version: 0.3.31 + resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: "@jridgewell/resolve-uri": ^3.1.0 "@jridgewell/sourcemap-codec": ^1.4.14 - checksum: 9d3c40d225e139987b50c48988f8717a54a8c994d8a948ee42e1412e08988761d0754d7d10b803061cc3aebf35f92a5dbbab493bd0e1a9ef9e89a2130e83ba34 + checksum: af8fda2431348ad507fbddf8e25f5d08c79ecc94594061ce402cf41bc5aba1a7b3e59bf0fd70a619b35f33983a3f488ceeba8faf56bff784f98bb5394a8b7d47 languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.28": - version: 0.3.31 - resolution: "@jridgewell/trace-mapping@npm:0.3.31" +"@jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: "@jridgewell/resolve-uri": ^3.1.0 "@jridgewell/sourcemap-codec": ^1.4.14 - checksum: af8fda2431348ad507fbddf8e25f5d08c79ecc94594061ce402cf41bc5aba1a7b3e59bf0fd70a619b35f33983a3f488ceeba8faf56bff784f98bb5394a8b7d47 + checksum: 9d3c40d225e139987b50c48988f8717a54a8c994d8a948ee42e1412e08988761d0754d7d10b803061cc3aebf35f92a5dbbab493bd0e1a9ef9e89a2130e83ba34 languageName: node linkType: hard @@ -2084,6 +2710,17 @@ __metadata: languageName: node linkType: hard +"@napi-rs/wasm-runtime@npm:^0.2.11": + version: 0.2.12 + resolution: "@napi-rs/wasm-runtime@npm:0.2.12" + dependencies: + "@emnapi/core": ^1.4.3 + "@emnapi/runtime": ^1.4.3 + "@tybys/wasm-util": ^0.10.0 + checksum: 676271082b2e356623faa1fefd552a82abb8c00f8218e333091851456c52c81686b98f77fcd119b9b2f4f215d924e4b23acd6401d9934157c80da17be783ec3d + languageName: node + linkType: hard + "@npmcli/agent@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/agent@npm:3.0.0" @@ -2123,6 +2760,13 @@ __metadata: languageName: node linkType: hard +"@pkgr/core@npm:^0.2.9": + version: 0.2.9 + resolution: "@pkgr/core@npm:0.2.9" + checksum: bb2fb86977d63f836f8f5b09015d74e6af6488f7a411dcd2bfdca79d76b5a681a9112f41c45bdf88a9069f049718efc6f3900d7f1de66a2ec966068308ae517f + languageName: node + linkType: hard + "@radix-ui/primitive@npm:1.1.3": version: 1.1.3 resolution: "@radix-ui/primitive@npm:1.1.3" @@ -3002,7 +3646,14 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^3.0.0": +"@sinclair/typebox@npm:^0.34.0": + version: 0.34.49 + resolution: "@sinclair/typebox@npm:0.34.49" + checksum: f503f25553a2fc299c8bdb018a150e08c9f9000601f3439d2d9b51a26668d2de374e1fb5aab8883dd11d54cb5e691950d38a67a8c02d1cf7056c3e01135ef667 + languageName: node + linkType: hard + +"@sinonjs/commons@npm:^3.0.0, @sinonjs/commons@npm:^3.0.1": version: 3.0.1 resolution: "@sinonjs/commons@npm:3.0.1" dependencies: @@ -3020,6 +3671,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/fake-timers@npm:^15.0.0": + version: 15.2.1 + resolution: "@sinonjs/fake-timers@npm:15.2.1" + dependencies: + "@sinonjs/commons": ^3.0.1 + checksum: 42f054e63972b87da71ed25c362238b710c50d375e9ffc53c14a76752b9f8039955999cee3e0f6e0bf1e908680d17e44004fc75ad1c3fc5637e2c90e9396f412 + languageName: node + linkType: hard + "@svgr/babel-plugin-add-jsx-attribute@npm:8.0.0": version: 8.0.0 resolution: "@svgr/babel-plugin-add-jsx-attribute@npm:8.0.0" @@ -3178,6 +3838,50 @@ __metadata: languageName: node linkType: hard +"@testing-library/jest-native@npm:^5.4.3": + version: 5.4.3 + resolution: "@testing-library/jest-native@npm:5.4.3" + dependencies: + chalk: ^4.1.2 + jest-diff: ^29.0.1 + jest-matcher-utils: ^29.0.1 + pretty-format: ^29.0.3 + redent: ^3.0.0 + peerDependencies: + react: ">=16.0.0" + react-native: ">=0.59" + react-test-renderer: ">=16.0.0" + checksum: 2a4ebfeff09523860771cfddac6fcc3faa2f855dc63255b9efc016e727132320f16f935cec9717d6d79cfa6715fce6ded877215c8ec85d236a5c3136a65b1020 + languageName: node + linkType: hard + +"@testing-library/react-native@npm:^13.3.3": + version: 13.3.3 + resolution: "@testing-library/react-native@npm:13.3.3" + dependencies: + jest-matcher-utils: ^30.0.5 + picocolors: ^1.1.1 + pretty-format: ^30.0.5 + redent: ^3.0.0 + peerDependencies: + jest: ">=29.0.0" + react: ">=18.2.0" + react-native: ">=0.71" + react-test-renderer: ">=18.2.0" + peerDependenciesMeta: + jest: + optional: true + checksum: 5688918384ce834e3667a56b72c8b776a2f9a5afae0a2738e7d0077f342b3ade7eca628cbe122943201caee75f3718379ef7b3ca00cd50c4ee607b4131d09505 + languageName: node + linkType: hard + +"@tootallnate/once@npm:2": + version: 2.0.0 + resolution: "@tootallnate/once@npm:2.0.0" + checksum: ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8 + languageName: node + linkType: hard + "@trysound/sax@npm:0.2.0": version: 0.2.0 resolution: "@trysound/sax@npm:0.2.0" @@ -3185,7 +3889,16 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:^7.1.14": +"@tybys/wasm-util@npm:^0.10.0": + version: 0.10.1 + resolution: "@tybys/wasm-util@npm:0.10.1" + dependencies: + tslib: ^2.4.0 + checksum: b8b281ffa9cd01cb6d45a4dddca2e28fd0cb6ad67cf091ba4a73ac87c0d6bd6ce188c332c489e87c20b0750b0b6fe3b99e30e1cd2227ec16da692f51c778944e + languageName: node + linkType: hard + +"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" dependencies: @@ -3242,7 +3955,7 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0": +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.6": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" checksum: 3feac423fd3e5449485afac999dcfcb3d44a37c830af898b689fadc65d26526460bedb889db278e0d4d815a670331796494d073a10ee6e3a6526301fe7415778 @@ -3258,7 +3971,7 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-reports@npm:^3.0.0": +"@types/istanbul-reports@npm:^3.0.0, @types/istanbul-reports@npm:^3.0.4": version: 3.0.4 resolution: "@types/istanbul-reports@npm:3.0.4" dependencies: @@ -3267,6 +3980,27 @@ __metadata: languageName: node linkType: hard +"@types/jest@npm:^30.0.0": + version: 30.0.0 + resolution: "@types/jest@npm:30.0.0" + dependencies: + expect: ^30.0.0 + pretty-format: ^30.0.0 + checksum: d80c0c30b2689693a2b5f5975ccc898fc194acd5a947ad3bc728c6f2d4ffad53da021b1c39b0c939d3ed4ee945c74f4fda800b6f1bd6283170e52cd3fe798411 + languageName: node + linkType: hard + +"@types/jsdom@npm:^20.0.0": + version: 20.0.1 + resolution: "@types/jsdom@npm:20.0.1" + dependencies: + "@types/node": "*" + "@types/tough-cookie": "*" + parse5: ^7.0.0 + checksum: d55402c5256ef451f93a6e3d3881f98339fe73a5ac2030588df056d6835df8367b5a857b48d27528289057e26dcdd3f502edc00cb877c79174cb3a4c7f2198c1 + languageName: node + linkType: hard + "@types/node@npm:*": version: 22.15.21 resolution: "@types/node@npm:22.15.21" @@ -3292,13 +4026,20 @@ __metadata: languageName: node linkType: hard -"@types/stack-utils@npm:^2.0.0": +"@types/stack-utils@npm:^2.0.0, @types/stack-utils@npm:^2.0.3": version: 2.0.3 resolution: "@types/stack-utils@npm:2.0.3" checksum: 72576cc1522090fe497337c2b99d9838e320659ac57fa5560fcbdcbafcf5d0216c6b3a0a8a4ee4fdb3b1f5e3420aa4f6223ab57b82fef3578bec3206425c6cf5 languageName: node linkType: hard +"@types/tough-cookie@npm:*": + version: 4.0.5 + resolution: "@types/tough-cookie@npm:4.0.5" + checksum: f19409d0190b179331586365912920d192733112a195e870c7f18d20ac8adb7ad0b0ff69dad430dba8bc2be09593453a719cfea92dc3bda19748fd158fe1498d + languageName: node + linkType: hard + "@types/uuid@npm:^10.0.0": version: 10.0.0 resolution: "@types/uuid@npm:10.0.0" @@ -3313,6 +4054,15 @@ __metadata: languageName: node linkType: hard +"@types/yargs@npm:^17.0.33": + version: 17.0.35 + resolution: "@types/yargs@npm:17.0.35" + dependencies: + "@types/yargs-parser": "*" + checksum: ebf1f5373388cfcbf9cfb5e56ce7a77c0ba2450420f26f3701010ca92df48cce7e14e4245ed1f17178a38ff8702467a6f4047742775b8e2fd06dec8f4f3501ce + languageName: node + linkType: hard + "@types/yargs@npm:^17.0.8": version: 17.0.33 resolution: "@types/yargs@npm:17.0.33" @@ -3329,96 +4079,282 @@ __metadata: languageName: node linkType: hard -"@urql/core@npm:^5.0.6, @urql/core@npm:^5.1.1": - version: 5.1.1 - resolution: "@urql/core@npm:5.1.1" - dependencies: - "@0no-co/graphql.web": ^1.0.5 - wonka: ^6.3.2 - checksum: c28736706abe5d0a0172bcde1c80807aba44606041347beba8e73d5237598034301cccad0169c4f63ba08f5bffe7b3a3ad95ee4a53a0d719ad5525f44b84dbcc +"@unrs/resolver-binding-android-arm-eabi@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-android-arm-eabi@npm:1.11.1" + conditions: os=android & cpu=arm languageName: node linkType: hard -"@urql/exchange-retry@npm:^1.3.0": - version: 1.3.1 - resolution: "@urql/exchange-retry@npm:1.3.1" - dependencies: - "@urql/core": ^5.1.1 - wonka: ^6.3.2 - peerDependencies: - "@urql/core": ^5.0.0 - checksum: c03c81900bdbd11211ce02e97ca4e8d1b36f08a3ad6fee9e9b23a60a59c9ff266500e2723b21a60d29927c0ba8cf5dec59600d2f615f6918ac50e10100d7e543 +"@unrs/resolver-binding-android-arm64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-android-arm64@npm:1.11.1" + conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@xmldom/xmldom@npm:^0.8.8": - version: 0.8.10 - resolution: "@xmldom/xmldom@npm:0.8.10" - checksum: 4c136aec31fb3b49aaa53b6fcbfe524d02a1dc0d8e17ee35bd3bf35e9ce1344560481cd1efd086ad1a4821541482528672306d5e37cdbd187f33d7fadd3e2cf0 +"@unrs/resolver-binding-darwin-arm64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-darwin-arm64@npm:1.11.1" + conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"abbrev@npm:^3.0.0": - version: 3.0.1 - resolution: "abbrev@npm:3.0.1" - checksum: e70b209f5f408dd3a3bbd0eec4b10a2ffd64704a4a3821d0969d84928cc490a8eb60f85b78a95622c1841113edac10161c62e52f5e7d0027aa26786a8136e02e +"@unrs/resolver-binding-darwin-x64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-darwin-x64@npm:1.11.1" + conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"abort-controller@npm:^3.0.0": - version: 3.0.0 - resolution: "abort-controller@npm:3.0.0" - dependencies: - event-target-shim: ^5.0.0 - checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75 +"@unrs/resolver-binding-freebsd-x64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-freebsd-x64@npm:1.11.1" + conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"accepts@npm:^1.3.7, accepts@npm:^1.3.8": - version: 1.3.8 - resolution: "accepts@npm:1.3.8" - dependencies: - mime-types: ~2.1.34 - negotiator: 0.6.3 - checksum: 50c43d32e7b50285ebe84b613ee4a3aa426715a7d131b65b786e2ead0fd76b6b60091b9916d3478a75f11f162628a2139991b6c03ab3f1d9ab7c86075dc8eab4 +"@unrs/resolver-binding-linux-arm-gnueabihf@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm-gnueabihf@npm:1.11.1" + conditions: os=linux & cpu=arm languageName: node linkType: hard -"acorn@npm:^8.14.0": - version: 8.14.1 - resolution: "acorn@npm:8.14.1" - bin: - acorn: bin/acorn - checksum: 260d9bb6017a1b6e42d31364687f0258f78eb20210b36ef2baad38fd619d78d4e95ff7dde9b3dbe0d81f137f79a8d651a845363a26e6985997f7b71145dc5e94 +"@unrs/resolver-binding-linux-arm-musleabihf@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm-musleabihf@npm:1.11.1" + conditions: os=linux & cpu=arm languageName: node linkType: hard -"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": - version: 7.1.3 - resolution: "agent-base@npm:7.1.3" - checksum: 87bb7ee54f5ecf0ccbfcba0b07473885c43ecd76cb29a8db17d6137a19d9f9cd443a2a7c5fd8a3f24d58ad8145f9eb49116344a66b107e1aeab82cf2383f4753 +"@unrs/resolver-binding-linux-arm64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm64-gnu@npm:1.11.1" + conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"anser@npm:^1.4.9": - version: 1.4.10 - resolution: "anser@npm:1.4.10" - checksum: 3823c64f8930d3d97f36e56cdf646fa6351f1227e25eee70c3a17697447cae4238fc3a309bb3bc2003cf930687fa72aed71426dbcf3c0a15565e120a7fee5507 +"@unrs/resolver-binding-linux-arm64-musl@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm64-musl@npm:1.11.1" + conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1": - version: 4.3.2 - resolution: "ansi-escapes@npm:4.3.2" - dependencies: - type-fest: ^0.21.3 - checksum: 93111c42189c0a6bed9cdb4d7f2829548e943827ee8479c74d6e0b22ee127b2a21d3f8b5ca57723b8ef78ce011fbfc2784350eb2bde3ccfccf2f575fa8489815 +"@unrs/resolver-binding-linux-ppc64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-ppc64-gnu@npm:1.11.1" + conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"ansi-regex@npm:^4.1.0": - version: 4.1.1 - resolution: "ansi-regex@npm:4.1.1" +"@unrs/resolver-binding-linux-riscv64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-riscv64-gnu@npm:1.11.1" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-riscv64-musl@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-riscv64-musl@npm:1.11.1" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-s390x-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-s390x-gnu@npm:1.11.1" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-x64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-x64-gnu@npm:1.11.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-x64-musl@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-x64-musl@npm:1.11.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@unrs/resolver-binding-wasm32-wasi@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-wasm32-wasi@npm:1.11.1" + dependencies: + "@napi-rs/wasm-runtime": ^0.2.11 + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@unrs/resolver-binding-win32-arm64-msvc@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-win32-arm64-msvc@npm:1.11.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@unrs/resolver-binding-win32-ia32-msvc@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-win32-ia32-msvc@npm:1.11.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@unrs/resolver-binding-win32-x64-msvc@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-win32-x64-msvc@npm:1.11.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@urql/core@npm:^5.0.6, @urql/core@npm:^5.1.1": + version: 5.1.1 + resolution: "@urql/core@npm:5.1.1" + dependencies: + "@0no-co/graphql.web": ^1.0.5 + wonka: ^6.3.2 + checksum: c28736706abe5d0a0172bcde1c80807aba44606041347beba8e73d5237598034301cccad0169c4f63ba08f5bffe7b3a3ad95ee4a53a0d719ad5525f44b84dbcc + languageName: node + linkType: hard + +"@urql/exchange-retry@npm:^1.3.0": + version: 1.3.1 + resolution: "@urql/exchange-retry@npm:1.3.1" + dependencies: + "@urql/core": ^5.1.1 + wonka: ^6.3.2 + peerDependencies: + "@urql/core": ^5.0.0 + checksum: c03c81900bdbd11211ce02e97ca4e8d1b36f08a3ad6fee9e9b23a60a59c9ff266500e2723b21a60d29927c0ba8cf5dec59600d2f615f6918ac50e10100d7e543 + languageName: node + linkType: hard + +"@xmldom/xmldom@npm:^0.8.8": + version: 0.8.10 + resolution: "@xmldom/xmldom@npm:0.8.10" + checksum: 4c136aec31fb3b49aaa53b6fcbfe524d02a1dc0d8e17ee35bd3bf35e9ce1344560481cd1efd086ad1a4821541482528672306d5e37cdbd187f33d7fadd3e2cf0 + languageName: node + linkType: hard + +"abab@npm:^2.0.6": + version: 2.0.6 + resolution: "abab@npm:2.0.6" + checksum: 6ffc1af4ff315066c62600123990d87551ceb0aafa01e6539da77b0f5987ac7019466780bf480f1787576d4385e3690c81ccc37cfda12819bf510b8ab47e5a3e + languageName: node + linkType: hard + +"abbrev@npm:^3.0.0": + version: 3.0.1 + resolution: "abbrev@npm:3.0.1" + checksum: e70b209f5f408dd3a3bbd0eec4b10a2ffd64704a4a3821d0969d84928cc490a8eb60f85b78a95622c1841113edac10161c62e52f5e7d0027aa26786a8136e02e + languageName: node + linkType: hard + +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: ^5.0.0 + checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75 + languageName: node + linkType: hard + +"accepts@npm:^1.3.7, accepts@npm:^1.3.8": + version: 1.3.8 + resolution: "accepts@npm:1.3.8" + dependencies: + mime-types: ~2.1.34 + negotiator: 0.6.3 + checksum: 50c43d32e7b50285ebe84b613ee4a3aa426715a7d131b65b786e2ead0fd76b6b60091b9916d3478a75f11f162628a2139991b6c03ab3f1d9ab7c86075dc8eab4 + languageName: node + linkType: hard + +"acorn-globals@npm:^7.0.0": + version: 7.0.1 + resolution: "acorn-globals@npm:7.0.1" + dependencies: + acorn: ^8.1.0 + acorn-walk: ^8.0.2 + checksum: 2a2998a547af6d0db5f0cdb90acaa7c3cbca6709010e02121fb8b8617c0fbd8bab0b869579903fde358ac78454356a14fadcc1a672ecb97b04b1c2ccba955ce8 + languageName: node + linkType: hard + +"acorn-walk@npm:^8.0.2": + version: 8.3.5 + resolution: "acorn-walk@npm:8.3.5" + dependencies: + acorn: ^8.11.0 + checksum: ea8b55206c8913ce1d71a62dc10215ad919e3caf64053de72038e0be25dd8ef1454ffdcb6f25ed3b5325204f7ae18131e7fbbdfd7554f9538985a570188c60a8 + languageName: node + linkType: hard + +"acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.8.1": + version: 8.16.0 + resolution: "acorn@npm:8.16.0" + bin: + acorn: bin/acorn + checksum: bbfa466cd0dbd18b4460a85e9d0fc2f35db999380892403c573261beda91f23836db2aa71fd3ae65e94424ad14ff8e2b7bd37c7a2624278fd89137cd6e448c41 + languageName: node + linkType: hard + +"acorn@npm:^8.14.0": + version: 8.14.1 + resolution: "acorn@npm:8.14.1" + bin: + acorn: bin/acorn + checksum: 260d9bb6017a1b6e42d31364687f0258f78eb20210b36ef2baad38fd619d78d4e95ff7dde9b3dbe0d81f137f79a8d651a845363a26e6985997f7b71145dc5e94 + languageName: node + linkType: hard + +"agent-base@npm:6": + version: 6.0.2 + resolution: "agent-base@npm:6.0.2" + dependencies: + debug: 4 + checksum: f52b6872cc96fd5f622071b71ef200e01c7c4c454ee68bc9accca90c98cfb39f2810e3e9aa330435835eedc8c23f4f8a15267f67c6e245d2b33757575bdac49d + languageName: node + linkType: hard + +"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": + version: 7.1.3 + resolution: "agent-base@npm:7.1.3" + checksum: 87bb7ee54f5ecf0ccbfcba0b07473885c43ecd76cb29a8db17d6137a19d9f9cd443a2a7c5fd8a3f24d58ad8145f9eb49116344a66b107e1aeab82cf2383f4753 + languageName: node + linkType: hard + +"anser@npm:^1.4.9": + version: 1.4.10 + resolution: "anser@npm:1.4.10" + checksum: 3823c64f8930d3d97f36e56cdf646fa6351f1227e25eee70c3a17697447cae4238fc3a309bb3bc2003cf930687fa72aed71426dbcf3c0a15565e120a7fee5507 + languageName: node + linkType: hard + +"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0, ansi-escapes@npm:^4.3.2": + version: 4.3.2 + resolution: "ansi-escapes@npm:4.3.2" + dependencies: + type-fest: ^0.21.3 + checksum: 93111c42189c0a6bed9cdb4d7f2829548e943827ee8479c74d6e0b22ee127b2a21d3f8b5ca57723b8ef78ce011fbfc2784350eb2bde3ccfccf2f575fa8489815 + languageName: node + linkType: hard + +"ansi-escapes@npm:^6.0.0": + version: 6.2.1 + resolution: "ansi-escapes@npm:6.2.1" + checksum: 4bdbabe0782a1d4007157798f8acab745d1d5e440c872e6792880d08025e0baababa6b85b36846e955fde7d1e4bf572cdb1fddf109de196e9388d7a1c55ce30d + languageName: node + linkType: hard + +"ansi-regex@npm:^4.1.0": + version: 4.1.1 + resolution: "ansi-regex@npm:4.1.1" checksum: b1a6ee44cb6ecdabaa770b2ed500542714d4395d71c7e5c25baa631f680fb2ad322eb9ba697548d498a6fd366949fc8b5bfcf48d49a32803611f648005b01888 languageName: node linkType: hard @@ -3455,7 +4391,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^5.0.0": +"ansi-styles@npm:^5.0.0, ansi-styles@npm:^5.2.0": version: 5.2.0 resolution: "ansi-styles@npm:5.2.0" checksum: d7f4e97ce0623aea6bc0d90dcd28881ee04cba06c570b97fd3391bd7a268eedfd9d5e2dd4fdcbdd82b8105df5faf6f24aaedc08eaf3da898e702db5948f63469 @@ -3476,7 +4412,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.3": +"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -3534,6 +4470,20 @@ __metadata: languageName: node linkType: hard +"async-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-function@npm:1.0.0" + checksum: 9102e246d1ed9b37ac36f57f0a6ca55226876553251a31fc80677e71471f463a54c872dc78d5d7f80740c8ba624395cccbe8b60f7b690c4418f487d8e9fd1106 + languageName: node + linkType: hard + +"async-generator-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-generator-function@npm:1.0.0" + checksum: 74a71a4a2dd7afd06ebb612f6d612c7f4766a351bedffde466023bf6dae629e46b0d2cd38786239e0fbf245de0c7df76035465e16d1213774a0efb22fec0d713 + languageName: node + linkType: hard + "async-limiter@npm:~1.0.0": version: 1.0.1 resolution: "async-limiter@npm:1.0.1" @@ -3541,6 +4491,13 @@ __metadata: languageName: node linkType: hard +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 7b78c451df768adba04e2d02e63e2d0bf3b07adcd6e42b4cf665cb7ce899bedd344c69a1dcbce355b5f972d597b25aaa1c1742b52cffd9caccb22f348114f6be + languageName: node + linkType: hard + "await-lock@npm:^2.2.2": version: 2.2.2 resolution: "await-lock@npm:2.2.2" @@ -3557,7 +4514,24 @@ __metadata: languageName: node linkType: hard -"babel-jest@npm:^29.7.0": +"babel-jest@npm:30.3.0": + version: 30.3.0 + resolution: "babel-jest@npm:30.3.0" + dependencies: + "@jest/transform": 30.3.0 + "@types/babel__core": ^7.20.5 + babel-plugin-istanbul: ^7.0.1 + babel-preset-jest: 30.3.0 + chalk: ^4.1.2 + graceful-fs: ^4.2.11 + slash: ^3.0.0 + peerDependencies: + "@babel/core": ^7.11.0 || ^8.0.0-0 + checksum: edcee70837cc3c88e2992e6bbaaf7bba2c0d56b215cdca0a0cee214d8a47360fbcdc45117ac50cba6cb077465d045edc1285812e6df45caad9a4786a12696a1d + languageName: node + linkType: hard + +"babel-jest@npm:^29.2.1, babel-jest@npm:^29.7.0": version: 29.7.0 resolution: "babel-jest@npm:29.7.0" dependencies: @@ -3587,6 +4561,28 @@ __metadata: languageName: node linkType: hard +"babel-plugin-istanbul@npm:^7.0.1": + version: 7.0.1 + resolution: "babel-plugin-istanbul@npm:7.0.1" + dependencies: + "@babel/helper-plugin-utils": ^7.0.0 + "@istanbuljs/load-nyc-config": ^1.0.0 + "@istanbuljs/schema": ^0.1.3 + istanbul-lib-instrument: ^6.0.2 + test-exclude: ^6.0.0 + checksum: 06195af9022a1a2dad23bc4f2f9c226d053304889ae2be23a32aa3df821d2e61055a8eb533f204b10ee9899120e4f52bef6f0c4ab84a960cb2211cf638174aa2 + languageName: node + linkType: hard + +"babel-plugin-jest-hoist@npm:30.3.0": + version: 30.3.0 + resolution: "babel-plugin-jest-hoist@npm:30.3.0" + dependencies: + "@types/babel__core": ^7.20.5 + checksum: 5d169656b7ef665a1d7669168ffe6f560aa190c98b2a13793a8d9c671cb107985bff2e93dc1af621d46fd81cae497f6476dc966a14a1f290318f2afdcaf88ddf + languageName: node + linkType: hard + "babel-plugin-jest-hoist@npm:^29.6.3": version: 29.6.3 resolution: "babel-plugin-jest-hoist@npm:29.6.3" @@ -3703,6 +4699,31 @@ __metadata: languageName: node linkType: hard +"babel-preset-current-node-syntax@npm:^1.2.0": + version: 1.2.0 + resolution: "babel-preset-current-node-syntax@npm:1.2.0" + dependencies: + "@babel/plugin-syntax-async-generators": ^7.8.4 + "@babel/plugin-syntax-bigint": ^7.8.3 + "@babel/plugin-syntax-class-properties": ^7.12.13 + "@babel/plugin-syntax-class-static-block": ^7.14.5 + "@babel/plugin-syntax-import-attributes": ^7.24.7 + "@babel/plugin-syntax-import-meta": ^7.10.4 + "@babel/plugin-syntax-json-strings": ^7.8.3 + "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + "@babel/plugin-syntax-numeric-separator": ^7.10.4 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + "@babel/plugin-syntax-private-property-in-object": ^7.14.5 + "@babel/plugin-syntax-top-level-await": ^7.14.5 + peerDependencies: + "@babel/core": ^7.0.0 || ^8.0.0-0 + checksum: 3608fa671cfa46364ea6ec704b8fcdd7514b7b70e6ec09b1199e13ae73ed346c51d5ce2cb6d4d5b295f6a3f2cad1fdeec2308aa9e037002dd7c929194cc838ea + languageName: node + linkType: hard + "babel-preset-expo@npm:~54.0.8": version: 54.0.8 resolution: "babel-preset-expo@npm:54.0.8" @@ -3742,6 +4763,18 @@ __metadata: languageName: node linkType: hard +"babel-preset-jest@npm:30.3.0": + version: 30.3.0 + resolution: "babel-preset-jest@npm:30.3.0" + dependencies: + babel-plugin-jest-hoist: 30.3.0 + babel-preset-current-node-syntax: ^1.2.0 + peerDependencies: + "@babel/core": ^7.11.0 || ^8.0.0-beta.1 + checksum: fd29c8ff5967c047006bde152cf5ac99ce2e1d573f6f044828cb4d06eab95b65549a38554ea97174bbe508006d2a7cb1370581d87aa73f6b3c2134f2d49aaf85 + languageName: node + linkType: hard + "babel-preset-jest@npm:^29.6.3": version: 29.6.3 resolution: "babel-preset-jest@npm:29.6.3" @@ -3947,6 +4980,16 @@ __metadata: languageName: node linkType: hard +"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: ^1.3.0 + function-bind: ^1.1.2 + checksum: b2863d74fcf2a6948221f65d95b91b4b2d90cfe8927650b506141e669f7d5de65cea191bf788838bc40d13846b7886c5bc5c84ab96c3adbcf88ad69a72fcdc6b + languageName: node + linkType: hard + "caller-callsite@npm:^2.0.0": version: 2.0.0 resolution: "caller-callsite@npm:2.0.0" @@ -3972,14 +5015,14 @@ __metadata: languageName: node linkType: hard -"callsites@npm:^3.0.0": +"callsites@npm:^3.0.0, callsites@npm:^3.1.0": version: 3.1.0 resolution: "callsites@npm:3.1.0" checksum: 072d17b6abb459c2ba96598918b55868af677154bec7e73d222ef95a8fdb9bbf7dae96a8421085cdad8cd190d86653b5b6dc55a4484f2e5b2e27d5e0c3fc15b3 languageName: node linkType: hard -"camelcase@npm:6, camelcase@npm:^6.2.0": +"camelcase@npm:6, camelcase@npm:^6.2.0, camelcase@npm:^6.3.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d @@ -4025,6 +5068,16 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^3.0.0": + version: 3.0.0 + resolution: "chalk@npm:3.0.0" + dependencies: + ansi-styles: ^4.1.0 + supports-color: ^7.1.0 + checksum: 8e3ddf3981c4da405ddbd7d9c8d91944ddf6e33d6837756979f7840a29272a69a5189ecae0ff84006750d6d1e92368d413335eab4db5476db6e6703a1d1e0505 + languageName: node + linkType: hard + "chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -4035,6 +5088,20 @@ __metadata: languageName: node linkType: hard +"char-regex@npm:^1.0.2": + version: 1.0.2 + resolution: "char-regex@npm:1.0.2" + checksum: b563e4b6039b15213114626621e7a3d12f31008bdce20f9c741d69987f62aeaace7ec30f6018890ad77b2e9b4d95324c9f5acfca58a9441e3b1dcdd1e2525d17 + languageName: node + linkType: hard + +"char-regex@npm:^2.0.0": + version: 2.0.2 + resolution: "char-regex@npm:2.0.2" + checksum: 4965154ccf32b39c0f31df79e17686ee22fb6ebea774b6128e1d020cf2b01a3319bb608bfa2dba53cd478bed2f1991ac5246bee5ff93d0217ff7514e404694ed + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -4084,6 +5151,20 @@ __metadata: languageName: node linkType: hard +"ci-info@npm:^4.2.0": + version: 4.4.0 + resolution: "ci-info@npm:4.4.0" + checksum: 3418954c9ca192d4ab7f88637835f8463a327dfcb1d9fdd2434f0aba2715d8b2b0e79fd1a4297cc4a35efc5728f8fd74f3b31cb741c948469a4c07dfe8df3675 + languageName: node + linkType: hard + +"cjs-module-lexer@npm:^2.1.0": + version: 2.2.0 + resolution: "cjs-module-lexer@npm:2.2.0" + checksum: 3179fdc21698574be988d505f747f765f10f6faa558e3f1bb90c9a48b1dbc6748dffa738706c6a9d07f676abaf460f7ddbd18c97322e002f183e9af7b518aa15 + languageName: node + linkType: hard + "cli-cursor@npm:^2.1.0": version: 2.1.0 resolution: "cli-cursor@npm:2.1.0" @@ -4136,6 +5217,20 @@ __metadata: languageName: node linkType: hard +"co@npm:^4.6.0": + version: 4.6.0 + resolution: "co@npm:4.6.0" + checksum: 5210d9223010eb95b29df06a91116f2cf7c8e0748a9013ed853b53f362ea0e822f1e5bb054fb3cefc645239a4cf966af1f6133a3b43f40d591f3b68ed6cf0510 + languageName: node + linkType: hard + +"collect-v8-coverage@npm:^1.0.0, collect-v8-coverage@npm:^1.0.2": + version: 1.0.3 + resolution: "collect-v8-coverage@npm:1.0.3" + checksum: ed1d1ebc9c05e7263fffa3ad6440031db6a1fdd9f574435aa689effcdfe9f2b93aba8ec600f9c7b99124cd6ff5d9415c17961d84ae829a72251a4fe668a49b63 + languageName: node + linkType: hard + "color-convert@npm:^1.9.0": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -4188,6 +5283,15 @@ __metadata: languageName: node linkType: hard +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: ~1.0.0 + checksum: 49fa4aeb4916567e33ea81d088f6584749fc90c7abec76fd516bf1c5aa5c79f3584b5ba3de6b86d26ddd64bae5329c4c7479343250cfe71c75bb366eae53bb7c + languageName: node + linkType: hard + "commander@npm:^12.0.0": version: 12.1.0 resolution: "commander@npm:12.1.0" @@ -4415,6 +5519,29 @@ __metadata: languageName: node linkType: hard +"cssom@npm:^0.5.0": + version: 0.5.0 + resolution: "cssom@npm:0.5.0" + checksum: 823471aa30091c59e0a305927c30e7768939b6af70405808f8d2ce1ca778cddcb24722717392438329d1691f9a87cb0183b64b8d779b56a961546d54854fde01 + languageName: node + linkType: hard + +"cssom@npm:~0.3.6": + version: 0.3.8 + resolution: "cssom@npm:0.3.8" + checksum: 24beb3087c76c0d52dd458be9ee1fbc80ac771478a9baef35dd258cdeb527c68eb43204dd439692bb2b1ae5272fa5f2946d10946edab0d04f1078f85e06bc7f6 + languageName: node + linkType: hard + +"cssstyle@npm:^2.3.0": + version: 2.3.0 + resolution: "cssstyle@npm:2.3.0" + dependencies: + cssom: ~0.3.6 + checksum: 5f05e6fd2e3df0b44695c2f08b9ef38b011862b274e320665176467c0725e44a53e341bc4959a41176e83b66064ab786262e7380fd1cabeae6efee0d255bb4e3 + languageName: node + linkType: hard + "csstype@npm:^3.0.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" @@ -4422,6 +5549,17 @@ __metadata: languageName: node linkType: hard +"data-urls@npm:^3.0.2": + version: 3.0.2 + resolution: "data-urls@npm:3.0.2" + dependencies: + abab: ^2.0.6 + whatwg-mimetype: ^3.0.0 + whatwg-url: ^11.0.0 + checksum: 033fc3dd0fba6d24bc9a024ddcf9923691dd24f90a3d26f6545d6a2f71ec6956f93462f2cdf2183cc46f10dc01ed3bcb36731a8208456eb1a08147e571fe2a76 + languageName: node + linkType: hard + "debug@npm:2.6.9, debug@npm:^2.2.0, debug@npm:^2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -4459,6 +5597,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.4.2": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 9302b990cd6f4da1c7602200002e40e15d15660374432963421d3cd6d81cc6e27e0a488356b030fee64650947e32e78bdbea245d596dadfeeeb02e146d485999 + languageName: node + linkType: hard + "decode-uri-component@npm:^0.2.2": version: 0.2.2 resolution: "decode-uri-component@npm:0.2.2" @@ -4466,6 +5611,18 @@ __metadata: languageName: node linkType: hard +"dedent@npm:^1.6.0": + version: 1.7.2 + resolution: "dedent@npm:1.7.2" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: 58f46def0e0310f4c6298f648fa1b1f2de074879f9035ff08285279f91060bb9b3c83d9c918b3ef2be3e08705f8858dc9139d9931832d89788d6efd3021c535d + languageName: node + linkType: hard + "deep-extend@npm:^0.6.0": version: 0.6.0 resolution: "deep-extend@npm:0.6.0" @@ -4496,6 +5653,13 @@ __metadata: languageName: node linkType: hard +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020 + languageName: node + linkType: hard + "depd@npm:2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -4517,6 +5681,13 @@ __metadata: languageName: node linkType: hard +"detect-newline@npm:^3.1.0": + version: 3.1.0 + resolution: "detect-newline@npm:3.1.0" + checksum: ae6cd429c41ad01b164c59ea36f264a2c479598e61cba7c99da24175a7ab80ddf066420f2bec9a1c57a6bead411b4655ff15ad7d281c000a89791f48cbe939e7 + languageName: node + linkType: hard + "detect-node-es@npm:^1.1.0": version: 1.1.0 resolution: "detect-node-es@npm:1.1.0" @@ -4524,6 +5695,13 @@ __metadata: languageName: node linkType: hard +"diff-sequences@npm:^29.6.3": + version: 29.6.3 + resolution: "diff-sequences@npm:29.6.3" + checksum: f4914158e1f2276343d98ff5b31fc004e7304f5470bf0f1adb2ac6955d85a531a6458d33e87667f98f6ae52ebd3891bb47d420bb48a5bd8b7a27ee25b20e33aa + languageName: node + linkType: hard + "dom-serializer@npm:^2.0.0": version: 2.0.0 resolution: "dom-serializer@npm:2.0.0" @@ -4542,6 +5720,15 @@ __metadata: languageName: node linkType: hard +"domexception@npm:^4.0.0": + version: 4.0.0 + resolution: "domexception@npm:4.0.0" + dependencies: + webidl-conversions: ^7.0.0 + checksum: ddbc1268edf33a8ba02ccc596735ede80375ee0cf124b30d2f05df5b464ba78ef4f49889b6391df4a04954e63d42d5631c7fcf8b1c4f12bc531252977a5f13d5 + languageName: node + linkType: hard + "domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": version: 5.0.3 resolution: "domhandler@npm:5.0.3" @@ -4595,6 +5782,17 @@ __metadata: languageName: node linkType: hard +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: ^1.0.1 + es-errors: ^1.3.0 + gopd: ^1.2.0 + checksum: 149207e36f07bd4941921b0ca929e3a28f1da7bd6b6ff8ff7f4e2f2e460675af4576eeba359c635723dc189b64cdd4787e0255897d5b135ccc5d15cb8685fc90 + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -4623,6 +5821,13 @@ __metadata: languageName: node linkType: hard +"emittery@npm:^0.13.1": + version: 0.13.1 + resolution: "emittery@npm:0.13.1" + checksum: 2b089ab6306f38feaabf4f6f02792f9ec85fc054fda79f44f6790e61bbf6bc4e1616afb9b232e0c5ec5289a8a452f79bfa6d905a6fd64e94b49981f0934001c6 + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -4667,6 +5872,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^6.0.0": + version: 6.0.1 + resolution: "entities@npm:6.0.1" + checksum: 937b952e81aca641660a6a07f70001c6821973dea3ae7f6a5013eadce94620f3ed2e9c745832d503c8811ce6e97704d8a0396159580c0e567d815234de7fdecf + languageName: node + linkType: hard + "entities@npm:~2.0.0": version: 2.0.3 resolution: "entities@npm:2.0.3" @@ -4695,21 +5907,56 @@ __metadata: languageName: node linkType: hard -"error-ex@npm:^1.3.1": - version: 1.3.2 - resolution: "error-ex@npm:1.3.2" +"error-ex@npm:^1.3.1": + version: 1.3.2 + resolution: "error-ex@npm:1.3.2" + dependencies: + is-arrayish: ^0.2.1 + checksum: c1c2b8b65f9c91b0f9d75f0debaa7ec5b35c266c2cac5de412c1a6de86d4cbae04ae44e510378cb14d032d0645a36925d0186f8bb7367bcc629db256b743a001 + languageName: node + linkType: hard + +"error-stack-parser@npm:^2.0.6": + version: 2.1.4 + resolution: "error-stack-parser@npm:2.1.4" + dependencies: + stackframe: ^1.3.4 + checksum: 3b916d2d14c6682f287c8bfa28e14672f47eafe832701080e420e7cdbaebb2c50293868256a95706ac2330fe078cf5664713158b49bc30d7a5f2ac229ded0e18 + languageName: node + linkType: hard + +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 0512f4e5d564021c9e3a644437b0155af2679d10d80f21adaf868e64d30efdfbd321631956f20f42d655fedb2e3a027da479fad3fa6048f768eb453a80a5f80a + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: ec1414527a0ccacd7f15f4a3bc66e215f04f595ba23ca75cdae0927af099b5ec865f9f4d33e9d7e86f512f252876ac77d4281a7871531a50678132429b1271b5 + languageName: node + linkType: hard + +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" dependencies: - is-arrayish: ^0.2.1 - checksum: c1c2b8b65f9c91b0f9d75f0debaa7ec5b35c266c2cac5de412c1a6de86d4cbae04ae44e510378cb14d032d0645a36925d0186f8bb7367bcc629db256b743a001 + es-errors: ^1.3.0 + checksum: 214d3767287b12f36d3d7267ef342bbbe1e89f899cfd67040309fc65032372a8e60201410a99a1645f2f90c1912c8c49c8668066f6bdd954bcd614dda2e3da97 languageName: node linkType: hard -"error-stack-parser@npm:^2.0.6": - version: 2.1.4 - resolution: "error-stack-parser@npm:2.1.4" +"es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" dependencies: - stackframe: ^1.3.4 - checksum: 3b916d2d14c6682f287c8bfa28e14672f47eafe832701080e420e7cdbaebb2c50293868256a95706ac2330fe078cf5664713158b49bc30d7a5f2ac229ded0e18 + es-errors: ^1.3.0 + get-intrinsic: ^1.2.6 + has-tostringtag: ^1.0.2 + hasown: ^2.0.2 + checksum: 789f35de4be3dc8d11fdcb91bc26af4ae3e6d602caa93299a8c45cf05d36cc5081454ae2a6d3afa09cceca214b76c046e4f8151e092e6fc7feeb5efb9e794fc6 languageName: node linkType: hard @@ -4748,7 +5995,25 @@ __metadata: languageName: node linkType: hard -"esprima@npm:^4.0.0, esprima@npm:~4.0.0": +"escodegen@npm:^2.0.0": + version: 2.1.0 + resolution: "escodegen@npm:2.1.0" + dependencies: + esprima: ^4.0.1 + estraverse: ^5.2.0 + esutils: ^2.0.2 + source-map: ~0.6.1 + dependenciesMeta: + source-map: + optional: true + bin: + escodegen: bin/escodegen.js + esgenerate: bin/esgenerate.js + checksum: 096696407e161305cd05aebb95134ad176708bc5cb13d0dcc89a5fcbb959b8ed757e7f2591a5f8036f8f4952d4a724de0df14cd419e29212729fa6df5ce16bf6 + languageName: node + linkType: hard + +"esprima@npm:^4.0.0, esprima@npm:^4.0.1, esprima@npm:~4.0.0": version: 4.0.1 resolution: "esprima@npm:4.0.1" bin: @@ -4758,6 +6023,20 @@ __metadata: languageName: node linkType: hard +"estraverse@npm:^5.2.0": + version: 5.3.0 + resolution: "estraverse@npm:5.3.0" + checksum: 072780882dc8416ad144f8fe199628d2b3e7bbc9989d9ed43795d2c90309a2047e6bc5979d7e2322a341163d22cfad9e21f4110597fe487519697389497e4e2b + languageName: node + linkType: hard + +"esutils@npm:^2.0.2": + version: 2.0.3 + resolution: "esutils@npm:2.0.3" + checksum: 22b5b08f74737379a840b8ed2036a5fb35826c709ab000683b092d9054e5c2a82c27818f12604bfc2a9a76b90b6834ef081edbc1c7ae30d1627012e067c6ec87 + languageName: node + linkType: hard + "etag@npm:~1.8.1": version: 1.8.1 resolution: "etag@npm:1.8.1" @@ -4786,6 +6065,57 @@ __metadata: languageName: node linkType: hard +"execa@npm:^5.1.1": + version: 5.1.1 + resolution: "execa@npm:5.1.1" + dependencies: + cross-spawn: ^7.0.3 + get-stream: ^6.0.0 + human-signals: ^2.1.0 + is-stream: ^2.0.0 + merge-stream: ^2.0.0 + npm-run-path: ^4.0.1 + onetime: ^5.1.2 + signal-exit: ^3.0.3 + strip-final-newline: ^2.0.0 + checksum: fba9022c8c8c15ed862847e94c252b3d946036d7547af310e344a527e59021fd8b6bb0723883ea87044dc4f0201f949046993124a42ccb0855cae5bf8c786343 + languageName: node + linkType: hard + +"exit-x@npm:^0.2.2": + version: 0.2.2 + resolution: "exit-x@npm:0.2.2" + checksum: c62a8e0f77b1de00059c2976ddb774c41d06969a4262d984a58cd51995be1fc0ce962329ea68722bba0c254adb3930cc3625dabaf079fe8031cd03e91db1ba51 + languageName: node + linkType: hard + +"expect@npm:30.3.0, expect@npm:^30.0.0": + version: 30.3.0 + resolution: "expect@npm:30.3.0" + dependencies: + "@jest/expect-utils": 30.3.0 + "@jest/get-type": 30.1.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + checksum: 1563465523364c6d01d7b8e7bf61a662f312b611757c755c1e9ed98dfa2ffac10664d5ed4119350cf8a8a64ec38eea5a2606d0c48af41dacb25f5b7e17c59468 + languageName: node + linkType: hard + +"expect@npm:^29.7.0": + version: 29.7.0 + resolution: "expect@npm:29.7.0" + dependencies: + "@jest/expect-utils": ^29.7.0 + jest-get-type: ^29.6.3 + jest-matcher-utils: ^29.7.0 + jest-message-util: ^29.7.0 + jest-util: ^29.7.0 + checksum: 9257f10288e149b81254a0fda8ffe8d54a7061cd61d7515779998b012579d2b8c22354b0eb901daf0145f347403da582f75f359f4810c007182ad3fb318b5c0c + languageName: node + linkType: hard + "expo-application@npm:~7.0.8": version: 7.0.8 resolution: "expo-application@npm:7.0.8" @@ -5128,7 +6458,7 @@ __metadata: languageName: node linkType: hard -"fb-watchman@npm:^2.0.0": +"fb-watchman@npm:^2.0.0, fb-watchman@npm:^2.0.2": version: 2.0.2 resolution: "fb-watchman@npm:2.0.2" dependencies: @@ -5212,7 +6542,7 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^4.1.0": +"find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" dependencies: @@ -5263,6 +6593,19 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.0": + version: 4.0.5 + resolution: "form-data@npm:4.0.5" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.8 + es-set-tostringtag: ^2.1.0 + hasown: ^2.0.2 + mime-types: ^2.1.12 + checksum: af8328413c16d0cded5fccc975a44d227c5120fd46a9e81de8acf619d43ed838414cc6d7792195b30b248f76a65246949a129a4dadd148721948f90cd6d4fb69 + languageName: node + linkType: hard + "freeport-async@npm:^2.0.0": version: 2.0.0 resolution: "freeport-async@npm:2.0.0" @@ -5293,7 +6636,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2": +"fsevents@npm:^2.3.2, fsevents@npm:^2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -5303,7 +6646,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@^2.3.2#~builtin": +"fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@^2.3.3#~builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -5319,6 +6662,13 @@ __metadata: languageName: node linkType: hard +"generator-function@npm:^2.0.0": + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 3bf87f7b0230de5d74529677e6c3ceb3b7b5d9618b5a22d92b45ce3876defbaf5a77791b25a61b0fa7d13f95675b5ff67a7769f3b9af33f096e34653519e873d + languageName: node + linkType: hard + "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -5333,6 +6683,27 @@ __metadata: languageName: node linkType: hard +"get-intrinsic@npm:^1.2.6": + version: 1.3.1 + resolution: "get-intrinsic@npm:1.3.1" + dependencies: + async-function: ^1.0.0 + async-generator-function: ^1.0.0 + call-bind-apply-helpers: ^1.0.2 + es-define-property: ^1.0.1 + es-errors: ^1.3.0 + es-object-atoms: ^1.1.1 + function-bind: ^1.1.2 + generator-function: ^2.0.0 + get-proto: ^1.0.1 + gopd: ^1.2.0 + has-symbols: ^1.1.0 + hasown: ^2.0.2 + math-intrinsics: ^1.1.0 + checksum: c02b3b6a445f9cd53e14896303794ac60f9751f58a69099127248abdb0251957174c6524245fc68579dc8e6a35161d3d94c93e665f808274716f4248b269436a + languageName: node + linkType: hard + "get-nonce@npm:^1.0.0": version: 1.0.1 resolution: "get-nonce@npm:1.0.1" @@ -5347,6 +6718,23 @@ __metadata: languageName: node linkType: hard +"get-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: ^1.0.1 + es-object-atoms: ^1.0.0 + checksum: 4fc96afdb58ced9a67558698b91433e6b037aaa6f1493af77498d7c85b141382cf223c0e5946f334fb328ee85dfe6edd06d218eaf09556f4bc4ec6005d7f5f7b + languageName: node + linkType: hard + +"get-stream@npm:^6.0.0": + version: 6.0.1 + resolution: "get-stream@npm:6.0.1" + checksum: e04ecece32c92eebf5b8c940f51468cd53554dcbb0ea725b2748be583c9523d00128137966afce410b9b051eb2ef16d657cd2b120ca8edafcf5a65e81af63cad + languageName: node + linkType: hard + "getenv@npm:^2.0.0": version: 2.0.0 resolution: "getenv@npm:2.0.0" @@ -5370,6 +6758,22 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.5.0": + version: 10.5.0 + resolution: "glob@npm:10.5.0" + dependencies: + foreground-child: ^3.1.0 + jackspeak: ^3.1.2 + minimatch: ^9.0.4 + minipass: ^7.1.2 + package-json-from-dist: ^1.0.0 + path-scurry: ^1.11.1 + bin: + glob: dist/esm/bin.mjs + checksum: cda96c074878abca9657bd984d2396945cf0d64283f6feeb40d738fe2da642be0010ad5210a1646244a5fc3511b0cab5a374569b3de5a12b8a63d392f18c6043 + languageName: node + linkType: hard + "glob@npm:^13.0.0": version: 13.0.0 resolution: "glob@npm:13.0.0" @@ -5411,7 +6815,14 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: cc6d8e655e360955bdccaca51a12a474268f95bb793fc3e1f2bdadb075f28bfd1fd988dab872daf77a61d78cbaf13744bc8727a17cfb1d150d76047d805375f3 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 @@ -5432,6 +6843,22 @@ __metadata: languageName: node linkType: hard +"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: b2316c7302a0e8ba3aaba215f834e96c22c86f192e7310bdf689dd0e6999510c89b00fbc5742571507cebf25764d68c988b3a0da217369a73596191ac0ce694b + languageName: node + linkType: hard + +"has-tostringtag@npm:^1.0.2": + version: 1.0.2 + resolution: "has-tostringtag@npm:1.0.2" + dependencies: + has-symbols: ^1.0.3 + checksum: 999d60bb753ad714356b2c6c87b7fb74f32463b8426e159397da4bde5bca7e598ab1073f4d8d4deafac297f2eb311484cd177af242776bf05f0d11565680468d + languageName: node + linkType: hard + "hasown@npm:^2.0.2": version: 2.0.2 resolution: "hasown@npm:2.0.2" @@ -5523,6 +6950,22 @@ __metadata: languageName: node linkType: hard +"html-encoding-sniffer@npm:^3.0.0": + version: 3.0.0 + resolution: "html-encoding-sniffer@npm:3.0.0" + dependencies: + whatwg-encoding: ^2.0.0 + checksum: 8d806aa00487e279e5ccb573366a951a9f68f65c90298eac9c3a2b440a7ffe46615aff2995a2f61c6746c639234e6179a97e18ca5ccbbf93d3725ef2099a4502 + languageName: node + linkType: hard + +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: d2df2da3ad40ca9ee3a39c5cc6475ef67c8f83c234475f24d8e9ce0dc80a2c82df8e1d6fa78ddd1e9022a586ea1bd247a615e80a5cd9273d90111ddda7d9e974 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.2.0 resolution: "http-cache-semantics@npm:4.2.0" @@ -5543,6 +6986,17 @@ __metadata: languageName: node linkType: hard +"http-proxy-agent@npm:^5.0.0": + version: 5.0.0 + resolution: "http-proxy-agent@npm:5.0.0" + dependencies: + "@tootallnate/once": 2 + agent-base: 6 + debug: 4 + checksum: e2ee1ff1656a131953839b2a19cd1f3a52d97c25ba87bd2559af6ae87114abf60971e498021f9b73f9fd78aea8876d1fb0d4656aac8a03c6caa9fc175f22b786 + languageName: node + linkType: hard + "http-proxy-agent@npm:^7.0.0": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" @@ -5560,6 +7014,16 @@ __metadata: languageName: node linkType: hard +"https-proxy-agent@npm:^5.0.1": + version: 5.0.1 + resolution: "https-proxy-agent@npm:5.0.1" + dependencies: + agent-base: 6 + debug: 4 + checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765 + languageName: node + linkType: hard + "https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.5": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" @@ -5570,7 +7034,14 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2": +"human-signals@npm:^2.1.0": + version: 2.1.0 + resolution: "human-signals@npm:2.1.0" + checksum: b87fd89fce72391625271454e70f67fe405277415b48bcc0117ca73d31fa23a4241787afdc8d67f5a116cf37258c052f59ea82daffa72364d61351423848e3b8 + languageName: node + linkType: hard + +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -5624,6 +7095,18 @@ __metadata: languageName: node linkType: hard +"import-local@npm:^3.2.0": + version: 3.2.0 + resolution: "import-local@npm:3.2.0" + dependencies: + pkg-dir: ^4.2.0 + resolve-cwd: ^3.0.0 + bin: + import-local-fixture: fixtures/cli.js + checksum: 0b0b0b412b2521739fbb85eeed834a3c34de9bc67e670b3d0b86248fc460d990a7b116ad056c084b87a693ef73d1f17268d6a5be626bb43c998a8b1c8a230004 + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -5631,6 +7114,13 @@ __metadata: languageName: node linkType: hard +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 824cfb9929d031dabf059bebfe08cf3137365e112019086ed3dcff6a0a7b698cb80cf67ccccde0e25b9e2d7527aa6cc1fed1ac490c752162496caba3e6699612 + languageName: node + linkType: hard + "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -5720,6 +7210,13 @@ __metadata: languageName: node linkType: hard +"is-generator-fn@npm:^2.1.0": + version: 2.1.0 + resolution: "is-generator-fn@npm:2.1.0" + checksum: a6ad5492cf9d1746f73b6744e0c43c0020510b59d56ddcb78a91cbc173f09b5e6beff53d75c9c5a29feb618bfef2bf458e025ecf3a57ad2268e2fb2569f56215 + languageName: node + linkType: hard + "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -5743,6 +7240,20 @@ __metadata: languageName: node linkType: hard +"is-potential-custom-element-name@npm:^1.0.1": + version: 1.0.1 + resolution: "is-potential-custom-element-name@npm:1.0.1" + checksum: ced7bbbb6433a5b684af581872afe0e1767e2d1146b2207ca0068a648fb5cab9d898495d1ac0583524faaf24ca98176a7d9876363097c2d14fee6dd324f3a1ab + languageName: node + linkType: hard + +"is-stream@npm:^2.0.0": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 + languageName: node + linkType: hard + "is-wsl@npm:^2.1.1, is-wsl@npm:^2.2.0": version: 2.2.0 resolution: "is-wsl@npm:2.2.0" @@ -5752,57 +7263,290 @@ __metadata: languageName: node linkType: hard -"isexe@npm:^2.0.0": - version: 2.0.0 - resolution: "isexe@npm:2.0.0" - checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e + languageName: node + linkType: hard + +"isobject@npm:^3.0.1": + version: 3.0.1 + resolution: "isobject@npm:3.0.1" + checksum: db85c4c970ce30693676487cca0e61da2ca34e8d4967c2e1309143ff910c207133a969f9e4ddb2dc6aba670aabce4e0e307146c310350b298e74a31f7d464703 + languageName: node + linkType: hard + +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 2367407a8d13982d8f7a859a35e7f8dd5d8f75aae4bb5484ede3a9ea1b426dc245aff28b976a2af48ee759fdd9be374ce2bd2669b644f31e76c5f46a2e29a831 + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^5.0.4": + version: 5.2.1 + resolution: "istanbul-lib-instrument@npm:5.2.1" + dependencies: + "@babel/core": ^7.12.3 + "@babel/parser": ^7.14.7 + "@istanbuljs/schema": ^0.1.2 + istanbul-lib-coverage: ^3.2.0 + semver: ^6.3.0 + checksum: bf16f1803ba5e51b28bbd49ed955a736488381e09375d830e42ddeb403855b2006f850711d95ad726f2ba3f1ae8e7366de7e51d2b9ac67dc4d80191ef7ddf272 + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^6.0.0, istanbul-lib-instrument@npm:^6.0.2": + version: 6.0.3 + resolution: "istanbul-lib-instrument@npm:6.0.3" + dependencies: + "@babel/core": ^7.23.9 + "@babel/parser": ^7.23.9 + "@istanbuljs/schema": ^0.1.3 + istanbul-lib-coverage: ^3.2.0 + semver: ^7.5.4 + checksum: 74104c60c65c4fa0e97cc76f039226c356123893929f067bfad5f86fe839e08f5d680354a68fead3bc9c1e2f3fa6f3f53cded70778e821d911e851d349f3545a + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: ^3.0.0 + make-dir: ^4.0.0 + supports-color: ^7.1.0 + checksum: fd17a1b879e7faf9bb1dc8f80b2a16e9f5b7b8498fe6ed580a618c34df0bfe53d2abd35bf8a0a00e628fb7405462576427c7df20bbe4148d19c14b431c974b21 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^5.0.0": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" + dependencies: + "@jridgewell/trace-mapping": ^0.3.23 + debug: ^4.1.1 + istanbul-lib-coverage: ^3.0.0 + checksum: 8dd6f2c1e2ecaacabeef8dc9ab52c4ed0a6036310002cf7f46ea6f3a5fb041da8076f5350e6a6be4c60cd4f231c51c73e042044afaf44820d857d92ecfb8ab6c + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.3": + version: 3.2.0 + resolution: "istanbul-reports@npm:3.2.0" + dependencies: + html-escaper: ^2.0.0 + istanbul-lib-report: ^3.0.0 + checksum: 72b4c8525276147908d28b0917bc675b1019836b638e50875521ca3b8ec63672681aa98dbab88a6f49ef798c08fe041d428abdcf84f4f3fcff5844eee54af65a + languageName: node + linkType: hard + +"jackspeak@npm:^3.1.2": + version: 3.4.3 + resolution: "jackspeak@npm:3.4.3" + dependencies: + "@isaacs/cliui": ^8.0.2 + "@pkgjs/parseargs": ^0.11.0 + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: be31027fc72e7cc726206b9f560395604b82e0fddb46c4cbf9f97d049bcef607491a5afc0699612eaa4213ca5be8fd3e1e7cd187b3040988b65c9489838a7c00 + languageName: node + linkType: hard + +"jest-changed-files@npm:30.3.0": + version: 30.3.0 + resolution: "jest-changed-files@npm:30.3.0" + dependencies: + execa: ^5.1.1 + jest-util: 30.3.0 + p-limit: ^3.1.0 + checksum: d4f03a0e2bd26c6e39d73618a361f0b11c8bcf0c479e3a32a1e9b3ceecafc26514c52d26dd3ee91ede1be72a2ef3d22a3443a0f71b1fec21ef462fb48e6de1e8 + languageName: node + linkType: hard + +"jest-circus@npm:30.3.0": + version: 30.3.0 + resolution: "jest-circus@npm:30.3.0" + dependencies: + "@jest/environment": 30.3.0 + "@jest/expect": 30.3.0 + "@jest/test-result": 30.3.0 + "@jest/types": 30.3.0 + "@types/node": "*" + chalk: ^4.1.2 + co: ^4.6.0 + dedent: ^1.6.0 + is-generator-fn: ^2.1.0 + jest-each: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + p-limit: ^3.1.0 + pretty-format: 30.3.0 + pure-rand: ^7.0.0 + slash: ^3.0.0 + stack-utils: ^2.0.6 + checksum: f4bd673e265da8021a737dd6903280809047763e36d6bc91d07aa279601f939e44f98cb8b2154f05a03e9cd4eec67c7f839959a90248da77e7b7f0ed3238672e + languageName: node + linkType: hard + +"jest-cli@npm:30.3.0": + version: 30.3.0 + resolution: "jest-cli@npm:30.3.0" + dependencies: + "@jest/core": 30.3.0 + "@jest/test-result": 30.3.0 + "@jest/types": 30.3.0 + chalk: ^4.1.2 + exit-x: ^0.2.2 + import-local: ^3.2.0 + jest-config: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + yargs: ^17.7.2 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: ./bin/jest.js + checksum: a5abb62d6b251b8e6d19c24c604343fb0acdbe16827e3ba6053685ba0ac95d7bf3745324e168ca86a63511c22f29a98c6f9bfebe08f3b7c19f4531b4aa577934 + languageName: node + linkType: hard + +"jest-config@npm:30.3.0": + version: 30.3.0 + resolution: "jest-config@npm:30.3.0" + dependencies: + "@babel/core": ^7.27.4 + "@jest/get-type": 30.1.0 + "@jest/pattern": 30.0.1 + "@jest/test-sequencer": 30.3.0 + "@jest/types": 30.3.0 + babel-jest: 30.3.0 + chalk: ^4.1.2 + ci-info: ^4.2.0 + deepmerge: ^4.3.1 + glob: ^10.5.0 + graceful-fs: ^4.2.11 + jest-circus: 30.3.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-runner: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + parse-json: ^5.2.0 + pretty-format: 30.3.0 + slash: ^3.0.0 + strip-json-comments: ^3.1.1 + peerDependencies: + "@types/node": "*" + esbuild-register: ">=3.4.0" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + checksum: a4f2f231ab674d4dc7f50de0c506854baefa3051467814a621cad38ae7024ac431a11d17e0cff4ef84d4edff7486a225a1a600df53fef054044c25e9157b9117 + languageName: node + linkType: hard + +"jest-diff@npm:30.3.0": + version: 30.3.0 + resolution: "jest-diff@npm: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 + checksum: ad49d2c602a8006725cb507143ffa6f19eb355a56ad7dffc10361ce51f74dee103db9233e1a1aa7020d8dae138ec071034ba05391bc1b0e738b69a4a994dbf29 languageName: node linkType: hard -"isexe@npm:^3.1.1": - version: 3.1.1 - resolution: "isexe@npm:3.1.1" - checksum: 7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e +"jest-diff@npm:^29.0.1, jest-diff@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-diff@npm:29.7.0" + dependencies: + chalk: ^4.0.0 + diff-sequences: ^29.6.3 + jest-get-type: ^29.6.3 + pretty-format: ^29.7.0 + checksum: 08e24a9dd43bfba1ef07a6374e5af138f53137b79ec3d5cc71a2303515335898888fa5409959172e1e05de966c9e714368d15e8994b0af7441f0721ee8e1bb77 languageName: node linkType: hard -"isobject@npm:^3.0.1": - version: 3.0.1 - resolution: "isobject@npm:3.0.1" - checksum: db85c4c970ce30693676487cca0e61da2ca34e8d4967c2e1309143ff910c207133a969f9e4ddb2dc6aba670aabce4e0e307146c310350b298e74a31f7d464703 +"jest-docblock@npm:30.2.0": + version: 30.2.0 + resolution: "jest-docblock@npm:30.2.0" + dependencies: + detect-newline: ^3.1.0 + checksum: 7074119d9919df539091e6b7f55c26858fafddb187ed1df90b2bc608544c2e6900384b97288ccd3b168f14bdcdf13281814337e1674a94d18991c8a0819aefad languageName: node linkType: hard -"istanbul-lib-coverage@npm:^3.2.0": - version: 3.2.2 - resolution: "istanbul-lib-coverage@npm:3.2.2" - checksum: 2367407a8d13982d8f7a859a35e7f8dd5d8f75aae4bb5484ede3a9ea1b426dc245aff28b976a2af48ee759fdd9be374ce2bd2669b644f31e76c5f46a2e29a831 +"jest-each@npm:30.3.0": + version: 30.3.0 + resolution: "jest-each@npm:30.3.0" + dependencies: + "@jest/get-type": 30.1.0 + "@jest/types": 30.3.0 + chalk: ^4.1.2 + jest-util: 30.3.0 + pretty-format: 30.3.0 + checksum: 66b38115c82422eefeebe1077978906b8ba065fd669b87035c696af7b609d28e1ad83d3dc71311e16065d37232064cd12f1dbc5b9329dd8fad240afae68935a5 languageName: node linkType: hard -"istanbul-lib-instrument@npm:^5.0.4": - version: 5.2.1 - resolution: "istanbul-lib-instrument@npm:5.2.1" +"jest-environment-jsdom@npm:^29.2.1": + version: 29.7.0 + resolution: "jest-environment-jsdom@npm:29.7.0" dependencies: - "@babel/core": ^7.12.3 - "@babel/parser": ^7.14.7 - "@istanbuljs/schema": ^0.1.2 - istanbul-lib-coverage: ^3.2.0 - semver: ^6.3.0 - checksum: bf16f1803ba5e51b28bbd49ed955a736488381e09375d830e42ddeb403855b2006f850711d95ad726f2ba3f1ae8e7366de7e51d2b9ac67dc4d80191ef7ddf272 + "@jest/environment": ^29.7.0 + "@jest/fake-timers": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/jsdom": ^20.0.0 + "@types/node": "*" + jest-mock: ^29.7.0 + jest-util: ^29.7.0 + jsdom: ^20.0.0 + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: 559aac134c196fccc1dfc794d8fc87377e9f78e894bb13012b0831d88dec0abd7ece99abec69da564b8073803be4f04a9eb4f4d1bb80e29eec0cb252c254deb8 languageName: node linkType: hard -"jackspeak@npm:^3.1.2": - version: 3.4.3 - resolution: "jackspeak@npm:3.4.3" +"jest-environment-node@npm:30.3.0": + version: 30.3.0 + resolution: "jest-environment-node@npm:30.3.0" dependencies: - "@isaacs/cliui": ^8.0.2 - "@pkgjs/parseargs": ^0.11.0 - dependenciesMeta: - "@pkgjs/parseargs": - optional: true - checksum: be31027fc72e7cc726206b9f560395604b82e0fddb46c4cbf9f97d049bcef607491a5afc0699612eaa4213ca5be8fd3e1e7cd187b3040988b65c9489838a7c00 + "@jest/environment": 30.3.0 + "@jest/fake-timers": 30.3.0 + "@jest/types": 30.3.0 + "@types/node": "*" + jest-mock: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + checksum: ff8cc442be96ff5042469ae95f689116caaf11e29f975ec085c1ded7c030c7ddaa18885c58a6f1c79233c5bf170d7440216f23dad60f5610e24b5d6f870a085a languageName: node linkType: hard @@ -5820,6 +7564,37 @@ __metadata: languageName: node linkType: hard +"jest-expo@npm:^55.0.11": + version: 55.0.11 + resolution: "jest-expo@npm:55.0.11" + dependencies: + "@expo/config": ~55.0.10 + "@expo/json-file": ^10.0.12 + "@jest/create-cache-key-function": ^29.2.1 + "@jest/globals": ^29.2.1 + babel-jest: ^29.2.1 + jest-environment-jsdom: ^29.2.1 + jest-snapshot: ^29.2.1 + jest-watch-select-projects: ^2.0.0 + jest-watch-typeahead: 2.2.1 + json5: ^2.2.3 + lodash: ^4.17.19 + react-test-renderer: 19.2.0 + server-only: ^0.0.1 + stacktrace-js: ^2.0.2 + peerDependencies: + expo: "*" + react-native: "*" + react-server-dom-webpack: ~19.0.4 || ~19.1.5 || ~19.2.4 + peerDependenciesMeta: + react-server-dom-webpack: + optional: true + bin: + jest: bin/jest.js + checksum: ba7113c05aa73de5880f8654abb1854f71ea5ccef28f1413c230432a0d838b79523841faac01b95aa44cc2f81996c662592d8075f1ab4fcef3bad2a8c4000785 + languageName: node + linkType: hard + "jest-get-type@npm:^29.6.3": version: 29.6.3 resolution: "jest-get-type@npm:29.6.3" @@ -5827,6 +7602,28 @@ __metadata: languageName: node linkType: hard +"jest-haste-map@npm:30.3.0": + version: 30.3.0 + resolution: "jest-haste-map@npm:30.3.0" + dependencies: + "@jest/types": 30.3.0 + "@types/node": "*" + anymatch: ^3.1.3 + fb-watchman: ^2.0.2 + fsevents: ^2.3.3 + graceful-fs: ^4.2.11 + jest-regex-util: 30.0.1 + jest-util: 30.3.0 + jest-worker: 30.3.0 + picomatch: ^4.0.3 + walker: ^1.0.8 + dependenciesMeta: + fsevents: + optional: true + checksum: bb547ef3ad970dad434c0b884465719a946820e2cabed3ade4dcd50c89445c19a57f0385bc0b366ecff3c263c7da0b4d60c9e92c6357822cb6e7414a73d64ac8 + languageName: node + linkType: hard + "jest-haste-map@npm:^29.7.0": version: 29.7.0 resolution: "jest-haste-map@npm:29.7.0" @@ -5850,6 +7647,57 @@ __metadata: languageName: node linkType: hard +"jest-leak-detector@npm:30.3.0": + version: 30.3.0 + resolution: "jest-leak-detector@npm:30.3.0" + dependencies: + "@jest/get-type": 30.1.0 + pretty-format: 30.3.0 + checksum: 950ce3266067dd983f80231ce753fdfb9fe167d810b4507d84e674205c2cb96d37f38615ae502fa9277dde497ee52ce581656b48709aacf9502a4f0006bfab0e + languageName: node + linkType: hard + +"jest-matcher-utils@npm:30.3.0, jest-matcher-utils@npm:^30.0.5": + version: 30.3.0 + resolution: "jest-matcher-utils@npm:30.3.0" + dependencies: + "@jest/get-type": 30.1.0 + chalk: ^4.1.2 + jest-diff: 30.3.0 + pretty-format: 30.3.0 + checksum: 3bc01ef81d001519fef75a32a0420c207664a829acbdc668bfa3c51e0a3ac2ddbb19c633e1e006ff63840bf231d915dfbe8dccef71e5ee842221ba1ff0da1946 + languageName: node + linkType: hard + +"jest-matcher-utils@npm:^29.0.1, jest-matcher-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-matcher-utils@npm:29.7.0" + dependencies: + chalk: ^4.0.0 + jest-diff: ^29.7.0 + jest-get-type: ^29.6.3 + pretty-format: ^29.7.0 + checksum: d7259e5f995d915e8a37a8fd494cb7d6af24cd2a287b200f831717ba0d015190375f9f5dc35393b8ba2aae9b2ebd60984635269c7f8cff7d85b077543b7744cd + languageName: node + linkType: hard + +"jest-message-util@npm:30.3.0": + version: 30.3.0 + resolution: "jest-message-util@npm:30.3.0" + dependencies: + "@babel/code-frame": ^7.27.1 + "@jest/types": 30.3.0 + "@types/stack-utils": ^2.0.3 + chalk: ^4.1.2 + graceful-fs: ^4.2.11 + picomatch: ^4.0.3 + pretty-format: 30.3.0 + slash: ^3.0.0 + stack-utils: ^2.0.6 + checksum: 2d4c01e880ca312917b79699cea1e1d176940ce2e45f0ccb80399c1bc93825ca8e38955ada432f4427a165822be5a11fd9e46528e427308853f3c444741d5983 + languageName: node + linkType: hard + "jest-message-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-message-util@npm:29.7.0" @@ -5867,6 +7715,17 @@ __metadata: languageName: node linkType: hard +"jest-mock@npm:30.3.0": + version: 30.3.0 + resolution: "jest-mock@npm:30.3.0" + dependencies: + "@jest/types": 30.3.0 + "@types/node": "*" + jest-util: 30.3.0 + checksum: bd015d5115ab74c40b37c9b9c46e49d16c8c9bdbfd50557a1805fe770a6422927a2f91252fc5f6aa8c061991be3c1bdbfc5f74ac26eb018311ec9ddb4e358df6 + languageName: node + linkType: hard + "jest-mock@npm:^29.7.0": version: 29.7.0 resolution: "jest-mock@npm:29.7.0" @@ -5878,13 +7737,189 @@ __metadata: languageName: node linkType: hard -"jest-regex-util@npm:^29.6.3": +"jest-pnp-resolver@npm:^1.2.3": + version: 1.2.3 + resolution: "jest-pnp-resolver@npm:1.2.3" + peerDependencies: + jest-resolve: "*" + peerDependenciesMeta: + jest-resolve: + optional: true + checksum: db1a8ab2cb97ca19c01b1cfa9a9c8c69a143fde833c14df1fab0766f411b1148ff0df878adea09007ac6a2085ec116ba9a996a6ad104b1e58c20adbf88eed9b2 + languageName: node + linkType: hard + +"jest-regex-util@npm:30.0.1": + version: 30.0.1 + resolution: "jest-regex-util@npm:30.0.1" + checksum: fa8dac80c3e94db20d5e1e51d1bdf101cf5ede8f4e0b8f395ba8b8ea81e71804ffd747452a6bb6413032865de98ac656ef8ae43eddd18d980b6442a2764ed562 + languageName: node + linkType: hard + +"jest-regex-util@npm:^29.0.0, jest-regex-util@npm:^29.6.3": version: 29.6.3 resolution: "jest-regex-util@npm:29.6.3" checksum: 0518beeb9bf1228261695e54f0feaad3606df26a19764bc19541e0fc6e2a3737191904607fb72f3f2ce85d9c16b28df79b7b1ec9443aa08c3ef0e9efda6f8f2a languageName: node linkType: hard +"jest-resolve-dependencies@npm:30.3.0": + version: 30.3.0 + resolution: "jest-resolve-dependencies@npm:30.3.0" + dependencies: + jest-regex-util: 30.0.1 + jest-snapshot: 30.3.0 + checksum: 3cecd5d952b03b38a425ccc6d4c98bba3f9413e268cc01a2724c2c91e45be7772ccf7ff38e17c417f3cc8a0ffaefd86cc2b61a7594d045dd2c4b06bdd031e882 + languageName: node + linkType: hard + +"jest-resolve@npm:30.3.0": + version: 30.3.0 + resolution: "jest-resolve@npm:30.3.0" + dependencies: + chalk: ^4.1.2 + graceful-fs: ^4.2.11 + jest-haste-map: 30.3.0 + jest-pnp-resolver: ^1.2.3 + jest-util: 30.3.0 + jest-validate: 30.3.0 + slash: ^3.0.0 + unrs-resolver: ^1.7.11 + checksum: b48dae837c85251897d423fdd636c53e1490e04b2ca1bed3906d91abab1bb0c1045166337362dba197520a86bae9aace22875678361a8663149c85f6aaabcf18 + languageName: node + linkType: hard + +"jest-runner@npm:30.3.0": + version: 30.3.0 + resolution: "jest-runner@npm:30.3.0" + dependencies: + "@jest/console": 30.3.0 + "@jest/environment": 30.3.0 + "@jest/test-result": 30.3.0 + "@jest/transform": 30.3.0 + "@jest/types": 30.3.0 + "@types/node": "*" + chalk: ^4.1.2 + emittery: ^0.13.1 + exit-x: ^0.2.2 + graceful-fs: ^4.2.11 + jest-docblock: 30.2.0 + jest-environment-node: 30.3.0 + jest-haste-map: 30.3.0 + jest-leak-detector: 30.3.0 + jest-message-util: 30.3.0 + jest-resolve: 30.3.0 + jest-runtime: 30.3.0 + jest-util: 30.3.0 + jest-watcher: 30.3.0 + jest-worker: 30.3.0 + p-limit: ^3.1.0 + source-map-support: 0.5.13 + checksum: 098ff88b4090d40a38901c314333ae9c7a6523b13e4e6378dbd323fd648fcb4948e6fab2d3eaa0e784f984910ace0c104eb8dced45e1f6fabd57a88daa90f4bd + languageName: node + linkType: hard + +"jest-runtime@npm:30.3.0": + version: 30.3.0 + resolution: "jest-runtime@npm:30.3.0" + dependencies: + "@jest/environment": 30.3.0 + "@jest/fake-timers": 30.3.0 + "@jest/globals": 30.3.0 + "@jest/source-map": 30.0.1 + "@jest/test-result": 30.3.0 + "@jest/transform": 30.3.0 + "@jest/types": 30.3.0 + "@types/node": "*" + chalk: ^4.1.2 + cjs-module-lexer: ^2.1.0 + collect-v8-coverage: ^1.0.2 + glob: ^10.5.0 + graceful-fs: ^4.2.11 + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + slash: ^3.0.0 + strip-bom: ^4.0.0 + checksum: 6f2ff5f3154d1481ef09ffc9760181b7636243c3bb6d1b3047f607a16f41dd248f6d00bed4cd846f1834ab0e58d48e5db444170f2f8b63e91ab36cfeaa6390a0 + languageName: node + linkType: hard + +"jest-snapshot@npm:30.3.0": + version: 30.3.0 + resolution: "jest-snapshot@npm:30.3.0" + dependencies: + "@babel/core": ^7.27.4 + "@babel/generator": ^7.27.5 + "@babel/plugin-syntax-jsx": ^7.27.1 + "@babel/plugin-syntax-typescript": ^7.27.1 + "@babel/types": ^7.27.3 + "@jest/expect-utils": 30.3.0 + "@jest/get-type": 30.1.0 + "@jest/snapshot-utils": 30.3.0 + "@jest/transform": 30.3.0 + "@jest/types": 30.3.0 + babel-preset-current-node-syntax: ^1.2.0 + chalk: ^4.1.2 + expect: 30.3.0 + graceful-fs: ^4.2.11 + jest-diff: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + pretty-format: 30.3.0 + semver: ^7.7.2 + synckit: ^0.11.8 + checksum: cc7b8562eff4da6ebedfad3f9f672433da3c5faf56036d7baccaf095b22112788a4d9614caf8b6e5055f266e55871cdf4672787c2802360cfd1d84dadcdd18d5 + languageName: node + linkType: hard + +"jest-snapshot@npm:^29.2.1, jest-snapshot@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-snapshot@npm:29.7.0" + dependencies: + "@babel/core": ^7.11.6 + "@babel/generator": ^7.7.2 + "@babel/plugin-syntax-jsx": ^7.7.2 + "@babel/plugin-syntax-typescript": ^7.7.2 + "@babel/types": ^7.3.3 + "@jest/expect-utils": ^29.7.0 + "@jest/transform": ^29.7.0 + "@jest/types": ^29.6.3 + babel-preset-current-node-syntax: ^1.0.0 + chalk: ^4.0.0 + expect: ^29.7.0 + graceful-fs: ^4.2.9 + jest-diff: ^29.7.0 + jest-get-type: ^29.6.3 + jest-matcher-utils: ^29.7.0 + jest-message-util: ^29.7.0 + jest-util: ^29.7.0 + natural-compare: ^1.4.0 + pretty-format: ^29.7.0 + semver: ^7.5.3 + checksum: 86821c3ad0b6899521ce75ee1ae7b01b17e6dfeff9166f2cf17f012e0c5d8c798f30f9e4f8f7f5bed01ea7b55a6bc159f5eda778311162cbfa48785447c237ad + languageName: node + linkType: hard + +"jest-util@npm:30.3.0": + version: 30.3.0 + resolution: "jest-util@npm:30.3.0" + dependencies: + "@jest/types": 30.3.0 + "@types/node": "*" + chalk: ^4.1.2 + ci-info: ^4.2.0 + graceful-fs: ^4.2.11 + picomatch: ^4.0.3 + checksum: 27309dd3cb6f495c005bddf5910acabcdae4919f4646bd0d0ea66a8c60d810cc111665de99cc4f6420dd1a3e695f274e9def6301ef3b22bcce67d4541e2d53e4 + languageName: node + linkType: hard + "jest-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-util@npm:29.7.0" @@ -5899,6 +7934,20 @@ __metadata: languageName: node linkType: hard +"jest-validate@npm:30.3.0": + version: 30.3.0 + resolution: "jest-validate@npm:30.3.0" + dependencies: + "@jest/get-type": 30.1.0 + "@jest/types": 30.3.0 + camelcase: ^6.3.0 + chalk: ^4.1.2 + leven: ^3.1.0 + pretty-format: 30.3.0 + checksum: 5a76ae7f253da5707065225416b75470aa9445b32e01a7e559399a7971e5ccfdf71f4768e45c33565e4895df4998bb8a292dda7be1c723318c5c6ab334cae7da + languageName: node + linkType: hard + "jest-validate@npm:^29.7.0": version: 29.7.0 resolution: "jest-validate@npm:29.7.0" @@ -5913,6 +7962,79 @@ __metadata: languageName: node linkType: hard +"jest-watch-select-projects@npm:^2.0.0": + version: 2.0.0 + resolution: "jest-watch-select-projects@npm:2.0.0" + dependencies: + ansi-escapes: ^4.3.0 + chalk: ^3.0.0 + prompts: ^2.2.1 + checksum: 67b7a08d8e7b5ecfba67d86f02be29e4917c4416c9f169246f10cc40792b1c5fa38fcfeb25195643db080ace1f4fdf2f827bd244e7cdff7512d1ddfbc94270f0 + languageName: node + linkType: hard + +"jest-watch-typeahead@npm:2.2.1": + version: 2.2.1 + resolution: "jest-watch-typeahead@npm:2.2.1" + dependencies: + ansi-escapes: ^6.0.0 + chalk: ^4.0.0 + jest-regex-util: ^29.0.0 + jest-watcher: ^29.0.0 + slash: ^5.0.0 + string-length: ^5.0.1 + strip-ansi: ^7.0.1 + peerDependencies: + jest: ^27.0.0 || ^28.0.0 || ^29.0.0 + checksum: 5ba8068209da273187065b8900495ca9d0fce13b090d2e0193e1b862f7e920ca808f8a0c4c2ea504e1646d38519083276fbb304dba728e16b9126c0734f8f8ee + languageName: node + linkType: hard + +"jest-watcher@npm:30.3.0": + version: 30.3.0 + resolution: "jest-watcher@npm:30.3.0" + dependencies: + "@jest/test-result": 30.3.0 + "@jest/types": 30.3.0 + "@types/node": "*" + ansi-escapes: ^4.3.2 + chalk: ^4.1.2 + emittery: ^0.13.1 + jest-util: 30.3.0 + string-length: ^4.0.2 + checksum: c3aad544fc29bc17f3d4cf6e6d6f1180e6d1eae36881968a2da16fc14ac03b7a10761e77971d3d12dba1852c34e34907c81eab5be8757568cb9502d204942fbd + languageName: node + linkType: hard + +"jest-watcher@npm:^29.0.0": + version: 29.7.0 + resolution: "jest-watcher@npm:29.7.0" + dependencies: + "@jest/test-result": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + ansi-escapes: ^4.2.1 + chalk: ^4.0.0 + emittery: ^0.13.1 + jest-util: ^29.7.0 + string-length: ^4.0.1 + checksum: 67e6e7fe695416deff96b93a14a561a6db69389a0667e9489f24485bb85e5b54e12f3b2ba511ec0b777eca1e727235b073e3ebcdd473d68888650489f88df92f + languageName: node + linkType: hard + +"jest-worker@npm:30.3.0": + version: 30.3.0 + resolution: "jest-worker@npm:30.3.0" + dependencies: + "@types/node": "*" + "@ungap/structured-clone": ^1.3.0 + jest-util: 30.3.0 + merge-stream: ^2.0.0 + supports-color: ^8.1.1 + checksum: 886803abcda389324aa824d1c0b800f7acd2eeb70b8ff364cecae22019dfd34157a99fe37e465b7a85bd28746591cee04ea85f2dc86002c44583dd8cf1250d20 + languageName: node + linkType: hard + "jest-worker@npm:^29.7.0": version: 29.7.0 resolution: "jest-worker@npm:29.7.0" @@ -5925,6 +8047,25 @@ __metadata: languageName: node linkType: hard +"jest@npm:^30.3.0": + version: 30.3.0 + resolution: "jest@npm:30.3.0" + dependencies: + "@jest/core": 30.3.0 + "@jest/types": 30.3.0 + import-local: ^3.2.0 + jest-cli: 30.3.0 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: ./bin/jest.js + checksum: b79fafc8e06b9b5727f7b13c0e230f904c40c642d663a0f1f58de358dda4ba5dcbbc5ae398a461296b77a7f5d9d75f5d8dc659905c8183b21ebf3584c882e547 + languageName: node + linkType: hard + "jimp-compact@npm:0.16.1": version: 0.16.1 resolution: "jimp-compact@npm:0.16.1" @@ -6016,6 +8157,45 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:^20.0.0": + version: 20.0.3 + resolution: "jsdom@npm:20.0.3" + dependencies: + abab: ^2.0.6 + acorn: ^8.8.1 + acorn-globals: ^7.0.0 + cssom: ^0.5.0 + cssstyle: ^2.3.0 + data-urls: ^3.0.2 + decimal.js: ^10.4.2 + domexception: ^4.0.0 + escodegen: ^2.0.0 + form-data: ^4.0.0 + html-encoding-sniffer: ^3.0.0 + http-proxy-agent: ^5.0.0 + https-proxy-agent: ^5.0.1 + is-potential-custom-element-name: ^1.0.1 + nwsapi: ^2.2.2 + parse5: ^7.1.1 + saxes: ^6.0.0 + symbol-tree: ^3.2.4 + tough-cookie: ^4.1.2 + w3c-xmlserializer: ^4.0.0 + webidl-conversions: ^7.0.0 + whatwg-encoding: ^2.0.0 + whatwg-mimetype: ^3.0.0 + whatwg-url: ^11.0.0 + ws: ^8.11.0 + xml-name-validator: ^4.0.0 + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: 6e2ae21db397133a061b270c26d2dbc0b9051733ea3b896a7ece78d79f475ff0974f766a413c1198a79c793159119169f2335ddb23150348fbfdcfa6f3105536 + languageName: node + linkType: hard + "jsesc@npm:^3.0.2": version: 3.1.0 resolution: "jsesc@npm:3.1.0" @@ -6319,6 +8499,13 @@ __metadata: languageName: node linkType: hard +"lodash@npm:^4.17.19": + version: 4.17.23 + resolution: "lodash@npm:4.17.23" + checksum: 7daad39758a72872e94651630fbb54ba76868f904211089721a64516ce865506a759d9ad3d8ff22a2a49a50a09db5d27c36f22762d21766e47e3ba918d6d7bab + languageName: node + linkType: hard + "log-symbols@npm:^2.2.0": version: 2.2.0 resolution: "log-symbols@npm:2.2.0" @@ -6381,6 +8568,15 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: ^7.5.3 + checksum: bf0731a2dd3aab4db6f3de1585cea0b746bb73eb5a02e3d8d72757e376e64e6ada190b1eddcde5b2f24a81b688a9897efd5018737d05e02e2a671dda9cff8a8a + languageName: node + linkType: hard + "make-fetch-happen@npm:^14.0.3": version: 14.0.3 resolution: "make-fetch-happen@npm:14.0.3" @@ -6431,6 +8627,13 @@ __metadata: languageName: node linkType: hard +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 0e513b29d120f478c85a70f49da0b8b19bc638975eca466f2eeae0071f3ad00454c621bf66e16dd435896c208e719fc91ad79bbfba4e400fe0b372e7c1c9c9a2 + languageName: node + linkType: hard + "mdn-data@npm:2.0.14": version: 2.0.14 resolution: "mdn-data@npm:2.0.14" @@ -7180,7 +9383,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.27, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -7205,6 +9408,20 @@ __metadata: languageName: node linkType: hard +"mimic-fn@npm:^2.1.0": + version: 2.1.0 + resolution: "mimic-fn@npm:2.1.0" + checksum: d2421a3444848ce7f84bd49115ddacff29c15745db73f54041edc906c14b131a38d05298dae3081667627a59b2eb1ca4b436ff2e1b80f69679522410418b478a + languageName: node + linkType: hard + +"min-indent@npm:^1.0.0": + version: 1.0.1 + resolution: "min-indent@npm:1.0.1" + checksum: bfc6dd03c5eaf623a4963ebd94d087f6f4bbbfd8c41329a7f09706b0cb66969c4ddd336abeb587bc44bc6f08e13bf90f0b374f9d71f9f01e04adc2cd6f083ef1 + languageName: node + linkType: hard + "minimatch@npm:^10.1.1": version: 10.1.1 resolution: "minimatch@npm:10.1.1" @@ -7396,6 +9613,22 @@ __metadata: languageName: node linkType: hard +"napi-postinstall@npm:^0.3.0": + version: 0.3.4 + resolution: "napi-postinstall@npm:0.3.4" + bin: + napi-postinstall: lib/cli.js + checksum: 01672ae6568e2b3a6d985371f1504a6e1c791aa308b94c9f89736fde8251b7b8ab3227d1a5ede8d0eb0552099e069970b038c6958052c01b2bdc5aae31f0a88c + languageName: node + linkType: hard + +"natural-compare@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare@npm:1.4.0" + checksum: 23ad088b08f898fc9b53011d7bb78ec48e79de7627e01ab5518e806033861bef68d5b0cd0e2205c2f36690ac9571ff6bcb05eb777ced2eeda8d4ac5b44592c3d + languageName: node + linkType: hard + "negotiator@npm:0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" @@ -7528,6 +9761,15 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^4.0.1": + version: 4.0.1 + resolution: "npm-run-path@npm:4.0.1" + dependencies: + path-key: ^3.0.0 + checksum: 5374c0cea4b0bbfdfae62da7bbdf1e1558d338335f4cacf2515c282ff358ff27b2ecb91ffa5330a8b14390ac66a1e146e10700440c1ab868208430f56b5f4d23 + languageName: node + linkType: hard + "nth-check@npm:^2.0.1": version: 2.1.1 resolution: "nth-check@npm:2.1.1" @@ -7544,6 +9786,13 @@ __metadata: languageName: node linkType: hard +"nwsapi@npm:^2.2.2": + version: 2.2.23 + resolution: "nwsapi@npm:2.2.23" + checksum: 7af519de08381df9dc0c913d817255cb21e33671641603f6cdabe8cb04b18b32aca1477fdc5dfe08b2039125afa3216d3ef01a3c2603a97d114e842d9414e0c3 + languageName: node + linkType: hard + "ob1@npm:0.81.5": version: 0.81.5 resolution: "ob1@npm:0.81.5" @@ -7621,6 +9870,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^5.1.2": + version: 5.1.2 + resolution: "onetime@npm:5.1.2" + dependencies: + mimic-fn: ^2.1.0 + checksum: 2478859ef817fc5d4e9c2f9e5728512ddd1dbc9fb7829ad263765bb6d3b91ce699d6e2332eef6b7dff183c2f490bd3349f1666427eaba4469fba0ac38dfd0d34 + languageName: node + linkType: hard + "open@npm:^7.0.3": version: 7.4.2 resolution: "open@npm:7.4.2" @@ -7798,6 +10056,15 @@ __metadata: languageName: node linkType: hard +"parse5@npm:^7.0.0, parse5@npm:^7.1.1": + version: 7.3.0 + resolution: "parse5@npm:7.3.0" + dependencies: + entities: ^6.0.0 + checksum: ffd040c4695d93f0bc370e3d6d75c1b352178514af41be7afa212475ea5cead1d6e377cd9d4cec6a5e2bcf497ca50daf9e0088eadaa37dbc271f60def08fdfcd + languageName: node + linkType: hard + "parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" @@ -7833,7 +10100,7 @@ __metadata: languageName: node linkType: hard -"path-key@npm:^3.1.0": +"path-key@npm:^3.0.0, path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" checksum: 55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 @@ -7916,7 +10183,7 @@ __metadata: languageName: node linkType: hard -"pirates@npm:^4.0.1, pirates@npm:^4.0.4, pirates@npm:^4.0.6": +"pirates@npm:^4.0.1, pirates@npm:^4.0.4, pirates@npm:^4.0.6, pirates@npm:^4.0.7": version: 4.0.7 resolution: "pirates@npm:4.0.7" checksum: 3dcbaff13c8b5bc158416feb6dc9e49e3c6be5fddc1ea078a05a73ef6b85d79324bbb1ef59b954cdeff000dbf000c1d39f32dc69310c7b78fbada5171b583e40 @@ -7932,6 +10199,15 @@ __metadata: languageName: node linkType: hard +"pkg-dir@npm:^4.2.0": + version: 4.2.0 + resolution: "pkg-dir@npm:4.2.0" + dependencies: + find-up: ^4.0.0 + checksum: 9863e3f35132bf99ae1636d31ff1e1e3501251d480336edb1c211133c8d58906bed80f154a1d723652df1fda91e01c7442c2eeaf9dc83157c7ae89087e43c8d6 + languageName: node + linkType: hard + "plist@npm:^3.0.5": version: 3.1.0 resolution: "plist@npm:3.1.0" @@ -7982,7 +10258,18 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^29.7.0": +"pretty-format@npm:30.3.0, pretty-format@npm:^30.0.0, pretty-format@npm:^30.0.5": + version: 30.3.0 + resolution: "pretty-format@npm:30.3.0" + dependencies: + "@jest/schemas": 30.0.5 + ansi-styles: ^5.2.0 + react-is: ^18.3.1 + checksum: 99bb09b51551fb710143a2c0b8270acc2f7723d51cdb5824fe55a0061e04af8bf07c47acd565121cb5b266dfc7a3ecfe77bf85b4f96aacbbeb6f2df0ba246a9c + languageName: node + linkType: hard + +"pretty-format@npm:^29.0.3, pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" dependencies: @@ -8010,6 +10297,9 @@ __metadata: "@react-native/metro-config": ^0.76.3 "@react-navigation/drawer": ^7.5.6 "@swmansion/react-native-detour": ^0.4.0 + "@testing-library/jest-native": ^5.4.3 + "@testing-library/react-native": ^13.3.3 + "@types/jest": ^30.0.0 "@types/react": ~19.1.10 expo: ^54.0.0 expo-application: ~7.0.8 @@ -8028,6 +10318,8 @@ __metadata: expo-splash-screen: ~31.0.12 expo-sqlite: ~16.0.10 expo-status-bar: ~3.0.9 + jest: ^30.3.0 + jest-expo: ^55.0.11 metro-config: ^0.81.0 react: 19.1.0 react-native: 0.81.5 @@ -8050,6 +10342,7 @@ __metadata: react-native-toast-message: ^2.3.0 react-native-wheel-scrollview-picker: ^2.0.6 react-native-worklets: 0.5.1 + react-test-renderer: 19.1.0 typescript: ~5.9.2 zustand: ^5.0.4 languageName: unknown @@ -8095,7 +10388,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.3.2": +"prompts@npm:^2.2.1, prompts@npm:^2.3.2": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -8116,13 +10409,29 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.1": +"psl@npm:^1.1.33": + version: 1.15.0 + resolution: "psl@npm:1.15.0" + dependencies: + punycode: ^2.3.1 + checksum: 6f777d82eecfe1c2406dadbc15e77467b186fec13202ec887a45d0209a2c6fca530af94a462a477c3c4a767ad892ec9ede7c482d98f61f653dd838b50e89dc15 + languageName: node + linkType: hard + +"punycode@npm:^2.1.1, punycode@npm:^2.3.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: bb0a0ceedca4c3c57a9b981b90601579058903c62be23c5e8e843d2c2d4148a3ecf029d5133486fb0e1822b098ba8bba09e89d6b21742d02fa26bda6441a6fb2 languageName: node linkType: hard +"pure-rand@npm:^7.0.0": + version: 7.0.1 + resolution: "pure-rand@npm:7.0.1" + checksum: 4f543b97a487857a791b8e4c139aad54937397dc8177f1353f7da88556bfa40f5c32bfce3856843b1c3fc3a00b8472cceb22957c10b21c14e59e36a02ec9353b + languageName: node + linkType: hard + "qrcode-terminal@npm:0.11.0": version: 0.11.0 resolution: "qrcode-terminal@npm:0.11.0" @@ -8144,6 +10453,13 @@ __metadata: languageName: node linkType: hard +"querystringify@npm:^2.1.1": + version: 2.2.0 + resolution: "querystringify@npm:2.2.0" + checksum: 5641ea231bad7ef6d64d9998faca95611ed4b11c2591a8cae741e178a974f6a8e0ebde008475259abe1621cb15e692404e6b6626e927f7b849d5c09392604b15 + languageName: node + linkType: hard + "queue@npm:6.0.2": version: 6.0.2 resolution: "queue@npm:6.0.2" @@ -8207,7 +10523,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^18.0.0": +"react-is@npm:^18.0.0, react-is@npm:^18.3.1": version: 18.3.1 resolution: "react-is@npm:18.3.1" checksum: e20fe84c86ff172fc8d898251b7cc2c43645d108bf96d0b8edf39b98f9a2cae97b40520ee7ed8ee0085ccc94736c4886294456033304151c3f94978cec03df21 @@ -8221,6 +10537,13 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^19.2.0": + version: 19.2.4 + resolution: "react-is@npm:19.2.4" + checksum: 646d4f0f11b50131f3f7ac5b50d12b79de35be20fa84bce33fd3fc91557589e388562cb6203b5237098fd983cd1cdf12c5fb952b7f9903ac014de9a5a6cc7e28 + languageName: node + linkType: hard + "react-native-audio-api@npm:^0.11.7": version: 0.11.7 resolution: "react-native-audio-api@npm:0.11.7" @@ -8628,6 +10951,30 @@ __metadata: languageName: node linkType: hard +"react-test-renderer@npm:19.1.0": + version: 19.1.0 + resolution: "react-test-renderer@npm:19.1.0" + dependencies: + react-is: ^19.1.0 + scheduler: ^0.26.0 + peerDependencies: + react: ^19.1.0 + checksum: e060239f8fbfbe54c2fa0e1d7a9eb1ee77787b0b29fd75ba6170dc22cd637c3a9052a82285db6be5a269309a65b8e8c40ddc8103620fbe1652cce693fd608acc + languageName: node + linkType: hard + +"react-test-renderer@npm:19.2.0": + version: 19.2.0 + resolution: "react-test-renderer@npm:19.2.0" + dependencies: + react-is: ^19.2.0 + scheduler: ^0.27.0 + peerDependencies: + react: ^19.2.0 + checksum: f33306398e6eda59df37a392af845712566465b478b7a5bad57f4a1642e96b5ccbe3f8bebcea22a298311a6a8cb2b2d809ce92a85bb2adc0f5cc1a7878feba33 + languageName: node + linkType: hard + "react@npm:19.1.0": version: 19.1.0 resolution: "react@npm:19.1.0" @@ -8647,6 +10994,16 @@ __metadata: languageName: node linkType: hard +"redent@npm:^3.0.0": + version: 3.0.0 + resolution: "redent@npm:3.0.0" + dependencies: + indent-string: ^4.0.0 + strip-indent: ^3.0.0 + checksum: fa1ef20404a2d399235e83cc80bd55a956642e37dd197b4b612ba7327bf87fa32745aeb4a1634b2bab25467164ab4ed9c15be2c307923dd08b0fe7c52431ae6b + languageName: node + linkType: hard + "regenerate-unicode-properties@npm:^10.2.0": version: 10.2.0 resolution: "regenerate-unicode-properties@npm:10.2.0" @@ -8727,6 +11084,22 @@ __metadata: languageName: node linkType: hard +"requires-port@npm:^1.0.0": + version: 1.0.0 + resolution: "requires-port@npm:1.0.0" + checksum: eee0e303adffb69be55d1a214e415cf42b7441ae858c76dfc5353148644f6fd6e698926fc4643f510d5c126d12a705e7c8ed7e38061113bdf37547ab356797ff + languageName: node + linkType: hard + +"resolve-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "resolve-cwd@npm:3.0.0" + dependencies: + resolve-from: ^5.0.0 + checksum: 546e0816012d65778e580ad62b29e975a642989108d9a3c5beabfb2304192fa3c9f9146fbdfe213563c6ff51975ae41bac1d3c6e047dd9572c94863a057b4d81 + languageName: node + linkType: hard + "resolve-from@npm:^3.0.0": version: 3.0.0 resolution: "resolve-from@npm:3.0.0" @@ -8889,13 +11262,29 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:0.26.0": +"saxes@npm:^6.0.0": + version: 6.0.0 + resolution: "saxes@npm:6.0.0" + dependencies: + xmlchars: ^2.2.0 + checksum: d3fa3e2aaf6c65ed52ee993aff1891fc47d5e47d515164b5449cbf5da2cbdc396137e55590472e64c5c436c14ae64a8a03c29b9e7389fc6f14035cf4e982ef3b + languageName: node + linkType: hard + +"scheduler@npm:0.26.0, scheduler@npm:^0.26.0": version: 0.26.0 resolution: "scheduler@npm:0.26.0" checksum: c63a9f1c0e5089b537231cff6c11f75455b5c8625ae09535c1d7cd0a1b0c77ceecdd9f1074e5e063da5d8dc11e73e8033dcac3361791088be08a6e60c0283ed9 languageName: node linkType: hard +"scheduler@npm:^0.27.0": + version: 0.27.0 + resolution: "scheduler@npm:0.27.0" + checksum: 92644ead0a9443e20f9d24132fe93675b156209b9eeb35ea245f8a86768d0cc0fcca56f341eeef21d9b6dd8e72d6d5e260eb5a41d34b05cd605dd45a29f572ef + languageName: node + linkType: hard + "semver@npm:7.7.2, semver@npm:^7.1.3, semver@npm:^7.3.5, semver@npm:^7.5.4, semver@npm:^7.6.0": version: 7.7.2 resolution: "semver@npm:7.7.2" @@ -8923,21 +11312,21 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.6.3": - version: 7.7.3 - resolution: "semver@npm:7.7.3" +"semver@npm:^7.5.3, semver@npm:^7.7.2, semver@npm:^7.7.3": + version: 7.7.4 + resolution: "semver@npm:7.7.4" bin: semver: bin/semver.js - checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 + checksum: 9b4a6a58e98b9723fafcafa393c9d4e8edefaa60b8dfbe39e30892a3604cf1f45f52df9cfb1ae1a22b44c8b3d57fec8a9bb7b3e1645431587cb272399ede152e languageName: node linkType: hard -"semver@npm:^7.7.3": - version: 7.7.4 - resolution: "semver@npm:7.7.4" +"semver@npm:^7.6.3": + version: 7.7.3 + resolution: "semver@npm:7.7.3" bin: semver: bin/semver.js - checksum: 9b4a6a58e98b9723fafcafa393c9d4e8edefaa60b8dfbe39e30892a3604cf1f45f52df9cfb1ae1a22b44c8b3d57fec8a9bb7b3e1645431587cb272399ede152e + checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 languageName: node linkType: hard @@ -9071,7 +11460,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -9126,6 +11515,13 @@ __metadata: languageName: node linkType: hard +"slash@npm:^5.0.0": + version: 5.1.0 + resolution: "slash@npm:5.1.0" + checksum: 70434b34c50eb21b741d37d455110258c42d2cf18c01e6518aeb7299f3c6e626330c889c0c552b5ca2ef54a8f5a74213ab48895f0640717cacefeef6830a1ba4 + languageName: node + linkType: hard + "slugify@npm:^1.3.4, slugify@npm:^1.6.6": version: 1.6.6 resolution: "slugify@npm:1.6.6" @@ -9178,6 +11574,16 @@ __metadata: languageName: node linkType: hard +"source-map-support@npm:0.5.13": + version: 0.5.13 + resolution: "source-map-support@npm:0.5.13" + dependencies: + buffer-from: ^1.0.0 + source-map: ^0.6.0 + checksum: 933550047b6c1a2328599a21d8b7666507427c0f5ef5eaadd56b5da0fd9505e239053c66fe181bf1df469a3b7af9d775778eee283cbb7ae16b902ddc09e93a97 + languageName: node + linkType: hard + "source-map-support@npm:^0.5.16, source-map-support@npm:~0.5.20, source-map-support@npm:~0.5.21": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" @@ -9188,6 +11594,13 @@ __metadata: languageName: node linkType: hard +"source-map@npm:0.5.6": + version: 0.5.6 + resolution: "source-map@npm:0.5.6" + checksum: 390b3f5165c9631a74fb6fb55ba61e62a7f9b7d4026ae0e2bfc2899c241d71c1bccb8731c496dc7f7cb79a5f523406eb03d8c5bebe8448ee3fc38168e2d209c8 + languageName: node + linkType: hard + "source-map@npm:^0.5.6": version: 0.5.7 resolution: "source-map@npm:0.5.7" @@ -9232,7 +11645,16 @@ __metadata: languageName: node linkType: hard -"stack-utils@npm:^2.0.3": +"stack-generator@npm:^2.0.5": + version: 2.0.10 + resolution: "stack-generator@npm:2.0.10" + dependencies: + stackframe: ^1.3.4 + checksum: 4fc3978a934424218a0aa9f398034e1f78153d5ff4f4ff9c62478c672debb47dd58de05b09fc3900530cbb526d72c93a6e6c9353bacc698e3b1c00ca3dda0c47 + languageName: node + linkType: hard + +"stack-utils@npm:^2.0.3, stack-utils@npm:^2.0.6": version: 2.0.6 resolution: "stack-utils@npm:2.0.6" dependencies: @@ -9248,6 +11670,27 @@ __metadata: languageName: node linkType: hard +"stacktrace-gps@npm:^3.0.4": + version: 3.1.2 + resolution: "stacktrace-gps@npm:3.1.2" + dependencies: + source-map: 0.5.6 + stackframe: ^1.3.4 + checksum: 85daa232d138239b6ae0f4bcdd87d15d302a045d93625db17614030945b5314e204b5fbcf9bee5b6f4f9e6af5fca05f65c27fe910894b861ef6853b99470aa1c + languageName: node + linkType: hard + +"stacktrace-js@npm:^2.0.2": + version: 2.0.2 + resolution: "stacktrace-js@npm:2.0.2" + dependencies: + error-stack-parser: ^2.0.6 + stack-generator: ^2.0.5 + stacktrace-gps: ^3.0.4 + checksum: 081e786d56188ac04ac6604c09cd863b3ca2b4300ec061366cf68c3e4ad9edaa34fb40deea03cc23a05f442aa341e9171f47313f19bd588f9bec6c505a396286 + languageName: node + linkType: hard + "stacktrace-parser@npm:^0.1.10": version: 0.1.11 resolution: "stacktrace-parser@npm:0.1.11" @@ -9285,6 +11728,26 @@ __metadata: languageName: node linkType: hard +"string-length@npm:^4.0.1, string-length@npm:^4.0.2": + version: 4.0.2 + resolution: "string-length@npm:4.0.2" + dependencies: + char-regex: ^1.0.2 + strip-ansi: ^6.0.0 + checksum: ce85533ef5113fcb7e522bcf9e62cb33871aa99b3729cec5595f4447f660b0cefd542ca6df4150c97a677d58b0cb727a3fe09ac1de94071d05526c73579bf505 + languageName: node + linkType: hard + +"string-length@npm:^5.0.1": + version: 5.0.1 + resolution: "string-length@npm:5.0.1" + dependencies: + char-regex: ^2.0.0 + strip-ansi: ^7.0.1 + checksum: 71f73b8c8a743e01dcd001bcf1b197db78d5e5e53b12bd898cddaf0961be09f947dfd8c429783db3694b55b05cb5a51de6406c5085ff1aaa10c4771440c8396d + languageName: node + linkType: hard + "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -9334,6 +11797,36 @@ __metadata: languageName: node linkType: hard +"strip-bom@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-bom@npm:4.0.0" + checksum: 9dbcfbaf503c57c06af15fe2c8176fb1bf3af5ff65003851a102749f875a6dbe0ab3b30115eccf6e805e9d756830d3e40ec508b62b3f1ddf3761a20ebe29d3f3 + languageName: node + linkType: hard + +"strip-final-newline@npm:^2.0.0": + version: 2.0.0 + resolution: "strip-final-newline@npm:2.0.0" + checksum: 69412b5e25731e1938184b5d489c32e340605bb611d6140344abc3421b7f3c6f9984b21dff296dfcf056681b82caa3bb4cc996a965ce37bcfad663e92eae9c64 + languageName: node + linkType: hard + +"strip-indent@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-indent@npm:3.0.0" + dependencies: + min-indent: ^1.0.0 + checksum: 18f045d57d9d0d90cd16f72b2313d6364fd2cb4bf85b9f593523ad431c8720011a4d5f08b6591c9d580f446e78855c5334a30fb91aa1560f5d9f95ed1b4a0530 + languageName: node + linkType: hard + +"strip-json-comments@npm:^3.1.1": + version: 3.1.1 + resolution: "strip-json-comments@npm:3.1.1" + checksum: 492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 + languageName: node + linkType: hard + "strip-json-comments@npm:~2.0.1": version: 2.0.1 resolution: "strip-json-comments@npm:2.0.1" @@ -9384,7 +11877,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.0.0": +"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -9434,6 +11927,22 @@ __metadata: languageName: node linkType: hard +"symbol-tree@npm:^3.2.4": + version: 3.2.4 + resolution: "symbol-tree@npm:3.2.4" + checksum: 6e8fc7e1486b8b54bea91199d9535bb72f10842e40c79e882fc94fb7b14b89866adf2fd79efa5ebb5b658bc07fb459ccce5ac0e99ef3d72f474e74aaf284029d + languageName: node + linkType: hard + +"synckit@npm:^0.11.8": + version: 0.11.12 + resolution: "synckit@npm:0.11.12" + dependencies: + "@pkgr/core": ^0.2.9 + checksum: a53fb563d01ba8912a111b883fc3c701e267896ff8273e7aba9001f5f74711e125888f4039e93060795cd416122cf492ae419eb10a6a3e3b00e830917669d2cf + languageName: node + linkType: hard + "tar@npm:^7.4.3": version: 7.4.3 resolution: "tar@npm:7.4.3" @@ -9580,6 +12089,27 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^4.1.2": + version: 4.1.4 + resolution: "tough-cookie@npm:4.1.4" + dependencies: + psl: ^1.1.33 + punycode: ^2.1.1 + universalify: ^0.2.0 + url-parse: ^1.5.3 + checksum: 5815059f014c31179a303c673f753f7899a6fce94ac93712c88ea5f3c26e0c042b5f0c7a599a00f8e0feeca4615dba75c3dffc54f3c1a489978aa8205e09307c + languageName: node + linkType: hard + +"tr46@npm:^3.0.0": + version: 3.0.0 + resolution: "tr46@npm:3.0.0" + dependencies: + punycode: ^2.1.1 + checksum: 44c3cc6767fb800490e6e9fd64fd49041aa4e49e1f6a012b34a75de739cc9ed3a6405296072c1df8b6389ae139c5e7c6496f659cfe13a04a4bff3a1422981270 + languageName: node + linkType: hard + "ts-interface-checker@npm:^0.1.9": version: 0.1.13 resolution: "ts-interface-checker@npm:0.1.13" @@ -9587,7 +12117,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: e4aba30e632b8c8902b47587fd13345e2827fa639e7c3121074d5ee0880723282411a8838f830b55100cbe4517672f84a2472667d355b81e8af165a55dc6203a @@ -9723,6 +12253,13 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^0.2.0": + version: 0.2.0 + resolution: "universalify@npm:0.2.0" + checksum: e86134cb12919d177c2353196a4cc09981524ee87abf621f7bc8d249dbbbebaec5e7d1314b96061497981350df786e4c5128dbf442eba104d6e765bc260678b5 + languageName: node + linkType: hard + "unpipe@npm:~1.0.0": version: 1.0.0 resolution: "unpipe@npm:1.0.0" @@ -9730,6 +12267,73 @@ __metadata: languageName: node linkType: hard +"unrs-resolver@npm:^1.7.11": + version: 1.11.1 + resolution: "unrs-resolver@npm:1.11.1" + dependencies: + "@unrs/resolver-binding-android-arm-eabi": 1.11.1 + "@unrs/resolver-binding-android-arm64": 1.11.1 + "@unrs/resolver-binding-darwin-arm64": 1.11.1 + "@unrs/resolver-binding-darwin-x64": 1.11.1 + "@unrs/resolver-binding-freebsd-x64": 1.11.1 + "@unrs/resolver-binding-linux-arm-gnueabihf": 1.11.1 + "@unrs/resolver-binding-linux-arm-musleabihf": 1.11.1 + "@unrs/resolver-binding-linux-arm64-gnu": 1.11.1 + "@unrs/resolver-binding-linux-arm64-musl": 1.11.1 + "@unrs/resolver-binding-linux-ppc64-gnu": 1.11.1 + "@unrs/resolver-binding-linux-riscv64-gnu": 1.11.1 + "@unrs/resolver-binding-linux-riscv64-musl": 1.11.1 + "@unrs/resolver-binding-linux-s390x-gnu": 1.11.1 + "@unrs/resolver-binding-linux-x64-gnu": 1.11.1 + "@unrs/resolver-binding-linux-x64-musl": 1.11.1 + "@unrs/resolver-binding-wasm32-wasi": 1.11.1 + "@unrs/resolver-binding-win32-arm64-msvc": 1.11.1 + "@unrs/resolver-binding-win32-ia32-msvc": 1.11.1 + "@unrs/resolver-binding-win32-x64-msvc": 1.11.1 + napi-postinstall: ^0.3.0 + dependenciesMeta: + "@unrs/resolver-binding-android-arm-eabi": + optional: true + "@unrs/resolver-binding-android-arm64": + optional: true + "@unrs/resolver-binding-darwin-arm64": + optional: true + "@unrs/resolver-binding-darwin-x64": + optional: true + "@unrs/resolver-binding-freebsd-x64": + optional: true + "@unrs/resolver-binding-linux-arm-gnueabihf": + optional: true + "@unrs/resolver-binding-linux-arm-musleabihf": + optional: true + "@unrs/resolver-binding-linux-arm64-gnu": + optional: true + "@unrs/resolver-binding-linux-arm64-musl": + optional: true + "@unrs/resolver-binding-linux-ppc64-gnu": + optional: true + "@unrs/resolver-binding-linux-riscv64-gnu": + optional: true + "@unrs/resolver-binding-linux-riscv64-musl": + optional: true + "@unrs/resolver-binding-linux-s390x-gnu": + optional: true + "@unrs/resolver-binding-linux-x64-gnu": + optional: true + "@unrs/resolver-binding-linux-x64-musl": + optional: true + "@unrs/resolver-binding-wasm32-wasi": + optional: true + "@unrs/resolver-binding-win32-arm64-msvc": + optional: true + "@unrs/resolver-binding-win32-ia32-msvc": + optional: true + "@unrs/resolver-binding-win32-x64-msvc": + optional: true + checksum: 10f829c06c30d041eaf6a8a7fd59268f1cad5b723f1399f1ec64f0d79be2809f6218209d06eab32a3d0fcd7d56034874f3a3f95292fdb53fa1f8279de8fcb0c5 + languageName: node + linkType: hard + "update-browserslist-db@npm:^1.1.3": version: 1.1.3 resolution: "update-browserslist-db@npm:1.1.3" @@ -9758,6 +12362,16 @@ __metadata: languageName: node linkType: hard +"url-parse@npm:^1.5.3": + version: 1.5.10 + resolution: "url-parse@npm:1.5.10" + dependencies: + querystringify: ^2.1.1 + requires-port: ^1.0.0 + checksum: fbdba6b1d83336aca2216bbdc38ba658d9cfb8fc7f665eb8b17852de638ff7d1a162c198a8e4ed66001ddbf6c9888d41e4798912c62b4fd777a31657989f7bdf + languageName: node + linkType: hard + "use-callback-ref@npm:^1.3.3": version: 1.3.3 resolution: "use-callback-ref@npm:1.3.3" @@ -9841,6 +12455,17 @@ __metadata: languageName: node linkType: hard +"v8-to-istanbul@npm:^9.0.1": + version: 9.3.0 + resolution: "v8-to-istanbul@npm:9.3.0" + dependencies: + "@jridgewell/trace-mapping": ^0.3.12 + "@types/istanbul-lib-coverage": ^2.0.1 + convert-source-map: ^2.0.0 + checksum: ded42cd535d92b7fd09a71c4c67fb067487ef5551cc227bfbf2a1f159a842e4e4acddaef20b955789b8d3b455b9779d036853f4a27ce15007f6364a4d30317ae + languageName: node + linkType: hard + "validate-npm-package-name@npm:^5.0.0": version: 5.0.1 resolution: "validate-npm-package-name@npm:5.0.1" @@ -9874,6 +12499,15 @@ __metadata: languageName: node linkType: hard +"w3c-xmlserializer@npm:^4.0.0": + version: 4.0.0 + resolution: "w3c-xmlserializer@npm:4.0.0" + dependencies: + xml-name-validator: ^4.0.0 + checksum: eba070e78deb408ae8defa4d36b429f084b2b47a4741c4a9be3f27a0a3d1845e277e3072b04391a138f7e43776842627d1334e448ff13ff90ad9fb1214ee7091 + languageName: node + linkType: hard + "walker@npm:^1.0.7, walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -9906,6 +12540,22 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^7.0.0": + version: 7.0.0 + resolution: "webidl-conversions@npm:7.0.0" + checksum: f05588567a2a76428515333eff87200fae6c83c3948a7482ebb109562971e77ef6dc49749afa58abb993391227c5697b3ecca52018793e0cb4620a48f10bd21b + languageName: node + linkType: hard + +"whatwg-encoding@npm:^2.0.0": + version: 2.0.0 + resolution: "whatwg-encoding@npm:2.0.0" + dependencies: + iconv-lite: 0.6.3 + checksum: 7087810c410aa9b689cbd6af8773341a53cdc1f3aae2a882c163bd5522ec8ca4cdfc269aef417a5792f411807d5d77d50df4c24e3abb00bb60192858a40cc675 + languageName: node + linkType: hard + "whatwg-fetch@npm:^3.0.0": version: 3.6.20 resolution: "whatwg-fetch@npm:3.6.20" @@ -9913,6 +12563,13 @@ __metadata: languageName: node linkType: hard +"whatwg-mimetype@npm:^3.0.0": + version: 3.0.0 + resolution: "whatwg-mimetype@npm:3.0.0" + checksum: ce08bbb36b6aaf64f3a84da89707e3e6a31e5ab1c1a2379fd68df79ba712a4ab090904f0b50e6693b0dafc8e6343a6157e40bf18fdffd26e513cf95ee2a59824 + languageName: node + linkType: hard + "whatwg-url-without-unicode@npm:8.0.0-3": version: 8.0.0-3 resolution: "whatwg-url-without-unicode@npm:8.0.0-3" @@ -9924,6 +12581,16 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^11.0.0": + version: 11.0.0 + resolution: "whatwg-url@npm:11.0.0" + dependencies: + tr46: ^3.0.0 + webidl-conversions: ^7.0.0 + checksum: ed4826aaa57e66bb3488a4b25c9cd476c46ba96052747388b5801f137dd740b73fde91ad207d96baf9f17fbcc80fc1a477ad65181b5eb5fa718d27c69501d7af + languageName: node + linkType: hard + "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -10003,6 +12670,16 @@ __metadata: languageName: node linkType: hard +"write-file-atomic@npm:^5.0.1": + version: 5.0.1 + resolution: "write-file-atomic@npm:5.0.1" + dependencies: + imurmurhash: ^0.1.4 + signal-exit: ^4.0.1 + checksum: 8dbb0e2512c2f72ccc20ccedab9986c7d02d04039ed6e8780c987dc4940b793339c50172a1008eed7747001bfacc0ca47562668a069a7506c46c77d7ba3926a9 + languageName: node + linkType: hard + "ws@npm:^6.2.3": version: 6.2.3 resolution: "ws@npm:6.2.3" @@ -10027,6 +12704,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.11.0": + version: 8.20.0 + resolution: "ws@npm:8.20.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 2b31d24a53690770564a033c21ea48390f84d23fbc5abc14b2bbec4e112846f2f3ca66caee769a73fb8bc89ba16b452a6911a553e9742bbc75bccb79e203953e + languageName: node + linkType: hard + "ws@npm:^8.12.1": version: 8.18.2 resolution: "ws@npm:8.18.2" @@ -10052,6 +12744,13 @@ __metadata: languageName: node linkType: hard +"xml-name-validator@npm:^4.0.0": + version: 4.0.0 + resolution: "xml-name-validator@npm:4.0.0" + checksum: af100b79c29804f05fa35aa3683e29a321db9b9685d5e5febda3fa1e40f13f85abc40f45a6b2bf7bee33f68a1dc5e8eaef4cec100a304a9db565e6061d4cb5ad + languageName: node + linkType: hard + "xml2js@npm:0.6.0": version: 0.6.0 resolution: "xml2js@npm:0.6.0" @@ -10076,6 +12775,13 @@ __metadata: languageName: node linkType: hard +"xmlchars@npm:^2.2.0": + version: 2.2.0 + resolution: "xmlchars@npm:2.2.0" + checksum: 8c70ac94070ccca03f47a81fcce3b271bd1f37a591bf5424e787ae313fcb9c212f5f6786e1fa82076a2c632c0141552babcd85698c437506dfa6ae2d58723062 + languageName: node + linkType: hard + "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" @@ -10120,7 +12826,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.6.2": +"yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: