259 lines
9.3 KiB
TypeScript
259 lines
9.3 KiB
TypeScript
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import { StorageService } from '../../services/storageService';
|
|
import { Plant } from '../../types';
|
|
|
|
jest.mock('@react-native-async-storage/async-storage', () => ({
|
|
getItem: jest.fn(),
|
|
setItem: jest.fn(),
|
|
removeItem: jest.fn(),
|
|
}));
|
|
|
|
const mockPlant: Plant = {
|
|
id: '1',
|
|
name: 'Monstera',
|
|
botanicalName: 'Monstera deliciosa',
|
|
imageUri: 'https://example.com/img.jpg',
|
|
dateAdded: '2024-01-01',
|
|
careInfo: { waterIntervalDays: 7, light: 'Partial Shade', temp: '18-24°C' },
|
|
lastWatered: '2024-01-01',
|
|
};
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('StorageService', () => {
|
|
describe('getPlants', () => {
|
|
it('returns empty array when no data stored', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(null);
|
|
const result = await StorageService.getPlants();
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('returns parsed plants when data exists', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify([mockPlant]));
|
|
const result = await StorageService.getPlants();
|
|
expect(result).toEqual([mockPlant]);
|
|
});
|
|
|
|
it('returns empty array on error', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('fail'));
|
|
const result = await StorageService.getPlants();
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('savePlant', () => {
|
|
it('prepends plant to existing list', async () => {
|
|
const existing: Plant = { ...mockPlant, id: '2', name: 'Ficus' };
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify([existing]));
|
|
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
await StorageService.savePlant(mockPlant);
|
|
|
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
|
'greenlens_plants',
|
|
JSON.stringify([mockPlant, existing])
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('deletePlant', () => {
|
|
it('removes plant by id', async () => {
|
|
const plant2: Plant = { ...mockPlant, id: '2' };
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify([mockPlant, plant2]));
|
|
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
await StorageService.deletePlant('1');
|
|
|
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
|
'greenlens_plants',
|
|
JSON.stringify([plant2])
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('updatePlant', () => {
|
|
it('updates existing plant', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify([mockPlant]));
|
|
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
const updated = { ...mockPlant, name: 'Updated Monstera' };
|
|
await StorageService.updatePlant(updated);
|
|
|
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
|
'greenlens_plants',
|
|
JSON.stringify([updated])
|
|
);
|
|
});
|
|
|
|
it('does nothing if plant not found', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify([mockPlant]));
|
|
|
|
await StorageService.updatePlant({ ...mockPlant, id: 'nonexistent' });
|
|
|
|
expect(AsyncStorage.setItem).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('getLanguage', () => {
|
|
it('returns stored language', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue('en');
|
|
const result = await StorageService.getLanguage();
|
|
expect(result).toBe('en');
|
|
});
|
|
|
|
it('defaults to de when no language stored', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(null);
|
|
const result = await StorageService.getLanguage();
|
|
expect(result).toBe('de');
|
|
});
|
|
|
|
it('defaults to de on error', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('fail'));
|
|
const result = await StorageService.getLanguage();
|
|
expect(result).toBe('de');
|
|
});
|
|
});
|
|
|
|
describe('saveLanguage', () => {
|
|
it('stores language', async () => {
|
|
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
await StorageService.saveLanguage('es');
|
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith('greenlens_language', 'es');
|
|
});
|
|
});
|
|
|
|
describe('getAppearanceMode', () => {
|
|
it('returns stored appearance mode', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue('dark');
|
|
const result = await StorageService.getAppearanceMode();
|
|
expect(result).toBe('dark');
|
|
});
|
|
|
|
it('defaults to system when value is invalid', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue('invalid');
|
|
const result = await StorageService.getAppearanceMode();
|
|
expect(result).toBe('system');
|
|
});
|
|
});
|
|
|
|
describe('saveAppearanceMode', () => {
|
|
it('stores appearance mode', async () => {
|
|
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
await StorageService.saveAppearanceMode('light');
|
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith('greenlens_appearance_mode', 'light');
|
|
});
|
|
});
|
|
|
|
describe('getColorPalette', () => {
|
|
it('returns stored palette', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue('ocean');
|
|
const result = await StorageService.getColorPalette();
|
|
expect(result).toBe('ocean');
|
|
});
|
|
|
|
it('defaults to forest when value is invalid', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue('invalid');
|
|
const result = await StorageService.getColorPalette();
|
|
expect(result).toBe('forest');
|
|
});
|
|
});
|
|
|
|
describe('saveColorPalette', () => {
|
|
it('stores palette', async () => {
|
|
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
await StorageService.saveColorPalette('sunset');
|
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith('greenlens_color_palette', 'sunset');
|
|
});
|
|
});
|
|
|
|
describe('profile name', () => {
|
|
it('returns stored profile name', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue('Taylor');
|
|
const result = await StorageService.getProfileName();
|
|
expect(result).toBe('Taylor');
|
|
});
|
|
|
|
it('falls back to default profile name when empty', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(' ');
|
|
const result = await StorageService.getProfileName();
|
|
expect(result).toBe('Alex Rivera');
|
|
});
|
|
|
|
it('stores normalized profile name', async () => {
|
|
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
await StorageService.saveProfileName(' Morgan ');
|
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith('greenlens_profile_name', 'Morgan');
|
|
});
|
|
});
|
|
|
|
describe('lexicon search history', () => {
|
|
it('returns empty history when not set', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(null);
|
|
const result = await StorageService.getLexiconSearchHistory();
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('returns parsed history entries', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify(['Monstera', 'Aloe']));
|
|
const result = await StorageService.getLexiconSearchHistory();
|
|
expect(result).toEqual(['Monstera', 'Aloe']);
|
|
});
|
|
|
|
it('returns empty history on malformed JSON', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue('{bad json}');
|
|
const result = await StorageService.getLexiconSearchHistory();
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('saves new query at front', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify(['Monstera', 'Aloe']));
|
|
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
await StorageService.saveLexiconSearchQuery('Ficus');
|
|
|
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
|
'greenlens_lexicon_search_history',
|
|
JSON.stringify(['Ficus', 'Monstera', 'Aloe'])
|
|
);
|
|
});
|
|
|
|
it('deduplicates case and diacritic variants by moving entry to front', async () => {
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify(['Monstera', 'Aloe']));
|
|
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
await StorageService.saveLexiconSearchQuery('monstera');
|
|
|
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
|
'greenlens_lexicon_search_history',
|
|
JSON.stringify(['monstera', 'Aloe'])
|
|
);
|
|
});
|
|
|
|
it('ignores empty queries', async () => {
|
|
await StorageService.saveLexiconSearchQuery(' ');
|
|
expect(AsyncStorage.setItem).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('limits history to 10 entries', async () => {
|
|
const history = ['q1', 'q2', 'q3', 'q4', 'q5', 'q6', 'q7', 'q8', 'q9', 'q10'];
|
|
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(JSON.stringify(history));
|
|
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
await StorageService.saveLexiconSearchQuery('q11');
|
|
|
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
|
'greenlens_lexicon_search_history',
|
|
JSON.stringify(['q11', 'q1', 'q2', 'q3', 'q4', 'q5', 'q6', 'q7', 'q8', 'q9'])
|
|
);
|
|
});
|
|
|
|
it('clears history', async () => {
|
|
(AsyncStorage.removeItem as jest.Mock).mockResolvedValue(undefined);
|
|
await StorageService.clearLexiconSearchHistory();
|
|
expect(AsyncStorage.removeItem).toHaveBeenCalledWith('greenlens_lexicon_search_history');
|
|
});
|
|
});
|
|
});
|