Flutter widget tests do not pass when run together - flutter

I am trying write to some tests for a simple credit card form widget.
Each test passes when run individually but the second one fails and throws the following exception when I run both of them.
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following IndexError was thrown running a test:
RangeError (index): Index out of range: no indices are valid: 0
I tried different approaches such as resetting testedWidget to null in tearDown hook but nothing worked as intended.
Here is the test file I wrote:
void main() {
final validCardNumber = '1234123412341234';
final validExpirationDate = '11/25';
final invalidExpirationDate = '32/25';
final validCvv = '123';
Widget testedWidget;
setUp(() {
testedWidget = testableAddCardScreen(AddCardFormScreen());
});
testWidgets('Form is submittable if inputs are valid', (
WidgetTester tester,
) async {
await tester.pumpWidget(testedWidget);
await tester.pumpAndSettle();
final inputs = find.byType(TextInput);
await tester.enterText(inputs.at(0), validCardNumber);
await tester.enterText(inputs.at(1), validExpirationDate);
await tester.enterText(inputs.at(2), validCvv);
await tester.pumpAndSettle();
final button = evaluateWidget<Button>(find.byType(Button));
expect(button.isDisabled, false);
});
testWidgets('Form is not submittable if inputs are invalid', (
WidgetTester tester,
) async {
await tester.pumpWidget(testedWidget);
await tester.pumpAndSettle();
final inputs = find.byType(TextInput);
await tester.enterText(inputs.at(0), validCardNumber);
await tester.enterText(inputs.at(1), invalidExpirationDate);
await tester.enterText(inputs.at(2), validCvv);
await tester.pumpAndSettle();
final button = evaluateWidget<Button>(find.byType(Button));
expect(button.isDisabled, true);
});
}
I also use some utils functions to make widgets testable.
Widget testableAddCardScreen(Widget child) {
return testableLocalizedWidgetFactory(
ChangeNotifierProvider(
create: (_) => AddCardProvider(
apiClientService: MockApiClientService(),
successCallback: () {},
failureCallback: () {},
),
child: child,
),
);
}

Related

The following assertion was thrown while running async test code: pumpAndSettle timed out

In flutter test i have error:
The following assertion was thrown while running async test code:pumpAndSettle timed out
My test script:
testWidgets('drawer widget test', (WidgetTester tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
await tester.runAsync(() async {
when(operatorRepository.getCurrentSelectedOperator()).thenAnswer((_) async => operator);
await tester.pumpWidget(MaterialApp(home: ScaffoldDrawerTest(operatorRepository)));
expect(find.text('-1'), findsOneWidget);
expect(find.text(''), findsOneWidget);
expect(find.text('Warszawa'), findsNothing);
await tester.tap(find.byType(IconButton).first);
await tester.pumpAndSettle();
expect(find.text('Warszawa'), findsWidgets);
await tester.pumpAndSettle();
await tester.tap(find.byType(ListTile).at(2));
expect(find.text('Warszawa'), findsNothing);
});
});
I try change pumpAndSettle to for (int i = 0; i < 5; i++) { await tester.pump(Duration(seconds: 1)); }
but then i to receive error
Actual: _TextFinder:<zero widgets with text "Warszawa" (ignoring offstage widgets)>
Which: means none were found but some were expected
In ScaffoldDrawerTest i have Consumer and loading widget, when viewModel is loaded then i show another widgets when is widget text "Warszawa"

How to wait until the Finder is visible for next code execution in Flutter integration test?

Information:
I have created a sample Flutter unit test to test the login screen where I have email & password as input field and a login button.
Requirement:
Need to test false cases and for that, I have written code as per the below steps.
Open main.dart
Filled the email & password field
onTap event is done on the login button. Over here API will be called and loader is displayed on the screen until API gets a success or failure response.
Need to check if failure dialog is displayed with a message.
Issue/Query:
Now when the API is calling I want to wait when the loader is visible until the loader is gone. So, as of now I just put a manual delay to execute the next code but I want to make it dynamic. So, let me know how we can put dynamic delay based on the loader visible?
Code:
void main() {
group('App Test', () {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Login Fail Test', (WidgetTester tester) async {
await app.main();
await tester.pumpAndSettle();
await tester.pump(new Duration(seconds: 2));
final emailField = find.byType(TextFormField).first;
final passwordField = find.byType(TextFormField).last;
final loginButton = find.byType(RaisedButton).first;
await tester.enterText(emailField, 'Test');
await tester.pumpAndSettle();
await tester.pump(new Duration(seconds: 1));
await tester.enterText(passwordField, 'Test123');
await tester.pumpAndSettle();
await tester.pump(new Duration(seconds: 1));
await tester.tap(loginButton);
await tester.pumpAndSettle();
await tester.pump(new Duration(seconds: 3));
final dialog = find.byType(AlertDialog).first;
await tester.element(dialog);
await tester.pumpAndSettle();
await tester.pump(new Duration(seconds: 1));
final dialogButton = find.byType(FlatButton).first;
await tester.tap(dialogButton);
await tester.pumpAndSettle();
await tester.pump(new Duration(seconds: 2));
});
}
I have a file called utils.dart for functionality like this. In this case I use the following function which will basically poll until the finder is valid
// utils.dart
Future<void> pumpUntilFound(
WidgetTester tester,
Finder finder, {
Duration timeout = const Duration(seconds: 10),
}) async {
bool timerDone = false;
final timer = Timer(timeout, () => timerDone = true);
while (timerDone != true) {
await tester.pump();
final found = tester.any(finder);
if (found) {
timerDone = true;
}
}
timer.cancel();
}
You can also make it throw an exception if it times out, but the error messages aren't helpful, so I usually follow it up with an expect
It would look like
// my_test.dart
final fab = find.byKey(const ValueKey('fab'));
await pumpUntilFound(widgetTester, fab);
expect(fab, findsOneWidget);
Try wrapping like this:
testWidgets('test',
(WidgetTester tester) async {
await tester.runAsync(() async {
// test code here
});
});
If you use:
await tester.pumpAndSettle();
And then:
final widget = find.byKey(Key('whatever'));
It will find dinamically

