I have a widget that displays a list of data from an api and I'm trying to write tests for it's various states starting with it's empty state.
Currently my test pumps the widget in question, the widget makes a network call which I'm mocking and then it checks to see if the empty state text is shown.
This test passes when I run the test on a device using
flutter run --flavor myTestApp -t test/booking/booking_list_widget_test.dart
But fails when I run it from the IDE (IntelliJ) the failure exception is:
The following TestFailure object was thrown running a test:
Expected: exactly one matching node in the widget tree
Actual: _TextFinder:<zero widgets with text "You're all caught up." (ignoring offstage widgets)>
Which: means none were found but one was expected
It seems to not be waiting for the Duration given to tester.pump and I've tried wrapping it in a tester.runAsync and using pump and settle etc but i cannot get it to pass.
Any ideas welcome I cant share the widget tree but I can share the test
void main() {
setupFirebaseAuthMocks();
setUpAll(
() async {
SharedPreferences.setMockInitialValues(
{
Constants.USER_ID: '1234567890',
},
);
},
);
testWidgets(
'emptyListTest',
(tester) async {
await _initializeDependencies(tester);
final dio = di.getIt.get<Dio>();
final dioAdapter = DioAdapter(dio: dio);
dioAdapter.onGet(
'/url/user/1234567890',
(server) => server.reply(
200,
BookingResponse(
(b) => b
..data = <Booking>[].toBuiltList().toBuilder()
..pagination = Pagination((b) => b
..last = ''
..first = ''
..next = ''
..skip = 0
..count = 0).toBuilder(),
),
),
);
final testApp = await tester.runAsync(
() => wordskiiTestApp(
widgetUnderTest: BookingView(),
),
);
await tester.pumpWidget(testApp!);
await tester.pump(const Duration(seconds: 1));
expect(find.text('AVAILABLE'), findsOneWidget);
// debugDumpApp();
expect(
find.text(
'You\'re all caught up.',
),
findsOneWidget,
);
},
);
}
Future<void> _initializeDependencies(WidgetTester tester) async {
await tester.runAsync(di.getIt.reset);
await tester.runAsync(di.initTesting);
await tester.runAsync(di.getIt.allReady);
}
On which Widget is 'You\'re all caught up' expected? I had similar issues encountered previously when I tried find.textContaining() on a ListView item. I was able to solve my issue by finding out which widget the Text should appear.
On mine, I had to use find.widgetWithText(InkWell, String). To find out the which Widget it is, you can tap on the widget displayed on screen when you've run the test (i.e. on an emulator). The logs should display the tapped Widget.
Related
I'm using widget testing in Flutter. I'm using the game template from here
https://github.com/flutter/samples/tree/main/game_template
and my test doesn't work as expected:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:escape_team/main.dart';
void main() {
testWidgets('Main menu can be started', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp(
inAppPurchaseController: null,
playerProgressPersistence: null,
settingsPersistence: null,
));
expect(find.text('PLAY'), findsOneWidget);
await tester.tap(find.text('PLAY'));
await tester.pumpAndSettle();
expect(find.text('MISSION SELECT'), findsOneWidget);
});
}
results in:
00:00 +0: Main menu can be started
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: exactly one matching node in the widget tree
Actual: _TextFinder:<zero widgets with text "MISSION SELECT" (ignoring offstage widgets)>
Which: means none were found but one was expected
However, when I manually tap the 'PLAY' button in my app, the "MISSION SELECT" screen appears. How can I find out what's going on - and wrong?
UPDATE:
I did some more research, I guess it's caused by a JSON that's being loaded by my app before the main menu is started.
missionLoader
.readJson('assets/json/missions_en.json')
.then((value) => GoRouter.of(context).go('/play'));
This calls
final String response = await rootBundle.loadString(jsonFile);
... and rootBundle file loading isn't supported out of the box in Flutter tests, as I see it?
I tried adding
WidgetsFlutterBinding.ensureInitialized();
to my app's main() method, as well as to the test's main method, but both did not help. Any further leads would be greatly appreciated! Thank you!
I am trying to learn how to use Hive for local storage and Riverpod for state managment in my flutter app.
final _localStorageProvider = FutureProvider<Box>(
(ref) async {
final appDocumentDir = await path_provider.getApplicationDocumentsDirectory();
Hive
..init(appDocumentDir.path)
..registerAdapter(QuizOptionModelAdapter());
return Hive.openBox(keys.localStorageBoxKey);
}
);
final _localDataSourceProvider = Provider<QuestionLocalDataSourceImpl>(
(ref) => QuestionLocalDataSourceImpl(
uploadQuestionBox: ref.watch(_localStorageProvider).maybeWhen(
data:(d) => d,
error:(e, trace) {
print("***************************");
print(e);
print(trace);
print("***************************");
throw Exception("FAILED");
},
orElse: () => throw Exception("OR ELSE")
),
),
);
My problem is, I am not sure what other return value I can use during error & orElse phase other than exceptions. Since I am using exception, very briefly before the actual page loads, I get an ugly error page as shown in the screenshot, which disappers in 1 second after the actual elements are rendered.
Any help to avoid throwing errors during error & orElse phase would be really helpful.
So far have tried these solutions without any luck
final _localDataSourceProvider = Provider<QuestionLocalDataSourceImpl>(
(ref) => QuestionLocalDataSourceImpl(
uploadQuestionBox: ref.watch(_localStorageProvider).maybeWhen(
data:(d) => d,
orElse: () => AsyncValue.loading() as Box
),
),
);
I think the issue is that you try to deal with error and orElse within the Provider. A more common way to do this is to watch the FutureProvider within a Widget's build method, and to render a proper data Widget if the data from the FutureProvider is available, and another Widget if the data is not yet available (loading) or if there is an error. That way, all three cases of the Provider's when method return the same type: a Widget.
From the Riverpod docs, see this example:
final configurationsProvider = FutureProvider<Configuration>((ref) async {
final uri = Uri.parse('configs.json');
final rawJson = await File.fromUri(uri).readAsString();
return Configuration.fromJson(json.decode(rawJson));
});
class Example extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
final configs = ref.watch(configurationsProvider);
// Use Riverpod's built-in support
// for error/loading states using "when":
return configs.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error $err'),
data: (configs) => Text('data: ${configs.host}'),
);
}
}
In your case, if you want to retain the _localDataSourceProvider then my suggestion would be to make that also a FutureProvider that 'wraps' the _localStorageProvider, and to watch the _localDataSourceProvider within the build method of the Widget where you render the data. There, you can use the .when(...) clauses for loading (for example to show a CircularProgressIndicator until the data is loaded), data to render the data, and if necessary error to capture any errors that Hive may generate through the FutureProvider.
I'm trying to run a flutter test where a widget displays an error page when the Future provided to it throws an error (via FutureBuilder).
However the line where I create the future seems to be making the test fail.
final futureError = Future.delayed(Duration(milliseconds: 20))
.then((value) => throw Error());
with the message
Failed to load "D:\Projects\flutter\....dart": Instance of 'Error'
Putting it inside the test function resolved the issue
testWidgets('...',
(WidgetTester tester) async {
await tester.runAsync(() async {
final futureError = Future.error('error');
// ...
(it was in the group method prior to this)
I am widget-testing the app in flutter, there is 3 conditions under the block listener on the gui side of the app.
two of the three conditions determine the building of the two different SnackBars, those two SnackBars has different Keys (error-snackbar and success-snackbar), when test is run it says:
2 of the widget with the same key<'error-snackbar'> was found.
because of this I need to expect two widget with findsNWidget(2)
this is the code of the test:
testWidgets("Expects email to be invalid and shows error SnackBar",
(WidgetTester tester) async {
await tester.runAsync(() async {
//build the tree of widgets
final widget = _buildTestableStateWidget();
await tester.pumpWidget(widget);
await tester.pumpAndSettle();
accountBloc.emit(AccountState(
event: event,
error: ErrorModel(key: 'ERROR_GENERAL'),
loading: true,
));
await tester.pumpAndSettle();
expect(find.byKey(Key("error-snackbar")), findsNWidgets(2));
});
});
Any Idea why this is happening ?
just comment if you need more code and I will provide it.
"Keys must be unique amongst the Elements with the same parent."
https://api.flutter.dev/flutter/foundation/Key-class.html
Since you are trying to find 2 widgets with the same key, do they share a common parent? Then it's not allowed. You should try to build the finder in another way, Keys are made to find a specific widget.
I'm trying out Flutter, but I'm having trouble getting the UI to update consistently. I'd like to show a status message while a long-running async method is called, but the setState() call I make just before calling the long-running method doesn't seem to cause my build() method to get invoked.
I've created a simple example that calculates the Fibonacci number for a randomly selected number between 25 and 30. In my sample code/app, hitting the "calc" button calls _calc(). _calc() picks a random number, sets a status message "Calculating Fib of $num..." tied to a text widget (_status) and updates it with setState(); then calls the async _fib() routine to calculate the number; then updates _status with the result using setState(). Additionally, the build() method prints the value of _status to the console, which can be used to see when build() is invoked.
In practice, when the button is pressed, the first status message does not appear either in the debug console, or on the UI. Doing a bit of experimentation, I added a pseudo sleep function that I call just prior to calling _fib(). This sometimes causes the first setState() call to work properly - invoking build(). The longer I make the sleep, the more often it works. (I'm using values from a few milliseconds up to a full second).
So my question are: What am I doing wrong? and What's the right way to do this? Using the pseudo sleep is obviously not the correct solution.
Other, probably not too relevant info: My dev environment is Android Studio 3.1.2 on a Win10 machine. Using Android SDK 27.0.3, with Flutter beta 0.3.2. My target device is the emulator for a pixel2 running Android 8.1. Also, sorry if my lack of 'new' keywords is off-putting, but from what I read in Dart 2 release notes, it's not usually necessary now.
import 'package:flutter/material.dart';
import "dart:async";
import "dart:math";
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Debug Toy',
home: MyWidget(),
);
}
}
class MyWidget extends StatefulWidget {
#override
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
String _status = "Initialized";
final rand = Random();
Future sleep1() async {
return new Future.delayed(const Duration(milliseconds: 100),() => "1");
}
Future<Null> _resetState() async {
setState(() { _status = "State Reset"; });
}
Future<Null> _calc() async {
// calculate something that takes a while
int num = 25 + rand.nextInt(5);
setState(() { _status = "Calculating Fib of $num..."; });
//await sleep1(); // without this, the status above does not appear
int fn = await _fib(num);
// update the display
setState(() { _status = "Fib($num) = $fn"; });
}
Future<int> _fib(int n) async {
if (n<=0) return 0;
if ((n==1) || (n==2)) return 1;
return await _fib(n-1) + await _fib(n-2);
}
#override
Widget build(BuildContext context) {
print("Build called with status: $_status");
return Scaffold(
appBar: AppBar(title: Text('Flutter Debug Toy')),
body: Column(
children: <Widget>[
Container(
child: Row(children: <Widget>[
RaisedButton( child: Text("Reset"), onPressed: _resetState, ),
RaisedButton( child: Text("Calc"), onPressed: _calc, )
]),
),
Text(_status),
],
),
);
}
}
Let's start by going to one extreme and rewriting fib as fibSync
int fibSync(int n) {
if (n <= 0) return 0;
if (n == 1 || n == 2) return 1;
return fibSync(n - 1) + fibSync(n - 2);
}
and calling that
Future<Null> _calc() async {
// calculate something that takes a while
int num = 25 + rand.nextInt(5);
setState(() {
_status = "Calculating Fib of $num...";
});
//await Future.delayed(Duration(milliseconds: 100));
int fn = fibSync(num);
// update the display
setState(() {
_status = "Fib($num) = $fn";
});
}
The first setState just marks the Widget as needing to be rebuilt and (without the 'sleep') continues straight into the calculation, never giving the framework the chance to rebuild the Widget, so the 'Calculating' message isn't displayed. The second setState is called after the calculation and once again (redundantly) marks the Widget as needing to be rebuilt.
So, the execution order is:
Set status to Calculating, mark Widget as dirty
Perform the synchronous calculation
Set status to Result, mark Widget as dirty (redundantly)
Framework finally gets chance to rebuild; build method is called
When we uncomment the 'sleep', the execution order changes to
Set status to Calculating, mark Widget as dirty
'Sleep', allowing the framework to call build
Perform the synchronous calculation
Set status to Result, mark Widget as dirty (again)
Framework calls build
(As an aside, note how the synchronous fib calculation is an order of magnitude faster because it doesn't have to do all the microtask scheduling.)
Let's re-consider the async calculation. What's the motivation of making it async? So that the UI remains responsive during the calculation? As you've seen, that doesn't achieve the desired effect. You still only have one thread of execution, and you aren't allowing any gaps in execution for callbacks and rendering to occur. Sleeping for 100ms is not compute bound, so drawing etc can occur.
We use async functions to wait for external events, like replies from web servers, where we don't have anything to do until the reply arrives, and we can use that time to keep rendering the display, reacting to gestures, etc.
For compute bound stuff, you need a second thread of execution which is achieved with an Isolate. An isolate has its own heap, so you have to pass it its data, it works away in its own space, then passes back some results. You can also stop it, if it's taking too long, or the user cancels, etc.
(There are much less computationally expensive ways to calculate fibs, but I guess we're using the recursive version as a good example of an O(n^2) function, right?)