From bcf89f586c6dd80f240ac3fb80d01ee3c5f6bcd0 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 22 May 2026 09:34:24 +0300 Subject: Fix Material 3 theming review issues (task fb) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Roll back ThemeNotifier state on SharedPreferences write failure so in-memory and disk never diverge - Fix stale comment: scaffoldBackgroundColor ← --bg-body (not background) - Fix source attribution: theme.css not docs/theming.md - Make _ThemeToggle const-constructible; remove incorrect ignore comment - Inline _buildSegmentedButton into _ThemeToggle.build (was a trivial passthrough) - Add defensive isNotEmpty comment on SegmentedButton.onSelectionChanged - Hoist buildLightTheme()/buildDarkTheme() to module-level finals in main.dart so ThemeData is built once at startup rather than on every rebuild - Add _ThemeToggle tests: initial segment selection and segment tap dispatch Co-Authored-By: Claude Sonnet 4.6 --- .../test/screens/settings_screen_test.dart | 110 +++++++++++++++++++++ 1 file changed, 110 insertions(+) (limited to 'player-android/test/screens') diff --git a/player-android/test/screens/settings_screen_test.dart b/player-android/test/screens/settings_screen_test.dart index 71c2d7d..ecbc94d 100644 --- a/player-android/test/screens/settings_screen_test.dart +++ b/player-android/test/screens/settings_screen_test.dart @@ -21,6 +21,7 @@ import 'package:player_android/api/dio_client.dart'; import 'package:player_android/providers/api_client_provider.dart'; import 'package:player_android/providers/auth_state_provider.dart'; import 'package:player_android/providers/settings_provider.dart'; +import 'package:player_android/providers/theme_provider.dart'; import 'package:player_android/screens/settings_screen.dart'; // --------------------------------------------------------------------------- @@ -65,6 +66,25 @@ class _FakeSettingsNotifier extends SettingsNotifier { } } +/// In-memory [ThemeNotifier] that bypasses [SharedPreferences] in tests. +/// +/// Starts at [ThemeMode.system] (the default) and records the last mode passed +/// to [setThemeMode] so tests can assert on the captured value. +class _FakeThemeNotifier extends ThemeNotifier { + // Tracks the last mode applied via setThemeMode for test assertions. + ThemeMode? capturedMode; + + @override + Future build() async => ThemeMode.system; + + @override + Future setThemeMode(ThemeMode mode) async { + capturedMode = mode; + // Mirror the production behaviour: update in-memory state immediately. + state = AsyncData(mode); + } +} + // --------------------------------------------------------------------------- // Helper: pump SettingsScreen inside a minimal ProviderScope. // --------------------------------------------------------------------------- @@ -259,6 +279,96 @@ void main() { }); }); + // -------------------------------------------------------------------------- + // Theme toggle + // -------------------------------------------------------------------------- + + group('theme toggle', () { + testWidgets('renders with system segment selected by default', + (tester) async { + final fakeStorage = _FakeTokenStorage().._token = 'alice'; + final fakeSettings = _FakeSettingsNotifier('http://10.0.2.2:8080'); + final fakeTheme = _FakeThemeNotifier(); + + final router = GoRouter( + initialLocation: '/', + routes: [ + GoRoute( + path: '/', + builder: (_, __) => const SettingsScreen(), + ), + GoRoute( + path: '/login', + builder: (_, __) => const Scaffold(body: Text('Login')), + ), + ], + ); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + tokenStorageProvider.overrideWithValue(fakeStorage), + settingsProvider.overrideWith(() => fakeSettings), + themeProvider.overrideWith(() => fakeTheme), + ], + child: MaterialApp.router(routerConfig: router), + ), + ); + await tester.pumpAndSettle(); + + // The segmented button should be present with the system segment selected. + final button = tester.widget>( + find.byKey(const Key('settings_theme_toggle')), + ); + expect(button.selected, equals({ThemeMode.system})); + }); + + testWidgets('tapping a segment calls setThemeMode with the right ThemeMode', + (tester) async { + final fakeStorage = _FakeTokenStorage().._token = 'alice'; + final fakeSettings = _FakeSettingsNotifier('http://10.0.2.2:8080'); + final fakeTheme = _FakeThemeNotifier(); + + final router = GoRouter( + initialLocation: '/', + routes: [ + GoRoute( + path: '/', + builder: (_, __) => const SettingsScreen(), + ), + GoRoute( + path: '/login', + builder: (_, __) => const Scaffold(body: Text('Login')), + ), + ], + ); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + tokenStorageProvider.overrideWithValue(fakeStorage), + settingsProvider.overrideWith(() => fakeSettings), + themeProvider.overrideWith(() => fakeTheme), + ], + child: MaterialApp.router(routerConfig: router), + ), + ); + await tester.pumpAndSettle(); + + // Scroll the segmented button into view before tapping — the settings + // screen content may exceed the test viewport height. + await tester.ensureVisible(find.byKey(const Key('settings_theme_toggle'))); + await tester.pumpAndSettle(); + + // Tap the "Dark" segment label. + await tester.tap(find.text('Dark')); + await tester.pumpAndSettle(); + + // The fake notifier should have received ThemeMode.dark. + expect(fakeTheme.capturedMode, equals(ThemeMode.dark)); + }); + }); + // -------------------------------------------------------------------------- // Logout flow // -------------------------------------------------------------------------- -- cgit v1.2.3