import { applyPreferencesPatch, clearAllUserPreferenceCaches, normalizePreferences, normalizePreferencesPatch, } from '../src/lib/preferences-context' import AsyncStorage from '@react-native-async-storage/async-storage' import { getMobileThemePalette, resolveEffectiveTheme, } from '../src/theme/mobile-theme' describe('mobile preferences model', () => { test('rejects malformed cached values and restores safe defaults', () => { const normalized = normalizePreferences({ themeMode: 'neon', locale: 'unknown', hapticEnabled: 'yes', autoPolishEnabled: false, onboardingVersion: -4, tutorialCompletedAt: 'not-a-date', revision: 0, }) expect(normalized).toMatchObject({ themeMode: 'system', locale: 'ko', hapticEnabled: true, autoPolishEnabled: false, onboardingVersion: 0, tutorialCompletedAt: null, revision: 1, }) }) test('accepts only supported fields in a pending patch', () => { expect(normalizePreferencesPatch({ themeMode: 'light', locale: 'en', hapticEnabled: false, preferredSttModel: ' cloud-default ', revision: 999, unknownSetting: true, })).toEqual({ themeMode: 'light', locale: 'en', hapticEnabled: false, preferredSttModel: 'cloud-default', }) }) test('applies an offline patch without discarding server metadata', () => { const original = normalizePreferences({ themeMode: 'dark', locale: 'ko', hapticEnabled: true, autoPolishEnabled: true, preferredSttModel: 'cloud-default', preferredLlmModel: 'claude', onboardingVersion: 1, tutorialCompletedAt: '2026-08-21T01:00:00.000Z', revision: 8, updatedAt: '2026-08-21T01:05:00.000Z', }) const updated = applyPreferencesPatch(original, { themeMode: 'light', hapticEnabled: false, }) expect(updated.themeMode).toBe('light') expect(updated.hapticEnabled).toBe(false) expect(updated.preferredSttModel).toBe('cloud-default') expect(updated.preferredLlmModel).toBe('claude') expect(updated.revision).toBe(8) expect(updated.updatedAt).toBe('2026-08-21T01:05:00.000Z') }) test('logout purge removes account caches but preserves installation settings', async () => { const getAllKeys = AsyncStorage.getAllKeys as jest.MockedFunction< typeof AsyncStorage.getAllKeys > const multiRemove = AsyncStorage.multiRemove as jest.MockedFunction< typeof AsyncStorage.multiRemove > getAllKeys.mockResolvedValueOnce([ '@d3ro/mobile/preferences/installation-v1', '@d3ro/mobile/preferences/user-v1/account-a', '@d3ro/mobile/preferences/user-v1/account-b', '@another/app/key', ]) await clearAllUserPreferenceCaches() expect(multiRemove).toHaveBeenCalledWith([ '@d3ro/mobile/preferences/user-v1/account-a', '@d3ro/mobile/preferences/user-v1/account-b', ]) }) }) describe('mobile theme resolution', () => { test('system mode follows the device and falls back to dark', () => { expect(resolveEffectiveTheme('system', 'light')).toBe('light') expect(resolveEffectiveTheme('system', 'dark')).toBe('dark') expect(resolveEffectiveTheme('system', null)).toBe('dark') }) test('an explicit theme does not change with the device setting', () => { expect(resolveEffectiveTheme('light', 'dark')).toBe('light') expect(resolveEffectiveTheme('dark', 'light')).toBe('dark') expect(getMobileThemePalette('light').bg.app) .not.toBe(getMobileThemePalette('dark').bg.app) }) })