Flutter widget test does not trigger DropdownButton.onChanged when selecting another item - flutter

I am writing a Flutter web app, and adding some widget tests to my codebase. I am having difficulty making flutter_test work as intended. The current problem I face is trying to select a value in a DropdownButton.
Below is the complete widget test code that reproduces the problem:
void main() {
group('description', () {
testWidgets('description', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Card(
child: Column(
children: [
Expanded(
child: DropdownButton(
key: Key('LEVEL'),
items: [
DropdownMenuItem<String>(
key: Key('Greater'),
value: 'Greater',
child: Text('Greater'),
),
DropdownMenuItem<String>(
key: Key('Lesser'),
value: 'Lesser',
child: Text('Lesser'),
),
],
onChanged: (value) {
print('$value');
},
value: 'Lesser',
),
)
],
),
),
));
expect((tester.widget(find.byKey(Key('LEVEL'))) as DropdownButton).value,
equals('Lesser'));
await tester.tap(find.byKey(Key('LEVEL')));
await tester.tap(find.byKey(Key('Greater')));
await tester.pumpAndSettle();
expect((tester.widget(find.byKey(Key('LEVEL'))) as DropdownButton).value,
equals('Greater'));
});
});
}
This test fails on the final expectation -- expect(widget.value, equals('Greater'));
The onChanged callback is never invoked, as I can see in the debugger, or looking for my print statement in the output.
What is the magic to test the behavior of a DropdownButton?

While testing it is important to add a tester.pump() call after any user interaction related code.
The testing of dropdown button is slightly different from usual widgets. For all the cases it is better to refer here which is the actual test for dropdownbutton.
Some hints while testing.
DropdownButton is composed of an 'IndexedStack' for the button and a normal stack for the list of menu items.
Somehow the key and the text you assign for DropDownMenuItem is given to both the widgets in the above mentioned stacks.
While tapping choose the last element in the returned widgets list.
Also the dropdown button takes some time to animate so we call tester.pump twice as suggested in the referred tests from flutter.
The value property of the DropdownButton is not changed automatically. It has to set using setState. So your last line of assertion is wrong and will not work unless you wrap your test in a StatefulBuilder like here.
For more details on how to use DropDownButton check this post
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('description', () {
testWidgets('description', (WidgetTester tester) async {
String changedValue = 'Lesser';
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Center(
child: RepaintBoundary(
child: Card(
child: Column(
children: [
Expanded(
child: DropdownButton(
key: Key('LEVEL'),
items: [
DropdownMenuItem<String>(
key: ValueKey<String>('Greater'),
value: 'Greater',
child: Text('Greater'),
),
DropdownMenuItem<String>(
key: Key('Lesser'),
value: 'Lesser',
child: Text('Lesser'),
),
],
onChanged: (value) {
print('$value');
changedValue = value;
},
value: 'Lesser',
),
)
],
),
),
),
),
),
));
expect((tester.widget(find.byKey(Key('LEVEL'))) as DropdownButton).value,
equals('Lesser'));
// Here before the menu is open we have one widget with text 'Lesser'
await tester.tap(find.text('Lesser'));
// Calling pump twice once comple the the action and
// again to finish the animation of closing the menu.
await tester.pump();
await tester.pump(Duration(seconds: 1));
// after opening the menu we have two widgets with text 'Greater'
// one in index stack of the dropdown button and one in the menu .
// apparently the last one is from the menu.
await tester.tap(find.text('Greater').last);
await tester.pump();
await tester.pump(Duration(seconds: 1));
/// We directly verify the value updated in the onchaged function.
expect(changedValue, 'Greater');
/// The follwing expectation is wrong because you haven't updated the value
/// of dropdown button.
// expect((tester.widget(find.byKey(Key('LEVEL'))) as DropdownButton).value,
// equals('Greater'));
});
});
}

Related

Flutter update refresh previous page when page has been pushed via a stateless widget

