mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
at main 10 kB view raw
1import 'package:flutter/material.dart'; 2import 'package:flutter_riverpod/flutter_riverpod.dart'; 3import 'package:flutter_test/flutter_test.dart'; 4import 'package:lazurite/src/app/theme_controller.dart'; 5import 'package:lazurite/src/app/theming/packs/oxocarbon_theme_pack.dart'; 6import 'package:lazurite/src/app/theming/theme_variant.dart'; 7import 'package:lazurite/src/infrastructure/db/daos/local_settings_dao.dart'; 8import 'package:mocktail/mocktail.dart'; 9 10class MockLocalSettingsDao extends Mock implements LocalSettingsDao {} 11 12/// Test-specific ThemeController that avoids ThemeFactory.buildThemeData() 13/// which triggers Google Fonts loading requiring TestWidgetsFlutterBinding. 14/// 15/// This version also skips async loading to avoid Riverpod lifecycle issues. 16class TestThemeController extends ThemeController { 17 @override 18 ThemeState build() => _buildTestState(ThemeMode.dark, 'oxocarbon', false); 19 20 @override 21 Future<void> setThemeMode(ThemeMode mode) async { 22 state = _buildTestState(mode, state.currentPackId, state.dynamicColorEnabled); 23 await ref.read(localSettingsDaoProvider).set(ThemeSettingsKeys.themeMode, mode.name); 24 } 25 26 @override 27 Future<void> setThemePack(String packId) async { 28 final resolved = _resolvePackId(packId); 29 if (resolved == state.currentPackId) return; 30 state = _buildTestState(state.themeMode, resolved, state.dynamicColorEnabled); 31 await ref.read(localSettingsDaoProvider).set(ThemeSettingsKeys.themePackId, resolved); 32 } 33 34 @override 35 ThemeData buildThemeData(ThemeVariant variant) { 36 return ThemeData( 37 useMaterial3: true, 38 brightness: variant.brightness, 39 primaryColor: variant.brightness == Brightness.dark ? Colors.red : Colors.blue, 40 ); 41 } 42 43 @override 44 ThemeData buildThemeDataFromScheme(ColorScheme scheme) { 45 return ThemeData( 46 useMaterial3: true, 47 brightness: scheme.brightness, 48 colorScheme: scheme, 49 primaryColor: Colors.green, 50 ); 51 } 52 53 ThemeState _buildTestState(ThemeMode mode, String packId, bool dynamicColorEnabled) => 54 ThemeState( 55 themeMode: mode, 56 currentPackId: packId, 57 dynamicColorEnabled: dynamicColorEnabled, 58 lightTheme: ThemeData.light(useMaterial3: true), 59 darkTheme: ThemeData.dark(useMaterial3: true), 60 ); 61 62 String _resolvePackId(String? packId) { 63 if (packId == null) return 'oxocarbon'; 64 final packs = ref.read(availableThemePacksProvider); 65 return packs.where((p) => p.id == packId).firstOrNull?.id ?? 'oxocarbon'; 66 } 67} 68 69void main() { 70 late MockLocalSettingsDao mockDao; 71 late ProviderContainer container; 72 73 setUp(() { 74 mockDao = MockLocalSettingsDao(); 75 when(() => mockDao.get(any())).thenAnswer((_) async => null); 76 when(() => mockDao.set(any(), any())).thenAnswer((_) async {}); 77 }); 78 79 tearDown(() { 80 container.dispose(); 81 }); 82 83 ProviderContainer createContainer() { 84 return ProviderContainer( 85 overrides: [ 86 localSettingsDaoProvider.overrideWithValue(mockDao), 87 availableThemePacksProvider.overrideWithValue([oxocarbonPack]), 88 themeControllerProvider.overrideWith(TestThemeController.new), 89 ], 90 ); 91 } 92 93 group('ThemeController', () { 94 test('defaults to oxocarbon pack and dark mode', () { 95 container = createContainer(); 96 97 final state = container.read(themeControllerProvider); 98 99 expect(state.themeMode, ThemeMode.dark); 100 expect(state.currentPackId, 'oxocarbon'); 101 expect(state.dynamicColorEnabled, false); 102 expect(state.lightTheme, isA<ThemeData>()); 103 expect(state.darkTheme, isA<ThemeData>()); 104 }); 105 106 test('setThemeMode updates state', () async { 107 container = createContainer(); 108 final notifier = container.read(themeControllerProvider.notifier); 109 110 await notifier.setThemeMode(ThemeMode.light); 111 112 expect(container.read(themeControllerProvider).themeMode, ThemeMode.light); 113 }); 114 115 test('setThemeMode persists to database', () async { 116 container = createContainer(); 117 final notifier = container.read(themeControllerProvider.notifier); 118 119 await notifier.setThemeMode(ThemeMode.light); 120 121 verify(() => mockDao.set(ThemeSettingsKeys.themeMode, 'light')).called(1); 122 }); 123 124 test('setThemeMode to system mode persists correctly', () async { 125 container = createContainer(); 126 final notifier = container.read(themeControllerProvider.notifier); 127 128 await notifier.setThemeMode(ThemeMode.system); 129 130 expect(container.read(themeControllerProvider).themeMode, ThemeMode.system); 131 verify(() => mockDao.set(ThemeSettingsKeys.themeMode, 'system')).called(1); 132 }); 133 134 test('setThemePack with same pack is no-op', () async { 135 container = createContainer(); 136 final notifier = container.read(themeControllerProvider.notifier); 137 138 await notifier.setThemePack('oxocarbon'); 139 140 expect(container.read(themeControllerProvider).currentPackId, 'oxocarbon'); 141 verifyNever(() => mockDao.set(ThemeSettingsKeys.themePackId, any())); 142 }); 143 144 test('setThemePack falls back to default for unknown pack', () async { 145 container = createContainer(); 146 final notifier = container.read(themeControllerProvider.notifier); 147 148 await notifier.setThemePack('nonexistent'); 149 150 expect(container.read(themeControllerProvider).currentPackId, 'oxocarbon'); 151 }); 152 153 test('toggle switches between light and dark modes', () async { 154 container = createContainer(); 155 final notifier = container.read(themeControllerProvider.notifier); 156 157 notifier.toggle(); 158 await Future<void>.delayed(const Duration(milliseconds: 10)); 159 expect(container.read(themeControllerProvider).themeMode, ThemeMode.light); 160 161 notifier.toggle(); 162 await Future<void>.delayed(const Duration(milliseconds: 10)); 163 expect(container.read(themeControllerProvider).themeMode, ThemeMode.dark); 164 }); 165 166 test('toggle persists mode changes', () async { 167 container = createContainer(); 168 final notifier = container.read(themeControllerProvider.notifier); 169 170 notifier.toggle(); 171 await Future<void>.delayed(const Duration(milliseconds: 10)); 172 173 verify(() => mockDao.set(ThemeSettingsKeys.themeMode, 'light')).called(1); 174 }); 175 176 group('Dynamic Colors', () { 177 test('toggleDynamicColor updates state and persists', () async { 178 container = createContainer(); 179 final notifier = container.read(themeControllerProvider.notifier); 180 181 await notifier.toggleDynamicColor(); 182 183 expect(container.read(themeControllerProvider).dynamicColorEnabled, true); 184 verify(() => mockDao.set(ThemeSettingsKeys.dynamicColorEnabled, 'true')).called(1); 185 186 await notifier.toggleDynamicColor(); 187 188 expect(container.read(themeControllerProvider).dynamicColorEnabled, false); 189 verify(() => mockDao.set(ThemeSettingsKeys.dynamicColorEnabled, 'false')).called(1); 190 }); 191 192 test('uses dynamic colors when enabled and schemes available', () async { 193 container = createContainer(); 194 final notifier = container.read(themeControllerProvider.notifier); 195 196 await notifier.toggleDynamicColor(); 197 198 final lightScheme = ColorScheme.fromSeed( 199 seedColor: Colors.purple, 200 brightness: Brightness.light, 201 ); 202 final darkScheme = ColorScheme.fromSeed( 203 seedColor: Colors.purple, 204 brightness: Brightness.dark, 205 ); 206 207 notifier.setSystemSchemes(lightScheme, darkScheme); 208 209 final state = container.read(themeControllerProvider); 210 expect( 211 state.lightTheme.primaryColor, 212 Colors.green, 213 reason: 'Should use buildThemeDataFromScheme', 214 ); 215 expect(state.darkTheme.primaryColor, Colors.green); 216 }); 217 218 test('falls back to pack colors if schemes unavailable', () async { 219 container = createContainer(); 220 final notifier = container.read(themeControllerProvider.notifier); 221 222 await notifier.toggleDynamicColor(); 223 224 final state = container.read(themeControllerProvider); 225 expect(state.darkTheme.primaryColor, Colors.red); 226 }); 227 228 test('falls back to pack colors if dynamic color disabled', () async { 229 container = createContainer(); 230 final notifier = container.read(themeControllerProvider.notifier); 231 232 final lightScheme = ColorScheme.fromSeed( 233 seedColor: Colors.purple, 234 brightness: Brightness.light, 235 ); 236 final darkScheme = ColorScheme.fromSeed( 237 seedColor: Colors.purple, 238 brightness: Brightness.dark, 239 ); 240 notifier.setSystemSchemes(lightScheme, darkScheme); 241 242 final state = container.read(themeControllerProvider); 243 expect(state.dynamicColorEnabled, false); 244 expect(state.lightTheme.primaryColor, isNot(Colors.green)); 245 }); 246 }); 247 }); 248 249 group('ThemeState', () { 250 test('copyWith creates modified copy', () { 251 final state = ThemeState( 252 themeMode: ThemeMode.dark, 253 currentPackId: 'oxocarbon', 254 lightTheme: ThemeData.light(), 255 darkTheme: ThemeData.dark(), 256 ); 257 258 final copied = state.copyWith(themeMode: ThemeMode.light); 259 260 expect(copied.themeMode, ThemeMode.light); 261 expect(copied.currentPackId, 'oxocarbon'); 262 }); 263 264 test('copyWith preserves unchanged values', () { 265 final lightTheme = ThemeData.light(); 266 final darkTheme = ThemeData.dark(); 267 final state = ThemeState( 268 themeMode: ThemeMode.dark, 269 currentPackId: 'oxocarbon', 270 lightTheme: lightTheme, 271 darkTheme: darkTheme, 272 ); 273 274 final copied = state.copyWith(themeMode: ThemeMode.light); 275 276 expect(copied.lightTheme, same(lightTheme)); 277 expect(copied.darkTheme, same(darkTheme)); 278 }); 279 }); 280 281 group('ThemeSettingsKeys', () { 282 test('has correct key values', () { 283 expect(ThemeSettingsKeys.themeMode, 'themeMode'); 284 expect(ThemeSettingsKeys.themePackId, 'themePackId'); 285 expect(ThemeSettingsKeys.dynamicColorEnabled, 'dynamicColorEnabled'); 286 }); 287 }); 288}