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.
173 lines
6.7 KiB
TypeScript
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);
|
|
});
|
|
});
|