So here is the problem.
TabScreen() with 3 pages and one fabcontainer button (Stateless widget).
When pressed the fabcontainer will give you the chances of make one upload, after the upload i would like to refresh one of the page of the tabscreen.
return Container(
height: 45.0,
width: 45.0,
// ignore: missing_required_param
child: FabContainer(
icon: Ionicons.add_outline,
mini: true,
),
);
}
OnTap of the fabcontainer:
Navigator.pop(context);
Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) => CreatePost(),
),
);
},
Cannot add a .then(){setState... } because it is a stateless widget and i need to set the state of a precise page, not of the fabcontainer.
Any idea?
Thanks!
Define a updateUi method inside your TabScreen (which defines the pages)
TabScreen:
void updateUi(){
// here your logic to change the ui
// call setState after you made your changes
setState(() => {});
}
Pass this function as a constructor param to your FabContainer button
FabContainer(
icon: Ionicons.add_outline,
mini: true,
callback: updateUi,
),
Define it in your FabContainer class
final Function() callback;
Call it to update the ui
callback.call();
So what Ozan suggested was a very good beginning but i could not access the stateful widget in order to set the state.
What i did on top of Ozan's suggestion was giving the state a globalkey:
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
Assigning it to the scaffold:
return Scaffold(
key: scaffoldKey,
Making the state public removing the _MyPizzasState -> MyPizzasState
Creating a method to refresh the data:
refreshData() {
pizzas = postService.getMyPizzas();
setState(() {
});
}
Assigning a key during the creation of the MyPizzaPage:
final myPizzasKey = GlobalKey<MyPizzasState>();
{
'title': 'My Pizza',
'icon': Ionicons.pizza_sharp,
'page': MyPizzas(key: myPizzasKey),
'index': 0,
},
And, how Ozan said once i received the callback :
buildFab() {
return Container(
height: 45.0,
width: 45.0,
// ignore: missing_required_param
child: FabContainer(
icon: Ionicons.add_outline,
mini: true,
callback: refreshMyPizzas,
),
);
}
void refreshMyPizzas() {
print("Refreshing");
myPizzasKey.currentState?.refreshData();
}

Pass On Get Results from one class to another - Flutter

The data unLockCard is properly created in the main class where the button is placed.
When I moved the button to a dialog in a different class - the unLockCard is lost. I receive the error message
What is the best way to pass on unLockCard[number] = tarots[0]; into a different widget or class.
Homepage
List<bool> flips = [false, false, false, false];
List tarots = [];
List unLockCard = [];
Widget _buildTarotCard(key, number, title) {
return Column(
children: [
FlipCard(
key: key,
flipOnTouch: true,
front: GestureDetector(
onTap: () {
tarots.shuffle();
key.currentState.toggleCard();
setState(() {
flips[number] = true;
});
unLockCard[number] = tarots[0];
tarots.removeAt(0);
},
Dialog
Widget _showDialog(BuildContext context) {
Future.delayed(Duration.zero, () => showAlert(context));
return Container(
color: Color(0xFF2C3D50),
);
}
void showAlert(BuildContext context) {
List unLockCard = [];
Dialogs.materialDialog(
color: colorTitle,
msg: 'Congratulations, you won 500 points',
msgStyle: TextStyle(color: Colors.white),
title: 'Congratulations',
titleStyle: TextStyle(color: Colors.white),
lottieBuilder: Lottie.asset('assets/lottie/spirituality.json',
fit: BoxFit.contain,
),
dialogWidth: kIsWeb ? 0.3 : null,
context: context,
actions: [
NeumorphicButton(
onPressed: () => Get.toNamed(Routes.DETAILS,
arguments: unLockCard.sublist(0, 4)),
margin: EdgeInsets.symmetric(horizontal: 8.0),
The code is a bit cluttered. You are building a new List unLockCard = []; in the showAlert() method even though you have one already in the HomePage. I suggest you create a CardController class that deals with everything card related and use it in all the views you need. One very elegant way I found is by using the GetX library (https://github.com/jonataslaw/getx). You can declutter your code using a GetView and a GetController (https://github.com/jonataslaw/getx#getview). However, if you don't want to add a new library to your project, you can achieve the same results by having a single point that deals with card actions (i.e. holds a single instance of the unlockCard list and updates it accordingly).

Widget Test Doesn't Fire DropdownButton onChanged

I have a widget test that taps an item in the DropdownButton. That should fire the onChanged callback but it doesn't. Here is the test code. The mock is Mockito.
void main() {
//Use a dummy instead of the fake. The fake does too much stuff
final mockServiceClient = MockTheServiceClient();
final apiClient = GrpcApiClient(client: mockServiceClient);
when(mockServiceClient.logEvent(any))
.thenAnswer((_) => MockResponseFuture(LogEventResponse()));
testWidgets("select asset type", (tester) async {
//sets the screen size
tester.binding.window.physicalSizeTestValue = const Size(3840, 2160);
// resets the screen to its orinal size after the test end
addTearDown(tester.binding.window.clearPhysicalSizeTestValue);
await tester.pumpWidget(AssetApp(apiClient), const Duration(seconds: 5));
//Construct key with '{DDLKey}_{Id}'
await tester
.tap(find.byKey(ValueKey("${assetTypeDropDownKey.value}_PUMP")));
await tester.pumpAndSettle(const Duration(seconds: 5));
verify(mockServiceClient.logEvent(any)).called(1);
});
}
This is the build method of the widget:
#override
Widget build(BuildContext context) {
return DropdownButton<DropDownItemDefinition>(
underline: Container(),
dropdownColor: Theme.of(context).cardColor,
hint: Text(
hintText,
style: Theme.of(context).textTheme.button,
),
//TODO: Use the theme here
icon: Icon(
Icons.arrow_drop_down,
color: Theme.of(context).dividerColor,
),
value: getValue(),
onChanged: (ddd) {
setState(() {
onValueChanged(ddd!);
});
},
items: itemss.map<DropdownMenuItem<DropDownItemDefinition>>((value) {
return DropdownMenuItem<DropDownItemDefinition>(
key: ValueKey(
"${(key is ValueKey) ? (key as ValueKey?)?.value.toString() :
''}_${value.id}"),
value: value,
child: Tooltip(
message: value.toolTipText,
child: Container(
margin: dropdownPadding,
child: Text(value.displayText,
style: Theme.of(context).textTheme.headline3))),
);
}).toList(),
);
}
Note that the onValueChanged function calls the logEvent call. The onChanged callback never happens and the test fails. This is the code it should fire.
Future onAssetTypeChange(DropDownItemDefinition newValue) async {
await assetApiClient.logChange(record.id, newValue, DateTime.now());
}
Why does the callback never fire?
Note: I made another widget test and the Mock does verify that the client was called correctly. I think there is some issue with the callback as part of the widget test.
You need to first instruct the driver to tap on the DropdownButton itself, and then, after the dropdown popup shows up, tap on the DropdownMenuItem.
The driver can't find a DropdownMenuItem from the dropdown if the dropdown itself is not active/painted on the screen.

Flutter Hooks Riverpod not updating widget despite provider being refreshed

I've been studying flutter for a couple of months and I am now experimenting with Hooks and Riverpod which would be very important so some results can be cached by the provider and reused and only really re-fetched when there's an update.
But I hit a point here with an issue where I can't wrap my head around the provider update to reflect in the Widget. Full example can be checked out from here -> https://github.com/codespair/riverpod_update_issue I've added some debug printing and I can see the provider is properly refreshed but the changes don't reflect on the widget.
The example has a working sample provider:
// create simple FutureProvider with respective future call next
final futureListProvider =
FutureProvider.family<List<String>, int>((ref, value) => _getList(value));
// in a real case there would be an await call inside this function to network or local db or file system, etc...
Future<List<String>> _getList(int value) async {
List<String> result = [...validValues];
if (value == -1) {
// do nothing just return original result...
} else {
result = []..add(result[value]);
}
debugPrint('Provider refreshed, result => $result');
return result;
}
a drop down list when changed refreshes the provider:
Container(
alignment: Alignment.center,
padding: EdgeInsets.fromLTRB(5, 2, 5, 1),
child: DropdownButton<String>(
key: UniqueKey(),
value: dropDownValue.value.toString(),
icon: Icon(Icons.arrow_drop_down),
iconSize: 24,
elevation: 16,
underline: Container(
height: 1,
color: Theme.of(context).primaryColor,
),
onChanged: (String? newValue) {
dropDownValue.value = newValue!;
context
.refresh(futureListProvider(intFromString(newValue)));
},
items: validValues
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: Theme.of(context).primaryTextTheme.subtitle1,
),
);
}).toList(),
),
),
And a simple list which uses the provider elements to render which despite the provider being properly refreshed as you can see in the debugPrinting it never updates:
Container(
key: UniqueKey(),
height: 200,
child: stringListProvider.when(
data: (stringList) {
debugPrint('List from Provider.when $stringList');
return MyListWidget(stringList);
// return _buildList(stringList);
},
loading: () => CircularProgressIndicator(),
error: (_, __) => Text('OOOPsss error'),
),
),
]),
class MyListWidget extends HookWidget {
final GlobalKey<ScaffoldState> _widgetKey = GlobalKey<ScaffoldState>();
final List<String> stringList;
MyListWidget(this.stringList);
#override
Widget build(BuildContext context) {
debugPrint('stringList in MyListWidget.build $stringList');
return ListView.builder(
key: _widgetKey,
itemCount: stringList.length,
itemBuilder: (BuildContext context, int index) {
return Card(
key: UniqueKey(),
child: Padding(
padding: EdgeInsets.all(10), child: Text(stringList[index])),
);
},
);
}
As I am evaluating approaches to develop some applications I am getting inclined to adopt a more straightforward approach to handle such cases so I am also open to evaluate simpler, more straightforward approaches but I really like some of the features like the useMemoized, useState from hooks_riverpod.
One thing I wanted to note before we get started is you can still use useMemoized, useState, etc. without hooks_riverpod, with flutter_hooks.
As far as your problem, you are misusing family. When you pass a new value into family, you are actually creating another provider. That's why your list prints correctly, because it is, but the correct result is stuck in a ProviderFamily you aren't reading.
The simpler approach is to create a StateProvider that you use to store the currently selected value and watch that provider from your FutureProvider. It will update the list automatically without having to refresh.
final selectedItemProvider = StateProvider<int>((_) => -1);
final futureListProvider = FutureProvider<List<String>>((ref) async {
final selected = ref.watch(selectedItemProvider).state;
return _getList(selected);
});
DropdownButton<String>(
...
onChanged: (String? newValue) {
dropDownValue.value = newValue!;
context.read(selectedItemProvider).state = intFromString(newValue);
},
}

How to open DropdownButton when other widget is tapped, in Flutter?

I need to have a DropdownButton's list of options open/show programmatically when some other widget is tapped. I know that this may not be UI-best-practice and all, but I need this behavior:
As an example, in a structure like the one below, I may need to have taping Text("every") to open the neighboring DropdownButton's dropdown list, behaviors similar to clicking a <select>'s label in HTML.
Row(children: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('every'),
),
Expanded(
child: DropdownButton<String>(
value: _data['every'],
onChanged: (String val) => setState(() => _data['every'] = val),
items: _every_options.map<DropdownMenuItem<String>>(
(String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
},
).toList(),
isExpanded: true,
),
),
]);
NOTE: I am in need though of the general solution to this problem, not just how to make that Text behave somewhat "like a HTML label" in the tree below. It may need to be triggered to open by maybe a further away button etc.
The other answer is the best way to do this, but as requested by the OP in comments, here are two very "hacky" ways to achieve this, yet without implementing custom widgets.
1. Access DropdownButton widget tree directly using GlobalKey
If we look at the source code of DropdownButton, we can notice that it uses GestureDetector to handle taps. However, it's not a direct descendant of DropdownButton, and we cannot depend on tree structure of other widgets, so the only reasonably stable way to find the detector is to do the search recursively.
One example is worth a thousand explanations:
class DemoDropdown extends StatefulWidget {
#override
InputDropdownState createState() => DemoDropdownState();
}
class DemoDropdownState<T> extends State<DemoDropdown> {
/// This is the global key, which will be used to traverse [DropdownButton]s widget tree
GlobalKey _dropdownButtonKey;
void openDropdown() {
GestureDetector detector;
void searchForGestureDetector(BuildContext element) {
element.visitChildElements((element) {
if (element.widget != null && element.widget is GestureDetector) {
detector = element.widget;
return false;
} else {
searchForGestureDetector(element);
}
return true;
});
}
searchForGestureDetector(_dropdownButtonKey.currentContext);
assert(detector != null);
detector.onTap();
}
#override
Widget build(BuildContext context) {
final dropdown = DropdownButton<int>(
key: _dropdownButtonKey,
items: [
DropdownMenuItem(value: 1, child: Text('1')),
DropdownMenuItem(value: 2, child: Text('2')),
DropdownMenuItem(value: 3, child: Text('3')),
],
onChanged: (int value) {},
);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Offstage(child: dropdown),
FlatButton(onPressed: openDropdown, child: Text('CLICK ME')),
],
);
}
}
2. Use Actions.invoke
One of the recent features of Flutter is Actions (I'm not sure what it's meant for, I've only noticed it today after flutter upgrade), and DropdownButton uses it for reacting to different... well, actions.
So a little tiny bit less hacky way to trigger the button would be to find the context of Actions widget and invoke the necessary action.
There are two advantages of this approach: firstly, Actions widget is a bit higher in the tree, so traversing that tree wouldn't be as long as with GestureDetector, and secondly, Actions seems to be a more generic mechanism than gesture detection, so it's less likely to disappear from DropdownButton in the future.
// The rest of the code is the same
void openDropdown() {
_dropdownButtonKey.currentContext.visitChildElements((element) {
if (element.widget != null && element.widget is Semantics) {
element.visitChildElements((element) {
if (element.widget != null && element.widget is Actions) {
element.visitChildElements((element) {
Actions.invoke(element, Intent(ActivateAction.key));
return false;
});
}
});
}
});
}
It's one (of many) designed API limitations...
The easiest approach to accomplish what you want, without modifying the SDK, copy dropdown.dart, and create your own version of it, let's say custom_dropdown.dart, and paste the code there ...
in line 546, rename the class to CustomDropdownButton, and in line 660 and 663 rename _DropdownButtonState to CustomDropdownButtonState, ( we need the state class to be exposed outside the file ).
Now you can do whatever you want with it,
although you were interested in the _handleTap(), to open the overlay menu options.
Instead of making _handleTap() public, and refactor the code, add another method like:
(line 726)
void callTap() => _handleTap();
Now, change your code to use your DropdownButton instead of the Flutter's DropdownButton, the key is to "set the key" (Global one) :P
// some stateful widget implementation.
Map<String, String> _data;
List<String> _every_options;
// we need the globalKey to access the State.
final GlobalKey dropdownKey = GlobalKey();
#override
void initState() {
_every_options = List.generate(10, (i) => "item $i");
_data = {'every': _every_options.first};
simulateClick();
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Row(children: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('every'),
),
Expanded(
child: CustomDropdownButton<String>(
key: dropdownKey,
value: _data['every'],
onChanged: (String val) => setState(() => _data['every'] = val),
items: _every_options
.map((str) => DropdownMenuItem(
value: str,
child: Text(str),
))
.toList(),
isExpanded: true,
),
),
]),
);
}
void simulateClick() {
Timer(Duration(seconds: 2), () {
// here's the "magic" to retrieve the state... not very elegant, but works.
CustomDropdownButtonState state = dropdownKey.currentState;
state.callTap();
});
}