diff --git a/.vscode/launch.json b/.vscode/launch.json index 822d34d..1d2b113 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,6 +8,7 @@ "name": "dev (android)", "request": "launch", "type": "dart", + "flutterMode": "profile", "args": [ "--flavor", "dev", @@ -52,5 +53,17 @@ ], "program": "${workspaceFolder}/lib/main_prod.dart", }, + { + "name": "Integration Test", + "request": "launch", + "type": "dart", + "args": [ + "--flavor", + "prod", + "--dart-define", + "cameraPreviewAspectRatio=240/320", + ], + "program": "${workspaceFolder}/integration_test/widget_dialog_animated_test.dart", + }, ], } \ No newline at end of file diff --git a/integration_test/mocks/application_mock.dart b/integration_test/mocks/application_mock.dart new file mode 100644 index 0000000..be2de3b --- /dev/null +++ b/integration_test/mocks/application_mock.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:lightmeter/data/models/theme_type.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/theme_provider.dart'; +import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:mocktail/mocktail.dart'; + +class _MockUserPreferencesService extends Mock implements UserPreferencesService {} + +class ApplicationMock extends StatefulWidget { + final Widget child; + + const ApplicationMock({required this.child, super.key}); + + @override + State createState() => _ApplicationMockState(); +} + +class _ApplicationMockState extends State { + late final _MockUserPreferencesService userPreferencesService; + + @override + void initState() { + super.initState(); + userPreferencesService = _MockUserPreferencesService(); + when(() => userPreferencesService.themeType).thenReturn(ThemeType.light); + when(() => userPreferencesService.primaryColor) + .thenReturn(ThemeProvider.primaryColorsList.first); + when(() => userPreferencesService.dynamicColor).thenReturn(false); + } + + @override + Widget build(BuildContext context) { + return InheritedWidgetBase( + data: userPreferencesService, + child: ThemeProvider( + child: Builder( + builder: (context) { + return MaterialApp( + theme: context.listen(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + builder: (context, child) => MediaQuery( + data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + child: child!, + ), + home: widget.child, + ); + }, + ), + ), + ); + } +} diff --git a/integration_test/widget_dialog_animated_test.dart b/integration_test/widget_dialog_animated_test.dart new file mode 100644 index 0000000..5bea94d --- /dev/null +++ b/integration_test/widget_dialog_animated_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:lightmeter/data/models/film.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart'; + +import 'mocks/application_mock.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('AnimatedDialogPicker test', () { + testWidgets('Tap on `ReadingValueContainer`, verify opened', (tester) async { + await tester.pumpWidget(const ApplicationMock(child: AnimatedPickerTest())); + expect(find.text('Film'), findsOneWidget); + expect(find.text('None'), findsOneWidget); + + await tester.tap(find.byType(AnimatedDialogPicker)); + await tester.pumpAndSettle(Dimens.durationL); + expect(find.text('Film'), findsNWidgets(2)); + expect(find.text('None'), findsNWidgets(2)); + }); + }); +} + +class AnimatedPickerTest extends StatefulWidget { + const AnimatedPickerTest({super.key}); + + @override + State createState() => _AnimatedPickerTestState(); +} + +class _AnimatedPickerTestState extends State { + Film _selectedFilm = Film.values.first; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: _FilmPicker( + values: Film.values, + selectedValue: _selectedFilm, + onChanged: (value) { + setState(() { + _selectedFilm = value; + }); + }, + ), + ), + ); + } +} + +class _FilmPicker extends StatelessWidget { + final List values; + final Film selectedValue; + final ValueChanged onChanged; + + const _FilmPicker({ + required this.values, + required this.selectedValue, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return AnimatedDialogPicker( + icon: Icons.camera_roll, + title: "Film", + selectedValue: selectedValue, + values: values, + itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? 'None' : value.name), + onChanged: onChanged, + closedChild: ReadingValueContainer.singleValue( + value: ReadingValue( + label: "Film", + value: selectedValue.name.isEmpty ? 'None' : selectedValue.name, + ), + ), + ); + } +} diff --git a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart index cea97da..f0a4305 100644 --- a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart +++ b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart @@ -294,6 +294,7 @@ class _AnimatedSwitcher extends StatelessWidget { return Stack( alignment: Alignment.center, children: [ + // https://api.flutter.dev/flutter/widgets/Opacity-class.html#performance-considerations-for-opacity-animation Opacity( opacity: closedOpacityAnimation.value, child: Transform.scale( @@ -304,10 +305,15 @@ class _AnimatedSwitcher extends StatelessWidget { ), ), ), - Opacity( - opacity: openedOpacityAnimation.value, - child: openedChild, - ), + + /// When dialog is only started expanding there is too little horizontal space, + /// which leads to the failed ListTile assertion (listTileWidget != leading.width). + /// So we show the picker only when it makes sense as it begins to be less opaque. + if (openedOpacityAnimation.value != 0) + Opacity( + opacity: openedOpacityAnimation.value, + child: openedChild, + ), ], ); } diff --git a/pubspec.yaml b/pubspec.yaml index 7f4fcb5..9f0fa2a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,8 @@ dev_dependencies: flutter_test: sdk: flutter google_fonts: 3.0.1 + integration_test: + sdk: flutter lint: 2.1.2 mocktail: 0.3.0 test: 1.24.1