Widget Test Doesn't Fire DropdownButton onChanged - flutter

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.

Related

How to place a Loader on the screen while an API action is being performed in Flutter

I am trying to show a loader when a form is submitted to the server so that there isn't another submission of the same form until and unless the API sends back a response. I have tried something like the below code but it just doesn't seem to work as the Circular Progress indicator seems to not show up and rather, the screen remains as it is until the server sends back a response. As a result of this, the user gets confused as to whether or not their requests got submitted, and in the process, they end up posting the same form another time only to find out later that their were multiple submissions. I will include snippets of the code that has the CircularProgressIndicator() to prevent another submission and the widget that has the API call code.
bool isSelected = false;
isSelected
? const CircularProgressIndicator() : Container(
child: Center(
child: AppButtonStyle(
label: 'Submit',
onPressed: () {
if (_key.currentState!.validate()) { //This is the key of the Form that gets submitted
setState(() {
isSelected = true;
});
List<String> date = [
dateFormat.format(_dateTimeStart!).toString(),
dateFormat.format(_dateTimeEnd!).toString()
];
Map<String, dynamic> data = {
'leave_type': _selectedItem,
'dates': date,
'description': add
};
if (kDebugMode) {
print('DATA: $data');
}
Provider.of<LeaveViewModel>(context, listen: false)
.postLeaveRequests(data, context) //This here makes the API call
.then((value) {
setState(() {
isSelected = false;
_textController.clear();
_dateTimeStart = null;
_dateTimeEnd = null;
});
});
}
},
),
),
)
The API module:
class LeaveViewModel with ChangeNotifier {
final leaveRepository = LeaveRequestRepository();
Future<void> postLeaveRequests(dynamic data, BuildContext context) async {
SharedPreferences localStorage = await SharedPreferences.getInstance();
String authToken = localStorage.getString('token').toString();
leaveRepository.requestLeave(authToken, data).then((value) {
print('LEAVEEEEEE: $value');
Flushbar(
duration: const Duration(seconds: 4),
flushbarPosition: FlushbarPosition.BOTTOM,
borderRadius: BorderRadius.circular(10),
icon: const Icon(Icons.error, color: Colors.white),
// margin: const EdgeInsets.fromLTRB(100, 10, 100, 0),
title: 'Leave Request Submitted',
message: value.data.toString()
).show(context);
}).onError((error, stackTrace) {
Flushbar(
duration: const Duration(seconds: 4),
flushbarPosition: FlushbarPosition.BOTTOM,
borderRadius: BorderRadius.circular(10),
icon: const Icon(Icons.error, color: Colors.white),
// margin: const EdgeInsets.fromLTRB(100, 10, 100, 0),
title: 'Leave Request Failed',
message: error.toString()
).show(context);
});
}
}
Any help will be appreciated. Also, I'm open to the concept of using easy_loader 2.0.0 instead of CicularProgressIndicator() and would be very glad to read suggestions about it's usage in my code.
One problem in your code seems to be that you define isSelected in your build method. Every time you call setState, the build method is called to regenerate the widgets. And with each new call isSelected gets false as initial value. Define isSelected as class variable, so that it is not always on false.
The more elegant solution would be to work with a FutureBuilder
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html

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

Flutter DropDownMenu Button update an existing record

