How to expect that an exception is thrown in flutter widgets test? - flutter

I have this test:
testWidgets('Controller must only be used with one widget at a time',
(WidgetTester tester) async {
final CustomAnimatedWidgetController controller = CustomAnimatedWidgetController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
children: [
CustomAnimatedWidget(
child: Container(),
controller: controller,
),
CustomAnimatedWidget(// this declaration will throw an exception from initState of this widget
child: Container(),
controller: controller,
),
],
),
),
),
);
expect(tester.takeException(), isInstanceOf<Exception>());
});
Which is guaranteed to throw an exception of type Exception (due to using the controller two times), but the expect is not catching the exception. Instead, the test fails and I see my exception thrown in the console.
But here and here they say this is how it must be used. What is wrong with the test?

This post answer shows the action to be done by the tester (a click is simulated) that will cause an exception. We can use expect(tester.takeException(), isInstanceOf<AnExceptionClass>()); to ensure that an exception has been thrown as expected.
In your case the Widget cannot be rendered correctly. Widget test should be done with a correctly implemented Widget first.

Looking more into the code of pumpWidget, we see that it returns TestAsyncUtils.guard<void>(...) so the test is running in its own "zone" and that's why the exception was swallowed by the framework (even adding try catch around pumpWidget above didn't catch the error)
The only way I found is override FlutteError.onError function:
FlutterError.onError = (details){
// handle error here
};
now the test can pass and the error thrown during test are forwarded to this method.

Related

Flutter Widget Test: AutoRouter operation requested with a context that does not include an AutoRouter

I'm using the auto_route package in my flutter project and wrapped my App in MaterialApp.router.
Now I want to test a widget which calls AutoRouter.of(context).pop(); in one place.
My test looks like this:
testWidgets('my widget test',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: MyWidget(),
),
);
});
Pumping the widget understandably throws the following error message:
AutoRouter operation requested with a context that does not include an AutoRouter. The context used to retrieve the Router must be that of a widget that is a descendant of an AutoRouter widget.
So I somehow need to provide an AutoRouter instance above my widgetin my test.
My widget also uses MediaQuery and I needed to wrap my tested widget inside MaterialApp to avoid an error which was thrown due to MediaQuery.of(context) lookup.
I guess I need to do something similar with AutoRouter but couldn't figure it out yet. Is there something like a MockAutoRouter?
Happy for any help.
Okay I got it working.
AutoRouter.of(context) looks up an instance of StackRouter which is provided through StackRouterScope. So I mocked StackRouter and provided it via StackRouterScope:
#GenerateMocks([StackRouter])
...
await tester.pumpWidget(
StackRouterScope(
controller: MockStackRouter(),
stateHash: 0,
child: MyWidget(),
),
);
Now AutoRouter.of(context).pop() does not throw anymore.
It should also be possible now to stub the MockStackRouter and verify if e.g. pop is called.

Flutter widget test tap - would not hit test on the specified widget

