Widget testing to assert Text widgets style - flutter

I am building a Task/To do item widget. The Task widget is implemented using both a ListTile and Checkbox widgets.
class _TaskListItemState extends State<TaskListItem> {
#override
Widget build(BuildContext context) {
return InkWell(
onTap: _goToTaskScreen,
child: ListTile(
leading: Checkbox(
value: widget.task.isComplete, onChanged: _toggleTaskComplete),
title: Text(
widget.task.title,
style: TextStyle(
decoration: widget.task.isComplete
? TextDecoration.lineThrough
: null,
color: widget.task.isComplete ? Colors.grey : null),
),
),
);
}
void _goToTaskScreen() {...}
void _toggleTaskComplete(bool? value) {...});
}
}
I am trying to test the widget, that if the task is completed then widget's text style decoration should be strike-through, else normal.
testWidgets(
'if the isCompleted is true, then title text should be strikethrough',
(WidgetTester tester) async {
// Arrange
const testKey = ValueKey('my-key-1');
const testTitle = 'Demo title';
const testProjectID = 555;
final DateTime testDueDate = DateTime.now();
const testIsComplete = true;
final Task taskComplete = Task(
title: testTitle,
projectID: testProjectID,
dueDate: testDueDate,
isComplete: testIsComplete);
await tester.pumpWidget(MaterialApp(
home: Material(
child: TaskListItem(key: testKey, task: taskComplete),
),
));
final textFinder = find.byType(Text);
final textWidget = tester.firstWidget(textFinder);
expect(textWidget.style.decoration, TextDecoration.lineThrough);
});
App runs fine when runnng flutter run.
But this throws an error, when running flutter test:
est/widgets/task_list_item_test.dart:57:25: Error: The getter 'style'
isn't defined for the class 'Widget'.
'Widget' is from 'package:flutter/src/widgets/framework.dart' ('/C:/flutter/packages/flutter/lib/src/widgets/framework.dart'). Try
correcting the name to the name of an existing getter, or defining a
getter or field named 'style'.
expect(textWidget.style.decoration, TextDecoration.lineThrough);
^^^^^
I know I am missing something because I have only been learning flutter for over a week. Could you help me how to perform Widget testing to assert a Text widgets style. Thank you!

After running into this issue myself and looking into resolution for testing/assert styles on a Text widget, I found the find.byWidgetPredicate method to the most effect at achieving this.
For you case, you can use byWidgetPredicate like so:
// Define your finder with specific conditions here:
final textWithStrikeThrough = find.byWidgetPredicate(
(widget) =>
widget is Text &&
widget.style.decoration == TextDecoration.lineThrough,
description: '`Text` widget with strike through',
);
// Now you can use this finder like other finders.
expect(
textWithStrikeThrough,
findsOneWidget,
);

You could try casting the firstWidget method as a Text like this:
final textFinder = find.byType(Text);
final textWidget = tester.firstWidget<Text>(textFinder);
To ensure the type is stored the textWidget object.

Related

NoSuchMethodError when taking Screenshot

I am trying to take a Screanshot of a Stak with a list of iteams in it. It displays normaly and works, but when i try to take screenshot of the Widget I resive:
NoSuchMethodError (NoSuchMethodError: The getter 'stateWidget' was called on null.
Receiver: null
Tried calling: stateWidget)
(I use a Inhereted widget)
Her is the Widget I am trying to take a Screenshot of
class BlinkSkjerm extends StatelessWidget {
#override
Widget build(BuildContext context) {
final provider = InheritedDataProvider.of(context);
final data = provider.historikken[provider.index];
return SizedBox(
height: 400,
child: Stack(
children: data.inMoveableItemsList,
));
}
}
and her is the onPress funtion:
onPressed: () async {
final controler = ScreenshotController();
final bytes = await controler.captureFromWidget(BlinkSkjerm());
setState(() {
this.bytes = bytes;
});
}
you used InheritedDataProvider in wrong way. you did not provide data that needed in BlinkSkjerm.
you want to take screen shot from widget that not in the tree, but that widget need data that should provide before build it which you did not provide it.
this approach work this way:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InheritedDataProvider(
child: BlinkSkjerm(),
data:'some string',
)),
);
this way you can use
final provider = InheritedDataProvider.of(context);
and make sure it is not null.
for your situation I recommended to do something like this:
onPressed: () async {
final controler = ScreenshotController();
final bytes = await controler.captureFromWidget(InheritedDataProvider(
child: BlinkSkjerm(),
data:'some string',
));
setState(() {
this.bytes = bytes;
});
}
for more information see this page

