From 4ab4ccb54b334bfbe2e5d486977022430fb83e7b Mon Sep 17 00:00:00 2001 From: MarkG Date: Wed, 12 May 2021 17:42:48 +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 | 46 +++++---- 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 | 4 +- .../side_menu/side_menu_interactor_test.dart | 60 ++++++++++++ .../side_menu_interactor_test.mocks.dart | 97 +++++++++++++++++++ .../side_menu/side_menu_module_test.dart | 27 ++++++ .../side_menu/side_menu_presenter_test.dart | 52 ++++++++++ .../side_menu_presenter_test.mocks.dart | 71 ++++++++++++++ .../side_menu/side_menu_router_test.dart | 42 ++++++++ .../side_menu/side_menu_view_test.dart | 76 +++++++++++++++ .../side_menu/side_menu_view_test.mocks.dart | 32 ++++++ 18 files changed, 587 insertions(+), 23 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 90308a9..0335c4e 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 b6b35b6..ba57157 100644 --- a/lib/modules/home/components/top_bar.dart +++ b/lib/modules/home/components/top_bar.dart @@ -51,7 +51,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 f45697b..235eac8 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); sideMenuDidShow.voidListen(_sideMenuDidShow).addTo(disposeBag); sideMenuDidDismiss.voidListen(_sideMenuDidDismiss).addTo(disposeBag); @@ -32,7 +34,7 @@ class HomePresenterImpl extends HomePresenter final alertDialogDidClose = BehaviorSubject(); @override - final userAvatarDidTap = BehaviorSubject(); + final userAvatarButtonDidTap = BehaviorSubject(); @override final sideMenuDidDismiss = BehaviorSubject(); @@ -90,7 +92,7 @@ class HomePresenterImpl extends HomePresenter interactor.fetchSurveys(force: true); } - void _userAvatarDidTap() { + void _userAvatarButtonDidTap() { view.showSideMenu(); } diff --git a/lib/modules/home/home_view.dart b/lib/modules/home/home_view.dart index e5288d4..2dfe45b 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; @@ -18,6 +18,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"); @@ -25,6 +26,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); @@ -65,28 +67,15 @@ class _HomeViewImplState final _isLoading = BehaviorSubject.seeded(false); final _isUserInteractionEnabled = BehaviorSubject.seeded(true); final _sliderMenuContainerKey = GlobalKey(); + final _widgetsBinding = WidgetsBinding.instance!; @override void initState() { super.initState(); delegate?.stateDidInit.add(null); - WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { - final animationController = - _sliderMenuContainerKey.currentState?.animationController; - animationController?.addListener(() { - switch (animationController.status) { - case AnimationStatus.completed: - delegate?.sideMenuDidShow.add(null); - break; - case AnimationStatus.dismissed: - delegate?.sideMenuDidDismiss.add(null); - break; - default: - break; - } - }); - }); + _widgetsBinding + .scheduleFrameCallback(_listenSliderMenuContainerAnimationController); } @override @@ -130,4 +119,27 @@ class _HomeViewImplState void setUserInteractionEnable({required bool isEnabled}) { _isUserInteractionEnabled.add(isEnabled); } + + void _listenSliderMenuContainerAnimationController(Duration timestamp) { + final animationController = + _sliderMenuContainerKey.currentState?.animationController; + if (animationController == null) { + _widgetsBinding + .scheduleFrameCallback(_listenSliderMenuContainerAnimationController); + return; + } + + animationController.addListener(() { + switch (animationController.status) { + case AnimationStatus.completed: + delegate?.sideMenuDidShow.add(null); + break; + case AnimationStatus.dismissed: + delegate?.sideMenuDidDismiss.add(null); + break; + default: + break; + } + }); + } } diff --git a/lib/modules/side_menu/components/actions.dart b/lib/modules/side_menu/components/actions.dart index 71b193d..3e03d58 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 c876bc8..3c642c6 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 86a6d13..a475390 100644 --- a/lib/modules/side_menu/side_menu_view.dart +++ b/lib/modules/side_menu/side_menu_view.dart @@ -5,6 +5,9 @@ abstract class SideMenuViewDelegate { } abstract class SideMenuView extends View { + 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 6b11ee3..660b08a 100644 --- a/test/modules/home/home_presenter_test.dart +++ b/test/modules/home/home_presenter_test.dart @@ -136,5 +136,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 ab432f3..89cbb28 100644 --- a/test/modules/home/home_view_test.dart +++ b/test/modules/home/home_view_test.dart @@ -65,6 +65,12 @@ void main() { .thenAnswer((realInvocation) => generator.make(3)); when(delegate.didSwipeDown) .thenAnswer((realInvocation) => generator.make(4)); + when(delegate.sideMenuDidShow) + .thenAnswer((realInvocation) => generator.make(5)); + when(delegate.sideMenuDidDismiss) + .thenAnswer((realInvocation) => generator.make(6)); + when(delegate.userAvatarButtonDidTap) + .thenAnswer((realInvocation) => generator.make(7)); module = FakeModule( builder: () => const HomeViewImpl(), @@ -256,5 +262,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 df5f49f..fc93268 100644 --- a/test/modules/home/home_view_test.mocks.dart +++ b/test/modules/home/home_view_test.mocks.dart @@ -39,8 +39,8 @@ class MockHomeViewDelegate extends _i1.Mock implements _i3.HomeViewDelegate { 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( 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 0000000..1d4f258 --- /dev/null +++ b/test/modules/side_menu/side_menu_interactor_test.dart @@ -0,0 +1,60 @@ +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)); + + 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", () { + beforeEach(() { + when(authRepository.logout()) + .thenAnswer((realInvocation) => Future.error(Exception())); + interactor.logout(); + }); + + it("triggers delegate's logoutDidSuccess emits", () { + expect(delegate.logoutDidSuccess, emits(null)); + }); + }); + }); + }); +} 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 0000000..f580d64 --- /dev/null +++ b/test/modules/side_menu/side_menu_interactor_test.mocks.dart @@ -0,0 +1,97 @@ +// 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); +} + +/// 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> fetchSurveys({bool? force}) => + (super.noSuchMethod(Invocation.method(#fetchSurveys, [], {#force: force}), + 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); +} 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 0000000..b553500 --- /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 0000000..8028f56 --- /dev/null +++ b/test/modules/side_menu/side_menu_presenter_test.dart @@ -0,0 +1,52 @@ +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 '../../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; + + beforeEach(() { + buildContext = MockBuildContext(); + + view = MockSideMenuView(); + when(view.context).thenReturn(buildContext); + + router = MockSideMenuRouter(); + + interactor = MockSideMenuInteractor(); + + 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); + }); + }); + }); +} 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 0000000..b7b8964 --- /dev/null +++ b/test/modules/side_menu/side_menu_presenter_test.mocks.dart @@ -0,0 +1,71 @@ +// 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 _i4; +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 _FakeBuildContext extends _i1.Fake implements _i2.BuildContext {} + +/// A class which mocks [SideMenuView]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSideMenuView extends _i1.Mock implements _i3.SideMenuView { + MockSideMenuView() { + _i1.throwOnMissingStub(this); + } + + @override + set delegate(_i3.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(_i4.UserInfo? user) => + super.noSuchMethod(Invocation.method(#setUser, [user]), + returnValueForMissingStub: null); +} + +/// A class which mocks [SideMenuRouter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSideMenuRouter extends _i1.Mock implements _i3.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 _i3.SideMenuInteractor { + MockSideMenuInteractor() { + _i1.throwOnMissingStub(this); + } + + @override + set delegate(_i3.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 0000000..74ca211 --- /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 0000000..44e9ab2 --- /dev/null +++ b/test/modules/side_menu/side_menu_view_test.dart @@ -0,0 +1,76 @@ +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)); + + 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 0000000..e650049 --- /dev/null +++ b/test/modules/side_menu/side_menu_view_test.mocks.dart @@ -0,0 +1,32 @@ +// 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 logoutButtonDidTap => (super.noSuchMethod( + Invocation.getter(#logoutButtonDidTap), + returnValue: _FakeBehaviorSubject()) as _i2.BehaviorSubject); +}