I have been experimenting with my flutter drop down button.
Context of what I am doing.
I have an app that will create a job and give it to an available staff member. I have stored all my staff members in a list for the menu button. I will put the code below to show the creation of the job ticket drop down button. selectedTech is at the top of the program so that's not the issue
String selectedTech = "";
Container(
// margin: EdgeInsets.only(right: 20),
width: MediaQuery.of(context).size.width / 2.5,
child: DropdownButton(
hint: Text(
selectedTech,
style: TextStyle(color: Colors.blue),
),
isExpanded: true,
iconSize: 30.0,
style: TextStyle(color: Colors.blue),
items: listStaffUsers.map(
(val) {
return DropdownMenuItem<String>(
value: val,
child: Text(val),
);
},
).toList(),
onChanged: (val) {
setState(
() {
selectedTech = val.toString();
},
);
},
),
),
The above code works perfect.
However when I want to update the job ticket to change the available staff member I want to set the initial value of the drop down menu to the staff member assigned to the job, because it isn't always guaranteed that they change the staff member allocated to the job. When I set the selected value to my initial value I am locked with that value and cannot change it.
Here is the code I am using to update the staff member.
String selectedTech = "";
int the build method I add
selectedTech = widget.staff;
Container(
// margin: EdgeInsets.only(right: 20),
width: MediaQuery.of(context).size.width / 2.5,
child: DropdownButton(
hint: Text(
selectedTech,
style: TextStyle(color: Colors.blue),
),
isExpanded: true,
iconSize: 30.0,
style: TextStyle(color: Colors.blue),
items: listStaffUsers.map(
(val) {
return DropdownMenuItem<String>(
value: val,
child: Text(val),
);
},
).toList(),
onChanged: (val) {
setState(
() {
selectedTech = val.toString();
},
);
},
),
),
Any Guidance or examples will be greatly appreciated.
As I understand under the Widget build method you set
selectedTech = widget.staff and then return the widget like this:
Widget build(BuildContext context) {
selectedTech = widget.staff;
return Container( ...
This will systematically lock your selectedTech to widget.staff whenever the build method is called (when you call setState). I mean whenever you change the value of the dropdown, the value will not be set the actual value on the dropdown menu. Because you call setState, setState builds the widget from scratch and selectedTech = widget.staff is called in these steps.
Instead of in build method you should initialize it first, then continue to build method.
class _StaffHomeState extends State<StaffHome> {
String? selectedTech;
// Write a function to initialize the value of selectedTech
void initializeSelectedTech () {
selectedTech = widget.staff;
}
// Call this function in initState to initialize the value
#override
void initState() {
initializeSelectedTech();
super.initState();
}
// Then Widget build method
Widget build(BuildContext context) {
return Container( .....
By this way, you initialize first the value before build method and whenever state changes, the data will be persisted.
I hope it is helpful.

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

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

State does not update until button is pressed twice

I am trying to learn and work with APIs, I am using the Tiingo Stock API to get stock info. My current app is:
class _StockAppState extends State<StockApp> {
String out = "Enter Ticker and press Submit";
Map<String,String> headers = {
'Content-Type': 'application/json',
'Authorization' : <API KEY REMOVED>
};
void getPrice(String tick) async {
if(tick == ""){
out = "Enter Ticker and press Submit";
}else{
Response rep = await get('https://api.tiingo.com/tiingo/daily/$tick/prices', headers: headers);
if(rep.statusCode == 200){
List data = json.decode(rep.body);
Map dataS = data[0];
out = "Price: ${dataS['close']}";
}else{
out = "Error";
}
}
}
#override
void initState() {
super.initState();
}
TextEditingController ticker = new TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Stock App'),),
body: Column(
children: <Widget>[
TextField(
controller: ticker,
textAlign: TextAlign.left,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter Ticker',
hintStyle: TextStyle(color: Colors.grey),
),
),
FlatButton(
onPressed: () async {
FocusScope.of(context).unfocus();
setState(() {
getPrice(ticker.text);
});
},
child: Text('Submit')
),
Text(out),
],
),
);
}
}
So, basically, when you enter a ticker and press submit, the app will change the "out" string var to display the stock price. But for the app to update, I am having to press submit twice.
Could anyone help?
P.S.: I have removed my API key for security reasons.
It is because you have async method in your setState method.
The setState method will be called synchronously.
So here problem is when setState is performed and frame get refreshed your data from api has not arrived yet and it showing you the old data. When you again click the button your out variable have new data (from your first click) which will be shown on the screen and API will be called again.
To solve your problem
FlatButton(
onPressed: () async {
FocusScope.of(context).unfocus();
await getPrice(ticker.text);
setState(() {
});
},
child: Text('Submit')
),
So call the setState method after API call is completed.
To know more about async/await watch this video : https://www.youtube.com/watch?v=SmTCmDMi4BY