Flutter Integration testing failed for multiple test cases in a single file

I have a simple login page with email and password text field. A button to login and another to sign up. I tried to write integration testing for the Sign in page.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('''could type email and password in text filed''',
(WidgetTester tester) async {
await app.main();
await tester.pumpAndSettle();
final textFieldEmail = find.byType(InputTextWidget).first;
final textFieldPassword = find.byType(InputTextWidget).last;
await tester.enterText(textFieldEmail, "asis.adh#gmail.com");
await tester.pumpAndSettle();
expect(find.text("asis.adh#gmail.com"), findsOneWidget);
});
testWidgets(
'should redirect to Sign Up Page when create an account is tapped',
(WidgetTester tester) async {
await app.main();
await tester.pumpAndSettle();
final createAnAccount = find.text("Create an account");
await tester.tap(createAnAccount);
await tester.pumpAndSettle();
expect(find.byType(SignupPage), findsOneWidget);
expect(find.byType(LoginPage), findsNothing);
});
}
When I execute the test case, it fails with the following error:
The following ArgumentError was thrown running a test: Invalid
argument(s): Object/factory with type AmenitiesProvider is already
registered inside GetIt.
Here is my main.dart file
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
configureInjection(Environment.prod);
/// for registering the factory.
await Future.delayed(const Duration(seconds: 2));
runApp(RoopaApp());
}
I tried with a main_test.dart and adding configureInjection(Environment.test) but nothing changes. I am not sure how to fix the error. Is there a way to clean the app or destroy it before going to new test case. If I combine the both testcase into one then it works without any problem.
Here is configureInjection
#injectableInit
void configureInjection(String environment) {
$initGetIt(getIt, environment: environment);
}
I am using get_it and injection package for dependency injection.
Here is the auto generated initGetIt
GetIt $initGetIt(
GetIt get, {
String environment,
EnvironmentFilter environmentFilter,
}) {
final gh = GetItHelper(get, environment, environmentFilter);
final httpClientInjectableModule = _$HttpClientInjectableModule();
final flutterStorageModule = _$FlutterStorageModule();
gh.lazySingleton<AmenitiesProvider>(() => AmenitiesProvider());
gh.factory<Client>(() => httpClientInjectableModule.client);
gh.lazySingleton<FileProvider>(() => FileProvider());
gh.lazySingleton<FlutterSecureStorage>(
() => flutterStorageModule.secureStorate);
gh.factory<ProfilePageBloc>(() => ProfilePageBloc());
gh.factory<SplashScreenBloc>(() => SplashScreenBloc());
gh.lazySingleton<AuthLocalDataSourceProtocol>(
() => AuthLocalDataSource(secureStorage: get<FlutterSecureStorage>()));
gh.lazySingleton<AuthRemoteDataSourceProtocol>(
() => AuthRemoteDataSource(client: get<Client>()));
return get;
}
In my config page.
#injectableInit
void configureInjection(String environment) {
$initGetIt(getIt, environment: environment);
}
I just created a test environment and added following like, now its working as expected.
#injectableInit
void configureInjection(String environment) {
$initGetIt(getIt, environment: environment);
if (environment == Environment.test) {
getIt.allowReassignment = true;
}
}
tearDown(() async {
final getIt = GetIt.instance;
await getIt.reset();
});

Riverpod's StreamProvider yields StreamValue only once | Flutter & Hive

