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.
This commit is contained in:
1 parent
cb6eda0562
commit
434cba2a69
10 files changed
+671
-338
No files matched your search
@@ -0,0 +1,172 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user