Flutter Test: Look for a widget inside of another widget - flutter

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

Related

How do you combine two Finders in a Flutter widget test?

Using package:flutter_test, I can create a finder that finds widgets with a key:
expect(find.byKey(const ValueKey('counter')), findsOneWidget);
or by text:
expect(find.text('0'), findsOneWidget);
I can also find widgets descending from this widget:
expect(
find.descendant(
of: find.byKey(const ValueKey('counter')),
matching: find.text('0'),
),
findsNothing,
);
Or an ancestor:
expect(
find.ancestor(
of: find.text('0'),
matching: find.byKey(const ValueKey('counter')),
),
findsNothing,
);
But how do I combine these finders to verify that there is a widget with a 'counter' Key and with '0' as its text? For example:
Text(
'$_counter',
key: const Key('counter'),
style: Theme.of(context).textTheme.headline4,
),
I found this question while trying to combine finders myself. I ended up finding out how to combine finders, but while looking, I also found what I think is a better answer to your specific case. Sharing them both here.
Question 1: How do you verify properties on widgets using finders?
Since you're using keys, I'd suggest separating out the ideas of "finding the widget" and "validating the properties on the widget".
So, instead of trying to find and validate in one go, you can use the WidgetController.widget<T> method to get the widget, then validate properties on that widget separately using expect. In your case, it'd look something like this:
expect(
tester.widget<Text>(find.byKey(const ValueKey('counter'))).data,
equals('0'),
);
Question 2: How do you combine finders?
This is more generally what you're asking, so I figured I'd share my findings on that as well.
Here's what I came up with using extension methods and the ChainedFinder abstract class.
It creates the find.chained method that takes a list of Finders and links them together using the ChainedFinderLink class. It ensures that the finders in the list are applied in the order given, filtering the list of candidates further on each application.
import 'dart:collection';
import 'package:darq/darq.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_test/flutter_test.dart';
extension CommonFinderX on CommonFinders {
Finder chained(List<Finder> finders) {
assert(finders.isNotEmpty);
final findersQueue = Queue<Finder>.from(finders);
var current = findersQueue.removeFirst();
while (findersQueue.isNotEmpty) {
current = ChainedFinderLink(
parent: current,
finder: findersQueue.removeFirst(),
);
}
return current;
}
}
class ChainedFinderLink extends ChainedFinder {
ChainedFinderLink({
required Finder parent,
required this.finder,
}) : super(parent);
final Finder finder;
#override
String get description => '${parent.description} THEN ${finder.description}';
#override
Iterable<Element> filter(Iterable<Element> parentCandidates) {
/// We have to apply against the interection of `parentCandidates` and
/// `finder.allCandidates` because some finders (such as ancestor) filter
/// out invalid candidates through the `allCandidates` getter instead of
/// as part of the `apply` method itself.
return finder.apply(parentCandidates.intersect(finder.allCandidates));
}
}
It makes use of the darq library for the intersect method, but you could write your own if you didn't want to include another package.
For your case, it'd be used like this:
expect(
find.chained([
find.text('0'),
find.byKey(const ValueKey('counter')),
]),
findsOneWidget,
);
The simplest way is to use find.byWidgetPredicate
https://api.flutter.dev/flutter/flutter_test/CommonFinders/byWidgetPredicate.html
Here's an example of how to use it:
expect(find.byWidgetPredicate(
(Widget widget) => widget is Tooltip && widget.message == 'Back',
description: 'widget with tooltip "Back"',
), findsOneWidget);
It takes a function, bool Function(Widget widget), so in your case you could do something like this:
expect(find.byWidgetPredicate(
(Widget widget) => widget is Text && widget.data == '$_counter' && widget.key == const Key('counter'),
), findsOneWidget);

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

Text blanked out in Flutter flutter_test widget test golden screenshot

When I try widget testing my flutter app, I am unable to locate the desired widgets by their ID. As a result, I took a golden screenshot to see what was happening and it appears every instance of Text is being blanked out (in the same color as the text color should be).
See side-by-side of expected app appearance and then golden screenshot.
I've simplified my problem down to a finer grained example, see the code here:
testGoldens('Example', (WidgetTester tester) async {
tester.binding.window.physicalSizeTestValue = Size(1284, 2778);
addTearDown(tester.binding.window.clearPhysicalSizeTestValue);
Widget exampleWidget = Scaffold(
appBar: AppBar(),
body: Text('example', style: TextStyle(color: Colors.red)));
await tester.pumpWidget(MaterialApp(home: exampleWidget));
await expectLater(
find.byType(MaterialApp), matchesGoldenFile('signIn.png'));
});
And this produces this screenshot. (Notice even the debug sash is blanked out).
Anyone seen this before?
Thanks 🙏

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

How to write widget tree test to verify tree hierarchy

I am learning Flutter and I want to write one test for my simple MyAppBarWidget. Below is my widget
class MyAppBarWidget extends StatelessWidget{
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("My first widget"),
),
));
}
}
I want to test widget tree hierarchy as
MaterialApp has Scaffold property
Scaffold has AppBar property
AppBar has title property as Text
title is My first widget
Any suggestion what kind of test I should write
I tried below test
void main() {
testWidgets("verify app bar", (WidgetTester tester) async {
await tester.pumpWidget(MyAppBarWidget());
var byWidget = find.byType(MaterialApp);
var text = find.text("My first widget");
expect(byWidget, findsOneWidget);
expect(text, findsOneWidget);
});
}
But this test does not say that my text field is inside AppBar widget
Can someone help me how should I write test to verify this ?
Thanks
I suggest not to test widget hierarchy, you will change it often and always have to adjust the test without actually knowing anything when the test fails or succeeds. It is better to test functionality, the presence of something or the absence, tap events and interaction.
You can also look into golden (screenshot) tests to ensure that screens or pages don't change.
That being said, if you really want to do this you can use
find.ancestor(find.byType(AppBar), find.text("My first widget"));
EDIT
Or with newer versions of the test library, thanks Fred Grott:
find.ancestor(of: find.byType(AppBar), matching: find.text("My first widget"));