From 9347d0c63be0431cdcdb9ae6a9d886a3b9c5a0df Mon Sep 17 00:00:00 2001 From: MarkG Date: Thu, 24 Jun 2021 17:19:00 +0700 Subject: [PATCH] chore: write tests for side menu screen (#61) --- lib/modules/home/components/body.dart | 1 + lib/modules/home/components/top_bar.dart | 4 +- lib/modules/home/home_presenter.dart | 8 +- lib/modules/home/home_view.dart | 4 +- lib/modules/side_menu/components/actions.dart | 1 + lib/modules/side_menu/components/user.dart | 1 + lib/modules/side_menu/side_menu_view.dart | 3 + test/modules/home/home_presenter_test.dart | 30 +++++ test/modules/home/home_view_test.dart | 55 +++++++++ test/modules/home/home_view_test.mocks.dart | 40 +++++-- .../side_menu/side_menu_interactor_test.dart | 63 ++++++++++ .../side_menu_interactor_test.mocks.dart | 113 ++++++++++++++++++ .../side_menu/side_menu_module_test.dart | 27 +++++ .../side_menu/side_menu_presenter_test.dart | 65 ++++++++++ .../side_menu_presenter_test.mocks.dart | 81 +++++++++++++ .../side_menu/side_menu_router_test.dart | 42 +++++++ .../side_menu/side_menu_view_test.dart | 78 ++++++++++++ .../side_menu/side_menu_view_test.mocks.dart | 40 +++++++ 18 files changed, 639 insertions(+), 17 deletions(-) create mode 100644 test/modules/side_menu/side_menu_interactor_test.dart create mode 100644 test/modules/side_menu/side_menu_interactor_test.mocks.dart create mode 100644 test/modules/side_menu/side_menu_module_test.dart create mode 100644 test/modules/side_menu/side_menu_presenter_test.dart create mode 100644 test/modules/side_menu/side_menu_presenter_test.mocks.dart create mode 100644 test/modules/side_menu/side_menu_router_test.dart create mode 100644 test/modules/side_menu/side_menu_view_test.dart create mode 100644 test/modules/side_menu/side_menu_view_test.mocks.dart diff --git a/lib/modules/home/components/body.dart b/lib/modules/home/components/body.dart index 6ae7b4f8..8f78b7f0 100644 --- a/lib/modules/home/components/body.dart +++ b/lib/modules/home/components/body.dart @@ -22,6 +22,7 @@ class Body extends StatelessWidget { sliderMain: StreamsSelector0.value( stream: state._isUserInteractionEnabled, builder: (_, isUserInteractionEnabled, child) => IgnorePointer( + key: HomeView.mainIgnorePointer, ignoring: !isUserInteractionEnabled, child: child, ), diff --git a/lib/modules/home/components/top_bar.dart b/lib/modules/home/components/top_bar.dart index 6af0d197..2e90369d 100644 --- a/lib/modules/home/components/top_bar.dart +++ b/lib/modules/home/components/top_bar.dart @@ -48,7 +48,9 @@ class TopBar extends StatelessWidget { ], ), PlatformButton( - onPressed: () => state.delegate?.userAvatarDidTap.add(null), + key: HomeView.userAvatarButtonKey, + onPressed: () => + state.delegate?.userAvatarButtonDidTap.add(null), materialFlat: (_, __) => MaterialFlatButtonData( color: Colors.transparent, ), diff --git a/lib/modules/home/home_presenter.dart b/lib/modules/home/home_presenter.dart index 34d97146..1e57820a 100644 --- a/lib/modules/home/home_presenter.dart +++ b/lib/modules/home/home_presenter.dart @@ -9,7 +9,9 @@ class HomePresenterImpl extends HomePresenter stateDidInit.voidListen(_stateDidInit).addTo(disposeBag); showDetailButtonDidTap.listen(_showDetailButtonDidTap).addTo(disposeBag); didSwipeDown.voidListen(_didSwipeDown).addTo(disposeBag); - userAvatarDidTap.voidListen(_userAvatarDidTap).addTo(disposeBag); + userAvatarButtonDidTap + .voidListen(_userAvatarButtonDidTap) + .addTo(disposeBag); currentPageDidChange.listen(_currentPageDidChange).addTo(disposeBag); sideMenuDidShow.voidListen(_sideMenuDidShow).addTo(disposeBag); sideMenuDidDismiss.voidListen(_sideMenuDidDismiss).addTo(disposeBag); @@ -44,7 +46,7 @@ class HomePresenterImpl extends HomePresenter final alertDialogDidClose = BehaviorSubject(); @override - final userAvatarDidTap = BehaviorSubject(); + final userAvatarButtonDidTap = BehaviorSubject(); @override final sideMenuDidDismiss = BehaviorSubject(); @@ -123,7 +125,7 @@ class HomePresenterImpl extends HomePresenter interactor.fetchSurveysFromRemote(); } - void _userAvatarDidTap() { + void _userAvatarButtonDidTap() { view.showSideMenu(); } diff --git a/lib/modules/home/home_view.dart b/lib/modules/home/home_view.dart index a4973e0a..d0c729e9 100644 --- a/lib/modules/home/home_view.dart +++ b/lib/modules/home/home_view.dart @@ -7,7 +7,7 @@ abstract class HomeViewDelegate implements AlertViewMixinDelegate { BehaviorSubject get didSwipeDown; - BehaviorSubject get userAvatarDidTap; + BehaviorSubject get userAvatarButtonDidTap; BehaviorSubject get sideMenuDidShow; @@ -20,6 +20,7 @@ abstract class HomeView extends View with AlertViewMixin, ProgressHUDViewMixin { static const currentDateTextKey = Key("current_date_text"); static const userAvatarImageKey = Key("user_avatar_image"); + static const userAvatarButtonKey = Key("user_avatar_button"); static const titleTextSlideItemKey = Key("title_text_slide_item"); static const descriptionTextSlideItemKey = Key("description_text_slide_item"); static const backgroundImageSlideItemKey = Key("background_image_slide_item"); @@ -27,6 +28,7 @@ abstract class HomeView extends View static const skeletonKey = Key("skeleton_key"); static const showDetailButtonKey = Key("show_detail_button"); static const sliderMenuContainerKey = Key("slider_menu_container"); + static const mainIgnorePointer = Key("home_ignore_pointer"); static const dotPageControlHighlightColor = Colors.white; static const dotPageControlNormalColor = Color.fromRGBO(255, 255, 255, 0.2); diff --git a/lib/modules/side_menu/components/actions.dart b/lib/modules/side_menu/components/actions.dart index 71b193d1..3e03d58f 100644 --- a/lib/modules/side_menu/components/actions.dart +++ b/lib/modules/side_menu/components/actions.dart @@ -8,6 +8,7 @@ class Actions extends StatelessWidget { final state = context.findAncestorStateOfType<_SideMenuViewImplState>()!; return PlatformButton( + key: SideMenuView.logoutButtonKey, onPressed: () => state.delegate?.logoutButtonDidTap.add(null), cupertino: (_, __) => CupertinoButtonData( padding: EdgeInsets.zero, diff --git a/lib/modules/side_menu/components/user.dart b/lib/modules/side_menu/components/user.dart index c876bc81..3c642c6c 100644 --- a/lib/modules/side_menu/components/user.dart +++ b/lib/modules/side_menu/components/user.dart @@ -37,6 +37,7 @@ class User extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(18), child: Image( + key: SideMenuView.userAvatarImageKey, image: NetworkImage(user.avatarUrl!), ), ), diff --git a/lib/modules/side_menu/side_menu_view.dart b/lib/modules/side_menu/side_menu_view.dart index 59f8e1df..3b90581e 100644 --- a/lib/modules/side_menu/side_menu_view.dart +++ b/lib/modules/side_menu/side_menu_view.dart @@ -8,6 +8,9 @@ abstract class SideMenuViewDelegate with AlertViewMixinDelegate { abstract class SideMenuView extends View with AlertViewMixin { + static const userAvatarImageKey = Key("user_avatar_image"); + static const logoutButtonKey = Key("logout_button"); + void setUser(UserInfo user); } diff --git a/test/modules/home/home_presenter_test.dart b/test/modules/home/home_presenter_test.dart index 0d26c34c..4cca2c1c 100644 --- a/test/modules/home/home_presenter_test.dart +++ b/test/modules/home/home_presenter_test.dart @@ -171,5 +171,35 @@ void main() { verify(view.showUser(user)).called(1); }); }); + + describe("it's sideMenuDidShow emits", () { + beforeEach(() { + presenter.sideMenuDidShow.add(null); + }); + + it("trigger view to disable user interaction", () { + verify(view.setUserInteractionEnable(isEnabled: false)).called(1); + }); + }); + + describe("it's sideMenuDidDismiss emits", () { + beforeEach(() { + presenter.sideMenuDidDismiss.add(null); + }); + + it("trigger view to enable user interaction", () { + verify(view.setUserInteractionEnable(isEnabled: true)).called(1); + }); + }); + + describe("it's userAvatarButtonDidTap emits", () { + beforeEach(() { + presenter.userAvatarButtonDidTap.add(null); + }); + + it("trigger view to show side menu", () { + verify(view.showSideMenu()).called(1); + }); + }); }); } diff --git a/test/modules/home/home_view_test.dart b/test/modules/home/home_view_test.dart index 0b6703f4..3300fa42 100644 --- a/test/modules/home/home_view_test.dart +++ b/test/modules/home/home_view_test.dart @@ -61,6 +61,12 @@ void main() { .thenAnswer((realInvocation) => generator.make(4)); when(delegate.currentPageDidChange) .thenAnswer((realInvocation) => generator.make(5)); + when(delegate.sideMenuDidShow) + .thenAnswer((realInvocation) => generator.make(6)); + when(delegate.sideMenuDidDismiss) + .thenAnswer((realInvocation) => generator.make(7)); + when(delegate.userAvatarButtonDidTap) + .thenAnswer((realInvocation) => generator.make(8)); locator.registerSingleton(MockSideMenuPresenter()); locator.registerSingleton(MockSideMenuInteractor()); @@ -238,5 +244,54 @@ void main() { expect(delegate.showDetailButtonDidTap, emits(surveys.first)); }); }); + + describe("it's showSideMenu() is called", () { + beforeEach((tester) async { + await tester.pumpAndSettle(); + module.view.showSideMenu(); + await tester.pumpAndSettle(); + }); + + it("triggers delegate's sideMenuDidShow emits", (tester) async { + expect(delegate.sideMenuDidShow, emits(null)); + }); + + describe("then wipes from left to right", () { + beforeEach((tester) async { + final location = tester.getCenter(find.byKey(HomeView.bodyKey)); + await tester.flingFrom(location, Offset(location.dx, 0), location.dx); + await tester.pumpAndSettle(); + }); + + it("triggers delegate's sideMenuDidDismiss emits", (tester) async { + expect(delegate.sideMenuDidDismiss, emits(null)); + }); + }); + }); + + describe("it's user avatar button is tapped", () { + beforeEach((tester) async { + await tester.pumpAndSettle(); + await tester.tap(find.byKey(HomeView.userAvatarButtonKey)); + }); + + it("triggers delegate's userAvatarButtonDidTap emits", (tester) async { + expect(delegate.userAvatarButtonDidTap, emits(null)); + }); + }); + + describe("it's setUserInteractionEnable() is called", () { + beforeEach((tester) async { + module.view.setUserInteractionEnable(isEnabled: false); + await tester.pumpAndSettle(); + }); + + it("triggers main ignore pointer updates with correct ignoring value", + (tester) async { + final widget = tester + .widget(find.byKey(HomeView.mainIgnorePointer)); + expect(widget.ignoring, true); + }); + }); }); } diff --git a/test/modules/home/home_view_test.mocks.dart b/test/modules/home/home_view_test.mocks.dart index f644dde0..a7fe5270 100644 --- a/test/modules/home/home_view_test.mocks.dart +++ b/test/modules/home/home_view_test.mocks.dart @@ -2,11 +2,13 @@ // in survey/test/modules/home/home_view_test.dart. // Do not manually edit this file. +import 'package:flutter/src/widgets/framework.dart' as _i7; import 'package:mockito/mockito.dart' as _i1; import 'package:rxdart/src/subjects/behavior_subject.dart' as _i2; -import 'package:survey/models/survey_info.dart' as _i4; -import 'package:survey/modules/home/home_module.dart' as _i3; -import 'package:survey/modules/side_menu/side_menu_module.dart' as _i5; +import 'package:survey/models/survey_info.dart' as _i5; +import 'package:survey/models/user_info.dart' as _i3; +import 'package:survey/modules/home/home_module.dart' as _i4; +import 'package:survey/modules/side_menu/side_menu_module.dart' as _i6; // ignore_for_file: comment_references // ignore_for_file: unnecessary_parenthesis @@ -18,10 +20,12 @@ import 'package:survey/modules/side_menu/side_menu_module.dart' as _i5; class _FakeBehaviorSubject extends _i1.Fake implements _i2.BehaviorSubject {} +class _FakeUserInfo extends _i1.Fake implements _i3.UserInfo {} + /// A class which mocks [HomeViewDelegate]. /// /// See the documentation for Mockito's code generation for more information. -class MockHomeViewDelegate extends _i1.Mock implements _i3.HomeViewDelegate { +class MockHomeViewDelegate extends _i1.Mock implements _i4.HomeViewDelegate { MockHomeViewDelegate() { _i1.throwOnMissingStub(this); } @@ -31,17 +35,17 @@ class MockHomeViewDelegate extends _i1.Mock implements _i3.HomeViewDelegate { Invocation.getter(#stateDidInit), returnValue: _FakeBehaviorSubject()) as _i2.BehaviorSubject); @override - _i2.BehaviorSubject<_i4.SurveyInfo> get showDetailButtonDidTap => + _i2.BehaviorSubject<_i5.SurveyInfo> get showDetailButtonDidTap => (super.noSuchMethod(Invocation.getter(#showDetailButtonDidTap), - returnValue: _FakeBehaviorSubject<_i4.SurveyInfo>()) - as _i2.BehaviorSubject<_i4.SurveyInfo>); + returnValue: _FakeBehaviorSubject<_i5.SurveyInfo>()) + as _i2.BehaviorSubject<_i5.SurveyInfo>); @override _i2.BehaviorSubject get didSwipeDown => (super.noSuchMethod( Invocation.getter(#didSwipeDown), returnValue: _FakeBehaviorSubject()) as _i2.BehaviorSubject); @override - _i2.BehaviorSubject get userAvatarDidTap => (super.noSuchMethod( - Invocation.getter(#userAvatarDidTap), + _i2.BehaviorSubject get userAvatarButtonDidTap => (super.noSuchMethod( + Invocation.getter(#userAvatarButtonDidTap), returnValue: _FakeBehaviorSubject()) as _i2.BehaviorSubject); @override _i2.BehaviorSubject get sideMenuDidShow => (super.noSuchMethod( @@ -65,22 +69,34 @@ class MockHomeViewDelegate extends _i1.Mock implements _i3.HomeViewDelegate { /// /// See the documentation for Mockito's code generation for more information. class MockSideMenuInteractor extends _i1.Mock - implements _i5.SideMenuInteractor { + implements _i6.SideMenuInteractor { MockSideMenuInteractor() { _i1.throwOnMissingStub(this); } @override - set delegate(_i5.SideMenuInteractorDelegate? _delegate) => + _i3.UserInfo get authenticatedUser => + (super.noSuchMethod(Invocation.getter(#authenticatedUser), + returnValue: _FakeUserInfo()) as _i3.UserInfo); + @override + set delegate(_i6.SideMenuInteractorDelegate? _delegate) => super.noSuchMethod(Invocation.setter(#delegate, _delegate), returnValueForMissingStub: null); + @override + void logout() => super.noSuchMethod(Invocation.method(#logout, []), + returnValueForMissingStub: null); } /// A class which mocks [SideMenuRouter]. /// /// See the documentation for Mockito's code generation for more information. -class MockSideMenuRouter extends _i1.Mock implements _i5.SideMenuRouter { +class MockSideMenuRouter extends _i1.Mock implements _i6.SideMenuRouter { MockSideMenuRouter() { _i1.throwOnMissingStub(this); } + + @override + void replaceToLoginScreen(_i7.BuildContext? context) => + super.noSuchMethod(Invocation.method(#replaceToLoginScreen, [context]), + returnValueForMissingStub: null); } diff --git a/test/modules/side_menu/side_menu_interactor_test.dart b/test/modules/side_menu/side_menu_interactor_test.dart new file mode 100644 index 00000000..043f4827 --- /dev/null +++ b/test/modules/side_menu/side_menu_interactor_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:quick_test/quick_test.dart'; +import 'package:survey/modules/side_menu/side_menu_module.dart'; +import 'package:survey/repositories/auth_repository.dart'; +import 'package:survey/repositories/survey_repository.dart'; +import 'package:survey/services/locator/locator_service.dart'; +import '../../helpers/behavior_subject_generator.dart'; +import 'side_menu_interactor_test.mocks.dart'; + +@GenerateMocks([SideMenuInteractorDelegate, SurveyRepository, AuthRepository]) +void main() { + describe("a SideMenu interactor", () { + late SideMenuInteractor interactor; + late MockSideMenuInteractorDelegate delegate; + late MockAuthRepository authRepository; + late BehaviorSubjectGenerator generator; + + beforeEach(() { + generator = BehaviorSubjectGenerator(); + + delegate = MockSideMenuInteractorDelegate(); + when(delegate.logoutDidSuccess) + .thenAnswer((realInvocation) => generator.make(0)); + when(delegate.logoutDidFail) + .thenAnswer((realInvocation) => generator.make(1)); + + authRepository = MockAuthRepository(); + locator.registerSingleton(authRepository); + + interactor = SideMenuInteractorImpl(); + interactor.delegate = delegate; + }); + + describe("it's logout() is called", () { + context("when auth repository's logout() return success", () { + beforeEach(() { + when(authRepository.logout()) + .thenAnswer((realInvocation) => Future.value(null)); + interactor.logout(); + }); + + it("triggers delegate's logoutDidSuccess emits", () { + expect(delegate.logoutDidSuccess, emits(null)); + }); + }); + + context("when auth repository's logout() return failure", () { + final exception = Exception(); + beforeEach(() { + when(authRepository.logout()) + .thenAnswer((realInvocation) => Future.error(exception)); + interactor.logout(); + }); + + it("triggers delegate's logoutDidSuccess emits", () { + expect(delegate.logoutDidFail, emits(exception)); + }); + }); + }); + }); +} diff --git a/test/modules/side_menu/side_menu_interactor_test.mocks.dart b/test/modules/side_menu/side_menu_interactor_test.mocks.dart new file mode 100644 index 00000000..277ca134 --- /dev/null +++ b/test/modules/side_menu/side_menu_interactor_test.mocks.dart @@ -0,0 +1,113 @@ +// Mocks generated by Mockito 5.0.7 from annotations +// in survey/test/modules/side_menu/side_menu_interactor_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i5; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:rxdart/src/subjects/behavior_subject.dart' as _i2; +import 'package:survey/models/survey_info.dart' as _i6; +import 'package:survey/modules/side_menu/side_menu_module.dart' as _i3; +import 'package:survey/repositories/auth_repository.dart' as _i7; +import 'package:survey/repositories/survey_repository.dart' as _i4; + +// ignore_for_file: comment_references +// ignore_for_file: unnecessary_parenthesis + +// ignore_for_file: prefer_const_constructors + +// ignore_for_file: avoid_redundant_argument_values + +class _FakeBehaviorSubject extends _i1.Fake + implements _i2.BehaviorSubject {} + +/// A class which mocks [SideMenuInteractorDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSideMenuInteractorDelegate extends _i1.Mock + implements _i3.SideMenuInteractorDelegate { + MockSideMenuInteractorDelegate() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.BehaviorSubject get logoutDidSuccess => (super.noSuchMethod( + Invocation.getter(#logoutDidSuccess), + returnValue: _FakeBehaviorSubject()) as _i2.BehaviorSubject); + @override + _i2.BehaviorSubject get logoutDidFail => + (super.noSuchMethod(Invocation.getter(#logoutDidFail), + returnValue: _FakeBehaviorSubject()) + as _i2.BehaviorSubject); +} + +/// A class which mocks [SurveyRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSurveyRepository extends _i1.Mock implements _i4.SurveyRepository { + MockSurveyRepository() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future get isSurveysCached => + (super.noSuchMethod(Invocation.getter(#isSurveysCached), + returnValue: Future.value(false)) as _i5.Future); + @override + _i5.Future> fetchSurveysFromCached() => + (super.noSuchMethod(Invocation.method(#fetchSurveysFromCached, []), + returnValue: + Future>.value(<_i6.SurveyInfo>[])) + as _i5.Future>); + @override + _i5.Future> fetchSurveysFromRemote() => + (super.noSuchMethod(Invocation.method(#fetchSurveysFromRemote, []), + returnValue: + Future>.value(<_i6.SurveyInfo>[])) + as _i5.Future>); +} + +/// A class which mocks [AuthRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthRepository extends _i1.Mock implements _i7.AuthRepository { + MockAuthRepository() { + _i1.throwOnMissingStub(this); + } + + @override + bool get isAuthenticated => (super + .noSuchMethod(Invocation.getter(#isAuthenticated), returnValue: false) + as bool); + @override + _i5.Future login({String? email, String? password}) => + (super.noSuchMethod( + Invocation.method(#login, [], {#email: email, #password: password}), + returnValue: Future.value(null), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future logout() => + (super.noSuchMethod(Invocation.method(#logout, []), + returnValue: Future.value(null), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future attempt() => + (super.noSuchMethod(Invocation.method(#attempt, []), + returnValue: Future.value(null), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future fetchUser() => + (super.noSuchMethod(Invocation.method(#fetchUser, []), + returnValue: Future.value(null), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future attemptAndFetchUser() => + (super.noSuchMethod(Invocation.method(#attemptAndFetchUser, []), + returnValue: Future.value(null), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future resetPassword({String? email}) => (super.noSuchMethod( + Invocation.method(#resetPassword, [], {#email: email}), + returnValue: Future.value(null), + returnValueForMissingStub: Future.value()) as _i5.Future); +} diff --git a/test/modules/side_menu/side_menu_module_test.dart b/test/modules/side_menu/side_menu_module_test.dart new file mode 100644 index 00000000..b553500b --- /dev/null +++ b/test/modules/side_menu/side_menu_module_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:quick_test/quick_test.dart'; +import 'package:survey/modules/side_menu/side_menu_module.dart'; + +import '../../mocks/build_context.dart'; + +void main() { + describe("a SideMenu module", () { + late SideMenuModule module; + beforeEach(() { + module = SideMenuModule(); + }); + + describe("it's build is called", () { + late Widget widget; + + beforeEach(() { + widget = module.build(MockBuildContext()); + }); + + it("returns LandingViewImpl", () { + expect(widget, isA()); + }); + }); + }); +} diff --git a/test/modules/side_menu/side_menu_presenter_test.dart b/test/modules/side_menu/side_menu_presenter_test.dart new file mode 100644 index 00000000..b81e011f --- /dev/null +++ b/test/modules/side_menu/side_menu_presenter_test.dart @@ -0,0 +1,65 @@ +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:quick_test/quick_test.dart'; +import 'package:survey/models/user_info.dart'; +import 'package:survey/modules/side_menu/side_menu_module.dart'; + +import '../../mocks/build_context.dart'; +import 'side_menu_presenter_test.mocks.dart'; + +@GenerateMocks([SideMenuView, SideMenuRouter, SideMenuInteractor]) +void main() { + describe("a SideMenu presenter", () { + late SideMenuPresenterImpl presenter; + late MockSideMenuView view; + late MockSideMenuRouter router; + late MockSideMenuInteractor interactor; + late MockBuildContext buildContext; + final authenticatedUser = UserInfo(); + + beforeEach(() { + buildContext = MockBuildContext(); + + view = MockSideMenuView(); + when(view.context).thenReturn(buildContext); + + router = MockSideMenuRouter(); + + interactor = MockSideMenuInteractor(); + when(interactor.authenticatedUser).thenReturn(authenticatedUser); + + presenter = SideMenuPresenterImpl(); + presenter.configure(view: view, interactor: interactor, router: router); + }); + + describe("it's logoutButtonDidTap emits", () { + beforeEach(() { + presenter.logoutButtonDidTap.add(null); + }); + + it("triggers interactor to logout", () { + verify(interactor.logout()).called(1); + }); + }); + + describe("it's logoutDidSuccess emits", () { + beforeEach(() { + presenter.logoutDidSuccess.add(null); + }); + + it("triggers router to replace to Login screen", () { + verify(router.replaceToLoginScreen(buildContext)).called(1); + }); + }); + + describe("it's stateDidInit emits", () { + beforeEach(() { + presenter.stateDidInit.add(null); + }); + + it("triggers view to set user", () { + verify(view.setUser(authenticatedUser)).called(1); + }); + }); + }); +} diff --git a/test/modules/side_menu/side_menu_presenter_test.mocks.dart b/test/modules/side_menu/side_menu_presenter_test.mocks.dart new file mode 100644 index 00000000..457369e7 --- /dev/null +++ b/test/modules/side_menu/side_menu_presenter_test.mocks.dart @@ -0,0 +1,81 @@ +// Mocks generated by Mockito 5.0.7 from annotations +// in survey/test/modules/side_menu/side_menu_presenter_test.dart. +// Do not manually edit this file. + +import 'package:flutter/src/widgets/framework.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:survey/models/user_info.dart' as _i3; +import 'package:survey/modules/side_menu/side_menu_module.dart' as _i4; + +// ignore_for_file: comment_references +// ignore_for_file: unnecessary_parenthesis + +// ignore_for_file: prefer_const_constructors + +// ignore_for_file: avoid_redundant_argument_values + +class _FakeBuildContext extends _i1.Fake implements _i2.BuildContext {} + +class _FakeUserInfo extends _i1.Fake implements _i3.UserInfo {} + +/// A class which mocks [SideMenuView]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSideMenuView extends _i1.Mock implements _i4.SideMenuView { + MockSideMenuView() { + _i1.throwOnMissingStub(this); + } + + @override + set delegate(_i4.SideMenuViewDelegate? _delegate) => + super.noSuchMethod(Invocation.setter(#delegate, _delegate), + returnValueForMissingStub: null); + @override + _i2.BuildContext get context => + (super.noSuchMethod(Invocation.getter(#context), + returnValue: _FakeBuildContext()) as _i2.BuildContext); + @override + void setUser(_i3.UserInfo? user) => + super.noSuchMethod(Invocation.method(#setUser, [user]), + returnValueForMissingStub: null); + @override + void alert(Object? error) => + super.noSuchMethod(Invocation.method(#alert, [error]), + returnValueForMissingStub: null); +} + +/// A class which mocks [SideMenuRouter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSideMenuRouter extends _i1.Mock implements _i4.SideMenuRouter { + MockSideMenuRouter() { + _i1.throwOnMissingStub(this); + } + + @override + void replaceToLoginScreen(_i2.BuildContext? context) => + super.noSuchMethod(Invocation.method(#replaceToLoginScreen, [context]), + returnValueForMissingStub: null); +} + +/// A class which mocks [SideMenuInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSideMenuInteractor extends _i1.Mock + implements _i4.SideMenuInteractor { + MockSideMenuInteractor() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.UserInfo get authenticatedUser => + (super.noSuchMethod(Invocation.getter(#authenticatedUser), + returnValue: _FakeUserInfo()) as _i3.UserInfo); + @override + set delegate(_i4.SideMenuInteractorDelegate? _delegate) => + super.noSuchMethod(Invocation.setter(#delegate, _delegate), + returnValueForMissingStub: null); + @override + void logout() => super.noSuchMethod(Invocation.method(#logout, []), + returnValueForMissingStub: null); +} diff --git a/test/modules/side_menu/side_menu_router_test.dart b/test/modules/side_menu/side_menu_router_test.dart new file mode 100644 index 00000000..74ca211c --- /dev/null +++ b/test/modules/side_menu/side_menu_router_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:quick_test/quick_test.dart'; +import 'package:survey/modules/login/login_module.dart'; +import 'package:survey/modules/side_menu/side_menu_module.dart'; + +import '../../mocks/build_context.dart'; +import '../../mocks/navigator_state.dart'; + +void main() { + describe("a SideMenu router", () { + late SideMenuRouter router; + late MockBuildContext buildContext; + late MockNavigatorState navigatorState; + + beforeEach(() { + buildContext = MockBuildContext(); + navigatorState = MockNavigatorState(); + + router = SideMenuRouterImpl(); + }); + + describe("it's replaceToLoginScreen() is called", () { + beforeEach(() { + when(buildContext.findAncestorStateOfType()) + .thenReturn(navigatorState); + when(navigatorState.pushReplacementNamed(any)) + .thenAnswer((_) => Future.value()); + router.replaceToLoginScreen(buildContext); + }); + + it("triggers navigator to push replacement to Login screen", () { + final routePath = + verify(navigatorState.pushReplacementNamed(captureAny)) + .captured + .single as String; + expect(routePath, LoginModule.routePath); + }); + }); + }); +} diff --git a/test/modules/side_menu/side_menu_view_test.dart b/test/modules/side_menu/side_menu_view_test.dart new file mode 100644 index 00000000..022f63d3 --- /dev/null +++ b/test/modules/side_menu/side_menu_view_test.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:quick_test/quick_widget_test.dart'; +import 'package:survey/models/user_info.dart'; +import 'package:survey/core/viper/module.dart'; +import 'package:mockito/mockito.dart'; +import 'package:survey/modules/side_menu/side_menu_module.dart'; +import '../../fakers/fake_module.dart'; +import '../../helpers/behavior_subject_generator.dart'; +import '../../helpers/extensions/widget_tester.dart'; +import 'side_menu_view_test.mocks.dart'; + +@GenerateMocks([SideMenuViewDelegate]) +void main() { + describe("a SideMenu view", () { + late FakeModule module; + late MockSideMenuViewDelegate delegate; + late BehaviorSubjectGenerator generator; + + beforeEach((tester) async { + HttpOverrides.global = null; + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + + generator = BehaviorSubjectGenerator(); + delegate = MockSideMenuViewDelegate(); + when(delegate.logoutButtonDidTap) + .thenAnswer((realInvocation) => generator.make(1)); + when(delegate.stateDidInit) + .thenAnswer((realInvocation) => generator.make(2)); + + module = FakeModule( + builder: () => const SideMenuViewImpl(), + delegate: delegate, + ); + ViewState.overriddenModule = module; + await tester.pumpModule(module); + }); + + afterEach((_) async { + debugDefaultTargetPlatformOverride = null; + }); + + describe("it's setUser() is called", () { + final user = UserInfo(); + user.avatarUrl = "http://example.com"; + user.email = "email"; + + beforeEach((tester) async { + module.view.setUser(user); + await tester.pumpAndSettle(); + }); + + it("triggers user widget displays correct information", (tester) async { + expect(find.text(user.email!), findsOneWidget); + + final image = + tester.widget(find.byKey(SideMenuView.userAvatarImageKey)); + final provider = image.image as NetworkImage; + expect(provider.url, user.avatarUrl); + }); + }); + + describe("it's logout button is tapped", () { + beforeEach((tester) async { + await tester.tap(find.byKey(SideMenuView.logoutButtonKey)); + }); + + it("triggers delegate's logoutButtonDidTap emits", (tester) async { + expect(delegate.logoutButtonDidTap, emits(null)); + }); + }); + }); +} diff --git a/test/modules/side_menu/side_menu_view_test.mocks.dart b/test/modules/side_menu/side_menu_view_test.mocks.dart new file mode 100644 index 00000000..91d7189c --- /dev/null +++ b/test/modules/side_menu/side_menu_view_test.mocks.dart @@ -0,0 +1,40 @@ +// Mocks generated by Mockito 5.0.7 from annotations +// in survey/test/modules/side_menu/side_menu_view_test.dart. +// Do not manually edit this file. + +import 'package:mockito/mockito.dart' as _i1; +import 'package:rxdart/src/subjects/behavior_subject.dart' as _i2; +import 'package:survey/modules/side_menu/side_menu_module.dart' as _i3; + +// ignore_for_file: comment_references +// ignore_for_file: unnecessary_parenthesis + +// ignore_for_file: prefer_const_constructors + +// ignore_for_file: avoid_redundant_argument_values + +class _FakeBehaviorSubject extends _i1.Fake + implements _i2.BehaviorSubject {} + +/// A class which mocks [SideMenuViewDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSideMenuViewDelegate extends _i1.Mock + implements _i3.SideMenuViewDelegate { + MockSideMenuViewDelegate() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.BehaviorSubject get stateDidInit => (super.noSuchMethod( + Invocation.getter(#stateDidInit), + returnValue: _FakeBehaviorSubject()) as _i2.BehaviorSubject); + @override + _i2.BehaviorSubject get logoutButtonDidTap => (super.noSuchMethod( + Invocation.getter(#logoutButtonDidTap), + returnValue: _FakeBehaviorSubject()) as _i2.BehaviorSubject); + @override + _i2.BehaviorSubject get alertDialogDidClose => (super.noSuchMethod( + Invocation.getter(#alertDialogDidClose), + returnValue: _FakeBehaviorSubject()) as _i2.BehaviorSubject); +}