Compare commits

...

5 commits

Author SHA1 Message Date
Vadim
61620acf97 typo 2023-10-20 16:11:44 +02:00
Vadim
2486c32213 added //coverage:ignore to ServicesProvider 2023-10-20 16:04:00 +02:00
Vadim
a4e7f9d3f9 Update README.md 2023-10-20 15:56:57 +02:00
Vadim
2fe36e0d9e UserPreferencesProvider tests 2023-10-20 15:46:41 +02:00
Vadim
049d0c7690 added storage action verification for FilmsProvider tests 2023-10-20 09:18:44 +02:00
7 changed files with 445 additions and 58 deletions

View file

@ -1,5 +1,8 @@
<img src="resources/social_preview.png" width="100%" /> <img src="resources/social_preview.png" width="100%" />
![](https://github.com/vodemn/m3_lightmeter/actions/workflows/pr_check.yml/badge.svg)
![](https://github.com/vodemn/m3_lightmeter/actions/workflows/create_release.yml/badge.svg)
# Table of contents # Table of contents
- [Table of contents](#table-of-contents) - [Table of contents](#table-of-contents)

View file

@ -30,20 +30,23 @@ class ApplicationWrapper extends StatelessWidget {
builder: (_, snapshot) { builder: (_, snapshot) {
if (snapshot.data != null) { if (snapshot.data != null) {
final iapService = IAPStorageService(snapshot.data![0] as SharedPreferences); final iapService = IAPStorageService(snapshot.data![0] as SharedPreferences);
final userPreferencesService = UserPreferencesService(snapshot.data![0] as SharedPreferences);
final hasLightSensor = snapshot.data![1] as bool;
return ServicesProvider( return ServicesProvider(
caffeineService: const CaffeineService(), caffeineService: const CaffeineService(),
environment: env.copyWith(hasLightSensor: snapshot.data![1] as bool), environment: env.copyWith(hasLightSensor: hasLightSensor),
hapticsService: const HapticsService(), hapticsService: const HapticsService(),
lightSensorService: const LightSensorService(LocalPlatform()), lightSensorService: const LightSensorService(LocalPlatform()),
permissionsService: const PermissionsService(), permissionsService: const PermissionsService(),
userPreferencesService: userPreferencesService: userPreferencesService,
UserPreferencesService(snapshot.data![0] as SharedPreferences),
volumeEventsService: const VolumeEventsService(LocalPlatform()), volumeEventsService: const VolumeEventsService(LocalPlatform()),
child: EquipmentProfileProvider( child: EquipmentProfileProvider(
storageService: iapService, storageService: iapService,
child: FilmsProvider( child: FilmsProvider(
storageService: iapService, storageService: iapService,
child: UserPreferencesProvider( child: UserPreferencesProvider(
hasLightSensor: hasLightSensor,
userPreferencesService: userPreferencesService,
child: child, child: child,
), ),
), ),

View file

@ -7,6 +7,7 @@ import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart'; import 'package:lightmeter/data/volume_events_service.dart';
import 'package:lightmeter/environment.dart'; import 'package:lightmeter/environment.dart';
// coverage:ignore-start
class ServicesProvider extends InheritedWidget { class ServicesProvider extends InheritedWidget {
final CaffeineService caffeineService; final CaffeineService caffeineService;
final Environment environment; final Environment environment;
@ -34,3 +35,4 @@ class ServicesProvider extends InheritedWidget {
@override @override
bool updateShouldNotify(ServicesProvider oldWidget) => false; bool updateShouldNotify(ServicesProvider oldWidget) => false;
} }
// coverage:ignore-end

View file

@ -8,14 +8,20 @@ import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/res/theme.dart'; import 'package:lightmeter/res/theme.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class UserPreferencesProvider extends StatefulWidget { class UserPreferencesProvider extends StatefulWidget {
final bool hasLightSensor;
final UserPreferencesService userPreferencesService;
final Widget child; final Widget child;
const UserPreferencesProvider({required this.child, super.key}); const UserPreferencesProvider({
required this.hasLightSensor,
required this.userPreferencesService,
required this.child,
super.key,
});
static _UserPreferencesProviderState of(BuildContext context) { static _UserPreferencesProviderState of(BuildContext context) {
return context.findAncestorStateOfType<_UserPreferencesProviderState>()!; return context.findAncestorStateOfType<_UserPreferencesProviderState>()!;
@ -38,8 +44,7 @@ class UserPreferencesProvider extends StatefulWidget {
} }
static bool meteringScreenFeatureOf(BuildContext context, MeteringScreenLayoutFeature feature) { static bool meteringScreenFeatureOf(BuildContext context, MeteringScreenLayoutFeature feature) {
return InheritedModel.inheritFrom<_MeteringScreenLayoutModel>(context, aspect: feature)! return InheritedModel.inheritFrom<_MeteringScreenLayoutModel>(context, aspect: feature)!.data[feature]!;
.data[feature]!;
} }
static StopType stopTypeOf(BuildContext context) { static StopType stopTypeOf(BuildContext context) {
@ -65,28 +70,20 @@ class UserPreferencesProvider extends StatefulWidget {
State<UserPreferencesProvider> createState() => _UserPreferencesProviderState(); State<UserPreferencesProvider> createState() => _UserPreferencesProviderState();
} }
class _UserPreferencesProviderState extends State<UserPreferencesProvider> class _UserPreferencesProviderState extends State<UserPreferencesProvider> with WidgetsBindingObserver {
with WidgetsBindingObserver { late EvSourceType _evSourceType;
UserPreferencesService get userPreferencesService => late StopType _stopType = widget.userPreferencesService.stopType;
ServicesProvider.of(context).userPreferencesService; late MeteringScreenLayoutConfig _meteringScreenLayout = widget.userPreferencesService.meteringScreenLayout;
late SupportedLocale _locale = widget.userPreferencesService.locale;
late bool dynamicColor = userPreferencesService.dynamicColor; late ThemeType _themeType = widget.userPreferencesService.themeType;
late EvSourceType evSourceType; late Color _primaryColor = widget.userPreferencesService.primaryColor;
late MeteringScreenLayoutConfig meteringScreenLayout = late bool _dynamicColor = widget.userPreferencesService.dynamicColor;
userPreferencesService.meteringScreenLayout;
late Color primaryColor = userPreferencesService.primaryColor;
late StopType stopType = userPreferencesService.stopType;
late SupportedLocale locale = userPreferencesService.locale;
late ThemeType themeType = userPreferencesService.themeType;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
evSourceType = userPreferencesService.evSourceType; _evSourceType = widget.userPreferencesService.evSourceType;
evSourceType = evSourceType == EvSourceType.sensor && _evSourceType = _evSourceType == EvSourceType.sensor && !widget.hasLightSensor ? EvSourceType.camera : _evSourceType;
!ServicesProvider.of(context).environment.hasLightSensor
? EvSourceType.camera
: evSourceType;
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
} }
@ -109,9 +106,8 @@ class _UserPreferencesProviderState extends State<UserPreferencesProvider>
late final DynamicColorState state; late final DynamicColorState state;
late final Color? dynamicPrimaryColor; late final Color? dynamicPrimaryColor;
if (lightDynamic != null && darkDynamic != null) { if (lightDynamic != null && darkDynamic != null) {
if (dynamicColor) { if (_dynamicColor) {
dynamicPrimaryColor = dynamicPrimaryColor = (_themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary;
(_themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary;
state = DynamicColorState.enabled; state = DynamicColorState.enabled;
} else { } else {
dynamicPrimaryColor = null; dynamicPrimaryColor = null;
@ -124,13 +120,13 @@ class _UserPreferencesProviderState extends State<UserPreferencesProvider>
return _UserPreferencesModel( return _UserPreferencesModel(
brightness: _themeBrightness, brightness: _themeBrightness,
dynamicColorState: state, dynamicColorState: state,
evSourceType: evSourceType, evSourceType: _evSourceType,
locale: locale, locale: _locale,
primaryColor: dynamicPrimaryColor ?? primaryColor, primaryColor: dynamicPrimaryColor ?? _primaryColor,
stopType: stopType, stopType: _stopType,
themeType: themeType, themeType: _themeType,
child: _MeteringScreenLayoutModel( child: _MeteringScreenLayoutModel(
data: meteringScreenLayout, data: _meteringScreenLayout,
child: widget.child, child: widget.child,
), ),
); );
@ -140,65 +136,65 @@ class _UserPreferencesProviderState extends State<UserPreferencesProvider>
void enableDynamicColor(bool enable) { void enableDynamicColor(bool enable) {
setState(() { setState(() {
dynamicColor = enable; _dynamicColor = enable;
}); });
userPreferencesService.dynamicColor = enable; widget.userPreferencesService.dynamicColor = enable;
} }
void toggleEvSourceType() { void toggleEvSourceType() {
if (!ServicesProvider.of(context).environment.hasLightSensor) { if (!widget.hasLightSensor) {
return; return;
} }
setState(() { setState(() {
switch (evSourceType) { switch (_evSourceType) {
case EvSourceType.camera: case EvSourceType.camera:
evSourceType = EvSourceType.sensor; _evSourceType = EvSourceType.sensor;
case EvSourceType.sensor: case EvSourceType.sensor:
evSourceType = EvSourceType.camera; _evSourceType = EvSourceType.camera;
} }
}); });
userPreferencesService.evSourceType = evSourceType; widget.userPreferencesService.evSourceType = _evSourceType;
} }
void setLocale(SupportedLocale locale) { void setLocale(SupportedLocale locale) {
S.load(Locale(locale.intlName)).then((value) { S.load(Locale(locale.intlName)).then((value) {
setState(() { setState(() {
this.locale = locale; _locale = locale;
}); });
userPreferencesService.locale = locale; widget.userPreferencesService.locale = locale;
}); });
} }
void setMeteringScreenLayout(MeteringScreenLayoutConfig config) { void setMeteringScreenLayout(MeteringScreenLayoutConfig config) {
setState(() { setState(() {
meteringScreenLayout = config; _meteringScreenLayout = config;
}); });
userPreferencesService.meteringScreenLayout = meteringScreenLayout; widget.userPreferencesService.meteringScreenLayout = _meteringScreenLayout;
} }
void setPrimaryColor(Color primaryColor) { void setPrimaryColor(Color primaryColor) {
setState(() { setState(() {
this.primaryColor = primaryColor; _primaryColor = primaryColor;
}); });
userPreferencesService.primaryColor = primaryColor; widget.userPreferencesService.primaryColor = primaryColor;
} }
void setStopType(StopType stopType) { void setStopType(StopType stopType) {
setState(() { setState(() {
this.stopType = stopType; _stopType = stopType;
}); });
userPreferencesService.stopType = stopType; widget.userPreferencesService.stopType = stopType;
} }
void setThemeType(ThemeType themeType) { void setThemeType(ThemeType themeType) {
setState(() { setState(() {
this.themeType = themeType; _themeType = themeType;
}); });
userPreferencesService.themeType = themeType; widget.userPreferencesService.themeType = themeType;
} }
Brightness get _themeBrightness { Brightness get _themeBrightness {
switch (themeType) { switch (_themeType) {
case ThemeType.light: case ThemeType.light:
return Brightness.light; return Brightness.light;
case ThemeType.dark: case ThemeType.dark:
@ -258,8 +254,7 @@ class _UserPreferencesModel extends InheritedModel<_Aspect> {
_UserPreferencesModel oldWidget, _UserPreferencesModel oldWidget,
Set<_Aspect> dependencies, Set<_Aspect> dependencies,
) { ) {
return (dependencies.contains(_Aspect.dynamicColorState) && return (dependencies.contains(_Aspect.dynamicColorState) && dynamicColorState != oldWidget.dynamicColorState) ||
dynamicColorState != oldWidget.dynamicColorState) ||
(dependencies.contains(_Aspect.evSourceType) && evSourceType != oldWidget.evSourceType) || (dependencies.contains(_Aspect.evSourceType) && evSourceType != oldWidget.evSourceType) ||
(dependencies.contains(_Aspect.locale) && locale != oldWidget.locale) || (dependencies.contains(_Aspect.locale) && locale != oldWidget.locale) ||
(dependencies.contains(_Aspect.stopType) && stopType != oldWidget.stopType) || (dependencies.contains(_Aspect.stopType) && stopType != oldWidget.stopType) ||

View file

@ -17,10 +17,7 @@ class SelectableInheritedModel<T> extends InheritedModel<SelectableAspect> {
bool updateShouldNotify(SelectableInheritedModel oldWidget) => true; bool updateShouldNotify(SelectableInheritedModel oldWidget) => true;
@override @override
bool updateShouldNotifyDependent( bool updateShouldNotifyDependent(SelectableInheritedModel oldWidget, Set<SelectableAspect> dependencies) {
SelectableInheritedModel oldWidget,
Set<SelectableAspect> dependencies,
) {
if (dependencies.contains(SelectableAspect.list)) { if (dependencies.contains(SelectableAspect.list)) {
return true; return true;
} else if (dependencies.contains(SelectableAspect.selected)) { } else if (dependencies.contains(SelectableAspect.selected)) {

View file

@ -110,6 +110,9 @@ void main() {
expectFilmsCount(mockFilms.length + 1); expectFilmsCount(mockFilms.length + 1);
expectFilmsInUseCount(mockFilms.length + 1); expectFilmsInUseCount(mockFilms.length + 1);
expectSelectedFilmName(''); expectSelectedFilmName('');
verify(() => mockIAPStorageService.filmsInUse = mockFilms.skip(0).toList()).called(1);
verifyNever(() => mockIAPStorageService.selectedFilm = const Film.other());
}, },
); );
@ -131,6 +134,9 @@ void main() {
expectFilmsCount(mockFilms.length + 1); expectFilmsCount(mockFilms.length + 1);
expectFilmsInUseCount(mockFilms.length + 1); expectFilmsInUseCount(mockFilms.length + 1);
expectSelectedFilmName(mockFilms.first.name); expectSelectedFilmName(mockFilms.first.name);
verifyNever(() => mockIAPStorageService.filmsInUse = any<List<Film>>());
verify(() => mockIAPStorageService.selectedFilm = mockFilms.first).called(1);
}, },
); );
@ -148,6 +154,7 @@ void main() {
expectFilmsInUseCount(1); expectFilmsInUseCount(1);
expectSelectedFilmName(''); expectSelectedFilmName('');
verifyNever(() => mockIAPStorageService.filmsInUse = any<List<Film>>());
verify(() => mockIAPStorageService.selectedFilm = const Film.other()).called(1); verify(() => mockIAPStorageService.selectedFilm = const Film.other()).called(1);
}, },
); );
@ -163,6 +170,7 @@ void main() {
expectFilmsInUseCount(1); expectFilmsInUseCount(1);
expectSelectedFilmName(''); expectSelectedFilmName('');
verifyNever(() => mockIAPStorageService.filmsInUse = any<List<Film>>());
verifyNever(() => mockIAPStorageService.selectedFilm = const Film.other()); verifyNever(() => mockIAPStorageService.selectedFilm = const Film.other());
}, },
); );
@ -187,6 +195,9 @@ void main() {
expectFilmsCount(mockFilms.length + 1); expectFilmsCount(mockFilms.length + 1);
expectFilmsInUseCount((mockFilms.length - 1) + 1); expectFilmsInUseCount((mockFilms.length - 1) + 1);
expectSelectedFilmName(''); expectSelectedFilmName('');
verify(() => mockIAPStorageService.filmsInUse = mockFilms.skip(1).toList()).called(1);
verify(() => mockIAPStorageService.selectedFilm = const Film.other()).called(1);
}, },
); );
}, },

View file

@ -0,0 +1,376 @@
import 'package:dynamic_color/test_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/res/theme.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
import 'package:mocktail/mocktail.dart';
class _MockUserPreferencesService extends Mock implements UserPreferencesService {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late _MockUserPreferencesService mockUserPreferencesService;
setUpAll(() {
mockUserPreferencesService = _MockUserPreferencesService();
});
setUp(() {
when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera);
when(() => mockUserPreferencesService.stopType).thenReturn(StopType.third);
when(() => mockUserPreferencesService.meteringScreenLayout).thenReturn({
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.histogram: true,
});
when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en);
when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light);
when(() => mockUserPreferencesService.primaryColor).thenReturn(primaryColorsList[5]);
when(() => mockUserPreferencesService.dynamicColor).thenReturn(false);
});
tearDown(() {
reset(mockUserPreferencesService);
});
Future<void> pumpTestWidget(
WidgetTester tester, {
bool hasLightSensor = true,
required WidgetBuilder builder,
}) async {
await tester.pumpWidget(
UserPreferencesProvider(
hasLightSensor: hasLightSensor,
userPreferencesService: mockUserPreferencesService,
child: _Application(builder: builder),
),
);
}
group('[evSourceType]', () {
Future<void> pumpEvTestApplication(WidgetTester tester, {required bool hasLightSensor}) async {
await pumpTestWidget(
tester,
hasLightSensor: hasLightSensor,
builder: (context) => Column(
children: [
Text('EV source type: ${UserPreferencesProvider.evSourceTypeOf(context)}'),
ElevatedButton(
onPressed: UserPreferencesProvider.of(context).toggleEvSourceType,
child: const Text('toggleEvSourceType'),
),
],
),
);
}
void expectEvSource(EvSourceType evSourceType) {
expect(find.text("EV source type: $evSourceType"), findsOneWidget);
}
testWidgets(
'Init evSourceType when has sensor & stored sensor',
(tester) async {
when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.sensor);
await pumpEvTestApplication(tester, hasLightSensor: true);
expectEvSource(EvSourceType.sensor);
},
);
testWidgets(
'Init evSourceType when has no sensor & stored camera',
(tester) async {
when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera);
await pumpEvTestApplication(tester, hasLightSensor: false);
expectEvSource(EvSourceType.camera);
},
);
testWidgets(
'Init evSourceType when has no sensor & stored sensor -> Reset to camera',
(tester) async {
when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.sensor);
await pumpEvTestApplication(tester, hasLightSensor: false);
expectEvSource(EvSourceType.camera);
},
);
testWidgets(
'Try toggleEvSourceType() when has no sensor',
(tester) async {
when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera);
await pumpEvTestApplication(tester, hasLightSensor: false);
await tester.tap(find.text('toggleEvSourceType'));
await tester.pumpAndSettle();
verifyNever(() => mockUserPreferencesService.evSourceType = EvSourceType.sensor);
},
);
testWidgets(
'Try toggleEvSourceType() when has sensor',
(tester) async {
when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera);
await pumpEvTestApplication(tester, hasLightSensor: true);
await tester.tap(find.text('toggleEvSourceType'));
await tester.pumpAndSettle();
expectEvSource(EvSourceType.sensor);
verify(() => mockUserPreferencesService.evSourceType = EvSourceType.sensor).called(1);
await tester.tap(find.text('toggleEvSourceType'));
await tester.pumpAndSettle();
expectEvSource(EvSourceType.camera);
verify(() => mockUserPreferencesService.evSourceType = EvSourceType.camera).called(1);
},
);
});
testWidgets(
'Set different stop type',
(tester) async {
when(() => mockUserPreferencesService.stopType).thenReturn(StopType.third);
await pumpTestWidget(
tester,
builder: (context) => Column(
children: [
Text('Stop type: ${UserPreferencesProvider.stopTypeOf(context)}'),
ElevatedButton(
onPressed: () => UserPreferencesProvider.of(context).setStopType(StopType.full),
child: const Text('setStopType'),
),
],
),
);
expect(find.text("Stop type: ${StopType.third}"), findsOneWidget);
await tester.tap(find.text('setStopType'));
await tester.pumpAndSettle();
expect(find.text("Stop type: ${StopType.full}"), findsOneWidget);
verify(() => mockUserPreferencesService.stopType = StopType.full).called(1);
},
);
testWidgets(
'Set metering screen layout config',
(tester) async {
await pumpTestWidget(
tester,
builder: (context) {
final config = UserPreferencesProvider.meteringScreenConfigOf(context);
return Column(
children: [
...List.generate(
config.length,
(index) => Text('${config.keys.toList()[index]}: ${config.values.toList()[index]}'),
),
...List.generate(
MeteringScreenLayoutFeature.values.length,
(index) => Text(
'${MeteringScreenLayoutFeature.values[index]}: ${UserPreferencesProvider.meteringScreenFeatureOf(context, MeteringScreenLayoutFeature.values[index])}',
),
),
ElevatedButton(
onPressed: () => UserPreferencesProvider.of(context).setMeteringScreenLayout({
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.extremeExposurePairs: false,
MeteringScreenLayoutFeature.filmPicker: false,
MeteringScreenLayoutFeature.histogram: true,
}),
child: const Text(''),
),
],
);
},
);
// Match `findsNWidgets(2)` to verify that `meteringScreenFeatureOf` specific results are the same as the whole config
expect(find.text("${MeteringScreenLayoutFeature.equipmentProfiles}: true"), findsNWidgets(2));
expect(find.text("${MeteringScreenLayoutFeature.extremeExposurePairs}: true"), findsNWidgets(2));
expect(find.text("${MeteringScreenLayoutFeature.filmPicker}: true"), findsNWidgets(2));
expect(find.text("${MeteringScreenLayoutFeature.histogram}: true"), findsNWidgets(2));
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.text("${MeteringScreenLayoutFeature.equipmentProfiles}: true"), findsNWidgets(2));
expect(find.text("${MeteringScreenLayoutFeature.extremeExposurePairs}: false"), findsNWidgets(2));
expect(find.text("${MeteringScreenLayoutFeature.filmPicker}: false"), findsNWidgets(2));
expect(find.text("${MeteringScreenLayoutFeature.histogram}: true"), findsNWidgets(2));
verify(
() => mockUserPreferencesService.meteringScreenLayout = {
MeteringScreenLayoutFeature.extremeExposurePairs: false,
MeteringScreenLayoutFeature.filmPicker: false,
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.histogram: true,
},
).called(1);
},
);
testWidgets(
'Set different locale',
(tester) async {
when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en);
await pumpTestWidget(
tester,
builder: (context) => ElevatedButton(
onPressed: () => UserPreferencesProvider.of(context).setLocale(SupportedLocale.fr),
child: Text('${UserPreferencesProvider.localeOf(context)}'),
),
);
expect(find.text("${SupportedLocale.en}"), findsOneWidget);
await tester.tap(find.text("${SupportedLocale.en}"));
await tester.pumpAndSettle();
expect(find.text("${SupportedLocale.fr}"), findsOneWidget);
verify(() => mockUserPreferencesService.locale = SupportedLocale.fr).called(1);
},
);
group('[theme]', () {
testWidgets(
'Set dark theme type',
(tester) async {
when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light);
await pumpTestWidget(
tester,
builder: (context) => Column(
children: [
ElevatedButton(
onPressed: () => UserPreferencesProvider.of(context).setThemeType(ThemeType.dark),
child: Text('${UserPreferencesProvider.themeTypeOf(context)}'),
),
Text('${Theme.of(context).colorScheme.brightness}')
],
),
);
expect(find.text("${ThemeType.light}"), findsOneWidget);
expect(find.text("${Brightness.light}"), findsOneWidget);
await tester.tap(find.text("${ThemeType.light}"));
await tester.pumpAndSettle();
expect(find.text("${ThemeType.dark}"), findsOneWidget);
expect(find.text("${Brightness.dark}"), findsOneWidget);
verify(() => mockUserPreferencesService.themeType = ThemeType.dark).called(1);
},
);
testWidgets(
'Set systemDefault theme type and toggle platform brightness',
(tester) async {
when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light);
await pumpTestWidget(
tester,
builder: (context) => Column(
children: [
ElevatedButton(
onPressed: () => UserPreferencesProvider.of(context).setThemeType(ThemeType.systemDefault),
child: Text('${UserPreferencesProvider.themeTypeOf(context)}'),
),
Text('${Theme.of(context).colorScheme.brightness}')
],
),
);
TestWidgetsFlutterBinding.instance.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
expect(find.text("${ThemeType.light}"), findsOneWidget);
expect(find.text("${Brightness.light}"), findsOneWidget);
await tester.tap(find.text("${ThemeType.light}"));
await tester.pumpAndSettle();
expect(find.text("${ThemeType.systemDefault}"), findsOneWidget);
expect(find.text("${Brightness.dark}"), findsOneWidget);
verify(() => mockUserPreferencesService.themeType = ThemeType.systemDefault).called(1);
TestWidgetsFlutterBinding.instance.platformDispatcher.platformBrightnessTestValue = Brightness.light;
await tester.pumpAndSettle();
expect(find.text("${ThemeType.systemDefault}"), findsOneWidget);
expect(find.text("${Brightness.light}"), findsOneWidget);
},
);
testWidgets(
'Set primary color',
(tester) async {
when(() => mockUserPreferencesService.primaryColor).thenReturn(primaryColorsList[5]);
await pumpTestWidget(
tester,
builder: (context) => ElevatedButton(
onPressed: () => UserPreferencesProvider.of(context).setPrimaryColor(primaryColorsList[7]),
child: Text('${UserPreferencesProvider.themeOf(context).primaryColor}'),
),
);
expect(find.text("${primaryColorsList[5]}"), findsOneWidget);
await tester.tap(find.text("${primaryColorsList[5]}"));
await tester.pumpAndSettle();
expect(find.text("${primaryColorsList[7]}"), findsOneWidget);
verify(() => mockUserPreferencesService.primaryColor = primaryColorsList[7]).called(1);
},
);
testWidgets(
'Dynamic colors not available',
(tester) async {
when(() => mockUserPreferencesService.dynamicColor).thenReturn(true);
await pumpTestWidget(
tester,
builder: (context) => ElevatedButton(
onPressed: () => UserPreferencesProvider.of(context).enableDynamicColor(false),
child: Text('${UserPreferencesProvider.dynamicColorStateOf(context)}'),
),
);
await tester.pumpAndSettle();
expect(
find.text("${DynamicColorState.unavailable}"),
findsOneWidget,
reason:
"Even though dynamic colors usage is enabled, the core palette can be unavailable. Therefore `DynamicColorState` is also unavailable.",
);
},
);
testWidgets(
'Toggle dynamic color state',
(tester) async {
DynamicColorTestingUtils.setMockDynamicColors(corePalette: CorePalette.of(0xffffffff));
when(() => mockUserPreferencesService.dynamicColor).thenReturn(true);
await pumpTestWidget(
tester,
builder: (context) => ElevatedButton(
onPressed: () => UserPreferencesProvider.of(context).enableDynamicColor(false),
child: Text('${UserPreferencesProvider.dynamicColorStateOf(context)}'),
),
);
await tester.pumpAndSettle();
expect(find.text("${DynamicColorState.enabled}"), findsOneWidget);
await tester.tap(find.text("${DynamicColorState.enabled}"));
await tester.pumpAndSettle();
expect(find.text("${DynamicColorState.disabled}"), findsOneWidget);
verify(() => mockUserPreferencesService.dynamicColor = false).called(1);
},
);
});
}
class _Application extends StatelessWidget {
final WidgetBuilder builder;
const _Application({required this.builder});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: UserPreferencesProvider.themeOf(context),
home: Scaffold(body: Center(child: Builder(builder: builder))),
);
}
}