Files
SpotiFLAC-Mobile/test/settings_persistence_test.dart
T

154 lines
5.3 KiB
Dart

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 = <Map<String, dynamic>>[];
final _started = <int, Completer<void>>{};
final _released = <int, Completer<bool>>{};
Future<void> whenStarted(int index) =>
(_started[index] ??= Completer<void>()).future;
Completer<bool> release(int index) => _released[index] ??= Completer<bool>();
@override
Future<bool> setString(String key, String value) {
expect(key, 'app_settings');
final index = writes.length;
writes.add(jsonDecode(value) as Map<String, dynamic>);
(_started[index] ??= Completer<void>()).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<SharedPreferences> 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<void>.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;
},
);
}