import 'dart:async'; import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/utils/logger.dart'; class _Preferences implements SharedPreferences { final writes = >[]; final _started = >{}; final _released = >{}; Future whenStarted(int index) => (_started[index] ??= Completer()).future; Completer release(int index) => _released[index] ??= Completer(); @override Future setString(String key, String value) { expect(key, 'app_settings'); final index = writes.length; writes.add(jsonDecode(value) as Map); (_started[index] ??= Completer()).complete(); return release(index).future; } @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _Settings extends SettingsNotifier { _Settings(_Preferences preferences) : super(preferences: () async => preferences); _Settings.withLoader(Future Function() load) : super(preferences: load); @override AppSettings build() => const AppSettings(); } void main() { test('queued save callers await the complete coalescing drain', () async { final preferences = _Preferences(); final container = ProviderContainer( overrides: [settingsProvider.overrideWith(() => _Settings(preferences))], ); addTearDown(container.dispose); final notifier = container.read(settingsProvider.notifier); final first = notifier.useAppFolderStorage('/output/first'); await preferences.whenStarted(0); final second = notifier.setNetworkDownloadFolder('remote:first', 'First'); final third = notifier.setNetworkDownloadFolder('remote:latest', 'Latest'); var firstCompleted = false; var secondCompleted = false; var thirdCompleted = false; unawaited(first.then((_) => firstCompleted = true)); unawaited(second.then((_) => secondCompleted = true)); unawaited(third.then((_) => thirdCompleted = true)); await Future.delayed(Duration.zero); expect( [firstCompleted, secondCompleted, thirdCompleted], [false, false, false], ); preferences.release(0).complete(true); await preferences.whenStarted(1); expect(preferences.writes.length, 2); expect(preferences.writes.last['networkDownloadFolder'], 'remote:latest'); expect(preferences.writes.last['networkDownloadLabel'], 'Latest'); expect(preferences.writes.last['downloadDirectory'], '/output/first'); expect( [firstCompleted, secondCompleted, thirdCompleted], [false, false, false], ); preferences.release(1).complete(true); await Future.wait([first, second, third]); expect( [firstCompleted, secondCompleted, thirdCompleted], [true, true, true], ); expect(preferences.writes.length, 2); }); test( 'failed active save keeps logged-error API and releases its owner', () async { final preferences = _Preferences(); final container = ProviderContainer( overrides: [ settingsProvider.overrideWith(() => _Settings(preferences)), ], ); addTearDown(container.dispose); final notifier = container.read(settingsProvider.notifier); final first = notifier.useAppFolderStorage('/output/first'); await preferences.whenStarted(0); final queued = notifier.setNetworkDownloadFolder( 'remote:queued', 'Queued', ); preferences.release(0).completeError(StateError('settings_save_failed')); await Future.wait([first, queued]); expect( LogBuffer().entries.any( (entry) => entry.tag == 'SettingsProvider' && entry.message.contains('settings_save_failed'), ), isTrue, ); final retry = notifier.useAppFolderStorage('/output/retry'); await preferences.whenStarted(1); expect(preferences.writes.last['downloadDirectory'], '/output/retry'); expect(preferences.writes.last['networkDownloadFolder'], 'remote:queued'); preferences.release(1).complete(true); await retry; }, ); test( 'synchronous preference failure does not retain a finished drain', () async { final preferences = _Preferences(); var loads = 0; final container = ProviderContainer( overrides: [ settingsProvider.overrideWith( () => _Settings.withLoader(() { if (loads++ == 0) throw StateError('settings_preferences_failed'); return Future.value(preferences); }), ), ], ); addTearDown(container.dispose); final notifier = container.read(settingsProvider.notifier); await notifier.useAppFolderStorage('/output/first'); expect(preferences.writes, isEmpty); final retry = notifier.useAppFolderStorage('/output/retry'); await preferences.whenStarted(0); expect(loads, 2); expect(preferences.writes.single['downloadDirectory'], '/output/retry'); preferences.release(0).complete(true); await retry; }, ); }