feat(flutter/settings): Appearance section with System/Light/Dark picker
Three RadioListTiles between Profile and Password sections. Tap →
themeModeProvider.notifier.set(...) → MaterialApp rebuilds with the
new theme via the existing themeMode wiring.
System has the only subtitle ("Match the device setting") — the
light/dark options are self-explanatory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:minstrel/auth/auth_provider.dart';
|
||||
import 'package:minstrel/settings/settings_screen.dart';
|
||||
import 'package:minstrel/theme/theme_data.dart';
|
||||
import 'package:minstrel/theme/theme_mode_provider.dart';
|
||||
|
||||
class _MockStorage extends Mock implements FlutterSecureStorage {}
|
||||
|
||||
class _StubTheme extends ThemeModeController {
|
||||
_StubTheme(this._initial);
|
||||
final AppThemeMode _initial;
|
||||
@override
|
||||
Future<AppThemeMode> build() async => _initial;
|
||||
}
|
||||
|
||||
void main() {
|
||||
late _MockStorage storage;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue('');
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
storage = _MockStorage();
|
||||
when(() => storage.read(key: any(named: 'key'))).thenAnswer((_) async => null);
|
||||
when(() => storage.write(key: any(named: 'key'), value: any(named: 'value')))
|
||||
.thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
testWidgets('renders three appearance radios + reflects the current mode', (t) async {
|
||||
await t.pumpWidget(ProviderScope(
|
||||
overrides: [
|
||||
secureStorageProvider.overrideWithValue(storage),
|
||||
themeModeProvider.overrideWith(() => _StubTheme(AppThemeMode.dark)),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: buildDarkTheme(),
|
||||
home: const SettingsScreen(),
|
||||
),
|
||||
));
|
||||
await t.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('appearance_system')), findsOneWidget);
|
||||
expect(find.byKey(const Key('appearance_light')), findsOneWidget);
|
||||
expect(find.byKey(const Key('appearance_dark')), findsOneWidget);
|
||||
|
||||
// The dark radio should be selected.
|
||||
final darkRadio = t.widget<RadioListTile<AppThemeMode>>(
|
||||
find.byKey(const Key('appearance_dark')),
|
||||
);
|
||||
expect(darkRadio.groupValue, AppThemeMode.dark);
|
||||
expect(darkRadio.value, AppThemeMode.dark);
|
||||
});
|
||||
|
||||
testWidgets('tapping the light radio writes "light" to storage', (t) async {
|
||||
await t.pumpWidget(ProviderScope(
|
||||
overrides: [
|
||||
secureStorageProvider.overrideWithValue(storage),
|
||||
themeModeProvider.overrideWith(() => _StubTheme(AppThemeMode.system)),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: buildDarkTheme(),
|
||||
home: const SettingsScreen(),
|
||||
),
|
||||
));
|
||||
await t.pumpAndSettle();
|
||||
|
||||
await t.tap(find.byKey(const Key('appearance_light')));
|
||||
await t.pumpAndSettle();
|
||||
|
||||
verify(() => storage.write(key: 'theme_mode', value: 'light')).called(1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user