import { act, fireEvent, render, screen } from '@testing-library/react-native'; import React from 'react'; import SettingsScreen from '../app/settings/index'; import * as api from '../services/lmStudioApi'; import { DEFAULT_SERVER_CONFIG } from '../services/types'; import { useAppStore } from '../store/useAppStore'; jest.mock('../services/lmStudioApi'); let mockSystemScheme: 'light' | 'dark' = 'light'; jest.mock('../hooks/useColorScheme', () => ({ useColorScheme: () => mockSystemScheme, })); const mockApi = api as jest.Mocked; const state = () => useAppStore.getState(); const hostInput = () => screen.getByPlaceholderText('localhost'); const portInput = () => screen.getByPlaceholderText('1234'); const keyInput = () => screen.getByPlaceholderText('Leave empty for local'); beforeEach(() => { jest.resetAllMocks(); mockSystemScheme = 'light'; useAppStore.setState(useAppStore.getInitialState(), true); mockApi.getModels.mockResolvedValue([]); }); describe('SettingsScreen connection form', () => { it('is filled from the stored configuration and hides "Save Changes" until something changes', () => { useAppStore.setState({ serverConfig: { host: '192.168.0.5', port: 4321, apiKey: 'k' } }); render(); expect(hostInput().props.value).toBe('192.168.0.5'); expect(portInput().props.value).toBe('4321'); expect(keyInput().props.value).toBe('k'); expect(screen.queryByText('Save Changes')).toBeNull(); }); it('shows "Save Changes" for each changed field', () => { render(); fireEvent.changeText(hostInput(), 'other'); expect(screen.getByText('Save Changes')).toBeTruthy(); fireEvent.changeText(hostInput(), DEFAULT_SERVER_CONFIG.host); expect(screen.queryByText('Save Changes')).toBeNull(); fireEvent.changeText(portInput(), '999'); expect(screen.getByText('Save Changes')).toBeTruthy(); fireEvent.changeText(portInput(), String(DEFAULT_SERVER_CONFIG.port)); expect(screen.queryByText('Save Changes')).toBeNull(); fireEvent.changeText(keyInput(), 'secret'); expect(screen.getByText('Save Changes')).toBeTruthy(); }); it('saves trimmed values', () => { render(); fireEvent.changeText(hostInput(), ' 192.168.0.9 '); fireEvent.changeText(portInput(), '8080'); fireEvent.changeText(keyInput(), ' key '); fireEvent.press(screen.getByText('Save Changes')); expect(state().serverConfig).toEqual({ host: '192.168.0.9', port: 8080, apiKey: 'key' }); }); it('falls back to localhost:1234 and no API key when the fields are blank or invalid', () => { useAppStore.setState({ serverConfig: { host: 'lan', port: 4321, apiKey: 'k' } }); render(); fireEvent.changeText(hostInput(), ' '); fireEvent.changeText(portInput(), 'abc'); fireEvent.changeText(keyInput(), ' '); fireEvent.press(screen.getByText('Save Changes')); expect(state().serverConfig).toEqual({ host: 'localhost', port: 1234, apiKey: undefined }); }); }); describe('SettingsScreen connection test', () => { it('saves the configuration, reports success and loads the models', async () => { mockApi.testConnection.mockResolvedValue(true); render(); fireEvent.changeText(hostInput(), '10.0.0.2'); await act(async () => { fireEvent.press(screen.getByText('Test Connection')); }); expect(mockApi.testConnection).toHaveBeenCalledWith({ host: '10.0.0.2', port: 1234, apiKey: undefined }); expect(state().serverConfig.host).toBe('10.0.0.2'); expect(state().isConnected).toBe(true); expect(mockApi.getModels).toHaveBeenCalledTimes(1); expect(screen.getByText('Connection successful. API is ready.')).toBeTruthy(); expect(screen.getByText('Connected')).toBeTruthy(); }); it('reports failure and does not load models', async () => { mockApi.testConnection.mockResolvedValue(false); render(); await act(async () => { fireEvent.press(screen.getByText('Test Connection')); }); expect(state().isConnected).toBe(false); expect(mockApi.getModels).not.toHaveBeenCalled(); expect(screen.getByText('Connection failed. Verify host and port.')).toBeTruthy(); expect(screen.getByText('Disconnected')).toBeTruthy(); }); it('shows a spinner and ignores presses while the test is running', async () => { let resolveTest: (ok: boolean) => void = () => {}; mockApi.testConnection.mockReturnValue(new Promise((resolve) => (resolveTest = resolve))); render(); await act(async () => { fireEvent.press(screen.getByText('Test Connection')); }); expect(screen.queryByText('Test Connection')).toBeNull(); await act(async () => { resolveTest(false); }); expect(screen.getByText('Test Connection')).toBeTruthy(); }); it('clears a previous result when the configuration is saved again', async () => { mockApi.testConnection.mockResolvedValue(false); render(); await act(async () => { fireEvent.press(screen.getByText('Test Connection')); }); expect(screen.getByText(/Connection failed/)).toBeTruthy(); fireEvent.changeText(hostInput(), 'changed'); fireEvent.press(screen.getByText('Save Changes')); expect(screen.queryByText(/Connection failed/)).toBeNull(); }); }); describe('SettingsScreen dark mode switch', () => { const darkSwitch = () => screen.UNSAFE_getByType(require('react-native').Switch); it('is on for the dark preference and switches to light', () => { useAppStore.setState({ theme: 'dark' }); render(); expect(darkSwitch().props.value).toBe(true); fireEvent(darkSwitch(), 'valueChange', false); expect(state().theme).toBe('light'); }); it('is off for the light preference and switches to dark', () => { useAppStore.setState({ theme: 'light' }); render(); expect(darkSwitch().props.value).toBe(false); fireEvent(darkSwitch(), 'valueChange', true); expect(state().theme).toBe('dark'); }); it('follows the system scheme when the preference is "system"', () => { mockSystemScheme = 'dark'; const first = render(); expect(darkSwitch().props.value).toBe(true); first.unmount(); mockSystemScheme = 'light'; render(); expect(darkSwitch().props.value).toBe(false); }); });