Skip to content

Commit 390ebb6

Browse files
committed
test: Add unit tests for LoginBloc.
1 parent 0830408 commit 390ebb6

15 files changed

Lines changed: 351 additions & 34 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,8 @@ jobs:
5050
run: flutter pub run build_runner build --delete-conflicting-outputs
5151

5252
# Test
53-
# Temporary disabled due to absence of tests.
54-
# - name: Run tests
55-
# run: flutter test
53+
- name: Run tests
54+
run: flutter test --no-pub test/test.dart
5655

5756
build:
5857
runs-on: ubuntu-latest

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ app.*.map.json
4949
# Generated files
5050
**/*.g.dart
5151
**/*.freezed.dart
52+
coverage

.idea/runConfigurations/all_tests.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/runConfigurations/bloc_tests.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/runConfigurations/widget_tests.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/data/constants/strings.dart

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
class AppStrings {
2+
// Storage
23
static const String hiveBoxName = 'app_box';
3-
44
static const String authTokenKey = 'auth_token';
55
static const String userKey = 'user';
6+
7+
// Error
8+
static const String genericError = 'Something went wrong';
9+
static const String incorrectCredentials = 'Incorrect credentials';
10+
static const String noUserWithEmail =
11+
'No user associated with given email address';
12+
static const String passwordResetSuccess =
13+
'Success! Please check your email for a password reset link';
14+
static const String verifyFirst =
15+
'Please verify your email before attempting to log in\n'
16+
'Check your email for the verification link';
617
}

lib/presentation/login/bloc/login_bloc.dart

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
44
import 'package:get/get.dart';
55

66
import '../../../data/constants/routes.dart';
7+
import '../../../data/constants/strings.dart';
78
import '../../../domain/models/status.dart';
89
import '../../../domain/repositories/user_repository.dart';
910

@@ -12,7 +13,7 @@ part 'login_event.dart';
1213
part 'login_state.dart';
1314

