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

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

Related

Is there a workaround to allow passing through tap/gesture events to widgets below a ModalRoute?

I am trying to implement a nested router in my Flutter app using auto_router, however I have a map widget (flutter_map) that appears below the nested router which I would like to still be able to receive gestures. These gestures are by default blocked by ModalRoute, and by extension PageRoute.
Stack(
children: const [
MapWidget(), // need gestures to also get passed through to this
AutoRouter(), // renders child routes (PageRoute) which contain elements that should be clickable
],
),
Based on this GitHub issue, it appears that there's no way to disable the gesture-blocking behavior of ModalRoute. Are there any good workarounds to allow the scenario above to function as intended, with the child routes being able to receive gestures without blocking them from propagating to the map below?
At #pskink's advice, I ended up creating a custom route by extending TransitionRoute directly. While there is a pretty significant amount of logic in ModalRoute, most of this logic is not necessary for my particular use case.
This was the solution I came up with, a class that extends TransitionRoute and implements a transitionBuilder and pageBuilder which behave similarly to those in ModalRoute.
class TranslucentRoute<T> extends TransitionRoute<T> {
final bool _opaque;
final Duration _transitionDuration;
final RouteTransitionsBuilder _transitionBuilder;
final RoutePageBuilder _pageBuilder;
TranslucentRoute({
opaque = true,
transitionDuration = const Duration(milliseconds: 300),
required RouteTransitionsBuilder transitionBuilder,
required RoutePageBuilder pageBuilder,
RouteSettings? settings,
}):
_opaque = opaque,
_transitionDuration = transitionDuration,
_transitionBuilder = transitionBuilder,
_pageBuilder = pageBuilder,
super(settings: settings);
#override
Iterable<OverlayEntry> createOverlayEntries() {
return <OverlayEntry>[
OverlayEntry(
builder: (context) => _transitionBuilder(
context,
animation!,
secondaryAnimation!,
_pageBuilder(
context,
animation!,
secondaryAnimation!,
),
),
),
];
}
#override
bool get opaque => _opaque;
#override
Duration get transitionDuration => _transitionDuration;
}

How to create a map of widget references?

I am trying to create a Map<String, Widget> so that I can use it to dynamically determine which widget to use on the fly based on the key.
Scenario:
class WidgetA extends StatelessWidget {...}
class WidgetB extends StatelessWidget {...}
class WidgetC extends StatelessWidget {...}
class WidgetD extends StatelessWidget {...}
I want to create a map like:
const Map<String, Widget> map_to_widget = {
'widgetA': WidgetA,
'widgetB': WidgetB,
...
}
This will allow me to dynamically create widgets based on a list of widgets I need rendered. All of the above widgets would take in the same parameter so I would intend to use it like (still need to figure out how I would pass parameters):
return Container(
child: map_to_widget('widgetA')
)
When I try to create the map, I keep getting the The element type 'Type' can't be assigned to the map value type 'Widget'.dartmap_value_type_not_assignable error.
So the specific error comes from this:
'widgetA': WidgetA,
Above, WidgetA is not a widget, it is a type, similar to how 1 is an int but int is a type.
To fix this you would want to do something like this:
'widgetA': WidgetA(),
in order to actually create a widget.
The problem comes, as you say, when you want to pass parameters to those widgets. If the parameters are always the same no matter what, then you can pass them directly:
'widgetA': WidgetA(title: 'this is widget A'),
but if the parameters may change as you use the widgets, then you can't use a map of widgets for this, I propose a map of functions instead:
Map<String, Widget Function(String)> widgets {
'widgetA': (title) => WidgetA(title: title),
...
}
This way, when getting the widget:
widgets['widgetA']('this is my title');

Flutter, pasing parameters on StatefulWidget