I wrote a StreamProvider that I listen to right after startup to get all the information about a potentially logged in user. If there is no user, so the outcome would be null, the listener stays in loading state, so I decided to send back a default value of an empty user to let me know that the loading is done.
I had to do this, because Hive's watch() method is only triggered when data changes, which it does not at startup.
So after that, I want the watch() method to do its job, but the problem with that, are the following scenarios:
At startup: No user - Inserting a user -> watch method is triggered -> I get the inserted users data -> Deleting the logged in user -> watch method is not triggered.
At startup: Full user - Deleting the user -> watch method is triggered -> I get an empty user -> Inserting a user -> watch method is not triggered.
After some time I found out that I can make use of all CRUD operations as often as I want to and the Hive's box does what it should do, but the watch() method is not triggered anymore after it got triggered once.
The Streamprovider(s):
final localUsersBoxFutureProvider = FutureProvider<Box>((ref) async {
final usersBox = await Hive.openBox('users');
return usersBox;
});
final localUserStreamProvider = StreamProvider<User>((ref) async* {
final usersBox = await ref.watch(localUsersBoxFutureProvider.future);
yield* Stream.value(usersBox.get(0, defaultValue: User()));
yield* usersBox.watch(key: 0).map((usersBoxEvent) {
return usersBoxEvent.value == null ? User() : usersBoxEvent.value as User;
});
});
The Listener:
return localUserStream.when(
data: (data) {
if (data.name == null) {
print('Emitted data is an empty user');
} else {
print('Emitted data is a full user');
}
return Container(color: Colors.blue, child: Center(child: Row(children: [
RawMaterialButton(
onPressed: () async {
final globalResponse = await globalDatabaseService.signup({
'email' : 'name#email.com',
'password' : 'password',
'name' : 'My Name'
});
Map<String, dynamic> jsonString = jsonDecode(globalResponse.bodyString);
await localDatabaseService.insertUser(User.fromJSON(jsonString));
},
child: Text('Insert'),
),
RawMaterialButton(
onPressed: () async {
await localDatabaseService.removeUser();
},
child: Text('Delete'),
)
])));
},
loading: () {
return Container(color: Colors.yellow);
},
error: (e, s) {
return Container(color: Colors.red);
}
);
The CRUD methods:
Future<void> insertUser(User user) async {
Box usersBox = await Hive.openBox('users');
await usersBox.put(0, user);
await usersBox.close();
}
Future<User> readUser() async {
Box usersBox = await Hive.openBox('users');
User user = usersBox.get(0) as User;
await usersBox.close();
return user;
}
Future<void> removeUser() async {
Box usersBox = await Hive.openBox('users');
await usersBox.delete(0);
await usersBox.close();
}
Any idea how I can tell the StreamProvider that the watch() method should be kept alive, even if one value already got emitted?
but the watch() method is not triggered anymore after it got triggered
once
Thats because after every CRUD you're closing the box, so the stream (which uses that box) stop emitting values. It won't matter if you're calling it from somewhere outside riverpod (await Hive.openBox('users')) its calling the same reference. You should close the box only when you stop using it, I would recommend using autodispose with riverpod to close it when is no longer used and maybe put those CRUD methods in a class controlled by riverpod, so you have full control of the lifecycle of that box
final localUsersBoxFutureProvider = FutureProvider.autoDispose<Box>((ref) async {
final usersBox = await Hive.openBox('users');
ref.onDispose(() async => await usersBox?.close()); //this will close the box automatically when the provider is no longer used
return usersBox;
});
final localUserStreamProvider = StreamProvider.autoDispose<User>((ref) async* {
final usersBox = await ref.watch(localUsersBoxFutureProvider.future);
yield* Stream.value(usersBox.get(0, defaultValue: User()) as User);
yield* usersBox.watch(key: 0).map((usersBoxEvent) {
return usersBoxEvent.value == null ? User() : usersBoxEvent.value as User;
});
});
And in your methods use the same instance box from the localUsersBoxFutureProvider and don't close the box after each one, when you stop listening to the stream or localUsersBoxFutureProvider it will close itself

Flutter widget test - wait Future completion

I have a widget test in flutter that pumps the widget under test,
and when it opens, performs a background operation (staticWrapper.requestPermission()) which returns a Future and, based on its result, set a state.
The problem is that the test is not waiting this future to complete, test code:
/// When micro permission denied, should show error message.
testWidgets('When micro permission denied, should show error message.',
(WidgetTester tester) async {
when(staticWrapper.requestPermission(Permission.RecordAudio))
.thenAnswer((_) => Future.value(PermissionStatus.denied));
await tester.pumpWidget(widget);
final loginText = find.text(callScreen_microPermissionDenied);
expect(loginText, findsOneWidget);
});
Affected widget code:
void _requestMicroPermission() async {
final result =
await staticWrapper.requestPermission(Permission.RecordAudio);
debugPrint("Microphone permission status: $result");
if (result == PermissionStatus.authorized) {
native.init();
} else {
setState(() {
_loginText = tnsManager.getText(TranslationsManager.callScreen_microPermissionDenied);
});
}
}
The expect() method is called before the setState() call.
Any help?
Finally I solved the issue using WidgetTester#pumpAndSettle():
testWidgets('When micro permission denied, should show error message.',
(WidgetTester tester) async {
when(staticWrapper.requestPermission(Permission.RecordAudio))
.thenAnswer((_) => Future.value(PermissionStatus.denied));
await tester.pumpWidget(widget);
await tester.pump();
final loginText = find.text(callScreen_microPermissionDenied);
expect(loginText, findsOneWidget);
});