Does flutter_test support testing 'leaf' widgets?

Suppose I create a simple widget, such as this:
class RightArrowButton extends StatelessWidget {
const RightArrowButton({VoidCallback onPressed})
: this._onPressed = onPressed;
final VoidCallback _onPressed;
#override
Widget build(BuildContext context) {
return IconButton(
visualDensity: VisualDensity.compact,
onPressed: _onPressed,
icon: _buildIcon(iconData()),
);
}
/// Overrride to set a different icon.
IconData iconData() => Icons.play_arrow;
Widget _buildIcon(IconData icon) => Icon(icon, color: Colors.blue);
}
Further suppose that I wish to unit test this widget. I know, this is an incredibly simple widget, but I might be attempting to use TDD to write the widget in the first place. In any case, I could apply the concept to a more complex widget.
In this case, the test would be very simple: Construct the widget, passing it an onPressed function whose side-effects can be checked to validate that the onPressed method is actually called when someone taps on the widget.
Sample test code:
void main() {
testWidgets('RightArrowButton', (WidgetTester tester) async {
int counter = 0;
await tester.pumpWidget(RightArrowButton(
onPressed: () => counter++,
));
expect(0, equals(counter));
var button = find.byType(IconButton);
await tester.tap(button);
expect(1, equals(counter));
});
}
However, when I do this, I get the following error:
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building IconButton(Icon, padding: EdgeInsets.all(8.0), dirty):
No Material widget found.
IconButton widgets require a Material widget ancestor.
I understand the error after reading this SO question:
No Material widget found
So -- Is there a way to unit test widgets in the way I want? Or am I forced to test a whole assembly of widgets in order to get a valid Material widget ancestor (which is more like a small integration test)?
I found a solution which works. Flutter experts, is the following approach the only or best way to achieve my goal of testing individual widgets in isolation?
void main() {
testWidgets('arrow test', (WidgetTester tester) async {
int counter = 0;
// must include Card to get Material;
// must include MaterialApp to get Directionality
await tester.pumpWidget(MaterialApp(
home: Card(
child: RightArrowButton(
onPressed: () => counter++,
),
),
));
await tester.tap(find.byType(RightArrowButton));
expect(1, equals(counter));
});
}

Is there a way I can use Future values in Dart to fill Textfields?

I've been learning Dart with flutter and was creating Weather App for practices , I managed to successfully create the UI , I used Dark Sky weather API since it has 7 day weather forecast but instead of taking the long road of decoding json and models , I used Dark Sky Library.
Here's the code for the API
Future<Null> getWeather() async {
Location location = Location();
await location.getCurrentPosition();
var darksky = DarkSkyWeather(
MY_API_KEY,
units: Units.SI,
language: Language.English,
);
var forecast = await darksky.getForecast(
location.latitude, location.longitude, excludes: [
Exclude.Hourly,
Exclude.Minutely,
Exclude.Alerts,
Exclude.Flags
]);
print(forecast.currently.temperature.round());
print(forecast.daily.data[0].temperatureMax);
}
I wanted to use the forecast variable outside the functions and to fill the text fields such the humidity temperature among other data in the package . How can I access it ? any help will be appreciated .
Thanks
Take a look at this code
class _MyHomePageState extends State<MyHomePage> {
String _data; // This holds the data to be shown
#override
void initState() {
_data = "Press the button"; // This is the initial data // Set it in initState because you are using a stateful widget
super.initState();
}
// Call this to set the new data
void setData() {
setState(() { // Remember to use setState() else the changes won't appear
_data = 'Hello World';
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Message',
),
Text(
'$_data', // See how the data is fed into this text widget
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: setData, // When I press the button, the new data is set
child: Icon(Icons.send),
),
);
}
}
Output:
Initially:
After pressing the button:
Do the same thing in your code.
// Your code modified a bit
// Make sure this is a stateful widget because you are changing values
double _tempNow; // Declare two vars inside the class
double _tempMax;
// Your initState
#override
void initState() {
_tempNow = 0; // Initial values
_tempMax = 0;
super.initState();
}
// Build method
#override
Widget build(BuildContext context) {
// Your layout Code.....
Text(
'Current: $_tempNow' // Here you set the tempNow
),
......
Text(
'Maximum: $_tempMax' // and tempMax in the text widgets
),
// End
}
Future<Null> getWeather() async {
Location location = Location();
await location.getCurrentPosition();
var darksky = DarkSkyWeather(
"8810b3633934b8c1975c51a2311dc1d0",
units: Units.SI,
language: Language.English,
);
var forecast = await darksky.getForecast(
location.latitude, location.longitude, excludes: [
Exclude.Hourly,
Exclude.Minutely,
Exclude.Alerts,
Exclude.Flags
]);
// Change the values here.
// After the data is fetched, The changes will be made automatically
setState(() {
_tempNow = forecast.currently.temperature.round();
_tempMax = forecast.daily.data[0].temperatureMax;
});
}
In quite a similar manner, you can use text fields too. In that case, You will need to use TextEditingController and set the text there but, it seems in your case, you need read-only text to show the information to the user so a simple Text widget is enough

