Files
pyramid/test/settings_prefs_test.dart
T
Bernd Steckmeister 3698048b26 test: VoicePrefs-Fassade abgesichert - historische Keys, null/leer-Loeschen, Fallback 1.0 (3 neue Tests, gesamt 32)
Nagelt den dokumentierten Vertrag der M2-Fassade (d22f9c2) fest, damit
bestehende Nutzer-Einstellungen (Utas Geraet) bei weiteren Umbauten der
Call-Schicht nachweislich erhalten bleiben.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 17:54:06 +02:00

185 lines
7.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});
}