My widget test is failing after the following warning is outputted:
flutter: Warning: A call to tap() with finder "exactly one widget with text "Tab 2" (ignoring offstage widgets): Text("Tab 2", softWrap: no wrapping except at line break characters, overflow: fade, dependencies: [MediaQuery, DefaultTextStyle])" derived an Offset (Offset(600.0, 23.0)) that would not hit test on the specified widget.
flutter: Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.
The tap is never executed so the next part of the test fails. I put some delays in the test and it appears that the test is attempting to tap the correct widget - it is not offscreen, not obscured, and was able to receive pointer events in the past - not sure why it's currently failing.
Here is a minimal reproducible example:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 2,
child: Scaffold(
appBar: TabBar(
labelColor: Color(0xff8391e4),
tabs: [
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
],
),
body: TabBarView(
children: <Widget>[
Text('Tab 1 Text'),
Text('Tab 2 Text'),
],
),
),
),
);
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
testWidgets('My Test', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
// Warning thrown on this tap - tap never executed
await tester.tap(find.text('Tab 2'));
await tester.pumpAndSettle();
// Test fails here
expect(find.text('Tab 2 Text'), findsOneWidget);
});
}
Try to set ensureVisible() before tap():
// Warning thrown on this tap - tap never executed
await tester.ensureVisible(find.text('Tab 2'));
await tester.tap(find.text('Tab 2'));
await tester.pumpAndSettle();
Incase, anyone comes across this question in the future.
I had this same problem it was because I had animation still running in the background. The fix is to call await tester.pumpAndSettle(); which flushes out all pending animations.
I believe a side effect of ensureVisible() is something similar which is why it works.
I found a solution to my problem, but it may not be a universal solution. The app that this test is for is exclusively a web app so it's designed to run on larger screen sizes. When I'd run the test on chrome it would pass, but would fail when run heedlessly.
To fix, I run the test at a larger screen size and it now passes heedlessly.
So if you run into the A call to tap() ... that would not hit test on the specified widget error, adjusting the screen size might fix the issue.
testWidgets('My test', (WidgetTester tester) async {
// Here we use physicalSizeTestValue to adjust the test screen size to
// simulate running on a desktop computer which the app was designed for
tester.binding.window.physicalSizeTestValue = Size(1080, 1920);
tester.binding.window.devicePixelRatioTestValue = 1.0;
In my case, when I have modal screen, this works:
await tester.tap(find.byKey(Key('some_key')), warnIfMissed: false);

Flutter:- This AdWidget is already in the Widget tree If you placed this AdWidget in a list, make sure you create a new instance in the builder

void initState() {
// TODO: implement initState
bannerAd = BannerAd(
size: AdSize.banner,
adUnitId: AdMobService.bannerAdUnitId,
listener: BannerAdListener(
onAdLoaded: (_) {
setState(() {
_bstatus = true;
});
},
onAdFailedToLoad: (ad, err) {
print("failed to load banner$err");
ad.dispose();
},
),
request: new AdRequest());
bannerAd.load();
}
// code where it causes error
bottomNavigationBar: Container(
height: 50,
child: Stack(
children:[
Text("Ad space"),
if (_bstatus)
AdWidget(
ad: bannerAd,
key: UniqueKey(),
),
]
))
This is my code _bstatus is to check if banner is loaded or not.
On starting an app it works fine but when ads get reload it give the below error
The following assertion was thrown building AdWidget-[#06603](dirty, state: _AdWidgetState#c9e02):
This AdWidget is already in the Widget tree
If you placed this AdWidget in a list, make sure you create a new instance in the builder function
with a unique ad object.
Make sure you are not using the same ad object in more than one AdWidget.
But if I do hot reload it works fine
How could I resolve it?
This seem to be a duplicate of the following question:
Flutter :- This AdWidget is already in the Widget tree. How to disable this exception. And what does it mean?
The error is quite explicit, did you make sure to provide an unique ID ?
The problem is that you are putting the same widget again and again. You can fix this by creating a new stateful class and returning the Adwidget. This will build that same widget multiple timeā€”it works like a Builder. This solved my problem; I hope it will work for you too.

Flutter CachedNetworkImage errors: (CacheManager: Failed to download file) and (SocketException or HttpException)

I'm using the cached_network_image library. If I set it up like this:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('MyApp building');
return MaterialApp(
home: Scaffold(
body: HomeWidget(),
),
);
}
}
class HomeWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Container(
child: CachedNetworkImage(
imageUrl: "https://example.com/image/whatever.png",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
),
);
}
}
with a URL that is invalid (either because it returns a 404 or the server refuses the connection), then my IDE freezes and gives one of the following errors:
HttpExceptionWithStatus (HttpException: Invalid statusCode: 404, uri = https://example.com/image/whatever.png)
or this (if I change it to a non-existent host):
SocketException (SocketException: Failed host lookup: 'ksdhfkajsdhfkashdfkadshfk.com' (OS Error: No address associated with hostname, errno = 7))
What I would expect is for the CachedNetworkImage widget to just show the errorWidget, but it doesn't.
The creator of cached_network_image says (source):
If you have break on exceptions enabled the debugger stops, but that's because the dart VM doesn't always know whether an exception is caught or not. If you continue this still shouldn't freeze your app.
That was the key I needed. It is just the debugger stopping. You can hit the continue button or disable stopping on uncaught exceptions. The CachedNetworkImage widget will then show the errorWidget as expected.
In VS Code you can uncheck this in the bottom left of the debug pannel.
I believe there is a similar setting in Android studio based on this post.
I also experienced this issue, and could not find a flutter way to solve the 404 issue which crashes the app.
As a solution, I am using imagekit as a CDN and we can tell imagekit to display a default image if the original image is missing: https://docs.imagekit.io/features/default-images

Dispose provider in Widget Test

I'm performing some widget tests on my Flutter app with flutter_test.
It basically works fine except when my testing widget is a Consumer.
Here, I basically have a DeviceProvider objects that wraps an object into a ChangeNotifier so that updates are correctly propagated to consumers.
To be able to use it with a WidgetTester, I put my Consumer widget inside a ChangeNotifierProvider that intantiates a DeviceProvider.
_pumpTestableWidgetWithProvider(WidgetTester tester, Widget widget) async {
DeviceProvider device = DeviceProvider(Device());
await tester.pumpWidget(ChangeNotifierProvider(
create: (context) => device,
child: MaterialApp(home: widget)));
return device;
}
Then, I use the WidgetTester I have just pumped and check whether some text fields are in them.
testWidgets("Infos - default device infos", (WidgetTester tester) async {
DeviceProvider device = await _pumpTestableWidgetWithProvider(
tester, DeviceInfos());
expect(find.text("Battery state: "), findsOneWidget);
});
It returns with an error telling me that 'A Timer is still pending even after the widget tree was disposed'. Looking at the log, this timer corresponds to a _FakeTimer created with the DeviceProvider. I thus tried to manually dispose the provider by using
device.dispose();
But now, I have an error telling me that 'A DeviceProvider was used after being disposed.'
Does anyone have a solution for me ?
I was able to solve it using a different constructor for the changeNotifierProvider on the tests:
Widget buildWidget() {
return ChangeNotifierProvider.value(
value: locator<TaNaLeiProvider>(),
child: MaterialApp(
home: Scaffold(
body: SizedBox(
width: 640,
child: DrawerTaNaLei(),
),
),
),
);
}
Note that I'm using ChangeNotifierProvider.value instead of create.
I'm also using getIt to create singletons on my app.
locator.registerLazySingleton<TaNaLeiProvider>(() => TaNaLeiProvider());
Hope that helps!
Finally, I found the problem, I had an actual timer running inside my DeviceProvider and this timer was not canceled.
Thanks for your support !