Trouble implementing contextual aware icon button in flutter

I have a TextField and an IconButton in a row like so.
I would like the IconButton to be enabled only when there is text in the TextField. I am using the provider package for state management.
Here is the ChangeNotifier implementation.
class ChatMessagesProvider with ChangeNotifier{
List<ChatMessage> chatMessages = <ChatMessage>[];
bool messageTyped = false;
ChatMessagesProvider(this.chatMessages);
void newMessage(String textMessage){
ChatMessage message = ChatMessage(textMessage);
this.chatMessages.add(message);
notifyListeners();
}
int messageCount() => chatMessages.length;
void updateMessageTyped(bool typed){
this.messageTyped = typed;
// notifyListeners(); Un-comennting this makes the Text disappear everytime I type something on the text field
}
}
Here is the actual widget:
class TextCompose extends StatelessWidget {
final TextEditingController _composeTextEditingController = new TextEditingController();
TextCompose(this.chatMessagesProvider);
final ChatMessagesProvider chatMessagesProvider;
#override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
Flexible(
child: new TextField(
controller: _composeTextEditingController,
onSubmitted: (String text) {
_onMessageSubmitted(text, context);
},
onChanged: (String text){
if(text.length > 0){
chatMessagesProvider.updateMessageTyped(true);
print(text);
}
else{
chatMessagesProvider.updateMessageTyped(false);
print("No text typed");
}
},
decoration: new InputDecoration.collapsed(
hintText: "Enter message"
),
),
),
new Container(
margin: new EdgeInsets.all(8.0),
child: new IconButton(
color: Theme.of(context).accentColor,
icon: new Icon(Icons.send),
disabledColor: Colors.grey,
onPressed:chatMessagesProvider.messageTyped // This dosen't work
? () => _onMessageSubmitted(_composeTextEditingController.text, context)
: null,
),
)
],
),
);
}
void _onMessageSubmitted(String text, BuildContext context){
if(chatMessagesProvider.messageTyped) { // This works fine.
// clear the message compose text box
_composeTextEditingController.clear();
// add the message to provider.
chatMessagesProvider.newMessage(text);
// set the message typed to false
chatMessagesProvider.messageTyped = false;
}
I am using messageTyped from ChatMessageProvider to check to see if there is any text in the TextField. It seems to work fine when I check it in the _onMessageSubmitted method but not when I check its value in the onPressed property for the IconButton.
I know this because I can see the IconButton remains disabled(colour doesn't change from grey) when I type text, whereas I can hit the submit button on the virtual keyboard and the text is cleared from the TextField(as per call to _composeTextEditingController.clear())
Question:
why does chatMessagesProvider.messageTyped return the right value when called from the _onMessageSubmitted but not when it is called from the onPrssed attribute in the IconButton?
How would I debug something like this in Flutter, I would really like to drop a breakpoint in onPressedAttribute and see the value for chatMessagesProvider.messageTyped
Let me know if you need to see any more of my code.
onPressed:chatMessagesProvider.messageTyped this line is being executed during widget build time so it is always default value and it will never get refreshed until unless you rebuild the widget using notify listener or stateful widget.
Store the currently being typed message in your provider and make your send button enable/disable depends on whether currently being typed message is empty or not.
You say you are using 'provider_package' but you actually have no Provider in your layout. Instead you have a custom built ChangeNotifier with no listeners - you are indeed calling notifyListeners() but there are actually no listeners, so no rebuild is being triggered. A rebuild is needed in order for the button to change its onPressed function reference and implicitly its color.
As for debugging, you can set a breakpoint on the line with onPressed, but it will only be hit during a rebuild.
The most important thing to understand is that the function reference you give to onPressed will be invoked correctly, but a rebuild is needed for the widget to change visually.
Although your current ChangeNotifier implementation does not make much sense, simply wrapping your calls to updateMessageTyped within setState should solve the visual problem - and your breakpoint will also be hit after each typed/deleted character.
The simplest solution you can, first of all, make your widget StatefulWidget.
You need a boolean inside State class:
bool hasText = false;
Then create initState:
#override
void initState() {
_composeTextEditingController.addListener(() {
if (_composeTextEditingController.text.isNotEmpty) {
setState(() {
hasText = true;
});
} else {
setState(() {
hasText = false;
});
}
});
super.initState();
}
Also don't forget to dispose:
#override
void dispose() {
_composeTextEditingController.dispose();
super.dispose();
}
And finally your build method:
Row(
children: <Widget>[
Expanded(
child: TextField(
controller: _composeTextEditingController,
)),
if (hasText) IconButton(icon: Icon(Icons.send), onPressed: () {})
],
),

How to change TextField text from Store with flutter_flux?

In the flutter_flux example when we commit a new message, the _currentMessage is emptied but the TextField does not reflect that changes.
This is the code in the store:
triggerOnAction(commitCurrentMessageAction, (ChatUser me) {
final ChatMessage message =
new ChatMessage(sender: me, text: _currentMessage);
_messages.add(message);
_currentMessage = '';
});
The view uses a TextEditingController as a controller for the TextField Widget so I understand why it is not updated.
How can we empty the TextField from the Store with flutter_flux?
EDIT: The flutter_flux example has been updated since I posted this answer, and it now correctly discards message in the TextField but in a better way. You should check it out.
I think the correct way would be to move the TextEditingController to the ChatMessageStore, instead of simply keeping the currentMessage in that store. Then you would be able to empty the text field by calling clear() on the TextEditingController.
Generally speaking, the state values which would normally be kept in FooState in vanilla flutter would go into a Store when using flutter_flux. Since you would normally create and keep a TextEditingController in a State, I think it's more natural to keep it in a Store anyways.
The updated ChatMessageStore would look something like this:
class ChatMessageStore extends Store {
ChatMessageStore() {
triggerOnAction(commitCurrentMessageAction, (ChatUser me) {
final ChatMessage message =
new ChatMessage(sender: me, text: currentMessage);
_messages.add(message);
_msgController.clear();
});
}
final List<ChatMessage> _messages = <ChatMessage>[];
final TextEditingController _msgController = new TextEditingController();
List<ChatMessage> get messages =>
new List<ChatMessage>.unmodifiable(_messages);
TextEditingController get msgController => _msgController;
String get currentMessage => _msgController.text;
bool get isComposing => currentMessage.isNotEmpty;
}
Note that we no longer need the setCurrentMessageAction, as the TextEditingController would take care of the text value change itself.
Then, the msgController defined in ChatScreen widget could be removed and the _buildTextComposer could be updated accordingly.
Widget _buildTextComposer(BuildContext context, ChatMessageStore messageStore,
ChatUserStore userStore) {
final ValueChanged<String> commitMessage = (String _) {
commitCurrentMessageAction(userStore.me);
};
ThemeData themeData = Theme.of(context);
return new Row(children: <Widget>[
new Flexible(
child: new TextField(
key: const Key("msgField"),
controller: messageStore.msgController,
decoration: const InputDecoration(hintText: 'Enter message'),
onSubmitted: commitMessage)),
new Container(
margin: new EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton(
icon: new Icon(Icons.send),
onPressed:
messageStore.isComposing ? () => commitMessage(null) : null,
color: messageStore.isComposing
? themeData.accentColor
: themeData.disabledColor))
]);
}
Hope this helps.