1415
class LoginBloc extends Bloc<LoginEvent, LoginState> {
15-
LoginBloc() : super(LoginState(rememberMe: true)) {
16+
LoginBloc() : super(const LoginState()) {
1617
on<ToggleDialog>(_toggleDialog);
1718
on<PasswordInput>(_updatePasswordInput);
1819
on<Submit>(_submitForm);
@@ -22,6 +23,8 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
2223
}
2324

2425
void _submitForm(Submit event, Emitter<LoginState> emit) async {
26+
if (!state.isFormFilled()) return;
27+
2528
emit(state.copyWith(status: const Status.loading()));
2629

2730
final result = await UserRepository.login(
@@ -35,50 +38,43 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
3538
} else {
3639
if (result == 'Unverified') {
3740
emit(state.copyWith(
38-
status: const Status.error(
39-
'Please verify your email before attempting to log in\n'
40-
'Check your email for the verification link',
41-
),
41+
status: const Status.error(AppStrings.verifyFirst),
4242
));
4343
} else if (result == 'Unauthorized') {
4444
emit(state.copyWith(
45-
status: const Status.error('Incorrect credentials'),
45+
status: const Status.error(AppStrings.incorrectCredentials),
4646
));
4747
} else {
4848
emit(state.copyWith(
49-
status: const Status.error('Something went wrong'),
49+
status: const Status.error(AppStrings.genericError),
5050
));
5151
}
5252
}
5353
}
5454

5555
void _toggleDialog(ToggleDialog event, Emitter<LoginState> emit) async {
56-
if (event.email != null) {
56+
if (event.email == null) {
57+
emit(state.copyWith(showDialog: !state.showDialog));
58+
} else {
59+
if (event.email!.isEmpty) return;
5760
emit(state.copyWith(
5861
showDialog: !state.showDialog,
5962
status: const Status.loading(),
6063
));
61-
if (event.email!.isEmpty) return;
6264
final result = await UserRepository.resetPassword(event.email!);
6365
if (result == null) {
6466
emit(state.copyWith(
65-
status: const Status.error('Something went wrong'),
67+
status: const Status.error(AppStrings.genericError),
6668
));
6769
} else if (result) {
6870
emit(state.copyWith(
69-
status: const Status(
70-
'Success! Please check your email for a password reset link',
71-
),
71+
status: const Status(AppStrings.passwordResetSuccess),
7272
));
7373
} else {
7474
emit(state.copyWith(
75-
status: const Status.error(
76-
'No user associated with given email address',
77-
),
75+
status: const Status.error(AppStrings.noUserWithEmail),
7876
));
7977
}
80-
} else {
81-
emit(state.copyWith(showDialog: !state.showDialog));
8278
}
8379
}
8480

@@ -91,10 +87,18 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
9187
}
9288

9389
void _updatePasswordInput(PasswordInput event, Emitter<LoginState> emit) {
94-
emit(state.copyWith(password: event.value));
90+
emit(state.copyWith(
91+
isPasswordFocused: true,
92+
isUsernameFocused: false,
93+
password: event.value,
94+
));
9595
}
9696

9797
void _updateUsernameInput(UsernameInput event, Emitter<LoginState> emit) {
98-
emit(state.copyWith(username: event.value));
98+
emit(state.copyWith(
99+
isUsernameFocused: true,
100+
isPasswordFocused: false,
101+
username: event.value,
102+
));
99103
}
100104
}

lib/presentation/login/bloc/login_state.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ part of 'login_bloc.dart';
22

33
@freezed
44
class LoginState with _$LoginState {
5-
factory LoginState({
6-
required bool rememberMe,
5+
const factory LoginState({
6+
@Default(true) bool rememberMe,
77
@Default(false) bool isUsernameFocused,
88
@Default(false) bool isPasswordFocused,
99
@Default(true) bool obscurePassword,
@@ -15,7 +15,7 @@ class LoginState with _$LoginState {
1515

1616
const LoginState._();
1717

18-
bool isFormFilled() {
19-
return username.isNotEmpty && password.isNotEmpty;
20-
}
18+
bool isFormFilled() => username.isNotEmpty && password.isNotEmpty;
19+
20+
bool isLoginButtonActive() => isFormFilled() && status is! Loading;
2121
}

lib/presentation/login/widgets/login_button.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class LoginButton extends StatelessWidget {
1919
),
2020
onPressed: () {
2121
FocusScope.of(context).requestFocus(FocusNode());
22-
if (state.isFormFilled() && state.status is! Loading) {
22+
if (state.isLoginButtonActive()) {
2323
context.read<LoginBloc>().add(const Submit());
2424
}
2525
},

pubspec.lock

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ packages:
77
name: _fe_analyzer_shared
88
url: "https://pub.dartlang.org"
99
source: hosted
10-
version: "34.0.0"
10+
version: "31.0.0"
1111
analyzer:
1212
dependency: transitive
1313
description:
1414
name: analyzer
1515
url: "https://pub.dartlang.org"
1616
source: hosted
17-
version: "3.2.0"
17+
version: "2.8.0"
1818
args:
1919
dependency: transitive
2020
description:
@@ -36,6 +36,13 @@ packages:
3636
url: "https://pub.dartlang.org"
3737
source: hosted
3838
version: "8.0.2"
39+
bloc_test:
40+
dependency: "direct dev"
41+
description:
42+
name: bloc_test
43+
url: "https://pub.dartlang.org"
44+
source: hosted
45+
version: "9.0.2"
3946
boolean_selector:
4047
dependency: transitive
4148
description:
@@ -155,6 +162,13 @@ packages:
155162
url: "https://pub.dartlang.org"
156163
source: hosted
157164
version: "3.0.1"
165+
coverage:
166+
dependency: transitive
167+
description:
168+
name: coverage
169+
url: "https://pub.dartlang.org"
170+
source: hosted
171+
version: "1.0.3"
158172
crypto:
159173
dependency: transitive
160174
description:
@@ -169,6 +183,13 @@ packages:
169183
url: "https://pub.dartlang.org"
170184
source: hosted
171185
version: "2.2.1"
186+
diff_match_patch:
187+
dependency: transitive
188+
description:
189+
name: diff_match_patch
190+
url: "https://pub.dartlang.org"
191+
source: hosted
192+
version: "0.4.1"
172193
dio:
173194
dependency: "direct main"
174195
description:
@@ -344,7 +365,7 @@ packages:
344365
name: freezed
345366
url: "https://pub.dartlang.org"
346367
source: hosted
347-
version: "1.1.1"
368+
version: "1.1.0"
348369
freezed_annotation:
349370
dependency: "direct main"
350371
description:
@@ -492,13 +513,27 @@ packages:
492513
url: "https://pub.dartlang.org"
493514
source: hosted
494515
version: "1.0.1"
516+
mocktail:
517+
dependency: "direct dev"
518+
description:
519+
name: mocktail
520+
url: "https://pub.dartlang.org"
521+
source: hosted
522+
version: "0.2.0"
495523
nested:
496524
dependency: transitive
497525
description:
498526
name: nested
499527
url: "https://pub.dartlang.org"
500528
source: hosted
501529
version: "1.0.0"
530+
node_preamble:
531+
dependency: transitive
532+
description:
533+
name: node_preamble
534+
url: "https://pub.dartlang.org"
535+
source: hosted
536+
version: "2.0.1"
502537
package_config:
503538
dependency: transitive
504539
description:
@@ -695,6 +730,20 @@ packages:
695730
url: "https://pub.dartlang.org"
696731
source: hosted
697732
version: "1.2.0"
733+
shelf_packages_handler:
734+
dependency: transitive
735+
description:
736+
name: shelf_packages_handler
737+
url: "https://pub.dartlang.org"
738+
source: hosted
739+
version: "3.0.0"
740+
shelf_static:
741+
dependency: transitive
742+
description:
743+
name: shelf_static
744+
url: "https://pub.dartlang.org"
745+
source: hosted
746+
version: "1.1.0"
698747
shelf_web_socket:
699748
dependency: transitive
700749
description:
@@ -721,6 +770,20 @@ packages:
721770
url: "https://pub.dartlang.org"
722771
source: hosted
723772
version: "1.3.1"
773+
source_map_stack_trace:
774+
dependency: transitive
775+
description:
776+
name: source_map_stack_trace
777+
url: "https://pub.dartlang.org"
778+
source: hosted
779+
version: "2.1.0"
780+
source_maps:
781+
dependency: transitive
782+
description:
783+
name: source_maps
784+
url: "https://pub.dartlang.org"
785+
source: hosted
786+
version: "0.10.10"
724787
source_span:
725788
dependency: transitive
726789
description:
@@ -770,13 +833,27 @@ packages:
770833
url: "https://pub.dartlang.org"
771834
source: hosted
772835
version: "1.2.0"
836+
test:
837+
dependency: transitive
838+
description:
839+
name: test
840+
url: "https://pub.dartlang.org"
841+
source: hosted
842+
version: "1.19.5"
773843
test_api:
774844
dependency: transitive
775845
description:
776846
name: test_api
777847
url: "https://pub.dartlang.org"
778848
source: hosted
779849
version: "0.4.8"
850+
test_core:
851+
dependency: transitive
852+
description:
853+
name: test_core
854+
url: "https://pub.dartlang.org"
855+
source: hosted
856+
version: "0.4.9"
780857
timing:
781858
dependency: transitive
782859
description:
@@ -805,6 +882,13 @@ packages:
805882
url: "https://pub.dartlang.org"
806883
source: hosted
807884
version: "2.1.1"
885+
vm_service:
886+
dependency: transitive
887+
description:
888+
name: vm_service
889+
url: "https://pub.dartlang.org"
890+
source: hosted
891+
version: "7.5.0"
808892
watcher:
809893
dependency: transitive
810894
description:
@@ -819,6 +903,13 @@ packages:
819903
url: "https://pub.dartlang.org"
820904
source: hosted
821905
version: "2.1.0"
906+
webkit_inspection_protocol:
907+
dependency: transitive
908+
description:
909+
name: webkit_inspection_protocol
910+
url: "https://pub.dartlang.org"
911+
source: hosted
912+
version: "1.0.0"
822913
win32:
823914
dependency: transitive
824915
description:

0 commit comments

Comments
 (0)