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'); }); }); });