import 'package:flutter_test/flutter_test.dart'; import 'package:pyramid/core/settings_prefs.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// Tests für die Storage-Fassade, die laut M2-Modulschnitt zur Pflicht- /// Schnittstelle für alle Einstellungs-Zugriffe ausgebaut werden soll – /// dieses Vertragsnetz muss bei dem Umbau stehen bleiben. void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { // Frischer In-Memory-Store pro Test (setzt auch die gecachte // SharedPreferences-Instanz zurück). SharedPreferences.setMockInitialValues({}); }); group('BoolPref', () { test('lädt persistierten Wert und überstimmt damit den Default', () async { SharedPreferences.setMockInitialValues({'notif_sound': false}); final pref = BoolPref('notif_sound', true); expect(pref.state, true, reason: 'Default gilt, bis geladen ist'); await pumpEventQueue(); expect(pref.state, false, reason: 'persistierter Wert muss gewinnen'); }); test( 'set() sofort nach Erstellung gewinnt gegen den später ladenden ' 'persistierten Wert (Lade-Race)', () async { SharedPreferences.setMockInitialValues({'notif_sound': false}); final pref = BoolPref('notif_sound', false); final done = pref.set(true); // bevor _load fertig ist await pumpEventQueue(); await done; expect(pref.state, true, reason: 'die Nutzeraktion darf nicht vom Lade-Ergebnis ' 'zurücküberschrieben werden'); final p = await SharedPreferences.getInstance(); expect(p.getBool('notif_sound'), true); }); test('set/toggle ändern Zustand und persistieren', () async { final pref = BoolPref('notif_desktop', true); await pumpEventQueue(); pref.toggle(); await pumpEventQueue(); expect(pref.state, false); final p = await SharedPreferences.getInstance(); expect(p.getBool('notif_desktop'), false); }); }); group('StringSetPref', () { test('lädt persistierte Liste als Set, add/remove persistieren', () async { SharedPreferences.setMockInitialValues({ 'muted_rooms': ['!a:hs', '!b:hs'], }); final pref = StringSetPref('muted_rooms'); await pumpEventQueue(); expect(pref.state, {'!a:hs', '!b:hs'}); await pref.add('!c:hs'); await pref.remove('!a:hs'); final p = await SharedPreferences.getInstance(); expect(p.getStringList('muted_rooms')!.toSet(), {'!b:hs', '!c:hs'}); }); test( 'add() sofort nach Erstellung verliert keine bereits persistierten ' 'Einträge (Lade-Race)', () async { SharedPreferences.setMockInitialValues({ 'muted_rooms': ['!a:hs'], }); final pref = StringSetPref('muted_rooms'); final done = pref.add('!b:hs'); // bevor _load fertig ist await pumpEventQueue(); await done; expect(pref.state, {'!a:hs', '!b:hs'}); final p = await SharedPreferences.getInstance(); expect(p.getStringList('muted_rooms')!.toSet(), {'!a:hs', '!b:hs'}, reason: 'Read-Modify-Write auf ungeladenem State darf keine ' 'persistierten Einträge verlieren'); }); test('doppeltes add und remove von Fehlendem sind No-Ops', () async { final pref = StringSetPref('muted_rooms'); await pumpEventQueue(); await pref.add('!a:hs'); await pref.add('!a:hs'); await pref.remove('!gibtsnicht:hs'); expect(pref.state, {'!a:hs'}); }); }); group('Theme-Prefs', () { test('leerer Store liefert die dokumentierten Defaults', () async { final t = await loadThemePrefs(); expect(t.isDark, true); expect(t.accentIdx, 0); expect(t.radius, 12.0); expect(t.motion, 1.0); }); test('Roundtrip: gespeicherte Werte kommen zurück, Teil-Save lässt ' 'den Rest unangetastet', () async { await saveThemePrefs(isDark: false, accentIdx: 3, radius: 8, motion: 0.5); var t = await loadThemePrefs(); expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 3, 8.0, 0.5)); // Nur ein Feld ändern – die anderen dürfen nicht zurückfallen. await saveThemePrefs(accentIdx: 1); t = await loadThemePrefs(); expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 1, 8.0, 0.5)); }); }); group('Server-Banner', () { test('Roundtrip + Entfernen per null/leerem mxcUri', () async { await saveServerBanner('https://hs1', 'mxc://hs1/abc'); await saveServerBanner('https://hs2', 'mxc://hs2/def'); expect(await loadAllServerBanners(), {'https://hs1': 'mxc://hs1/abc', 'https://hs2': 'mxc://hs2/def'}); await saveServerBanner('https://hs1', null); await saveServerBanner('https://hs2', ''); expect(await loadAllServerBanners(), isEmpty); }); test('korruptes JSON im Store crasht nicht, sondern liefert leere Map', () async { SharedPreferences.setMockInitialValues( {'server_banners': '{kein json'}); expect(await loadAllServerBanners(), isEmpty); }); }); }