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('VoicePrefs', () { // Die Fassade (M2, 2026-07-06) hat drei dokumentierte Garantien: // historische voice_*-Keys unverändert, null/leer = Pref löschen, // Fallback-Lautstärke 1.0. Dieser Vertrag muss bei jedem weiteren // Umbau der Call-Schicht stehen bleiben. test('leerer Store: Geräte null (= System-Standard), Lautstärke 1.0', () async { final d = await VoicePrefs.loadDevices(); expect((d.mic, d.speaker, d.camera), (null, null, null)); expect(await VoicePrefs.loadOutputVolume(), 1.0); }); test('liest die historischen voice_*-Keys – Bestands-Einstellungen ' '(Utas Gerät!) bleiben nach dem Fassaden-Umbau erhalten', () async { SharedPreferences.setMockInitialValues({ 'voice_mic_device': 'mic-1', 'voice_speaker_device': 'spk-1', 'voice_camera_device': 'cam-1', 'voice_output_volume': 0.4, }); final d = await VoicePrefs.loadDevices(); expect((d.mic, d.speaker, d.camera), ('mic-1', 'spk-1', 'cam-1')); expect(await VoicePrefs.loadOutputVolume(), 0.4); }); test('Roundtrip unter denselben Keys; null/leer LÖSCHT das Pref', () async { await VoicePrefs.saveMic('mic-2'); await VoicePrefs.saveSpeaker('spk-2'); await VoicePrefs.saveCamera('cam-2'); await VoicePrefs.saveOutputVolume(0.75); final p = await SharedPreferences.getInstance(); // Key-Vertrag: exakt die historischen Keys, keine neuen Namen. expect(p.getString('voice_mic_device'), 'mic-2'); expect(p.getString('voice_speaker_device'), 'spk-2'); expect(p.getString('voice_camera_device'), 'cam-2'); expect(p.getDouble('voice_output_volume'), 0.75); await VoicePrefs.saveMic(null); await VoicePrefs.saveSpeaker(''); final d = await VoicePrefs.loadDevices(); expect((d.mic, d.speaker, d.camera), (null, null, 'cam-2')); expect(p.containsKey('voice_mic_device'), false, reason: 'null muss das Pref löschen (System-Standard), ' 'nicht einen leeren String speichern'); expect(p.containsKey('voice_speaker_device'), false, reason: 'leerer String zählt wie null'); }); }); 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); }); }); }