Can someone tell me what is happening in this program?
body: new ListView.builder(
itemBuilder: (BuildContext context, int index) {
return new StuffInTiles(listOfTiles[index]);
},
itemCount: listOfTiles.length,
),
),
);
}
}
class StuffInTiles extends StatefulWidget{
final MyTile myTile;
const StuffInTiles(this.myTile);
#override
StuffInTilesState createState() => StuffInTilesState();
}
class StuffInTilesState extends State<StuffInTiles> {
#override
Widget build(BuildContext context) {
return Container(child:
//Text(widget.myTile.title),);
_buildTiles(widget.myTile));
}
Widget _buildTiles(MyTile t) {
I want to understand how passing parameters works,why i have
const StuffInTiles(this.myTile);
in this program, what this code is doing?
in my class StuffInTilesState extends State<StuffInTiles> i don't have any constructor, so how this code is working? why my parameters just happen to be there? before i was learning C++, so this is like a magic to me
If you learned C++ you are probably familiar with initializer list, which Dart has as well. In C++ you could do something:
MyClass {
MyClass(MyTitle title) : this.myTitle = title;
final MyTitle myTitle;
}
Which is also valid in Dart. However, Dart allows you to shorthand the call, by automatically assigning the reference to the new class property, without using any intermediate variable.
MyClass(this.myTitle);
Which is basically the same but ensures that any given property won't be null unless you explicitly pass null.
There are other type of constructors available on Dart, such as private constructors, factory constructors and named constructors. You may want to check the official documentation to learn more about it.
By default, when you create classes in Dart, there is just a default constructor implemented.
For example, if you have a class named AwesomeWidget. It will have a default constructor AwesomeWidget() that lets you create an instance of this widget.
So you could use a default constructor in code like so:
//Example 1
return AwesomeWidget();
//Example 2
AwesomeWidget myWidget = AwesomeWidget();
//Example 3
//...
Row(
children: [
Text("Example Code!"),
AwesomeWidget(),
Text("Example Footer Code!"),
],
),
//...
Now if you want to pass some values or some data to your Widget classes then you use the code you have posted above in your question.
The question is: Why would we want to send data to our widgets?
Answer: The biggest use case is when we make our list items as separate widgets. For example, in my food app, I have to show my user's order history in a ListView, so for the UI of each individual list item, I will just make a reusable Widget called OrderHistoryListItem.
In that OrderHistoryListItem, you want to show the date and time of the object. And the order id, and how much the user paid in that order or any other details, so to display this, we send this data to our Reusable Widget List Item, which displays it. Simple as that.
And that is one of the reasons why we pass values to Widgets. As a programmer, you can make use of this handy feature for more complex scenarios, be creative!

Mock a Widget in Flutter tests

I am trying to create tests for my Flutter application. Simple example:
class MyWidget extends StatelessWidget {
#override
build(BuildContext context) {
return MySecondWidget();
}
}
I would like to verify that MyWidget is actually calling MySecondWidget without building MySecondWidget.
void main() {
testWidgets('It should call MySecondWidget', (WidgetTester tester) async {
await tester.pumpWidget(MyWidget());
expect(find.byType(MySecondWidget), findsOneWidget);
}
}
In my case this will not work because MySecondWidget needs some specific and complex setup (like an API key, a value in a Provider...). What I would like is to "mock" MySecondWidget to be an empty Container (for example) so it doesn't raise any error during the test.
How can I do something like that ?
There is nothing done out of the box to mock a widget. I'm going to write some examples/ideas on how to "mock"/replace a widget during a test (for example with a SizedBox.shrink().
But first, let me explain why I think this is not a good idea.
In Flutter you are building a widget tree. A specific widget has a parent and usually has one or several children.
Flutter chose a single pass layout algorithm for performance reasons (see this):
Flutter performs one layout per frame, and the layout algorithm works in a single pass. Constraints are passed down the tree by parent objects calling the layout method on each of their children. The children recursively perform their own layout and then return geometry up the tree by returning from their layout method. Importantly, once a render object has returned from its layout method, that render object will not be visited again until the layout for the next frame. This approach combines what might otherwise be separate measure and layout passes into a single pass and, as a result, each render object is visited at most twice during layout: once on the way down the tree, and once on the way up the tree.
From this, we need to understand that a parent needs its children to build to get their sizes and then render itself properly. If you remove its children, it might behave completely differently.
It is better to mock the services if possible. For example, if your child makes an HTTP request, you can mock the HTTP client:
HttpOverrides.runZoned(() {
// Operations will use MyHttpClient instead of the real HttpClient
// implementation whenever HttpClient is used.
}, createHttpClient: (SecurityContext? c) => MyHttpClient(c));
If the child needs a specific provider you can provide a dummy one:
testWidgets('My test', (tester) async {
tester.pumpWidget(
Provider<MyProvider>(
create: (_) => MyDummyProvider(),
child: MyWidget(),
),
);
});
If you still want to change a widget with another one during your tests, here are some ideas:
1. Use Platform.environment.containsKey('FLUTTER_TEST')
You can either import Platform from dart:io (not supported on web) or universal_io (supported on web).
and your build method could be:
#override
Widget build(BuildContext context) {
final isTest = Platform.environment.containsKey('FLUTTER_TEST');
if (isTest) return const SizedBox.shrink();
return // Your real implementation.
}
2. Use the annotation #visibleForTesting
You can annotate a parameter (ex: mockChild) that is only visible/usable in a test file:
class MyWidget extends StatelessWidget {
const MyWidget({
#visibleForTesting this.mockChild,
});
final Widget? child;
#override
Widget build(BuildContext context) {
return mockChild ?? // Your real widget implementation here.
}
}
And in your test:
tester.pumpWidget(
MyWidget(
mockChild: MyMockChild(),
),
);
You can mock MySecondWidget (eg using Mockito) but you do need to change your real code to create a MockMySecondWidget when in test mode, so it's not pretty. Flutter does not support object instantiation based on a Type (except through dart:mirrors but that is not compatible with Flutter), so you cannot 'inject' the type as a dependency. To determine if you are in test mode use Platform.environment.containsKey('FLUTTER_TEST') - best to determine this once upon startup and set the result as a global final variable, which will make any conditional statements quick.
One way to do it, is to wrap the child widget into a function, and pass the function to parent widget's constructor:
class MyWidget extends StatelessWidget {
final Widget Function() buildMySecondWidgetFn;
const MyWidget({
Key? key,
this.buildMySecondWidgetFn = _buildMySecondWidget
}): super(key: key);
#override
build(BuildContext context) {
return buildMySecondWidgetFn();
}
}
Widget _buildMySecondWidget() => MySecondWidget();
Then you can make up your mock widget, pass it thru buildMySecondWidgetFn in test.

Overwrite Paste Event for TextFormField

I have a TextFormField. Usually you can use the selection toolbar to copy/paste/select all and so on using long tap/double tap.
I want to overwrite the Paste Event. It shouldn't simple insert the current clipboard data but open a popup with several options to insert.
Is it possible to catch and overwrite the Paste event in any way? I saw something like handlePaste() for SelectionControls, but I don't know how to add this to my TextFormField.
Thanks in advance!
AFAIK, you can't exactly 'intercept' the standard toolbar. However, what you can do is to prevent the standard toolbar and make your own.
You can use wrap the textfield/textformfield under IgnorePointer. It will hide any tap gestures on the text field. Below is the code snippet.
IgnorePointer(
child: TextField(
focusNode: _textfieldFocusNode,
controller: _controller,
),
)
Now,you can wrap this IgnorePointer under GestureDetector and show your own menu. Like this :
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
FocusScope.of(context).requestFocus(_textfieldFocusNode);
},
onLongPress: () {
showMenu(____
}
)
This produces the result below and the sample implementation code is here
Found a way to override paste event. I'm not sure, that it is a right way, but it works.
In every TextField you have selectionControls, that provides a way to show and handle toolbar controls.
So, to catch paste event first:
create your own version of selection controls, for example
class AppCupertinoTextSelectionControls extends CupertinoTextSelectionControls {
AppCupertinoTextSelectionControls({
required this.onPaste,
});
ValueChanged<TextSelectionDelegate> onPaste;
#override
Future<void> handlePaste(final TextSelectionDelegate delegate) {
onPaste(delegate);
return super.handlePaste(delegate);
}
}
class AppMaterialTextSelectionControls extends MaterialTextSelectionControls {
AppMaterialTextSelectionControls({
required this.onPaste,
});
ValueChanged<TextSelectionDelegate> onPaste;
#override
Future<void> handlePaste(final TextSelectionDelegate delegate) {
onPaste(delegate);
return super.handlePaste(delegate);
}
}
then, initialise it in your state (for example in StatefulWidget it can looks like that, see below). To study how it used in TextField please see source here
TextSelectionControls? _selectionControls;
#override
void initState() {
if (widget.onPaste != null) {
if (Platform.isIOS) {
_selectionControls = AppCupertinoTextSelectionControls(
onPaste: widget.onPaste!,
);
} else {
_selectionControls = AppMaterialTextSelectionControls(
onPaste: widget.onPaste!,
);
}
}
super.initState();
}
Use callback for onPaste with a type ValueChanged<TextSelectionDelegate> and you can use the same code the Flutter team used to get Clipboard data:
Future<void> onPastePhone(final TextSelectionDelegate? delegate) async {
final TextSelection selection = phoneController.selection;
if (!selection.isValid) {
return;
}
// Snapshot the input before using `await`.
// See https://github.com/flutter/flutter/issues/11427
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
final text = data?.text ?? '';
if (text.isEmpty) {
return;
}
}
Then use selection controls in your TextField.
TextFormField(
selectionControls: _selectionControls,
)
Hope it helps.
I search for this problem. I think there is no proper way to solve this problem. I read about the Textfield class and found two solutions for it.
if you check TextField widget you can find that it will use EditableText to show its simple Text input. EditableText has a selectionControls property. this property is used to render the selection toolbar. also, I found that material and Cupertino have different implementation of it.
1st Solution: you can create your own custom TextField that will use EditableText and pass your custom selectionControl to your widget. I think this gonna be a very hard job to do. create your own implementation of the widget, handling animations, and...
2nd Solution: You can simply copy all related files of TextField in a new file and update it as you want. for this solution, I create a repo in GitHub. you can checkout source code to understand how you can show a dialog in the paste option. and this is how the code should work.
note: I just simply update paste function of the Material implementation of selectionControls. if you want you can also update the Cupertino selectionControls too.
note: also I added documents in everywhere I change the code.