diff --git a/examples/reminders/README.md b/examples/reminders/README.md index b4e39c72d..f90afaeee 100644 --- a/examples/reminders/README.md +++ b/examples/reminders/README.md @@ -103,9 +103,9 @@ To access the design system from your app, you have to import it from the follow ### Golden tests -A [golden test][golden_test_lnk] lets you generate golden master images of a widget or screen, and compare against them so you know your design is always pixel-perfect and there have been no subtle or breaking changes in UI between builds. To make this easier, we employ the use of the [golden_toolkit][golden_toolkit_lnk] package. +A [golden test][golden_test_lnk] lets you generate golden master images of a widget or screen, and compare against them so you know your design is always pixel-perfect and there have been no subtle or breaking changes in UI between builds. To make this easier, we employ the use of the [alchemist][alchemist_lnk] package. -To get started, you just need to generate a list of `LabeledDeviceBuilder` and pass it to the `runGoldenTests` function. That's done by calling `generateDeviceBuilder` with a label, the widget/screen to be tested, as well as a `Scenario`. They provide an optional `onCreate` function which lets us execute arbitrary behavior upon testing. Each `DeviceBuilder` will have two generated golden master files, one for each theme. +To get started, you just need to generate a list of `ScenarioBuilder` and pass it to the `runGoldenTests` function. That's done by calling `buildScenario` with a label, the widget/screen to be tested, as well as an optional list of `devices` to be build on. They provide an optional `customPumpBeforeTest` function which lets us execute `WidgetTesterCallback` upon testing. Each `DeviceBuilder` will have two generated golden master files, one for each theme. Due to the way fonts are loaded in tests, any custom fonts you intend to golden test should be included in `pubspec.yaml` @@ -156,7 +156,7 @@ In order to make the notifications work on your target platform, make sure you f [firebase_configs_lnk]: https://support.google.com/firebase/answer/7015592 [design_system_lnk]: https://uxdesign.cc/everything-you-need-to-know-about-design-systems-54b109851969 [golden_test_lnk]: https://medium.com/flutter-community/flutter-golden-tests-compare-widgets-with-snapshots-27f83f266cea -[golden_toolkit_lnk]: https://pub.dev/packages/golden_toolkit +[alchemist_lnk]: https://pub.dev/packages/alchemist [retrofit_lnk]: https://pub.dev/packages/retrofit [dio_lnk]: https://pub.dev/packages/dio [json_annotation_lnk]: https://pub.dev/packages/json_annotation diff --git a/examples/reminders/lib/base/common_blocs/coordinator_bloc.dart b/examples/reminders/lib/base/common_blocs/coordinator_bloc.dart index 84fd91fca..4ed08e2da 100644 --- a/examples/reminders/lib/base/common_blocs/coordinator_bloc.dart +++ b/examples/reminders/lib/base/common_blocs/coordinator_bloc.dart @@ -8,6 +8,7 @@ import 'dart:async'; import 'package:rx_bloc/rx_bloc.dart'; +import 'package:rx_bloc_list/extensions.dart'; import 'package:rx_bloc_list/models.dart'; import 'package:rxdart/rxdart.dart'; diff --git a/examples/reminders/lib/base/services/reminders_service.dart b/examples/reminders/lib/base/services/reminders_service.dart index 06c6013ab..1a15e5395 100644 --- a/examples/reminders/lib/base/services/reminders_service.dart +++ b/examples/reminders/lib/base/services/reminders_service.dart @@ -1,3 +1,4 @@ +import 'package:flutter_rx_bloc/rx_form.dart'; import 'package:rx_bloc_list/models.dart'; import '../models/reminder/reminder_model.dart'; @@ -10,6 +11,8 @@ class RemindersService { final RemindersRepository _repository; + static const _nameValidation = 'A title must be specified'; + /// Returns all reminders Future> getAll(ReminderModelRequest request) => _repository.getAll(request); @@ -43,4 +46,15 @@ class RemindersService { /// Returns the number of incomplete reminders Future getIncompleteCount() => _repository.getIncompleteCount(); + + String validateName(String name) { + if (name.trim().isEmpty) { + throw RxFieldException( + fieldValue: name, + error: _nameValidation, + ); + } + + return name; + } } diff --git a/examples/reminders/lib/feature_dashboard/blocs/dashboard_bloc.dart b/examples/reminders/lib/feature_dashboard/blocs/dashboard_bloc.dart index 43036b4e8..34d96849a 100644 --- a/examples/reminders/lib/feature_dashboard/blocs/dashboard_bloc.dart +++ b/examples/reminders/lib/feature_dashboard/blocs/dashboard_bloc.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:rx_bloc/rx_bloc.dart'; +import 'package:rx_bloc_list/extensions.dart'; import 'package:rx_bloc_list/models.dart'; import 'package:rxdart/rxdart.dart'; diff --git a/examples/reminders/lib/feature_reminder_manage/blocs/reminder_manage_bloc.dart b/examples/reminders/lib/feature_reminder_manage/blocs/reminder_manage_bloc.dart index ad7a7b60a..af67ef6de 100644 --- a/examples/reminders/lib/feature_reminder_manage/blocs/reminder_manage_bloc.dart +++ b/examples/reminders/lib/feature_reminder_manage/blocs/reminder_manage_bloc.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:flutter_rx_bloc/rx_form.dart'; import 'package:rx_bloc/rx_bloc.dart'; import 'package:rxdart/rxdart.dart'; @@ -18,7 +17,6 @@ abstract class ReminderManageBlocEvents { void update(ReminderModel reminder); /// Event used to update the name of the current model - @RxBlocEvent(type: RxBlocEventType.behaviour, seed: '') void setName(String title); /// Event used to initiate creation of a new reminder with the provided @@ -66,8 +64,6 @@ class ReminderManageBloc extends $ReminderManageBloc { final RemindersService _service; final CoordinatorBlocType _coordinatorBloc; - static const _nameValidation = 'A title must be specified'; - @override Stream _mapToShowErrorsState() => _$createEvent .isReminderNameValid(this) @@ -114,18 +110,9 @@ class ReminderManageBloc extends $ReminderManageBloc { .publish(); @override - Stream _mapToNameState() => _$setNameEvent.map(validateName); - - String validateName(String name) { - if (name.trim().isEmpty) { - throw RxFieldException( - fieldValue: name, - error: _nameValidation, - ); - } - - return name; - } + Stream _mapToNameState() => _$setNameEvent + .map((name) => _service.validateName(name)) + .shareReplay(maxSize: 1); @override Stream _mapToIsFormValidState() => Rx.combineLatest( diff --git a/examples/reminders/pubspec.lock b/examples/reminders/pubspec.lock index 2c83296f0..c08fc339e 100644 --- a/examples/reminders/pubspec.lock +++ b/examples/reminders/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.40" + alchemist: + dependency: "direct main" + description: + name: alchemist + sha256: "2dcbfb0fe2b108797831241e48bb8d2abb902d2626332fb98b5680fb1f6bdb3a" + url: "https://pub.dev" + source: hosted + version: "0.11.0" analyzer: dependency: transitive description: @@ -233,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" facebook_auth_desktop: dependency: transitive description: @@ -500,14 +516,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" - golden_toolkit: - dependency: "direct main" - description: - name: golden_toolkit - sha256: "8f74adab33154fe7b731395782797021f97d2edc52f7bfb85ff4f1b5c4a215f0" - url: "https://pub.dev" - source: hosted - version: "0.15.0" graphs: dependency: transitive description: @@ -808,7 +816,7 @@ packages: path: "../../packages/rx_bloc_list" relative: true source: path - version: "5.0.0" + version: "5.0.1" rx_bloc_test: dependency: "direct dev" description: @@ -1065,10 +1073,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: transitive description: diff --git a/examples/reminders/pubspec.yaml b/examples/reminders/pubspec.yaml index da9eb1dab..650224765 100644 --- a/examples/reminders/pubspec.yaml +++ b/examples/reminders/pubspec.yaml @@ -7,6 +7,7 @@ environment: sdk: '>=3.0.5 <4.0.0' dependencies: + alchemist: ^0.11.0 cloud_firestore: ^5.2.0 collection: ^1.18.0 cupertino_icons: ^1.0.4 @@ -25,7 +26,6 @@ dependencies: flutter_slidable: ^3.0.0 flutter_sticky_header: ^0.6.0 go_router: ^14.2.2 - golden_toolkit: ^0.15.0 grouped_list: ^6.0.0 intl: ^0.19.0 provider: ^6.0.1 diff --git a/examples/reminders/test/feature_dashboard/factory/dashboard_factory.dart b/examples/reminders/test/feature_dashboard/factory/dashboard_factory.dart new file mode 100644 index 000000000..3e8d5e35a --- /dev/null +++ b/examples/reminders/test/feature_dashboard/factory/dashboard_factory.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rx_bloc/flutter_rx_bloc.dart'; +import 'package:provider/provider.dart'; +import 'package:reminders/base/common_blocs/firebase_bloc.dart'; +import 'package:reminders/base/models/reminder/reminder_model.dart'; +import 'package:reminders/feature_dashboard/blocs/dashboard_bloc.dart'; +import 'package:reminders/feature_dashboard/models/dashboard_model.dart'; +import 'package:reminders/feature_dashboard/views/dashboard_page.dart'; +import 'package:reminders/feature_reminder_manage/blocs/reminder_manage_bloc.dart'; +import 'package:rx_bloc/rx_bloc.dart'; +import 'package:rx_bloc_list/models.dart'; + +import '../../mocks/bloc_mocks.dart'; +import '../../mocks/reminder_manage_mock.dart'; +import '../mock/dashboard_mock.dart'; + +Widget dashboardFactory({ + bool? isLoading, + String? errors, + Result? dashboardCounters, + PaginatedList? reminderModels, +}) => + Scaffold( + body: MultiProvider(providers: [ + RxBlocProvider.value( + value: reminderManageMockFactory()), + RxBlocProvider.value(value: createFirebaseBlocMock()), + RxBlocProvider.value( + value: dashboardMockFactory( + isLoading: isLoading, + errors: errors, + dashboardCounters: dashboardCounters, + reminderModels: reminderModels, + ), + ), + ], child: DashboardPage()), + ); diff --git a/examples/reminders/test/feature_dashboard/mock/dashboard_mock.dart b/examples/reminders/test/feature_dashboard/mock/dashboard_mock.dart new file mode 100644 index 000000000..870781485 --- /dev/null +++ b/examples/reminders/test/feature_dashboard/mock/dashboard_mock.dart @@ -0,0 +1,48 @@ +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:reminders/base/models/reminder/reminder_model.dart'; +import 'package:reminders/feature_dashboard/blocs/dashboard_bloc.dart'; +import 'package:reminders/feature_dashboard/models/dashboard_model.dart'; +import 'package:rx_bloc/rx_bloc.dart'; +import 'package:rx_bloc_list/models.dart'; +import 'package:rxdart/rxdart.dart'; + +import 'dashboard_mock.mocks.dart'; + +@GenerateMocks([DashboardBlocStates, DashboardBlocEvents, DashboardBlocType]) +DashboardBlocType dashboardMockFactory({ + bool? isLoading, + String? errors, + Result? dashboardCounters, + PaginatedList? reminderModels, +}) { + final blocMock = MockDashboardBlocType(); + final eventsMock = MockDashboardBlocEvents(); + final statesMock = MockDashboardBlocStates(); + + when(blocMock.events).thenReturn(eventsMock); + when(blocMock.states).thenReturn(statesMock); + + final isLoadingState = isLoading != null + ? Stream.value(isLoading).shareReplay(maxSize: 1) + : const Stream.empty(); + + final errorsState = errors != null + ? Stream.value(errors).shareReplay(maxSize: 1) + : const Stream.empty(); + + final dashboardCountersState = dashboardCounters != null + ? Stream.value(dashboardCounters).shareReplay(maxSize: 1) + : const Stream>.empty(); + + final reminderModelsState = reminderModels != null + ? Stream.value(reminderModels).shareReplay(maxSize: 1) + : const Stream>.empty(); + + when(statesMock.isLoading).thenAnswer((_) => isLoadingState); + when(statesMock.errors).thenAnswer((_) => errorsState); + when(statesMock.dashboardCounters).thenAnswer((_) => dashboardCountersState); + when(statesMock.reminderModels).thenAnswer((_) => reminderModelsState); + + return blocMock; +} diff --git a/examples/reminders/test/feature_dashboard/view/dashboard_golden_test.dart b/examples/reminders/test/feature_dashboard/view/dashboard_golden_test.dart new file mode 100644 index 000000000..9232f8588 --- /dev/null +++ b/examples/reminders/test/feature_dashboard/view/dashboard_golden_test.dart @@ -0,0 +1,27 @@ +import 'package:rx_bloc/rx_bloc.dart'; + +import '../../helpers/golden_helper.dart'; +import '../../stubs.dart'; +import '../factory/dashboard_factory.dart'; + +void main() { + runGoldenTests([ + buildScenario(widget: dashboardFactory(), scenario: 'dashboard_empty'), + buildScenario( + widget: dashboardFactory( + dashboardCounters: Result.success(Stubs.dashboardCountersModel), + reminderModels: Stubs.reminderPaginatedList, + ), //example: Stubs.success + scenario: 'dashboard_success'), + buildScenario( + widget: dashboardFactory(dashboardCounters: Result.loading()), + scenario: 'dashboard_loading', + customPumpBeforeTest: (tester) => + tester.pump(const Duration(milliseconds: 300))), + buildScenario( + widget: dashboardFactory( + dashboardCounters: Result.error(Stubs.throwable), + errors: Stubs.errorNoConnection), + scenario: 'dashboard_error') + ]); +} diff --git a/examples/reminders/test/feature_dashboard/view/goldens/ci/dark_theme/dashboard_empty.png b/examples/reminders/test/feature_dashboard/view/goldens/ci/dark_theme/dashboard_empty.png new file mode 100644 index 000000000..1b9347fc6 Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/ci/dark_theme/dashboard_empty.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/ci/dark_theme/dashboard_error.png b/examples/reminders/test/feature_dashboard/view/goldens/ci/dark_theme/dashboard_error.png new file mode 100644 index 000000000..e771904fa Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/ci/dark_theme/dashboard_error.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/ci/dark_theme/dashboard_loading.png b/examples/reminders/test/feature_dashboard/view/goldens/ci/dark_theme/dashboard_loading.png new file mode 100644 index 000000000..52486c6d5 Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/ci/dark_theme/dashboard_loading.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/ci/dark_theme/dashboard_success.png b/examples/reminders/test/feature_dashboard/view/goldens/ci/dark_theme/dashboard_success.png new file mode 100644 index 000000000..c06cbd664 Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/ci/dark_theme/dashboard_success.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/ci/light_theme/dashboard_empty.png b/examples/reminders/test/feature_dashboard/view/goldens/ci/light_theme/dashboard_empty.png new file mode 100644 index 000000000..3a5413dfc Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/ci/light_theme/dashboard_empty.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/ci/light_theme/dashboard_error.png b/examples/reminders/test/feature_dashboard/view/goldens/ci/light_theme/dashboard_error.png new file mode 100644 index 000000000..d3f6146b6 Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/ci/light_theme/dashboard_error.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/ci/light_theme/dashboard_loading.png b/examples/reminders/test/feature_dashboard/view/goldens/ci/light_theme/dashboard_loading.png new file mode 100644 index 000000000..364f3471b Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/ci/light_theme/dashboard_loading.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/ci/light_theme/dashboard_success.png b/examples/reminders/test/feature_dashboard/view/goldens/ci/light_theme/dashboard_success.png new file mode 100644 index 000000000..f9f671d95 Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/ci/light_theme/dashboard_success.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/dark_theme/dashboard_empty.png b/examples/reminders/test/feature_dashboard/view/goldens/dark_theme/dashboard_empty.png new file mode 100644 index 000000000..aae981261 Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/dark_theme/dashboard_empty.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/dark_theme/dashboard_error.png b/examples/reminders/test/feature_dashboard/view/goldens/dark_theme/dashboard_error.png new file mode 100644 index 000000000..5b225a077 Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/dark_theme/dashboard_error.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/dark_theme/dashboard_loading.png b/examples/reminders/test/feature_dashboard/view/goldens/dark_theme/dashboard_loading.png new file mode 100644 index 000000000..b42f8408e Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/dark_theme/dashboard_loading.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/dark_theme/dashboard_success.png b/examples/reminders/test/feature_dashboard/view/goldens/dark_theme/dashboard_success.png new file mode 100644 index 000000000..4e55317e9 Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/dark_theme/dashboard_success.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/light_theme/dashboard_empty.png b/examples/reminders/test/feature_dashboard/view/goldens/light_theme/dashboard_empty.png new file mode 100644 index 000000000..3a780f6fe Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/light_theme/dashboard_empty.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/light_theme/dashboard_error.png b/examples/reminders/test/feature_dashboard/view/goldens/light_theme/dashboard_error.png new file mode 100644 index 000000000..911a1d74b Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/light_theme/dashboard_error.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/light_theme/dashboard_loading.png b/examples/reminders/test/feature_dashboard/view/goldens/light_theme/dashboard_loading.png new file mode 100644 index 000000000..6704242f4 Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/light_theme/dashboard_loading.png differ diff --git a/examples/reminders/test/feature_dashboard/view/goldens/light_theme/dashboard_success.png b/examples/reminders/test/feature_dashboard/view/goldens/light_theme/dashboard_success.png new file mode 100644 index 000000000..9328ac9ab Binary files /dev/null and b/examples/reminders/test/feature_dashboard/view/goldens/light_theme/dashboard_success.png differ diff --git a/examples/reminders/test/feature_reminder_list/factory/reminder_list_factory.dart b/examples/reminders/test/feature_reminder_list/factory/reminder_list_factory.dart new file mode 100644 index 000000000..57e1bc8b1 --- /dev/null +++ b/examples/reminders/test/feature_reminder_list/factory/reminder_list_factory.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rx_bloc/flutter_rx_bloc.dart'; +import 'package:provider/provider.dart'; +import 'package:reminders/base/common_blocs/coordinator_bloc.dart'; +import 'package:reminders/base/common_blocs/firebase_bloc.dart'; +import 'package:reminders/base/models/reminder/reminder_model.dart'; +import 'package:reminders/base/services/reminders_service.dart'; +import 'package:reminders/feature_reminder_list/blocs/reminder_list_bloc.dart'; +import 'package:reminders/feature_reminder_list/services/reminder_list_service.dart'; +import 'package:reminders/feature_reminder_list/views/reminder_list_page.dart'; +import 'package:reminders/feature_reminder_manage/blocs/reminder_manage_bloc.dart'; +import 'package:rx_bloc_list/models.dart'; + +import '../../mocks/bloc_mocks.dart'; +import '../../mocks/coordinator_mock.dart'; +import '../../mocks/reminder_manage_mock.dart'; +import '../../mocks/service_mocks.dart'; +import '../mock/reminder_list_mock.dart'; + +Widget reminderListFactory({ + bool? isLoading, + String? errors, + PaginatedList? paginatedList, +}) => + Scaffold( + body: MultiProvider(providers: [ + Provider( + create: (context) => createRemindersServiceMock(), + ), + Provider( + create: (context) => createReminderListServiceMock(), + ), + RxBlocProvider.value( + value: coordinatorMockFactory(), + ), + RxBlocProvider.value( + value: reminderListMockFactory( + isLoading: isLoading, + errors: errors, + paginatedList: paginatedList, + ), + ), + RxBlocProvider.value( + value: createFirebaseBlocMock(), + ), + RxBlocProvider.value( + value: reminderManageMockFactory(), + ), + ], child: const ReminderListPage()), + ); diff --git a/examples/reminders/test/feature_reminder_list/mock/reminder_list_mock.dart b/examples/reminders/test/feature_reminder_list/mock/reminder_list_mock.dart new file mode 100644 index 000000000..ec50ef61f --- /dev/null +++ b/examples/reminders/test/feature_reminder_list/mock/reminder_list_mock.dart @@ -0,0 +1,42 @@ +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:reminders/base/models/reminder/reminder_model.dart'; +import 'package:reminders/feature_reminder_list/blocs/reminder_list_bloc.dart'; +import 'package:rx_bloc_list/models.dart'; +import 'package:rxdart/rxdart.dart'; + +import 'reminder_list_mock.mocks.dart'; + +@GenerateMocks( + [ReminderListBlocStates, ReminderListBlocEvents, ReminderListBlocType]) +ReminderListBlocType reminderListMockFactory({ + bool? isLoading, + String? errors, + PaginatedList? paginatedList, +}) { + final blocMock = MockReminderListBlocType(); + final eventsMock = MockReminderListBlocEvents(); + final statesMock = MockReminderListBlocStates(); + + when(blocMock.events).thenReturn(eventsMock); + when(blocMock.states).thenReturn(statesMock); + + final isLoadingState = isLoading != null + ? Stream.value(isLoading).shareReplay(maxSize: 1) + : const Stream.empty(); + + final errorsState = errors != null + ? Stream.value(errors).shareReplay(maxSize: 1) + : const Stream.empty(); + + final paginatedListState = paginatedList != null + ? Stream.value(paginatedList).shareReplay(maxSize: 1) + : const Stream>.empty(); + + when(statesMock.isLoading).thenAnswer((_) => isLoadingState); + when(statesMock.errors).thenAnswer((_) => errorsState); + when(statesMock.paginatedList).thenAnswer((_) => paginatedListState); + when(statesMock.refreshDone).thenAnswer((_) => Future.value(null)); + + return blocMock; +} diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/ci/dark_theme/reminder_list_empty.png b/examples/reminders/test/feature_reminder_list/view/goldens/ci/dark_theme/reminder_list_empty.png new file mode 100644 index 000000000..fcc01c935 Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/ci/dark_theme/reminder_list_empty.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/ci/dark_theme/reminder_list_error.png b/examples/reminders/test/feature_reminder_list/view/goldens/ci/dark_theme/reminder_list_error.png new file mode 100644 index 000000000..79470cf17 Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/ci/dark_theme/reminder_list_error.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/ci/dark_theme/reminder_list_loading.png b/examples/reminders/test/feature_reminder_list/view/goldens/ci/dark_theme/reminder_list_loading.png new file mode 100644 index 000000000..a68cc895d Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/ci/dark_theme/reminder_list_loading.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/ci/dark_theme/reminder_list_success.png b/examples/reminders/test/feature_reminder_list/view/goldens/ci/dark_theme/reminder_list_success.png new file mode 100644 index 000000000..b67c175f2 Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/ci/dark_theme/reminder_list_success.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/ci/light_theme/reminder_list_empty.png b/examples/reminders/test/feature_reminder_list/view/goldens/ci/light_theme/reminder_list_empty.png new file mode 100644 index 000000000..f2a0fa270 Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/ci/light_theme/reminder_list_empty.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/ci/light_theme/reminder_list_error.png b/examples/reminders/test/feature_reminder_list/view/goldens/ci/light_theme/reminder_list_error.png new file mode 100644 index 000000000..6e728d1bd Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/ci/light_theme/reminder_list_error.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/ci/light_theme/reminder_list_loading.png b/examples/reminders/test/feature_reminder_list/view/goldens/ci/light_theme/reminder_list_loading.png new file mode 100644 index 000000000..061e1d6c7 Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/ci/light_theme/reminder_list_loading.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/ci/light_theme/reminder_list_success.png b/examples/reminders/test/feature_reminder_list/view/goldens/ci/light_theme/reminder_list_success.png new file mode 100644 index 000000000..303f2911d Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/ci/light_theme/reminder_list_success.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/dark_theme/reminder_list_empty.png b/examples/reminders/test/feature_reminder_list/view/goldens/dark_theme/reminder_list_empty.png new file mode 100644 index 000000000..f1c0c0af1 Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/dark_theme/reminder_list_empty.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/dark_theme/reminder_list_error.png b/examples/reminders/test/feature_reminder_list/view/goldens/dark_theme/reminder_list_error.png new file mode 100644 index 000000000..8c086c2a4 Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/dark_theme/reminder_list_error.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/dark_theme/reminder_list_loading.png b/examples/reminders/test/feature_reminder_list/view/goldens/dark_theme/reminder_list_loading.png new file mode 100644 index 000000000..6bd19372a Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/dark_theme/reminder_list_loading.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/dark_theme/reminder_list_success.png b/examples/reminders/test/feature_reminder_list/view/goldens/dark_theme/reminder_list_success.png new file mode 100644 index 000000000..23533e1fb Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/dark_theme/reminder_list_success.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/light_theme/reminder_list_empty.png b/examples/reminders/test/feature_reminder_list/view/goldens/light_theme/reminder_list_empty.png new file mode 100644 index 000000000..67462c659 Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/light_theme/reminder_list_empty.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/light_theme/reminder_list_error.png b/examples/reminders/test/feature_reminder_list/view/goldens/light_theme/reminder_list_error.png new file mode 100644 index 000000000..7fa3d8f0f Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/light_theme/reminder_list_error.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/light_theme/reminder_list_loading.png b/examples/reminders/test/feature_reminder_list/view/goldens/light_theme/reminder_list_loading.png new file mode 100644 index 000000000..16def40fd Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/light_theme/reminder_list_loading.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/goldens/light_theme/reminder_list_success.png b/examples/reminders/test/feature_reminder_list/view/goldens/light_theme/reminder_list_success.png new file mode 100644 index 000000000..a13e21ab5 Binary files /dev/null and b/examples/reminders/test/feature_reminder_list/view/goldens/light_theme/reminder_list_success.png differ diff --git a/examples/reminders/test/feature_reminder_list/view/reminder_list_golden_test.dart b/examples/reminders/test/feature_reminder_list/view/reminder_list_golden_test.dart new file mode 100644 index 000000000..d94bdb20a --- /dev/null +++ b/examples/reminders/test/feature_reminder_list/view/reminder_list_golden_test.dart @@ -0,0 +1,24 @@ +import '../../helpers/golden_helper.dart'; +import '../../stubs.dart'; +import '../factory/reminder_list_factory.dart'; + +void main() { + runGoldenTests([ + buildScenario( + widget: reminderListFactory(paginatedList: Stubs.paginatedListEmpty), + scenario: 'reminder_list_empty'), + buildScenario( + widget: reminderListFactory(paginatedList: Stubs.reminderPaginatedList), + scenario: 'reminder_list_success'), + buildScenario( + widget: reminderListFactory( + paginatedList: Stubs.paginatedListEmpty.copyWith( + isLoading: true, + isInitialized: false, + )), + scenario: 'reminder_list_loading'), + buildScenario( + widget: reminderListFactory(errors: 'Error occur'), + scenario: 'reminder_list_error') + ]); +} diff --git a/examples/reminders/test/feature_reminder_manage/blocs/reminder_manage_test.dart b/examples/reminders/test/feature_reminder_manage/blocs/reminder_manage_test.dart new file mode 100644 index 000000000..6843c9d9e --- /dev/null +++ b/examples/reminders/test/feature_reminder_manage/blocs/reminder_manage_test.dart @@ -0,0 +1,180 @@ +import 'package:flutter_rx_bloc/rx_form.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:reminders/base/common_blocs/coordinator_bloc.dart'; +import 'package:reminders/base/models/reminder/reminder_model.dart'; +import 'package:reminders/base/services/reminders_service.dart'; +import 'package:reminders/feature_reminder_manage/blocs/reminder_manage_bloc.dart'; +import 'package:rx_bloc/rx_bloc.dart'; +import 'package:rx_bloc_test/rx_bloc_test.dart'; + +import '../../mocks/coordinator_mock.dart'; +import '../../mocks/service_mocks.dart'; +import '../../stubs.dart'; + +void main() { + late RemindersService reminderService; + late CoordinatorBlocType coordinatorBloc; + late CoordinatorEvents coordinatorEvents; + + void defineWhen() { + when(coordinatorBloc.events).thenReturn(coordinatorEvents); + when(coordinatorEvents + .reminderCreated(Result.success(Stubs.createdReminderNote))) + .thenAnswer((_) {}); + when(coordinatorEvents + .reminderDeleted(Result.success(Stubs.createdReminderNote))) + .thenAnswer((_) {}); + when(coordinatorEvents + .reminderUpdated(Result.success(Stubs.reminderPairNote))) + .thenAnswer((_) {}); + when(reminderService.create( + title: Stubs.noteNameValid, + dueDate: Stubs.dueDate, + complete: false, + )).thenAnswer((_) => Future.value(Stubs.createdReminderNote)); + when(reminderService.delete(Stubs.createdReminderNote.id)) + .thenAnswer((_) async {}); + when(reminderService.update(Stubs.updatedReminderNote)) + .thenAnswer((_) => Future.value(Stubs.reminderPairNote)); + when(reminderService.validateName(Stubs.emptyString)) + .thenThrow(Stubs.fieldException(Stubs.emptyString)); + when(reminderService.validateName(Stubs.noteNameValid)) + .thenReturn(Stubs.noteNameValid); + } + + ReminderManageBloc manageBloc() => + ReminderManageBloc(reminderService, coordinatorBloc); + setUp(() { + reminderService = createRemindersServiceMock(); + coordinatorBloc = coordinatorMockFactory(); + coordinatorEvents = coordinatorBloc.events; + }); + + group('test reminder_manage_bloc_dart state onDeleted', () { + rxBlocTest>( + 'test reminder_manage_bloc_dart state onDeleted', + build: () async { + defineWhen(); + return manageBloc(); + }, + act: (bloc) async { + bloc.events.delete(Stubs.createdReminderNote); + }, + state: (bloc) => bloc.states.onDeleted, + expect: [ + isA(), + isA>(), + ]); + }); + + group('test reminder_manage_bloc_dart state onCreated', () { + rxBlocTest>( + 'test reminder_manage_bloc_dart state onCreated', + build: () async { + defineWhen(); + return manageBloc(); + }, + act: (bloc) async { + bloc.states.name.listen((_) {}); + bloc.states.isFormValid.listen((_) {}); + bloc.events.setName(Stubs.noteNameValid); + bloc.events.create(dueDate: Stubs.dueDate, complete: false); + }, + state: (bloc) => bloc.states.onCreated, + expect: [ + isA(), + isA>(), + ]); + }); + + group('test reminder_manage_bloc_dart state name', () { + rxBlocTest( + 'test reminder_manage_bloc_dart state name - invalid name', + build: () async { + defineWhen(); + return manageBloc(); + }, + act: (bloc) async => bloc.events.setName(Stubs.emptyString), + state: (bloc) => bloc.states.name, + expect: [emitsError(isA>())]); + rxBlocTest( + 'test reminder_manage_bloc_dart state name - valid name', + build: () async { + defineWhen(); + return manageBloc(); + }, + act: (bloc) async { + bloc.events.setName(Stubs.noteNameValid); + }, + state: (bloc) => bloc.states.name, + expect: [ + Stubs.noteNameValid, + ]); + }); + + group('test reminder_manage_bloc_dart state showErrors', () { + rxBlocTest( + 'test reminder_manage_bloc_dart state showErrors - empty', + build: () async { + defineWhen(); + return manageBloc(); + }, + act: (bloc) async { + bloc.states.name.listen((_) {}); + bloc.events.setName(Stubs.noteNameValid); + bloc.events.create(dueDate: Stubs.dueDate, complete: false); + }, + state: (bloc) => bloc.states.showErrors, + expect: [false]); + + rxBlocTest( + 'test reminder_manage_bloc_dart state showErrors - has error', + build: () async { + defineWhen(); + return manageBloc(); + }, + act: (bloc) async { + bloc.events.setName(Stubs.emptyString); + bloc.events.create(dueDate: Stubs.dueDate, complete: false); + }, + state: (bloc) => bloc.states.showErrors, + expect: [false, true]); + }); + + group('test reminder_manage_bloc_dart state isFormValid', () { + rxBlocTest( + 'test reminder_manage_bloc_dart state isFormValid - empty', + build: () async { + defineWhen(); + return manageBloc(); + }, + act: (bloc) async { + bloc.states.name.listen((_) {}); + }, + state: (bloc) => bloc.states.isFormValid, + expect: const Iterable.empty()); + rxBlocTest( + 'test reminder_manage_bloc_dart state isFormValid - name valid', + build: () async { + defineWhen(); + return manageBloc(); + }, + act: (bloc) async { + bloc.events.setName(Stubs.noteNameValid); + }, + state: (bloc) => bloc.states.isFormValid, + expect: [true]); + rxBlocTest( + 'test reminder_manage_bloc_dart state isFormValid - name invalid', + build: () async { + defineWhen(); + return manageBloc(); + }, + act: (bloc) async { + bloc.events.setName(Stubs.emptyString); + }, + state: (bloc) => bloc.states.isFormValid, + expect: [false]); + }); +} diff --git a/examples/reminders/test/feature_splash/blocs/splash_test.dart b/examples/reminders/test/feature_splash/blocs/splash_test.dart new file mode 100644 index 000000000..575dab98c --- /dev/null +++ b/examples/reminders/test/feature_splash/blocs/splash_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:reminders/feature_splash/blocs/splash_bloc.dart'; +import 'package:reminders/lib_router/blocs/router_bloc.dart'; +import 'package:rx_bloc_test/rx_bloc_test.dart'; + +import '../../mocks/bloc_mocks.dart'; + +void main() { + late RouterBlocType navigationBloc; + late String? redirectLocation; + + void defineWhen() {} + + SplashBloc splashBloc() => SplashBloc( + navigationBloc, + redirectLocation: redirectLocation, + ); + setUp(() { + navigationBloc = createRouterBlocTypeMock(); + redirectLocation = '/'; + }); + + group('test splash_bloc_dart state isLoading', () { + rxBlocTest('test splash_bloc_dart state isLoading', + build: () async { + defineWhen(); + return splashBloc(); + }, + act: (bloc) async {}, + state: (bloc) => bloc.states.isLoading, + expect: [false, true]); + }); + + group('test splash_bloc_dart state errors', () { + rxBlocTest('test splash_bloc_dart state errors', + build: () async { + defineWhen(); + return splashBloc(); + }, + act: (bloc) async {}, + state: (bloc) => bloc.states.errors, + expect: const Iterable.empty()); + }); +} diff --git a/examples/reminders/test/feature_splash/factory/splash_factory.dart b/examples/reminders/test/feature_splash/factory/splash_factory.dart new file mode 100644 index 000000000..33b91c07c --- /dev/null +++ b/examples/reminders/test/feature_splash/factory/splash_factory.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rx_bloc/flutter_rx_bloc.dart'; +import 'package:provider/provider.dart'; +import 'package:reminders/feature_splash/blocs/splash_bloc.dart'; +import 'package:reminders/feature_splash/views/splash_page.dart'; +import '../mock/splash_mock.dart'; + +Widget splashFactory({ + bool? isLoading, + String? errors, +}) => + Scaffold( + body: MultiProvider(providers: [ + RxBlocProvider.value( + value: splashMockFactory( + isLoading: isLoading ?? false, + errors: errors, + ), + ), + ], child: const SplashPage()), + ); diff --git a/examples/reminders/test/feature_splash/mock/splash_mock.dart b/examples/reminders/test/feature_splash/mock/splash_mock.dart new file mode 100644 index 000000000..9615b73a6 --- /dev/null +++ b/examples/reminders/test/feature_splash/mock/splash_mock.dart @@ -0,0 +1,30 @@ +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:reminders/feature_splash/blocs/splash_bloc.dart'; +import 'package:rxdart/rxdart.dart'; + +import 'splash_mock.mocks.dart'; + +@GenerateMocks([SplashBlocStates, SplashBlocEvents, SplashBlocType]) +SplashBlocType splashMockFactory({ + required bool isLoading, + String? errors, +}) { + final blocMock = MockSplashBlocType(); + final eventsMock = MockSplashBlocEvents(); + final statesMock = MockSplashBlocStates(); + + when(blocMock.events).thenReturn(eventsMock); + when(blocMock.states).thenReturn(statesMock); + + final isLoadingState = Stream.value(isLoading).shareReplay(maxSize: 1); + + final errorsState = errors != null + ? Stream.value(errors).shareReplay(maxSize: 1) + : const Stream.empty(); + + when(statesMock.isLoading).thenAnswer((_) => isLoadingState); + when(statesMock.errors).thenAnswer((_) => errorsState); + + return blocMock; +} diff --git a/examples/reminders/test/feature_splash/view/goldens/ci/dark_theme/splash_error.png b/examples/reminders/test/feature_splash/view/goldens/ci/dark_theme/splash_error.png new file mode 100644 index 000000000..efab78496 Binary files /dev/null and b/examples/reminders/test/feature_splash/view/goldens/ci/dark_theme/splash_error.png differ diff --git a/examples/reminders/test/feature_splash/view/goldens/ci/dark_theme/splash_loading.png b/examples/reminders/test/feature_splash/view/goldens/ci/dark_theme/splash_loading.png new file mode 100644 index 000000000..632a46f2e Binary files /dev/null and b/examples/reminders/test/feature_splash/view/goldens/ci/dark_theme/splash_loading.png differ diff --git a/examples/reminders/test/feature_splash/view/goldens/ci/dark_theme/splash_success.png b/examples/reminders/test/feature_splash/view/goldens/ci/dark_theme/splash_success.png new file mode 100644 index 000000000..36f66edb2 Binary files /dev/null and b/examples/reminders/test/feature_splash/view/goldens/ci/dark_theme/splash_success.png differ diff --git a/examples/reminders/test/feature_splash/view/goldens/ci/light_theme/splash_error.png b/examples/reminders/test/feature_splash/view/goldens/ci/light_theme/splash_error.png new file mode 100644 index 000000000..a14f66efd Binary files /dev/null and b/examples/reminders/test/feature_splash/view/goldens/ci/light_theme/splash_error.png differ diff --git a/examples/reminders/test/feature_splash/view/goldens/ci/light_theme/splash_loading.png b/examples/reminders/test/feature_splash/view/goldens/ci/light_theme/splash_loading.png new file mode 100644 index 000000000..df17dbd3e Binary files /dev/null and b/examples/reminders/test/feature_splash/view/goldens/ci/light_theme/splash_loading.png differ diff --git a/examples/reminders/test/feature_splash/view/goldens/ci/light_theme/splash_success.png b/examples/reminders/test/feature_splash/view/goldens/ci/light_theme/splash_success.png new file mode 100644 index 000000000..4dd3bee77 Binary files /dev/null and b/examples/reminders/test/feature_splash/view/goldens/ci/light_theme/splash_success.png differ diff --git a/examples/reminders/test/feature_splash/view/goldens/dark_theme/splash_error.png b/examples/reminders/test/feature_splash/view/goldens/dark_theme/splash_error.png new file mode 100644 index 000000000..8d3cf35f8 Binary files /dev/null and b/examples/reminders/test/feature_splash/view/goldens/dark_theme/splash_error.png differ diff --git a/examples/reminders/test/feature_splash/view/goldens/dark_theme/splash_loading.png b/examples/reminders/test/feature_splash/view/goldens/dark_theme/splash_loading.png new file mode 100644 index 000000000..a27a0f262 Binary files /dev/null and b/examples/reminders/test/feature_splash/view/goldens/dark_theme/splash_loading.png differ diff --git a/examples/reminders/test/feature_splash/view/goldens/dark_theme/splash_success.png b/examples/reminders/test/feature_splash/view/goldens/dark_theme/splash_success.png new file mode 100644 index 000000000..d770668f2 Binary files /dev/null and b/examples/reminders/test/feature_splash/view/goldens/dark_theme/splash_success.png differ diff --git a/examples/reminders/test/feature_splash/view/goldens/light_theme/splash_error.png b/examples/reminders/test/feature_splash/view/goldens/light_theme/splash_error.png new file mode 100644 index 000000000..77e5105d4 Binary files /dev/null and b/examples/reminders/test/feature_splash/view/goldens/light_theme/splash_error.png differ diff --git a/examples/reminders/test/feature_splash/view/goldens/light_theme/splash_loading.png b/examples/reminders/test/feature_splash/view/goldens/light_theme/splash_loading.png new file mode 100644 index 000000000..3d2be0a30 Binary files /dev/null and b/examples/reminders/test/feature_splash/view/goldens/light_theme/splash_loading.png differ diff --git a/examples/reminders/test/feature_splash/view/goldens/light_theme/splash_success.png b/examples/reminders/test/feature_splash/view/goldens/light_theme/splash_success.png new file mode 100644 index 000000000..eb22ad3b2 Binary files /dev/null and b/examples/reminders/test/feature_splash/view/goldens/light_theme/splash_success.png differ diff --git a/examples/reminders/test/feature_splash/view/splash_golden_test.dart b/examples/reminders/test/feature_splash/view/splash_golden_test.dart new file mode 100644 index 000000000..8df8afed3 --- /dev/null +++ b/examples/reminders/test/feature_splash/view/splash_golden_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter_test/flutter_test.dart'; + +import '../../helpers/golden_helper.dart'; +import '../../stubs.dart'; +import '../factory/splash_factory.dart'; + +void main() { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + runGoldenTests([ + buildScenario(widget: splashFactory(), scenario: 'splash_success'), + buildScenario( + widget: splashFactory(isLoading: true), + scenario: 'splash_loading', + customPumpBeforeTest: (tester) => + tester.pump(const Duration(microseconds: 300))), + buildScenario( + widget: splashFactory(errors: Stubs.errorNoConnection), + scenario: 'splash_error') + ]); +} diff --git a/examples/reminders/test/flutter_test_config.dart b/examples/reminders/test/flutter_test_config.dart index e847b80c0..0bcded31a 100644 --- a/examples/reminders/test/flutter_test_config.dart +++ b/examples/reminders/test/flutter_test_config.dart @@ -1,9 +1,56 @@ import 'dart:async'; + +import 'package:alchemist/alchemist.dart'; import 'package:flutter/material.dart'; -import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'helpers/goldens_file_comparator.dart'; + +/// Flag indicating if the tests are running in a CI environment +/// Note: The environment variable may differ based on the platform. +const bool _isRunningInCi = bool.fromEnvironment('CI'); -Future testExecutable(FutureOr Function() testMain) async { - WidgetsApp.debugAllowBannerOverride = false; - await loadAppFonts(); - return testMain(); +/// Resolves the file path for the golden image based on the name and environment +FutureOr _filePathResolver(String name, String env) { + // Resolve the theme name by removing the theme from name and placing it + // in the correct directory + if (name.endsWith('_light')) { + name = 'light_theme/${name.replaceAll('_light', '')}'; + } else if (name.endsWith('_dark')) { + name = 'dark_theme/${name.replaceAll('_dark', '')}'; + } + + final fileName = 'goldens/${env.toLowerCase()}/$name.png'; + goldenFileComparator = GoldensFileComparator(fileName); + return fileName; } + +Future testExecutable(FutureOr Function() testMain) async => + AlchemistConfig.runWithConfig( + config: AlchemistConfig( + goldenTestTheme: GoldenTestTheme( + backgroundColor: Colors.grey, + borderColor: Colors.transparent, + nameTextStyle: const TextStyle( + color: Colors.black, + fontSize: 16, + ), + ), + platformGoldensConfig: const PlatformGoldensConfig( + enabled: !_isRunningInCi, + obscureText: false, + renderShadows: true, + filePathResolver: _filePathResolver, + ), + ciGoldensConfig: const CiGoldensConfig( + enabled: _isRunningInCi, + obscureText: false, + renderShadows: true, + filePathResolver: _filePathResolver, + ), + ), + run: () async { + WidgetsApp.debugAllowBannerOverride = false; + return testMain(); + }, + ); diff --git a/examples/reminders/test/helpers/enums/app_themes.dart b/examples/reminders/test/helpers/enums/app_themes.dart new file mode 100644 index 000000000..840db20e3 --- /dev/null +++ b/examples/reminders/test/helpers/enums/app_themes.dart @@ -0,0 +1,5 @@ +/// Enum to represent the available themes in the app +enum Themes { + light, + dark, +} diff --git a/examples/reminders/test/helpers/enums/golden_alignment.dart b/examples/reminders/test/helpers/enums/golden_alignment.dart new file mode 100644 index 000000000..7fc875144 --- /dev/null +++ b/examples/reminders/test/helpers/enums/golden_alignment.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +/// Enum to specify the alignment of the scenario within the resulting layout +enum GoldenAlignment { + top, + center, + bottom; + + /// Converts the [GoldenAlignment] to a [TableCellVerticalAlignment] + TableCellVerticalAlignment asCellAlignment() { + switch (this) { + case GoldenAlignment.top: + return TableCellVerticalAlignment.top; + case GoldenAlignment.center: + return TableCellVerticalAlignment.middle; + case GoldenAlignment.bottom: + return TableCellVerticalAlignment.bottom; + } + } +} diff --git a/examples/reminders/test/helpers/golden_helper.dart b/examples/reminders/test/helpers/golden_helper.dart index 6b8fb5046..cbac24304 100644 --- a/examples/reminders/test/helpers/golden_helper.dart +++ b/examples/reminders/test/helpers/golden_helper.dart @@ -1,137 +1,239 @@ +import 'package:alchemist/alchemist.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:reminders/app_extensions.dart'; +import 'package:reminders/base/theme/reminders_theme.dart'; -import 'package:reminders/base/theme/design_system.dart'; -import 'package:reminders/l10n/l10n.dart'; +import 'enums/app_themes.dart'; +import 'enums/golden_alignment.dart'; +import 'models/device.dart'; +import 'widgets/fixed_size_scenario_builder.dart'; +import 'widgets/scenario_builder.dart'; -import 'models/labeled_device_builder.dart'; -import 'models/scenario.dart'; +/// region Type definitions -enum Themes { light, dark } +/// Type definition for a function that wraps a widget with a theme and pumps it +typedef WidgetWithThemePump = Future Function( + WidgetTester, + Widget, + Themes? theme, +); + +/// Type definition for a function that accepts a [WidgetTester] parameter +typedef WidgetTesterCallback = Future Function(WidgetTester widgetTester); + +/// Default devices to run golden tests on +const _defaultDevices = [ + Device( + name: 'iPhone SE(2nd generation)', + size: Size(375, 667), + safeArea: EdgeInsets.only(top: 20), + devicePixelRatio: 2), + Device( + name: 'Google Pixel 4a', + size: Size(412, 732), + ), + Device( + name: 'iPhone 13 mini', + size: Size(375, 812), + safeArea: EdgeInsets.only(top: 44, bottom: 34), + devicePixelRatio: 3), + Device( + name: 'Google Pixel 5', + size: Size(393, 851), + safeArea: EdgeInsets.only(top: 24, bottom: 48), + devicePixelRatio: 2.75), + Device( + name: 'Samsung Galaxy S20', + size: Size(412, 915), + safeArea: EdgeInsets.only(top: 24, bottom: 48), + devicePixelRatio: 3), + Device( + name: 'Samsung Galaxy Tab S6 Landscape', + size: Size(1280, 800), + safeArea: EdgeInsets.only(top: 24, bottom: 48), + devicePixelRatio: 2), + Device( + name: 'Apple iPad Pro 12.9', + size: Size(1024, 1366), + safeArea: EdgeInsets.only(top: 24, bottom: 34), + devicePixelRatio: 2), +]; + +/// endregion -/// return a [LabeledDeviceBuilder] with a scenario rendered on all device sizes -/// -/// [widget] - to be rendered in the golden master -/// -/// [scenario] - [Scenario] which will be added to [DeviceBuilder] -LabeledDeviceBuilder generateDeviceBuilder({ +/// region Builders + +/// Convenience method that builds a [ScenarioBuilder] with a scenario rendered +/// on specified devices laid out in one row +ScenarioBuilder buildScenario({ required Widget widget, - required Scenario scenario, -}) { - final deviceBuilder = LabeledDeviceBuilder(label: scenario.name) - ..overrideDevicesForAllScenarios( - devices: [ - Device.phone, - Device.iphone11, - Device.tabletPortrait, - Device.tabletLandscape, - ], - ) - ..addScenario( + required String scenario, + WidgetTesterCallback? customPumpBeforeTest, + List devices = _defaultDevices, + EdgeInsets? scenarioPadding = const EdgeInsets.symmetric(horizontal: 4), +}) => + ScenarioBuilder( + name: scenario, widget: widget, - name: scenario.name, - onCreate: scenario.onCreate, + devices: devices, + customPumpBeforeTest: customPumpBeforeTest, + scenarioPadding: scenarioPadding, + columns: _defaultDevices.length, + goldenAlignment: GoldenAlignment.center, ); - return deviceBuilder; + +/// Convenience method that builds a [ScenarioBuilder] with a scenario rendered +/// on specified devices laid out in a grid +ScenarioBuilder buildScenarioGrid({ + required Widget widget, + required String scenario, + WidgetTesterCallback? customPumpBeforeTest, + GoldenAlignment? goldenAlignment, + int? columns, + List devices = _defaultDevices, + EdgeInsets? scenarioPadding = const EdgeInsets.all(4), +}) => + ScenarioBuilder( + name: scenario, + widget: widget, + devices: devices, + columns: columns, + customPumpBeforeTest: customPumpBeforeTest, + goldenAlignment: goldenAlignment ?? GoldenAlignment.top, + scenarioPadding: scenarioPadding, + ); + +/// endregion + +/// region Golden test runners + +/// Runs golden tests for a list of UI components in both light and dark mode, +/// all of the same size. +void runUiComponentGoldenTests({ + required List children, + required String scenario, + required Size size, + WidgetWithThemePump? customWrapAndPump, + WidgetTesterCallback? act, + EdgeInsets? scenarioPadding, + GoldenAlignment? goldenAlignment, +}) { + runGoldenTests( + [ + FixedSizeScenarioBuilder( + name: scenario, + size: size, + scenarioPadding: scenarioPadding, + goldenAlignment: goldenAlignment ?? GoldenAlignment.top, + children: children, + ) + ], + customWrapAndPump: customWrapAndPump, + act: act, + ); } -/// executes golden tests for each [LabeledDeviceBuilder] in every [theme] -/// -/// [deviceBuilders] - list of [LabeledDeviceBuilder] to be pumped -/// -/// [pumpFunction] (optional) - function for executing custom pumping -/// behavior instead of [pumpDeviceBuilderWithLocalizationsAndTheme] +/// Runs golden tests for a list of scenarios in both light and dark mode void runGoldenTests( - List deviceBuilders, { - Future Function(WidgetTester, DeviceBuilder, Themes? theme)? - pumpFunction, + List buildScenarios, { + WidgetWithThemePump? customWrapAndPump, + WidgetTesterCallback? act, }) { - for (final db in deviceBuilders) { - //test each DeviceBuilder in both light mode and dark mode + for (final scenario in buildScenarios) { for (final theme in Themes.values) { final themeName = theme.name; - final directory = '${themeName}_theme'; - - testGoldens('$db - $themeName', (tester) async { - pumpFunction != null - ? await pumpFunction.call(tester, db, theme) - : await pumpDeviceBuilderWithLocalizationsAndTheme( - tester, - db, - theme: theme, - ); - - await screenMatchesGolden( - tester, - '$directory/$db', - //defaults to pumpAndSettle, causing problems when testing animations - customPump: db.label.contains('loading') - ? (tester) => tester.pump(const Duration(milliseconds: 300)) - : null, - ); - }); + final scenarioName = scenario.name; + + goldenTest( + '$scenarioName - $themeName', + fileName: '${scenarioName}_$themeName', + builder: () => scenario, + pumpWidget: (tester, widget) => + customWrapAndPump?.call(tester, widget, theme) ?? + pumpDeviceBuilderWithLocalizationsAndTheme( + tester, + widget, + theme: theme, + ), + pumpBeforeTest: scenario.customPumpBeforeTest ?? onlyPumpAndSettle, + whilePerforming: + act != null ? (tester) async => () async => act(tester) : null, + ); } } } -/// calls [pumpDeviceBuilderWithMaterialApp] with localizations we need in this -/// app, and injects an optional theme +/// endregion + +/// region Pump helpers + +/// Pumps the provided [widget] and injects a [MaterialApp] wrapper, +/// localizations and theme. Future pumpDeviceBuilderWithLocalizationsAndTheme( WidgetTester tester, - DeviceBuilder builder, { + Widget widget, { Themes? theme, }) => - pumpDeviceBuilderWithMaterialApp( + pumpScenarioBuilderWithMaterialApp( tester, - builder, + widget, localizations: const [ AppLocalizations.delegate, + ...GlobalMaterialLocalizations.delegates, GlobalMaterialLocalizations.delegate, ], localeOverrides: AppLocalizations.supportedLocales, theme: theme == Themes.light - ? DesignSystem.fromBrightness(Brightness.light).theme - : DesignSystem.fromBrightness(Brightness.dark).theme, + ? RemindersTheme.buildTheme( + DesignSystem.fromBrightness(Brightness.light)) + : RemindersTheme.buildTheme( + DesignSystem.fromBrightness(Brightness.dark)), ); -/// Wraps a [DeviceBuilder] in a [materialAppWrapper] using any of the -/// parameters we specify and pumps it -/// -/// [tester] - [WidgetTester] DI -/// -/// [builder] - [DeviceBuilder] to be pupmped -/// -/// [platform] will override Theme's platform. -/// -/// [localizations] (optional) - -/// a list of [LocalizationsDelegate] that is required for this test -/// -/// [navigatorObserver] (optional) - -/// an interface for observing the behavior of a [Navigator]. -/// -/// [localeOverrides] (optional) - -/// sets supported supportedLocales, defaults to [Locale('en')] -/// -/// [theme] (optional) - Your app theme -Future pumpDeviceBuilderWithMaterialApp( +/// Pumps the provided [widget] and injects a [MaterialApp] wrapper +Future pumpScenarioBuilderWithMaterialApp( WidgetTester tester, - DeviceBuilder builder, { + Widget widget, { TargetPlatform platform = TargetPlatform.android, Iterable>? localizations, NavigatorObserver? navigatorObserver, Iterable? localeOverrides, ThemeData? theme, }) async { - await tester.pumpDeviceBuilder( - builder, - wrapper: materialAppWrapper( - platform: platform, - localizations: localizations, - navigatorObserver: navigatorObserver, - localeOverrides: localeOverrides, - theme: theme, - ), + await onlyPumpWidget( + tester, + _buildMaterialAppWrapper( + child: widget, + platform: platform, + localizations: localizations, + navigatorObserver: navigatorObserver, + localeOverrides: localeOverrides, + theme: theme, + )); +} + +/// Wraps the provided [child] with a [MaterialApp] to ensure that the golden +/// test is run in a consistent environment. +Widget _buildMaterialAppWrapper({ + required Widget child, + TargetPlatform platform = TargetPlatform.android, + Iterable>? localizations, + NavigatorObserver? navigatorObserver, + Iterable? localeOverrides, + ThemeData? theme, +}) { + return MaterialApp( + localizationsDelegates: localizations, + supportedLocales: localeOverrides ?? const [Locale('en')], + theme: theme?.copyWith(platform: platform), + debugShowCheckedModeBanner: true, + home: Material(child: child), + navigatorObservers: [ + if (navigatorObserver != null) navigatorObserver, + ], ); } + +/// endregion diff --git a/examples/reminders/test/helpers/goldens_file_comparator.dart b/examples/reminders/test/helpers/goldens_file_comparator.dart new file mode 100644 index 000000000..01bbb3067 --- /dev/null +++ b/examples/reminders/test/helpers/goldens_file_comparator.dart @@ -0,0 +1,58 @@ +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class GoldensFileComparator extends LocalFileComparator { + static const double _kGoldenDiffTolerance = 0.0; + + static final List _basePathKeywordsToRemove = [ + 'goldens', + 'macos', + 'windows', + 'linux', + 'ci', + 'light_theme', + 'dark_theme', + ]; + + GoldensFileComparator(String testFile) + : super(Uri.parse(_getTestFile(testFile))); + + @override + Future compare(Uint8List imageBytes, Uri golden) async { + final goldenUri = Uri.parse(golden.path.split('/').last); + + final ComparisonResult result = await GoldenFileComparator.compareLists( + imageBytes, + await getGoldenBytes(goldenUri), + ); + + if (!result.passed && result.diffPercent > _kGoldenDiffTolerance) { + final String error = await generateFailureOutput(result, golden, basedir); + throw FlutterError(error); + } + if (!result.passed) { + log('A tolerable difference of ${result.diffPercent * 100}% was found when ' + 'comparing $golden.'); + } + return result.passed || result.diffPercent <= _kGoldenDiffTolerance; + } + + @override + Future update(Uri golden, Uint8List imageBytes) { + final uri = Uri.parse(golden.pathSegments.last); + return super.update(uri, imageBytes); + } + + static String _getTestFile(String fileName) { + final baseDir = + (goldenFileComparator as LocalFileComparator).basedir.path.split('/'); + + baseDir.removeWhere( + (element) => _basePathKeywordsToRemove.contains(element), + ); + + return '${baseDir.join('/')}$fileName'; + } +} diff --git a/examples/reminders/test/helpers/models/device.dart b/examples/reminders/test/helpers/models/device.dart new file mode 100644 index 000000000..6e10c65d2 --- /dev/null +++ b/examples/reminders/test/helpers/models/device.dart @@ -0,0 +1,114 @@ +// Source: https://github.com/greendrop/flutter_news_sample/blob/develop/test/support/alchemist/device.dart + +import 'package:flutter/material.dart'; + +/// Configuration class used for setting up golden tests +class Device { + /// This [Device] is a configuration for golden test + const Device({ + required this.size, + required this.name, + this.devicePixelRatio = 1.0, + this.textScaleFactor = 1.0, + this.brightness = Brightness.light, + this.safeArea = EdgeInsets.zero, + }); + + /// Example of phone with smallest phone screens + static const Device phone = Device(name: 'phone', size: Size(375, 667)); + + /// Example of phone that matches specs of iphone11 + static const Device iphone11 = Device( + name: 'iphone11', + size: Size(414, 896), + devicePixelRatio: 1.0, + safeArea: EdgeInsets.only(top: 44, bottom: 34), + ); + + /// [tabletPortrait] example of tablet that in portrait mode + static const Device tabletPortrait = + Device(name: 'tablet_portrait', size: Size(1024, 1366)); + + /// [tabletLandscape] example of tablet that in landscape mode + static const Device tabletLandscape = + Device(name: 'tablet_landscape', size: Size(1366, 1024)); + + static List all = [ + phone, + phone.dark(), + phone.toLandscape(), + phone.toLandscape().dark(), + tabletPortrait, + tabletPortrait.dark(), + tabletLandscape, + tabletLandscape.dark(), + ]; + + /// The [name] of the device. Ex: Phone, Tablet, Watch + final String name; + + /// The screen [size] of the device. Ex: Size(1366, 1024)) + final Size size; + + /// Device Pixel Ratio + final double devicePixelRatio; + + /// Custom text scale factor + final double textScaleFactor; + + /// Platform brightness (light or dark mode) + final Brightness brightness; + + /// Insets to define a safe area + final EdgeInsets safeArea; + + /// Convenience function for [Device] modification + Device copyWith({ + Size? size, + double? devicePixelRatio, + String? name, + double? textScale, + Brightness? brightness, + EdgeInsets? safeArea, + }) { + return Device( + size: size ?? this.size, + devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio, + name: name ?? this.name, + textScaleFactor: textScale ?? textScaleFactor, + brightness: brightness ?? this.brightness, + safeArea: safeArea ?? this.safeArea, + ); + } + + /// Convenience method to copy the current device and apply dark theme + Device dark() { + return Device( + size: size, + devicePixelRatio: devicePixelRatio, + textScaleFactor: textScaleFactor, + brightness: Brightness.dark, + safeArea: safeArea, + name: '${name}_dark', + ); + } + + /// Convenience method to transform the current device to a landscape version + Device toLandscape() { + return copyWith(size: Size(size.height, size.width)); + } + + @override + String toString() { + return 'Device: $name, ' + '${size.width}x${size.height} @ $devicePixelRatio, ' + 'text: $textScaleFactor, $brightness, safe: $safeArea'; + } + + bool get isDark => brightness == Brightness.dark; + bool get isLight => brightness == Brightness.light; + bool get isPortrait => size.height > size.width; + bool get isLandscape => size.width > size.height; + bool get isTablet => size.shortestSide > 600; + bool get isPhone => size.shortestSide <= 600; +} diff --git a/examples/reminders/test/helpers/models/labeled_device_builder.dart b/examples/reminders/test/helpers/models/labeled_device_builder.dart deleted file mode 100644 index 049b1d74a..000000000 --- a/examples/reminders/test/helpers/models/labeled_device_builder.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:golden_toolkit/golden_toolkit.dart'; - -class LabeledDeviceBuilder extends DeviceBuilder { - LabeledDeviceBuilder({ - required this.label, - }) : super(); - - final String label; - - @override - String toString() => label; -} diff --git a/examples/reminders/test/helpers/models/scenario.dart b/examples/reminders/test/helpers/models/scenario.dart deleted file mode 100644 index e6074febd..000000000 --- a/examples/reminders/test/helpers/models/scenario.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class Scenario { - /// Describe scenarios to be rendered by [DeviceBuilder] - /// - /// [name] - name of scenario in golden snapshot 'e.g 'error_state' - /// - /// [onCreate] (optional) - executes arbitrary behavior upon widget creation - Scenario({ - required this.name, - this.onCreate, - }); - - final String name; - final Future Function(Key)? onCreate; - - Scenario copyWith({ - String? name, - Future Function(Key)? onCreate, - }) => - Scenario( - name: name ?? this.name, - onCreate: onCreate ?? this.onCreate, - ); -} diff --git a/examples/reminders/test/helpers/widgets/fixed_size_scenario_builder.dart b/examples/reminders/test/helpers/widgets/fixed_size_scenario_builder.dart new file mode 100644 index 000000000..630541bfa --- /dev/null +++ b/examples/reminders/test/helpers/widgets/fixed_size_scenario_builder.dart @@ -0,0 +1,53 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; + +import '../models/device.dart'; +import 'golden_test_device_scenario.dart'; +import 'scenario_builder.dart'; + +/// Widget that builds a [GoldenTestGroup] with a specified configuration on +/// a list of UI components, all of the same size. +/// Each UI component will be labeled with its key, if available. +class FixedSizeScenarioBuilder extends ScenarioBuilder { + FixedSizeScenarioBuilder({ + required this.children, + required Size size, + required super.name, + super.scenarioPadding, + super.goldenAlignment, + super.columns, + super.key, + }) : super( + devices: [ + Device( + name: name, + size: size, + ) + ], + widget: Container(), + ); + + /// List of widgets to be rendered in the scenario + final List children; + + @override + Widget build(BuildContext context) => GoldenTestGroup( + columns: columns, + children: [ + ...children.map( + (widget) => TableCell( + verticalAlignment: goldenAlignment.asCellAlignment(), + child: GoldenTestDeviceScenario( + device: devices.first, + scenarioName: + widget.key != null && widget.key is ValueKey + ? (widget.key as ValueKey).value + : name, + padding: scenarioPadding, + child: Scaffold(body: widget), + ), + ), + ), + ], + ); +} diff --git a/examples/reminders/test/helpers/widgets/golden_test_device_scenario.dart b/examples/reminders/test/helpers/widgets/golden_test_device_scenario.dart new file mode 100644 index 000000000..b165ab09e --- /dev/null +++ b/examples/reminders/test/helpers/widgets/golden_test_device_scenario.dart @@ -0,0 +1,53 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; + +import '../models/device.dart'; + +class GoldenTestDeviceScenario extends StatelessWidget { + const GoldenTestDeviceScenario({ + required this.device, + required this.scenarioName, + required this.child, + this.padding, + super.key, + }); + + /// The [device] specification by which to render the scenario + final Device device; + + /// The name of the scenario + final String scenarioName; + + /// The widget to render + final Widget child; + + /// Optional [padding] to be applied to the scenario + final EdgeInsets? padding; + + @override + Widget build(BuildContext context) => Padding( + padding: padding ?? EdgeInsets.zero, + child: GoldenTestScenario( + name: '$scenarioName - ${device.name}', + child: ClipRect( + child: MediaQuery( + data: MediaQuery.of(context).copyWith( + size: device.size, + padding: device.safeArea, + platformBrightness: device.brightness, + devicePixelRatio: device.devicePixelRatio, + textScaler: TextScaler.linear(device.textScaleFactor), + ), + child: SizedBox( + height: device.size.height, + width: device.size.width, + child: TickerMode( + enabled: false, + child: child, + ), + ), + ), + ), + ), + ); +} diff --git a/examples/reminders/test/helpers/widgets/scenario_builder.dart b/examples/reminders/test/helpers/widgets/scenario_builder.dart new file mode 100644 index 000000000..128399037 --- /dev/null +++ b/examples/reminders/test/helpers/widgets/scenario_builder.dart @@ -0,0 +1,62 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../enums/golden_alignment.dart'; +import '../models/device.dart'; +import 'golden_test_device_scenario.dart'; + +/// Widget that builds a [GoldenTestGroup] with a specified configuration on +/// a list of devices +class ScenarioBuilder extends StatelessWidget { + const ScenarioBuilder({ + required this.name, + required this.widget, + required this.devices, + this.scenarioPadding, + this.columns, + this.customPumpBeforeTest, + this.goldenAlignment = GoldenAlignment.top, + super.key, + }); + + /// Name of the scenario + final String name; + + /// Widget to be used for a golden test + final Widget widget; + + /// List of devices to render the scenario on + final List devices; + + /// Padding to be applied to individual scenarios + final EdgeInsets? scenarioPadding; + + /// The number of columns in the resulting golden image. If left unset, + /// the number of columns will be calculated based on the number of children. + final int? columns; + + /// The alignment of the scenario within the resulting layout + final GoldenAlignment goldenAlignment; + + /// A custom pump method that will be called before each test + final Future Function(WidgetTester)? customPumpBeforeTest; + + @override + Widget build(BuildContext context) => GoldenTestGroup( + columns: columns, + children: [ + ...devices.map( + (device) => TableCell( + verticalAlignment: goldenAlignment.asCellAlignment(), + child: GoldenTestDeviceScenario( + device: device, + scenarioName: name, + padding: scenarioPadding, + child: widget, + ), + ), + ) + ], + ); +} diff --git a/examples/reminders/test/mocks/bloc_mocks.dart b/examples/reminders/test/mocks/bloc_mocks.dart new file mode 100644 index 000000000..cdb0e2b1e --- /dev/null +++ b/examples/reminders/test/mocks/bloc_mocks.dart @@ -0,0 +1,12 @@ +import 'package:mockito/annotations.dart'; +import 'package:reminders/base/common_blocs/firebase_bloc.dart'; +import 'package:reminders/lib_router/blocs/router_bloc.dart'; + +import 'bloc_mocks.mocks.dart'; + +@GenerateMocks([ + FirebaseBloc, + RouterBlocType, +]) +MockFirebaseBloc createFirebaseBlocMock() => MockFirebaseBloc(); +MockRouterBlocType createRouterBlocTypeMock() => MockRouterBlocType(); diff --git a/examples/reminders/test/mocks/coordinator_mock.dart b/examples/reminders/test/mocks/coordinator_mock.dart new file mode 100644 index 000000000..38e2c280a --- /dev/null +++ b/examples/reminders/test/mocks/coordinator_mock.dart @@ -0,0 +1,21 @@ +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:reminders/base/common_blocs/coordinator_bloc.dart'; + +import 'coordinator_mock.mocks.dart'; + +@GenerateMocks([ + CoordinatorStates, + CoordinatorEvents, + CoordinatorBlocType, +]) +CoordinatorBlocType coordinatorMockFactory() { + final blocMock = MockCoordinatorBlocType(); + final eventsMock = MockCoordinatorEvents(); + final statesMock = MockCoordinatorStates(); + + when(blocMock.events).thenReturn(eventsMock); + when(blocMock.states).thenReturn(statesMock); + + return blocMock; +} diff --git a/examples/reminders/test/mocks/reminder_manage_mock.dart b/examples/reminders/test/mocks/reminder_manage_mock.dart new file mode 100644 index 000000000..eaf11c633 --- /dev/null +++ b/examples/reminders/test/mocks/reminder_manage_mock.dart @@ -0,0 +1,70 @@ +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:reminders/base/models/reminder/reminder_model.dart'; +import 'package:reminders/feature_reminder_manage/blocs/reminder_manage_bloc.dart'; +import 'package:rx_bloc/rx_bloc.dart'; +import 'package:rxdart/rxdart.dart'; + +import 'reminder_manage_mock.mocks.dart'; + +@GenerateMocks([ + ReminderManageBlocStates, + ReminderManageBlocEvents, + ReminderManageBlocType +]) +ReminderManageBlocType reminderManageMockFactory({ + bool? isFormValid, + bool? showErrors, + String? name, + Result? newReminderNote, + Result? deletedReminderNote, + Result? updatedReminderNote, +}) { + final blocMock = MockReminderManageBlocType(); + final eventsMock = MockReminderManageBlocEvents(); + final statesMock = MockReminderManageBlocStates(); + + when(blocMock.events).thenReturn(eventsMock); + when(blocMock.states).thenReturn(statesMock); + + final isFormValidState = (isFormValid != null + ? Stream.value(isFormValid) + : const Stream.empty()) + .shareReplay(maxSize: 1); + + final showErrorsState = (showErrors != null + ? Stream.value(showErrors) + : const Stream.empty()) + .shareReplay(maxSize: 1); + + final nameState = + (name != null ? Stream.value(name) : const Stream.empty()) + .shareReplay(maxSize: 1); + + final onCreatedState = (newReminderNote != null + ? Stream.value(newReminderNote) + : const Stream>.empty()) + .publishReplay(maxSize: 1) + ..connect(); + + final onDeletedState = (deletedReminderNote != null + ? Stream.value(deletedReminderNote) + : const Stream>.empty()) + .publishReplay(maxSize: 1) + ..connect(); + + final onUpdatedState = (updatedReminderNote != null + ? Stream.value(updatedReminderNote) + : const Stream>.empty()) + .publishReplay(maxSize: 1) + ..connect(); + + when(statesMock.isFormValid).thenAnswer((_) => isFormValidState); + when(statesMock.showErrors).thenAnswer((_) => showErrorsState); + when(statesMock.name).thenAnswer((_) => nameState); + when(statesMock.onCreated).thenAnswer((_) => onCreatedState); + when(statesMock.onDeleted).thenAnswer((_) => onDeletedState); + when(statesMock.onUpdated).thenAnswer((_) => onUpdatedState); + + return blocMock; +} diff --git a/examples/reminders/test/mocks/repository_mocks.dart b/examples/reminders/test/mocks/repository_mocks.dart new file mode 100644 index 000000000..2661f84f6 --- /dev/null +++ b/examples/reminders/test/mocks/repository_mocks.dart @@ -0,0 +1,14 @@ +import 'package:mockito/annotations.dart'; +import 'package:reminders/base/repositories/firebase_repository.dart'; +import 'package:reminders/base/repositories/reminders_repository.dart'; + +import 'repository_mocks.mocks.dart'; + +@GenerateMocks([ + RemindersRepository, + FirebaseRepository, +]) +MockRemindersRepository createRemindersRepositoryMock() => + MockRemindersRepository(); +MockFirebaseRepository createFirebaseRepositoryMock() => + MockFirebaseRepository(); diff --git a/examples/reminders/test/mocks/service_mocks.dart b/examples/reminders/test/mocks/service_mocks.dart new file mode 100644 index 000000000..7033e8703 --- /dev/null +++ b/examples/reminders/test/mocks/service_mocks.dart @@ -0,0 +1,19 @@ +import 'package:mockito/annotations.dart'; +import 'package:reminders/base/services/firebase_service.dart'; +import 'package:reminders/base/services/reminders_service.dart'; +import 'package:reminders/feature_dashboard/services/dashboard_service.dart'; +import 'package:reminders/feature_reminder_list/services/reminder_list_service.dart'; + +import 'service_mocks.mocks.dart'; + +@GenerateMocks([ + RemindersService, + ReminderListService, + DashboardService, + FirebaseService, +]) +MockRemindersService createRemindersServiceMock() => MockRemindersService(); +MockReminderListService createReminderListServiceMock() => + MockReminderListService(); +MockDashboardService createDashboardServiceMock() => MockDashboardService(); +MockFirebaseService createFirebaseServiceMock() => MockFirebaseService(); diff --git a/examples/reminders/test/stubs.dart b/examples/reminders/test/stubs.dart new file mode 100644 index 000000000..2a300cd68 --- /dev/null +++ b/examples/reminders/test/stubs.dart @@ -0,0 +1,72 @@ +import 'package:flutter_rx_bloc/rx_form.dart'; +import 'package:reminders/base/models/reminder/reminder_model.dart'; +import 'package:reminders/feature_dashboard/models/dashboard_model.dart'; +import 'package:rx_bloc_list/models.dart'; +import 'package:rxdart/rxdart.dart'; + +class Stubs { + static PaginatedList get reminderPaginatedList => + PaginatedList(list: [ + createReminderNote(id: '1', name: 'reminder_note_1', completed: true), + createReminderNote(id: '2', name: 'reminder_note_2'), + createReminderNote(id: '3', name: 'reminder_note_3'), + createReminderNote(id: '4', name: 'reminder_note_4'), + createReminderNote(id: '5', name: 'reminder_note_5', completed: true), + createReminderNote(id: '6', name: 'reminder_note_6'), + createReminderNote(id: '7', name: 'reminder_note_7', completed: true), + createReminderNote(id: '8', name: 'reminder_note_8', completed: true), + createReminderNote( + id: '9', + name: 'reminder_note_9', + ), + ], pageSize: 10, totalCount: 9, isLoading: false, isInitialized: true); + + static ReminderModel reminderNote(int index) => ReminderModel.fromIndex(index) + .copyWith(dueDate: DateTime(2027, index, index % 12)); + + static ReminderModel createReminderNote( + {required String id, required String name, bool completed = false}) => + ReminderModel(id: id, title: name, dueDate: dueDate, complete: false); + + static ReminderModel get createdReminderNote => ReminderModel( + id: '1', title: noteNameValid, dueDate: dueDate, complete: false); + + static ReminderModel get updatedReminderNote => ReminderModel( + id: '1', title: noteNameValid, dueDate: dueDate, complete: true); + + static ReminderPair get reminderPairNote => + ReminderPair(updated: updatedReminderNote, old: createdReminderNote); + + static DashboardCountersModel get dashboardCountersModel => + DashboardCountersModel( + incompleteCount: 5, + completeCount: 4, + ); + + static PaginatedList get paginatedListEmpty => + PaginatedList( + list: [], + pageSize: 10, + ); + + static BehaviorSubject> + get subjectOfPaginatedList => + BehaviorSubject>.seeded( + PaginatedList( + list: [], + pageSize: 10, + ), + ); + + static Exception get throwable => Exception(['An error occur!']); + + static RxFieldException fieldException(String fieldValue) => + RxFieldException( + error: 'A title must be specified', fieldValue: fieldValue); + + static const String errorNoConnection = + 'There is no internet connection. Please, try again later!'; + static const String emptyString = ''; + static const String noteNameValid = 'noteNameValid'; + static DateTime get dueDate => DateTime(2029); +}