I'm starting to learn flutter test unit, and i use firebase flutter login with bloc as the base. I changed some part of it on my code and the tests always fails. I tried to only changed the suspected code on flutter firebase login source code and the results is exactly the same as on my apps. Here the changed code:
signup_form.dart
...
class _EmailInput extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<SignUpCubit, SignUpState>(
buildWhen: (previous, current) => previous.email != current.email,
builder: (context, state) {
return TextField(
key: const Key('signUpForm_emailInput_textField'),
onChanged: (email) => context.read<SignUpCubit>().emailChanged(email),
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'email',
helperText: '',
errorText: state.email.invalid ? 'Email is invalid' : null,
),
);
},
);
}
}
...
sign_up_form_test.dart
...
testWidgets('invalid email error text when email is invalid',
(tester) async {
final email = MockEmail();
when(() => email.invalid).thenReturn(true);
when(() => signUpCubit.state).thenReturn(SignUpState(email: email));
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BlocProvider.value(
value: signUpCubit,
child: const SignUpForm(),
),
),
),
);
expect(find.text('Email is invalid'), findsOneWidget);
});
...
I changed the errorType so i don't need to bloat my view file (because i want to add more state later, like email registered, email invalid, email empty, and on password i can put like minimum 8 char, need uppercase etc.). I added errorText getter on Email model (this getter is not exist on vanilla flutter firebase login)
email.dart
...
/// Get error message
///
/// Return error in #String
String? get errorText {
String? errorMessage;
if (invalid) {
errorMessage = 'Email is invalid';
}
return errorMessage;
}
...
and so I changed the errorText on Textfield to
signup_form.dart
...
decoration: InputDecoration(
labelText: 'email',
helperText: '',
errorText: state.email.errorText,
),
...
they should act exactly the same right? because state.email.invalid is accessing invalid getter so in email.dart is excatly the same as accessing invalid directly. I even changed the code to like below but the results still same
email.dart
...
String? get errorText => invalid ? 'invalid email' : null;
...
Do I use wrong test code? or I misunderstand how unit test work? I really hope I can se behind the results when testing is running because when I test it myself, I see no difference between them. Even when I only change the errorText value from signup_form.dart back to original, the test unit is success again, (so only errorText: state.email.invalid ? 'Email is invalid' : null, and errorText: state.email.errorText, is difference, where errorText is String? get errorText => invalid ? 'Email is invalid' : null;).
P.S. I don't mind if you ask me to write more code here, I just don't want my questions become too long so I skipped most of the codes related to this. btw I'm using Cubit to detect the change of the state.
Related
I have a question about this course in Udemy where Im trying to set up Bloc pattern for authentication in Flutter. The problem is that I get a null value from start and also later when I type a required '#' from the snapshot.error of the StreamBuilder. It's set up so I should get an error message until I type in a '#' and then the message should go away. I have three relevant files: loginscreen, bloc and validators. What do you guys think can go wrong here?
Loginscreen:
class LoginScreen extends StatelessWidget {
#override
Widget build(context) {
return Container(
margin: EdgeInsets.all(20.0),
child: Column(
children: [
emailField(),
passwordField(),
//const SizedBox(height: 25.0),
Container(margin: const EdgeInsets.only(top: 25.0)),
submitButton(),
],
),
);
}
Widget emailField() {
//Listener for streams rebuilds builder function when found
return StreamBuilder(
stream: bloc.email,
//snapshot contains the value from the stream
builder: (context, AsyncSnapshot<String>? snapshot) {
return TextField(
onChanged: (newValue) {
print(newValue);
bloc.changeEmail(newValue);
},
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'you#example.com',
labelText: 'Email Address',
//This is null for some reason. Something is not right here
//First it shows error because no # and when pressed I get null
errorText: snapshot!.error.toString(),
),
);
});
}
Bloc:
class Bloc with Validators {
//We are working with Strings in the Streamcontrollers (marked with String type)
final _email = StreamController<String>();
final _password = StreamController<String>();
//Annotation with generic type in front. Not required though
// Add data to stream (access to stream)
// Apply the validation transform created
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password => _password.stream.transform(validatePassword);
// Change data (access to sink)
Function(String) get changeEmail => _email.sink.add;
Function(String) get changePassword => _password.sink.add;
dispose() {
_email.close();
_password.close();
}
}
final bloc = Bloc();
Validators:
class Validators {
final validateEmail =
StreamTransformer<String, String>.fromHandlers(handleData: (email, sink) {
if (email.contains('#')) {
sink.add(email);
} else {
sink.addError('Enter a valid email!');
}
});
final validatePassword = StreamTransformer<String, String>.fromHandlers(
handleData: (password, sink) {
if (password.length > 7) {
sink.add(password);
} else {
sink.addError('You must be at least 8 characters!');
}
});
}
After change to:
errorText: snapshot?.error?.toString() ?? snapshot?.data,
You are always reading the stream's error here:
errorText: snapshot!.error.toString(),
but you only add errors in this else:
if (email.contains('#')) {
sink.add(email);
} else {
sink.addError('Enter a valid email!');
}
You may try to replace how this value is read by:
//This is null for some reason. Something is not right here
//First it shows error because no # and when pressed I get null
errorText: snapshot?.error?.toString() ?? snapshot?.data,
In this way, if there is no error, errorText will get stream's data.
Update:
If you want to make this message completely go alway after an # be inserted, you may first take a look on how errorText works. From Flutter`s InptuDecorator source code:
/// Text that appears below the [InputDecorator.child] and the border.
///
/// If non-null, the border's color animates to red and the [helperText] is
/// not shown.
///
/// In a [TextFormField], this is overridden by the value returned from
/// [TextFormField.validator], if that is not null.
final String? errorText;
So, this message will be hidden if you not pass a String to it. What is happening in this code here:
errorText: snapshot!.error.toString(),
Is something like this: null.toString();. Dart can and will parse null to string and it will be transformed in a string having null as content.
To fix this, we add an ? mark after the error, like this: snapshot?.error?.toString(),. Now, toString() will only be invoked if error is not null.
Tip: do explore the source code of the widgets that you are using. They are widely commented and you can get information much faster than google random things 🤓
I have created a Flutter stateless dropdown widget that is dependent on some future list, I used the FutureBuilder to build the dropdown as soon as the future is resolved.
But I noticed that build method was being called at least twice. I know it is normal that the build method can be called multiple times when some state changes, but why was the dropdown was being rebuilt with the same data as the previous build call? I thought for sure when build is called, Flutter will rebuild the entire widget which also implies that the previous data will be destroyed as well.
This has resulted in duplication in the items of the dropdown.
I am not sure why it is happening. What did I miss?
class _PetTypeInput extends StatelessWidget {
#override
Widget build(BuildContext context) {
final petTypes = context.read<RegisterPetProfileCubit>().getPetTypes();
return FutureBuilder<List<PetType>>(
future: petTypes,
builder: (BuildContext context, AsyncSnapshot<List<PetType>> snapshot) {
List<PetType>? petKinds = [];
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
petKinds = snapshot.data;
return DropdownButtonFormField<String>(
key: const Key('registerForm_petKindInput_dropdownButtonFormField'),
decoration: const InputDecoration(
labelText: 'pet kind',
helperText: '',
errorText: null,
),
value: 'Dog',
onChanged: (petKindValue) => context
.read<RegisterPetProfileCubit>()
.petKindChanged(petKindValue!),
items: _buildItems(petKinds),
);
}
return const TextField(
enabled: false,
keyboardType: TextInputType.name,
decoration: InputDecoration(
labelText: 'pet kind',
helperText: '',
),
);
},
);
}
List<DropdownMenuItem<String>> _buildItems(List<PetType>? petKinds) {
final petTypes = petKinds!.fold(
<String, String>{},
(Map<String, String> petTypesMap, petType) {
petTypesMap[petType.id] = petType.label;
return petTypesMap;
},
);
List<String> items = petTypes.keys.toList();
return items.map((key) {
return DropdownMenuItem<String>(
key: Key(key),
child: Text(petTypes[key]!),
value: key,
);
}).toList();
}
}
I can definitely tell that there are no duplicates in the data.
How do I prevent appending the same data? Or clear the previous data of DropdownButtonFormField?
You can build _buildItems(petKinds), before return DropdownButtonFormField<String>( and passing item[0] value on DropdownButtonFormField value. And it will make sure the value contain on DropDownMenuItems. Also, make sure to have Unique value on each DropDownMenuItem.
I finally figured it out. The issue is not duplicate dropdown items but rather my initial value is not a valid value. My initial value was Dog when it should be the id of the Dog item.
So I grabed the first item object and then grabed the id of that item and supplied it as initial value.
The answers from this question helped me figured it out.
I am trying to do simple unit test for the form I have created. I referred some documents and YouTube videos to do that. But there are not many resources for unit testing in form and the available once are not compatible with current version (null safety).
Can any one explain me how I can do a unit test for my validation part and for maxLength in form field.
This is the code I tried but it is showing error and unable to continue.
class FullNameValidator{
static String? validate(String value){
return value.isEmpty ? 'Please enter full name' : null;
}
}
TextFormField(
decoration: InputDecoration(
labelText: "FullName",
labelStyle: TextStyle(color: Colors.black),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
maxLength: 30,
keyboardType: TextInputType.text,
controller: fullName,
validator: FullNameValidator.validate, //Showing error in here
),
The test I tried
void main(){
test('empty email returns error string', (){
var result = FullNameValidator.validate("");
expect(result, 'Please enter your full name');
});
}
Can anyone please explain how to do unit test for validation and maxLength.
I searched every document/ video I can, but unable to find a solution. Please help
It shows you this error because the validator property of a TextField is expecting a String? Function(String?) as its parameter and your FullNameValidator.validate is a String? Function(String), here's how you can fix that:
class FullNameValidator{
// Make your value nullable
static String? validate(String? value) {
// Now that value can be null you cannot be sure that isEmpty can be called
// so you need to provide a default value, in this case I am returning
// true if isEmpty cannot be called so it will return 'Please enter full name'.
return (value?.isEmpty ?? true) ? 'Please enter full name' : null;
}
}
You don't need to test maxLength as by precising a maxLength the String inside your TextField won't be able to be longer than the number you've defined.
If you really want to validate the length of your String you could change your method like this:
class FullNameValidator{
static String? validate(String? value, [int? maxLength]) {
if (value != null && maxLength != null) {
return value.length > maxLength ? 'This is too long' : null;
}
return (value?.isEmpty ?? true) ? 'Please enter full name' : null;
}
}
But then you would need to call your validator like this in your TextField:
TextFormField(
decoration: InputDecoration(
labelText: "FullName",
labelStyle: TextStyle(color: Colors.black),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
// maxLength: 30, // You don't need this property anymore if you rely on your validator
keyboardType: TextInputType.text,
controller: fullName,
validator: (value) => FullNameValidator.validate(value, 30),
),
I am facing a problem with TextField, since I am working hand in hand with TextEditingController()..text and onChanged, but when entering a new data, it is not reflected in the TextField. I made a print inside the onChanged this same one recognizes a new entry but the value to initiate continues without being updated. Inside the onChanged, I have a function which is in charge of validating what is entered and returning an error if necessary. When I comment the instruction before mentioned the TextField already allows to enter and to update what the user enters.
I hope you can help me, a feedback, tutorial, etc.
I would appreciate it.
TextField Code:
CustomTextField(
controller: TextEditingController()..text = datumAdministrative.name,
placeholder: Constants.selectDate,
helperText: Constants.requiredData,
keyboardType: TextInputType.text,
enable: true,
errorText: validationForm.name.error,
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.sentences,
onChanged: (String value) {
validationForm.changeName(value);
},
);
ValidationForm Code:
void changeName(String value) {
String pattern = r'(^[a-zA-Z ]*$)';
RegExp regExp = new RegExp(pattern);
regExp.hasMatch(value)
? _name = ValidationItem(value, null)
: _name = ValidationItem(null, Constants.nameAdministrativeMessage);
notifyListeners();
}
Try Using : TextController(text: "<Required Text>")
Also does the validation have to be every single time the user enter any word ?
If not, you can try validation everything at the end.
If you are trying to use reactive validation, make sure your CustomTextField is wrapped with the widget which is responsible for rebuilding the UI.. something like Consumer() when using provider package
The problem is probably caused because the TextEditingController object is getting discarded by the rebuilds made by Flutter because you are instantiating the TextEditingController inside a build method. You should save the instance of your controller elsewhere, like in a state object as shown by the official docs or in your case, you can create it and get it from your validationForm.
This is the example in the docs:
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
/// save the [TextEditingController] instance
final TextEditingController _controller = TextEditingController();
...
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(6),
child: TextFormField(
controller: _controller, // User your instance
decoration: const InputDecoration(border: OutlineInputBorder()),
),
),
);
}
}
Possible solution in your case:
CustomTextField(
controller: validationForm.myTextController,
placeholder: Constants.selectDate,
...
The issue
I'm using the BLoC pattern to validate TextFields.
For that I'm using StreamController class to sink and listen to the streams.
Although , the Validator is never called.
Classes & Widget
This is my validator class
class Validators {
final validatePassword = StreamTransformer<String,String>.fromHandlers(
handleData: (password,sink)
{
if(password.length > 4)
{
sink.add(password);
}else{
sink.add('Password must be atleast 5 characters');
}
}
);
}
Then I'm building the BLoC class
class Bloc with Validators{
final _passwordController = StreamController<String>();
Function(String) get addPasswordStream => _passwordController.sink.add;
Stream<String> get getPasswordStream => _passwordController.stream.transform(validatePassword);
}
final bloc = Block ()
And adding it to my Cusotm Widget by using the StreamBuilder class
Widget passwordField(){
return StreamBuilder(
stream: bloc.getPasswordStream,
builder: (context,snapshot){
return TextField(
onChanged: bloc.addPasswordStream,
obscureText: true,
decoration: InputDecoration(
hintText: 'password',
labelText: 'password',
errorText: snapshot.error
),
);
},
);
}
I've implemented the same method for the email validator and it works , but for some reason when I'm implementing it to the password TextField as well, it doesn't. I've used flutter clean and flutter run nothing changed. What have i completely missed ?