Files
LocalMind/__tests__/SettingsScreen.test.tsx
evandro 434cba2a69 refactor(settings): split SettingsScreen into section components and pure helpers
SettingsScreen CC 23 -> below 10. Form parsing, change detection and the dark-theme check moved to utils/ with unit tests; the dark switch now reads the OS scheme instead of comparing the background colour. Adds characterization tests for the screen.
2026-10-10 04:18:40 -03:00

173 lines
6.7 KiB
TypeScript

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<typeof api>;
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(<SettingsScreen />);
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(<SettingsScreen />);
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(<SettingsScreen />);
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(<SettingsScreen />);
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(<SettingsScreen />);
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(<SettingsScreen />);
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(<SettingsScreen />);
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(<SettingsScreen />);
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(<SettingsScreen />);
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(<SettingsScreen />);
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(<SettingsScreen />);
expect(darkSwitch().props.value).toBe(true);
first.unmount();
mockSystemScheme = 'light';
render(<SettingsScreen />);
expect(darkSwitch().props.value).toBe(false);
});
});