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

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);

Related

Flutter Test: Look for a widget inside of another widget

I am looking for the way to see that the widget I have found in the test, has certain text inside. In my case I want to look for the specific text not inside the complete RadioButtonGroup but inside of the found ElevatedButton --> firstButton. Is it possible?
testWidgets('Horizontal Radio Group builds a Row', (tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: simpleRadio
));
expect(find.byType(Row), findsOneWidget);
expect(find.byType(Column), findsNothing);
var optionButtons = find.byType(ElevatedButton);
expect(optionButtons, findsNWidgets(2));
ElevatedButton firstButton = tester.firstWidget(optionButtons);
});
Looking for something like: expect(firstButton.findByText('bla'), findsOneWidget);
I think what you're looking for is widgetWithText. This'll find a widget of widgetType that has a Text descendant with the given text.
So for your example, something like:
expect(find.widgetWithText(ElevatedButton, 'bla'), findsOneWidget);

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

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.

Finds Element without key

i'm still new in using flutter driver in testing, but as far as i know there are few identifiers that we can use to locate / identify elements, like By Text, By Type, etc
But the problem is, the app that i want to test doesn't have the identifier that i can use to locate them (please correct me if i'm wrong).. the widget code of the app looks like this
Widget _buildNextButton() {
return Align(
alignment: Alignment.bottomRight,
child: Container(
child: IconButton(
icon: Icon(Icons.arrow_forward),
onPressed: () => _controller.nextPage(),
),
),
);
}
where that widget is on a class that extends StatefulWidget.
How can i locate that icon in my test script and click it? can i use something like this? And what type of finder should i use? (byValueKey? bySemanticLabel? byType? or what?)
static final arrowKey = find.byValueKey(LoginKey.nextButton);
TestDriverUtil.tap(driver, arrowKey);
We have text and value checks here in Flutter Driver but if you don't have that you can always go the the hierarchy of app.
what I mean by hierarchy is so button has fix or specific parent right?
Let's take your example here, We have Align > Container > IconButton > Icon widget hierarchy which will not be true for others like there might be IconButton but not with the Container parent.
or StreamBuilder or anything that we can think of.
Widget _buildNextButton() {
return Align(
alignment: Alignment.bottomRight,
child: Container(
child: IconButton(
icon: Icon(Icons.arrow_forward),
onPressed: () => print("clicked button"),
),
),
);
}
This hierarchy should be atleast ideal for top bottom or bottom top approach.
Now what I mean by Top to bottom approach is Align must have IconButton and for bottom to up approach we are saying IconButton must have Align widget as parent.
Here i have taken top down approach so what I'm checking from below code is finding IconButton who is decendent of Align Widget.
also i added firstMatchOnly true as I was checking what happens if same hierarchy appears for both so
test('IconButton find and tap test', () async {
var findIconButton = find.descendant(of: find.byType("Align"), matching: find.byType("IconButton"), firstMatchOnly: true);
await driver.waitFor(findIconButton);
await driver.tap(findIconButton);
await Future.delayed(Duration(seconds: 3));
});
to check for multiple IconButtons with same Align as parent, we need to have some difference like parent should be having Text view or other widget.
find.descendant(of: find.ancestor(
of: find.byValue("somevalue"),
matching: find.byType("CustomWidgetClass")), matching: find.byType("IconButton"), firstMatchOnly: true)
usually I go something like above where I have split the code in seperate file and check for that widget.
But ultimately find something unique about that widget and you can work on that.
**In Lib directory dart class for connecting that widget**
class Testing extends StatelessWidget {
Testing();
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: YourClass(), // Next button containing class that need to test
);
}
}
**In Test directory**
testWidgets('Next widget field test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(Testing());
// find Widget
var buttonFind = find.byIcon(Icons.arrow_forward);
expect(buttonFind, findsOneWidget);
IconButton iconButton = tester.firstWidget(buttonFind);
expect(iconButton.color, Colors.blue);
});

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 !

Flutter loading Image asset too slow

So, I have a screen which shows a text and an image above the text and loads, in background, some data from the shared preferences.
But the image is taking forever to load, and when I remove the call to get the shared preferences data the image loads very quiclky. Do someone knows if I'm doing something wrong with this call?
here's the code:
class WelcomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
precacheImage(AssetImage(Images.tomato), context);
_loadData(context);
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
Strings.appName,
style: TextStyle(
color: AppColors.black,
fontSize: 50.0,
fontWeight: FontWeight.w600,
),
),
Image.asset(Images.tomato),
],
),
),
);
}
void _loadData(BuildContext context) async {
final PreferencesRepository repository = PreferenceRepositoryImp();
repository.loadAll().then(
(_) {
sleep(Duration(seconds: 2));
Navigator.pushNamedAndRemoveUntil(context, Routes.HOME, (_) => false);
},
);
}
}
and that's the image
You're using sleep(Duration(seconds: 2)); which halts everything. You should not be using sleep, especially inside a build method. Dart is single threaded so that means when it halts a thread, it prevents the UI from being built.
Also, why is _loadData marked as async if you're not using the await keyword? You only need it if you're going to use the await keyword, otherwise, remove it. If you want to make use of it, you should use await Future.delayed(Duration(seconds: 2)) which makes it wait. Here:
void _loadData(BuildContext context) async {
final PreferencesRepository repository = PreferenceRepositoryImp();
await repository.loadAll();
await Future.delayed(Duration(seconds: 2));
Navigator.pushNamedAndRemoveUntil(context, Routes.HOME, (_) => false);
}
How does await differ from sleep?
await makes Dart say "Hey, I'm going to keep my eye out for this in the future" (haha get it? Future? hahahhahaha I'm funny). Anyways, it won't continue the _loadData function because it wants that value but it will keep running (in your example) the build method and then when the Future comes back with a value, it will be like "Hey! There's that value I was looking for! Now I can keep running that function!"
sleep on the other hand makes Dart say "I'm going to take a nap for the duration of time I get" (in this case two seconds). It won't do anything until it "wakes up" since Dart is single-threaded. After it takes its two-second nap, it will continue the function and after it finishes, the build method will carry on, therefore, loading your image which explains why it takes so long.