Skip to content

Commit

Permalink
test(#59): criando teste para login view model
Browse files Browse the repository at this point in the history
  • Loading branch information
GabrielCostaDeOliveira committed Jan 17, 2025
1 parent 7c00605 commit ffc129a
Show file tree
Hide file tree
Showing 16 changed files with 466 additions and 147 deletions.
22 changes: 22 additions & 0 deletions lib/core/state/command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,25 @@ class Command0<T> extends Command<T> {
return _result!;
}
}

class Command1<T, A> extends Command<T> {
final Future<Result<T>> Function(A) action;

Command1(this.action);

Future<Result<T>> execute(A arg1) async {
await _execute(() => action(arg1));
return result!;
}
}

class Command2<T, A, B> extends Command<T> {
final Future<Result<T>> Function(A, B) action;

Command2(this.action);

Future<Result<T>> execute(A arg1, B arg2) async {
await _execute(() => action(arg1, arg2));
return result!;
}
}
4 changes: 1 addition & 3 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:aranduapp/core/di/locator.dart';
import 'package:flutter/material.dart';

void main() {
setupLocator();
runApp(const MyApp());
}

Expand All @@ -12,9 +13,6 @@ class MyApp extends StatelessWidget {

@override
Widget build(BuildContext context) {

setupLocator();

return MaterialApp(
theme: ThemeApp.themeData(),
darkTheme: ThemeApp.darkThemeData(),
Expand Down
5 changes: 3 additions & 2 deletions lib/ui/edit_password/view/edit_password_view.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:aranduapp/ui/edit_password/viewmodel/edit_password_viewmodel.dart';
import 'package:aranduapp/ui/shared/request_button.dart';
import 'package:aranduapp/ui/shared/command_button.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

Expand Down Expand Up @@ -80,7 +80,8 @@ class EditPasswordScreen extends StatelessWidget {
}

Widget _button(BuildContext context, EditPasswordViewModel viewModel) {
return Requestbutton(
return CommandButton(
tap: viewModel.editCommand.execute ,
command: viewModel.editCommand,
nameButton: "Enviar",
onErrorCallback: (e) {
Expand Down
5 changes: 3 additions & 2 deletions lib/ui/edit_profile/view/edit_profile_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:aranduapp/ui/edit_profile/viewmodel/edit_profile_viewmodel.dart'
import 'package:aranduapp/ui/login/viewmodel/login_viewmodel.dart';
import 'package:aranduapp/ui/shared/text_email.dart';
import 'package:aranduapp/ui/shared/text_name.dart';
import 'package:aranduapp/ui/shared/request_button.dart';
import 'package:aranduapp/ui/shared/command_button.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -89,7 +89,8 @@ class EditProfileScreen extends StatelessWidget {
}

Widget _saveButton(BuildContext context, EditProfileViewModel viewModel) {
return Requestbutton(
return CommandButton(
tap: viewModel.editCommand.execute ,
command: viewModel.editCommand,
nameButton: "Salvar",
onErrorCallback: (e) {
Expand Down
2 changes: 2 additions & 0 deletions lib/ui/login/di/di.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import 'package:aranduapp/ui/login/service/login_service.dart';
import 'package:aranduapp/ui/login/viewmodel/login_viewmodel.dart';
import 'package:get_it/get_it.dart';
import 'package:local_auth/local_auth.dart';

final GetIt locator = GetIt.instance;

void setupLoginDI() {

locator.registerLazySingleton(() => LoginService());
locator.registerLazySingleton(() => LocalAuthentication());

locator.registerFactory(() => LoginViewModel());
}
3 changes: 1 addition & 2 deletions lib/ui/login/service/login_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:aranduapp/ui/login/model/login_response.dart';
import 'package:dio/dio.dart';

class LoginService {
Future<Response> login(LoginRequest loginRequest) async {
Future<void> login(LoginRequest loginRequest) async {
Log.d('${loginRequest.email} ${loginRequest.password}');

Response response = await BaseApi.getInstance(auth: false).post(
Expand All @@ -24,7 +24,6 @@ class LoginService {
await StorageValue.getInstance()
.setRefreshToken(loginResponse.refreshToken);

return response;
}

Future<void> validateToken() async {
Expand Down
25 changes: 18 additions & 7 deletions lib/ui/login/view/login_view.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import 'package:aranduapp/core/log/log.dart';
import 'package:aranduapp/ui/login/model/login_request.dart';
import 'package:aranduapp/ui/navbar/view/navbar_view.dart';
import 'package:aranduapp/ui/shared/text_and_link.dart';
import 'package:aranduapp/ui/shared/request_button.dart';
import 'package:aranduapp/ui/shared/command_button.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:provider/provider.dart';
Expand All @@ -24,13 +25,17 @@ class Login extends StatelessWidget {
Widget build(BuildContext context) {
return ChangeNotifierProvider<LoginViewModel>.value(
value: GetIt.instance<LoginViewModel>(),
child: const LoginScreen(),
child: LoginScreen(),
);
}
}

class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
LoginScreen({super.key});

final GlobalKey<FormState> formKey = GlobalKey<FormState>();
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -133,16 +138,16 @@ class LoginScreen extends StatelessWidget {

Widget _formSection(LoginViewModel viewModel) {
return Form(
key: viewModel.formKey,
key: formKey,
child: Column(
children: <Widget>[
TextEmail(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
controller: viewModel.emailController,
controller: emailController,
),
TextPassWord(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
controller: viewModel.passwordController,
controller: passwordController,
),
],
),
Expand Down Expand Up @@ -177,7 +182,13 @@ class LoginScreen extends StatelessWidget {
Widget _loginButtonSection(BuildContext context) {
LoginViewModel viewModel = Provider.of<LoginViewModel>(context);

return Requestbutton(
return CommandButton(
tap: () {
if (formKey.currentState!.validate()) {
viewModel.loginCommand.execute(
LoginRequest(emailController.text, passwordController.text));
}
},
command: viewModel.loginCommand,
onErrorCallback: (String e) {
showDialog<Object>(
Expand Down
34 changes: 15 additions & 19 deletions lib/ui/login/viewmodel/login_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,19 @@ import 'package:aranduapp/ui/login/service/login_service.dart';
import 'package:aranduapp/ui/login/model/login_request.dart';

class LoginViewModel extends ChangeNotifier {
late Command0<void> loginCommand;
late Command1<void, LoginRequest> loginCommand;
late Command0<void> validadeTokenCommand;

final GlobalKey<FormState> formKey;
final TextEditingController emailController;
final TextEditingController passwordController;

LoginViewModel()
: formKey = GlobalKey<FormState>(),
emailController = TextEditingController(),
passwordController = TextEditingController() {
loginCommand = Command0<void>(loginWithEmailAndPassword);
LoginViewModel() {
loginCommand = Command1<void, LoginRequest>(loginWithEmailAndPassword);

validadeTokenCommand = Command0<void>(validateToken);
validadeTokenCommand.execute();
}

Future<Result<void>> loginWithEmailAndPassword() async {
if (!formKey.currentState!.validate()) {
return Result.error(Exception('Valores inválidos'));
}

await GetIt.instance<LoginService>().login(
LoginRequest(emailController.text, passwordController.text));
Future<Result<void>> loginWithEmailAndPassword(
LoginRequest loginRequest) async {
await GetIt.instance<LoginService>().login(loginRequest);

return Result.value(null);
}
Expand All @@ -44,7 +33,14 @@ class LoginViewModel extends ChangeNotifier {

Future<bool> loginWithDeviceAuth() async {
Log.d('init loginWithDeviceAuth');
return await LocalAuthentication()
.authenticate(localizedReason: 'Toque com o dedo no sensor para logar');
var auth = GetIt.instance<LocalAuthentication>();

if (await auth.canCheckBiometrics && await auth.isDeviceSupported()) {
return auth.authenticate(
localizedReason: 'Toque com o dedo no sensor para logar');
} else {
Log.d('Device authentication not available, returning true');
return true;
}
}
}
5 changes: 3 additions & 2 deletions lib/ui/recover_account/view/recover_account_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:aranduapp/ui/shared/error_popup.dart';
import 'package:aranduapp/ui/shared/text_and_link.dart';
import 'package:aranduapp/ui/shared/text_email.dart';
import 'package:aranduapp/ui/shared/title_slogan.dart';
import 'package:aranduapp/ui/shared/request_button.dart';
import 'package:aranduapp/ui/shared/command_button.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

Expand Down Expand Up @@ -52,7 +52,8 @@ class RecoverAccountScreen extends StatelessWidget {
])),
Padding(
padding: const EdgeInsets.only(top: 80),
child: Requestbutton(
child: CommandButton(
tap: viewModel.recoverCommand.execute ,
command: viewModel.recoverCommand,
onErrorCallback: (String e) {
showDialog<Object>(
Expand Down
5 changes: 3 additions & 2 deletions lib/ui/register_account/view/register_account_view.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:aranduapp/ui/shared/or_divider.dart';
import 'package:aranduapp/ui/shared/text_and_link.dart';
import 'package:aranduapp/ui/shared/text_name.dart';
import 'package:aranduapp/ui/shared/request_button.dart';
import 'package:aranduapp/ui/shared/command_button.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -110,7 +110,8 @@ class RegisterAccountScreen extends StatelessWidget {
Widget _buildRegisterButton(BuildContext context) {
final viewModel = Provider.of<RegisterAccountViewModel>(context);

return Requestbutton(
return CommandButton(
tap: viewModel.registerCommand.execute,
command: viewModel.registerCommand,
nameButton: 'Registrar',
onSuccessCallback: () {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import 'package:aranduapp/core/state/command.dart';
import 'package:flutter/material.dart';

class Requestbutton extends StatelessWidget {
final Command0 command;
class CommandButton extends StatelessWidget {
final Command command;
final String nameButton;

final VoidCallback onSuccessCallback;
final VoidCallback tap;
final ValueChanged<String> onErrorCallback;

const Requestbutton({
const CommandButton({
super.key,
required this.command,
required this.tap,
required this.nameButton,
required this.onSuccessCallback,
required this.onErrorCallback,
Expand All @@ -37,8 +39,9 @@ class Requestbutton extends StatelessWidget {
width: 291,
height: 64,
child: ElevatedButton(
key: const Key('elevated_button_key'),
onPressed: () async {
command.execute();
tap();
},
child: command.running
? const CircularProgressIndicator(value: null)
Expand Down
43 changes: 28 additions & 15 deletions test/ui/login/view/login_view_test.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:aranduapp/core/state/command.dart';
import 'package:aranduapp/ui/login/model/login_request.dart';
import 'package:aranduapp/ui/login/view/login_view.dart';
import 'package:aranduapp/ui/login/viewmodel/login_viewmodel.dart';
import 'package:aranduapp/ui/navbar/view/navbar_view.dart';
import 'package:aranduapp/ui/shared/command_button.dart';
import 'package:aranduapp/ui/shared/text_email.dart';
import 'package:aranduapp/ui/shared/text_password.dart';
import 'package:async/async.dart';
Expand All @@ -11,23 +13,23 @@ import 'package:get_it/get_it.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

@GenerateNiceMocks([MockSpec<LoginViewModel>(), MockSpec<Command0>()])
@GenerateNiceMocks(
[MockSpec<LoginViewModel>(), MockSpec<Command0>(), MockSpec<Command1>()])
import 'login_view_test.mocks.dart';

void main() {
late MockLoginViewModel mockViewModel;
late MockCommand0 mockLoginCommand;
late MockCommand1<void, LoginRequest> mockLoginCommand;
late MockCommand0 mockValidadeTokenCommand;

setUp(() async {
mockViewModel = MockLoginViewModel();
when(mockViewModel.formKey).thenReturn(GlobalKey<FormState>());
when(mockViewModel.emailController).thenReturn(TextEditingController());
when(mockViewModel.passwordController).thenReturn(TextEditingController());

mockLoginCommand = MockCommand0();
when(mockLoginCommand.execute())
mockLoginCommand = MockCommand1();

when(mockLoginCommand.execute(any))
.thenAnswer((_) async => Result.value(null));

when(mockViewModel.loginCommand).thenReturn(mockLoginCommand);

mockValidadeTokenCommand = MockCommand0();
Expand Down Expand Up @@ -86,6 +88,7 @@ void main() {

expect(find.byType(TextEmail), findsOneWidget);
expect(find.byType(TextPassWord), findsOneWidget);
expect(find.byType(CommandButton), findsOneWidget);
expect(find.text('Entrar'), findsOneWidget);
});

Expand All @@ -102,28 +105,38 @@ void main() {
await tester.enterText(find.byType(TextEmail), email);
await tester.enterText(find.byType(TextPassWord), password);

expect(mockViewModel.emailController.text, email);
expect(mockViewModel.passwordController.text, password);
await tester.tap(find.byKey(const Key('elevated_button_key')));

verify(mockLoginCommand.execute(argThat(
predicate<LoginRequest>(
(req) => req.email == email && req.password == password),
))).called(1);
});

testWidgets('Login is successful', (WidgetTester tester) async {
when(mockValidadeTokenCommand.isError).thenReturn(true);
when(mockLoginCommand.execute(any))
.thenAnswer((_) async => Result.value(null));

await tester.pumpWidget(createLoginScreen());
await tester.pumpAndSettle();

when(mockLoginCommand.execute())
.thenAnswer((_) async => Result.value(null));
const email = 'test@example.com';
const password = 'password123';

await tester.tap(find.text('Entrar'));
await tester.enterText(find.byType(TextEmail), email);
await tester.enterText(find.byType(TextPassWord), password);

await tester.tap(find.byKey(const Key('elevated_button_key')));
await tester.pumpAndSettle();

verify(mockLoginCommand.execute()).called(1);
verify(mockLoginCommand.execute(argThat(
predicate<LoginRequest>(
(req) => req.email == email && req.password == password)
))).called(1);

//TODO: verify navigation to navbar
// TODO: Verify navigation to navbar
});

testWidgets('Displays error when login fails', (WidgetTester tester) async {
//TODO:
});
Expand Down
Loading

0 comments on commit ffc129a

Please sign in to comment.