diff --git a/lib/ui/home/view/HomeView.dart b/lib/ui/home/view/HomeView.dart index 210af0a..e9a1c38 100644 --- a/lib/ui/home/view/HomeView.dart +++ b/lib/ui/home/view/HomeView.dart @@ -5,8 +5,7 @@ class HomeView extends StatelessWidget { @override Widget build(BuildContext context) { - // TODO: implement build - throw UnimplementedError(); + return const Text('Home', style: TextStyle(fontSize: 20)); } diff --git a/lib/ui/login/view/login_view.dart b/lib/ui/login/view/login_view.dart index 1066cc7..0481108 100644 --- a/lib/ui/login/view/login_view.dart +++ b/lib/ui/login/view/login_view.dart @@ -1,5 +1,7 @@ import 'package:aranduapp/core/log/Log.dart'; +import 'package:aranduapp/ui/navbar/view/navBarView.dart'; import 'package:aranduapp/ui/shared/TextAndLink.dart'; +import 'package:aranduapp/ui/shared/requestbutton.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -21,61 +23,49 @@ class Login extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProvider( - create: (context) => LoginViewModel(context), + create: (context) => LoginViewModel(), child: const LoginScreen(), ); } } -class LoginScreen extends StatefulWidget { +class LoginScreen extends StatelessWidget { const LoginScreen({super.key}); - @override - State createState() { - return _LoginScreenState(); - } -} - -class _LoginScreenState extends State { - late Future _future; - - @override - void initState() { - super.initState(); - _future = - Provider.of(context, listen: false).validateToken(); - } - @override Widget build(BuildContext context) { LoginViewModel viewModel = Provider.of(context); return Scaffold( - body: FutureBuilder( - future: _future, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return _loadingScreen(viewModel); - } else if (!snapshot.hasError) { - return _authDevice(viewModel); - } else { - return _emailAndPassword(viewModel); - } - })); + body: ListenableBuilder( + listenable: viewModel.validadeTokenCommand, + builder: (context, child) { + if (viewModel.validadeTokenCommand.isOk) { + return _authDevice(viewModel, context); + } else if (viewModel.validadeTokenCommand.isError) { + return _emailAndPassword(viewModel, context); + } else { + return _loadingScreen(viewModel, context); + } + }, + ), + ); } - Widget _loadingScreen(LoginViewModel viewModel) { + Widget _loadingScreen(LoginViewModel viewModel, BuildContext context) { return const Center( child: CircularProgressIndicator(value: null), ); } - Widget _authDevice(LoginViewModel viewModel) { + Widget _authDevice(LoginViewModel viewModel, BuildContext context) { Log.d("Mostrando tela de autorização do dispositivo"); viewModel.loginWithDeviceAuth().then((ok) { if (ok) { - viewModel.goToHome(); + WidgetsBinding.instance.addPostFrameCallback((_) { + goToNavbar(context); + }); } }); @@ -99,7 +89,9 @@ class _LoginScreenState extends State { child: ElevatedButton( onPressed: () async { viewModel.loginWithDeviceAuth().then((ok) { - viewModel.goToHome(); + WidgetsBinding.instance.addPostFrameCallback((_) { + goToNavbar(context); + }); }); }, child: const Text('Usar senha do celular'), @@ -110,7 +102,7 @@ class _LoginScreenState extends State { ); } - Widget _emailAndPassword(LoginViewModel viewModel) { + Widget _emailAndPassword(LoginViewModel viewModel, BuildContext context) { return SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -120,11 +112,11 @@ class _LoginScreenState extends State { const SizedBox(height: 80), const SizedBox(height: 10), _formSection(viewModel), - _forgotPasswordLink(), + _forgotPasswordLink(context), const SizedBox(height: 80), - _loginButtonSection(), + _loginButtonSection(context), const OrDivider(), - _loggingInWithOther(), + _loggingInWithOther(context), TextAndLink( text: 'É novo pro aqui?', link: 'Cria a sua conta', @@ -157,7 +149,7 @@ class _LoginScreenState extends State { ); } - Widget _forgotPasswordLink() { + Widget _forgotPasswordLink(BuildContext context) { return GestureDetector( onTap: () { Navigator.of(context).push( @@ -182,31 +174,24 @@ class _LoginScreenState extends State { ); } - Widget _loginButtonSection() { + Widget _loginButtonSection(BuildContext context) { LoginViewModel viewModel = Provider.of(context); - return SizedBox( - width: 291, - height: 64, - child: ElevatedButton( - onPressed: () { - viewModel.loginWithEmailAndPassword().then((_) { - viewModel.goToHome(); - }).catchError((e) => showDialog( - context: context, - builder: (BuildContext context) => - ErrorPopUp(content: Text('$e')), - )); - }, - child: Consumer( - builder: (context, value, child) => value.isLoading - ? const CircularProgressIndicator(value: null) - : const Text('Entrar'), - )), - ); + return Requestbutton( + command: viewModel.loginCommand, + onErrorCallback: (String e) { + showDialog( + context: context, + builder: (BuildContext context) => ErrorPopUp(content: Text(e)), + ); + }, + onSuccessCallback: () { + goToNavbar(context); + }, + nameButton: 'Entrar'); } - Widget _loggingInWithOther() { + Widget _loggingInWithOther(BuildContext context) { return GestureDetector( onTap: () => Log.d(""), child: Container( @@ -225,4 +210,12 @@ class _LoginScreenState extends State { ), ); } + + void goToNavbar(BuildContext context) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const NavbarView(), + ), + ); + } } diff --git a/lib/ui/login/viewModel/login_view_model.dart b/lib/ui/login/viewModel/login_view_model.dart index e941a46..d8a755a 100644 --- a/lib/ui/login/viewModel/login_view_model.dart +++ b/lib/ui/login/viewModel/login_view_model.dart @@ -1,51 +1,47 @@ import 'package:aranduapp/core/log/Log.dart'; +import 'package:aranduapp/core/state/command.dart'; import 'package:aranduapp/ui/navbar/view/navBarView.dart'; +import 'package:async/async.dart'; import 'package:flutter/material.dart'; import 'package:local_auth/local_auth.dart'; import 'package:aranduapp/ui/login/service/LoginService.dart'; import 'package:aranduapp/ui/login/model/LoginRequest.dart'; class LoginViewModel extends ChangeNotifier { - final BuildContext context; - bool isLoading; + late Command0 loginCommand; + late Command0 validadeTokenCommand; final GlobalKey formKey; final TextEditingController emailController; final TextEditingController passwordController; - LoginViewModel(this.context) - : isLoading = false, - formKey = GlobalKey(), + LoginViewModel() + : formKey = GlobalKey(), emailController = TextEditingController(), - passwordController = TextEditingController(); + passwordController = TextEditingController() { - Future loginWithEmailAndPassword() async { - // TODO use mutex to make this - if (isLoading) { - return; - } - - try { - isLoading = true; - super.notifyListeners(); + loginCommand = Command0(loginWithEmailAndPassword); - if (!formKey.currentState!.validate()) { - throw Exception('Valores inválidos'); - } + validadeTokenCommand = Command0(validateToken); + validadeTokenCommand.execute(); + } - await LoginService.login( - LoginRequest(emailController.text, passwordController.text)); - } catch (e) { - rethrow; - } finally { - isLoading = false; - notifyListeners(); + Future> loginWithEmailAndPassword() async { + if (!formKey.currentState!.validate()) { + return Result.error(Exception('Valores inválidos')); } + + await LoginService.login( + LoginRequest(emailController.text, passwordController.text)); + + return Result.value(null); } - Future validateToken() async { + Future> validateToken() async { await LoginService.validateToken(); + + return Result.value(null); } Future loginWithDeviceAuth() async { @@ -53,19 +49,4 @@ class LoginViewModel extends ChangeNotifier { return await LocalAuthentication() .authenticate(localizedReason: 'Toque com o dedo no sensor para logar'); } - - void goToHome() { - try { - if (context.mounted) { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => const NavbarView(), - ), - ); - } - } catch (e) { - Log.e(e); - rethrow; - } - } } diff --git a/test/ui/login/view/login_view_test.dart b/test/ui/login/view/login_view_test.dart index ce87e46..cf2186a 100644 --- a/test/ui/login/view/login_view_test.dart +++ b/test/ui/login/view/login_view_test.dart @@ -1,27 +1,46 @@ +import 'package:aranduapp/core/state/command.dart'; import 'package:aranduapp/ui/login/view/login_view.dart'; import 'package:aranduapp/ui/login/viewModel/login_view_model.dart'; +import 'package:aranduapp/ui/navbar/view/navBarView.dart'; import 'package:aranduapp/ui/shared/TextEmail.dart'; import 'package:aranduapp/ui/shared/TextPassword.dart'; +import 'package:async/async.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; -@GenerateNiceMocks([MockSpec()]) +@GenerateNiceMocks([MockSpec(), MockSpec()]) import 'login_view_test.mocks.dart'; void main() { late MockLoginViewModel mockViewModel; + late MockCommand0 mockLoginCommand; + late MockCommand0 mockValidadeTokenCommand; setUp(() { mockViewModel = MockLoginViewModel(); when(mockViewModel.formKey).thenReturn(GlobalKey()); when(mockViewModel.emailController).thenReturn(TextEditingController()); when(mockViewModel.passwordController).thenReturn(TextEditingController()); + + mockLoginCommand = MockCommand0(); + when(mockLoginCommand.execute()) + .thenAnswer((_) async => Result.value(null)); + when(mockViewModel.loginCommand).thenReturn(mockLoginCommand); + + mockValidadeTokenCommand = MockCommand0(); + when(mockViewModel.validadeTokenCommand) + .thenReturn(mockValidadeTokenCommand); + when(mockValidadeTokenCommand.execute()) + .thenAnswer((_) async => Result.value(null)); + when(mockValidadeTokenCommand.running).thenReturn(false); + when(mockValidadeTokenCommand.isError).thenReturn(false); + when(mockValidadeTokenCommand.isOk).thenReturn(false); }); - Widget createLoginScreen(LoginViewModel mockViewModel) { + Widget createLoginScreen() { return ChangeNotifierProvider.value( value: mockViewModel, child: const MaterialApp( @@ -30,26 +49,19 @@ void main() { ); } - testWidgets('Displays loading screen while waiting for Future', + testWidgets('Displays loading screen while waiting for validade token', (WidgetTester tester) async { - when(mockViewModel.validateToken()).thenAnswer( - (_) async => await Future.delayed(const Duration(seconds: 1))); - - await tester.pumpWidget(createLoginScreen(mockViewModel)); + when(mockValidadeTokenCommand.running).thenReturn(true); - await tester.pump(const Duration(milliseconds: 500)); + await tester.pumpWidget(createLoginScreen()); expect(find.byType(CircularProgressIndicator), findsOneWidget); - - await tester.pumpAndSettle(); - - expect(find.byType(CircularProgressIndicator), findsNothing); }); testWidgets('login with auth device', (WidgetTester tester) async { - when(mockViewModel.loginWithDeviceAuth()).thenAnswer((_) async => false); + when(mockValidadeTokenCommand.isOk).thenReturn(true); - await tester.pumpWidget(createLoginScreen(mockViewModel)); + await tester.pumpWidget(createLoginScreen()); await tester.pumpAndSettle(); when(mockViewModel.loginWithDeviceAuth()).thenAnswer((_) async => true); @@ -59,17 +71,17 @@ void main() { await tester.pumpAndSettle(); - verify(mockViewModel.goToHome()).called(1); + expect(find.byType(NavbarView), findsOneWidget); + verify(mockViewModel.loginWithDeviceAuth()).called(2); }); testWidgets( 'Login screen displays email and password fields and login button', (WidgetTester tester) async { - when(mockViewModel.validateToken()) - .thenAnswer((_) async => throw Exception('Token validation failed')); + when(mockValidadeTokenCommand.isError).thenReturn(true); - await tester.pumpWidget(createLoginScreen(mockViewModel)); + await tester.pumpWidget(createLoginScreen()); await tester.pumpAndSettle(); expect(find.byType(TextEmail), findsOneWidget); @@ -79,10 +91,9 @@ void main() { testWidgets('Test User Input for Email and Password', (WidgetTester tester) async { - when(mockViewModel.validateToken()) - .thenAnswer((_) async => throw Exception('Token validation failed')); + when(mockValidadeTokenCommand.isError).thenReturn(true); - await tester.pumpWidget(createLoginScreen(mockViewModel)); + await tester.pumpWidget(createLoginScreen()); await tester.pumpAndSettle(); const email = 'test@example.com'; @@ -95,24 +106,25 @@ void main() { expect(mockViewModel.passwordController.text, password); }); - testWidgets('Login is successful', - (WidgetTester tester) async { - when(mockViewModel.validateToken()) - .thenAnswer((_) async => throw Exception('Token validation failed')); + testWidgets('Login is successful', (WidgetTester tester) async { + when(mockValidadeTokenCommand.isError).thenReturn(true); - await tester.pumpWidget(createLoginScreen(mockViewModel)); + await tester.pumpWidget(createLoginScreen()); await tester.pumpAndSettle(); - await tester.tap(find.text('Entrar')); + when(mockLoginCommand.execute()) + .thenAnswer((_) async => Result.value(null)); - when(mockViewModel.loginWithEmailAndPassword()).thenAnswer( - (_) async => await Future.delayed(const Duration(seconds: 1))); + await tester.tap(find.text('Entrar')); await tester.pumpAndSettle(); - verify(mockViewModel.loginWithEmailAndPassword()).called(1); - verify(mockViewModel.goToHome()).called(1); + + verify(mockLoginCommand.execute()).called(1); + + //TODO: verify navigation to navbar }); testWidgets('Displays error when login fails', (WidgetTester tester) async { + //TODO: }); } diff --git a/test/ui/login/view/login_view_test.mocks.dart b/test/ui/login/view/login_view_test.mocks.dart index fa695eb..21468ae 100644 --- a/test/ui/login/view/login_view_test.mocks.dart +++ b/test/ui/login/view/login_view_test.mocks.dart @@ -3,10 +3,12 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i6; -import 'package:aranduapp/ui/login/viewModel/login_view_model.dart' as _i3; -import 'package:flutter/material.dart' as _i2; +import 'package:aranduapp/core/state/command.dart' as _i2; +import 'package:aranduapp/ui/login/viewModel/login_view_model.dart' as _i5; +import 'package:async/async.dart' as _i4; +import 'package:flutter/material.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: type=lint @@ -22,8 +24,8 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeBuildContext_0 extends _i1.SmartFake implements _i2.BuildContext { - _FakeBuildContext_0( +class _FakeCommand0_0 extends _i1.SmartFake implements _i2.Command0 { + _FakeCommand0_0( Object parent, Invocation parentInvocation, ) : super( @@ -32,8 +34,8 @@ class _FakeBuildContext_0 extends _i1.SmartFake implements _i2.BuildContext { ); } -class _FakeGlobalKey_1> - extends _i1.SmartFake implements _i2.GlobalKey { +class _FakeGlobalKey_1> + extends _i1.SmartFake implements _i3.GlobalKey { _FakeGlobalKey_1( Object parent, Invocation parentInvocation, @@ -44,7 +46,7 @@ class _FakeGlobalKey_1> } class _FakeTextEditingController_2 extends _i1.SmartFake - implements _i2.TextEditingController { + implements _i3.TextEditingController { _FakeTextEditingController_2( Object parent, Invocation parentInvocation, @@ -54,54 +56,80 @@ class _FakeTextEditingController_2 extends _i1.SmartFake ); } +class _FakeResult_3 extends _i1.SmartFake implements _i4.Result { + _FakeResult_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [LoginViewModel]. /// /// See the documentation for Mockito's code generation for more information. -class MockLoginViewModel extends _i1.Mock implements _i3.LoginViewModel { +class MockLoginViewModel extends _i1.Mock implements _i5.LoginViewModel { @override - _i2.BuildContext get context => (super.noSuchMethod( - Invocation.getter(#context), - returnValue: _FakeBuildContext_0( + _i2.Command0 get loginCommand => (super.noSuchMethod( + Invocation.getter(#loginCommand), + returnValue: _FakeCommand0_0( this, - Invocation.getter(#context), + Invocation.getter(#loginCommand), ), - returnValueForMissingStub: _FakeBuildContext_0( + returnValueForMissingStub: _FakeCommand0_0( this, - Invocation.getter(#context), + Invocation.getter(#loginCommand), ), - ) as _i2.BuildContext); + ) as _i2.Command0); @override - bool get isLoading => (super.noSuchMethod( - Invocation.getter(#isLoading), - returnValue: false, - returnValueForMissingStub: false, - ) as bool); + set loginCommand(_i2.Command0? _loginCommand) => super.noSuchMethod( + Invocation.setter( + #loginCommand, + _loginCommand, + ), + returnValueForMissingStub: null, + ); @override - set isLoading(bool? _isLoading) => super.noSuchMethod( + _i2.Command0 get validadeTokenCommand => (super.noSuchMethod( + Invocation.getter(#validadeTokenCommand), + returnValue: _FakeCommand0_0( + this, + Invocation.getter(#validadeTokenCommand), + ), + returnValueForMissingStub: _FakeCommand0_0( + this, + Invocation.getter(#validadeTokenCommand), + ), + ) as _i2.Command0); + + @override + set validadeTokenCommand(_i2.Command0? _validadeTokenCommand) => + super.noSuchMethod( Invocation.setter( - #isLoading, - _isLoading, + #validadeTokenCommand, + _validadeTokenCommand, ), returnValueForMissingStub: null, ); @override - _i2.GlobalKey<_i2.FormState> get formKey => (super.noSuchMethod( + _i3.GlobalKey<_i3.FormState> get formKey => (super.noSuchMethod( Invocation.getter(#formKey), - returnValue: _FakeGlobalKey_1<_i2.FormState>( + returnValue: _FakeGlobalKey_1<_i3.FormState>( this, Invocation.getter(#formKey), ), - returnValueForMissingStub: _FakeGlobalKey_1<_i2.FormState>( + returnValueForMissingStub: _FakeGlobalKey_1<_i3.FormState>( this, Invocation.getter(#formKey), ), - ) as _i2.GlobalKey<_i2.FormState>); + ) as _i3.GlobalKey<_i3.FormState>); @override - _i2.TextEditingController get emailController => (super.noSuchMethod( + _i3.TextEditingController get emailController => (super.noSuchMethod( Invocation.getter(#emailController), returnValue: _FakeTextEditingController_2( this, @@ -111,10 +139,10 @@ class MockLoginViewModel extends _i1.Mock implements _i3.LoginViewModel { this, Invocation.getter(#emailController), ), - ) as _i2.TextEditingController); + ) as _i3.TextEditingController); @override - _i2.TextEditingController get passwordController => (super.noSuchMethod( + _i3.TextEditingController get passwordController => (super.noSuchMethod( Invocation.getter(#passwordController), returnValue: _FakeTextEditingController_2( this, @@ -124,7 +152,7 @@ class MockLoginViewModel extends _i1.Mock implements _i3.LoginViewModel { this, Invocation.getter(#passwordController), ), - ) as _i2.TextEditingController); + ) as _i3.TextEditingController); @override bool get hasListeners => (super.noSuchMethod( @@ -134,43 +162,167 @@ class MockLoginViewModel extends _i1.Mock implements _i3.LoginViewModel { ) as bool); @override - _i4.Future loginWithEmailAndPassword() => (super.noSuchMethod( + _i6.Future<_i4.Result> loginWithEmailAndPassword() => + (super.noSuchMethod( Invocation.method( #loginWithEmailAndPassword, [], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i6.Future<_i4.Result>.value(_FakeResult_3( + this, + Invocation.method( + #loginWithEmailAndPassword, + [], + ), + )), + returnValueForMissingStub: + _i6.Future<_i4.Result>.value(_FakeResult_3( + this, + Invocation.method( + #loginWithEmailAndPassword, + [], + ), + )), + ) as _i6.Future<_i4.Result>); @override - _i4.Future validateToken() => (super.noSuchMethod( + _i6.Future<_i4.Result> validateToken() => (super.noSuchMethod( Invocation.method( #validateToken, [], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i6.Future<_i4.Result>.value(_FakeResult_3( + this, + Invocation.method( + #validateToken, + [], + ), + )), + returnValueForMissingStub: + _i6.Future<_i4.Result>.value(_FakeResult_3( + this, + Invocation.method( + #validateToken, + [], + ), + )), + ) as _i6.Future<_i4.Result>); @override - _i4.Future loginWithDeviceAuth() => (super.noSuchMethod( + _i6.Future loginWithDeviceAuth() => (super.noSuchMethod( Invocation.method( #loginWithDeviceAuth, [], ), - returnValue: _i4.Future.value(false), - returnValueForMissingStub: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i6.Future.value(false), + returnValueForMissingStub: _i6.Future.value(false), + ) as _i6.Future); + + @override + void addListener(dynamic listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void removeListener(dynamic listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); @override - void goToHome() => super.noSuchMethod( + void notifyListeners() => super.noSuchMethod( Invocation.method( - #goToHome, + #notifyListeners, [], ), returnValueForMissingStub: null, ); +} + +/// A class which mocks [Command0]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCommand0 extends _i1.Mock implements _i2.Command0 { + @override + _i6.Future<_i4.Result> Function() get action => (super.noSuchMethod( + Invocation.getter(#action), + returnValue: () => _i6.Future<_i4.Result>.value(_FakeResult_3( + this, + Invocation.getter(#action), + )), + returnValueForMissingStub: () => + _i6.Future<_i4.Result>.value(_FakeResult_3( + this, + Invocation.getter(#action), + )), + ) as _i6.Future<_i4.Result> Function()); + + @override + bool get isError => (super.noSuchMethod( + Invocation.getter(#isError), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get isOk => (super.noSuchMethod( + Invocation.getter(#isOk), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get running => (super.noSuchMethod( + Invocation.getter(#running), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i6.Future<_i4.Result> execute() => (super.noSuchMethod( + Invocation.method( + #execute, + [], + ), + returnValue: _i6.Future<_i4.Result>.value(_FakeResult_3( + this, + Invocation.method( + #execute, + [], + ), + )), + returnValueForMissingStub: + _i6.Future<_i4.Result>.value(_FakeResult_3( + this, + Invocation.method( + #execute, + [], + ), + )), + ) as _i6.Future<_i4.Result>); @override void addListener(dynamic listener) => super.noSuchMethod(