Tap, drag, and enter text
Many widgets not only display information, but also respond
to user interaction. This includes buttons that can be tapped,
and TextField
for entering text.
To test these interactions, you need a way to simulate them
in the test environment. For this purpose, use the
WidgetTester
library.
The WidgetTester
provides methods for entering text,
tapping, and dragging.
In many cases, user interactions update the state of the app. In the test
environment, Flutter doesn’t automatically rebuild widgets when the state
changes. To ensure that the widget tree is rebuilt after simulating a user
interaction, call the pump()
or pumpAndSettle()
methods provided by the WidgetTester
.
This recipe uses the following steps:
- Create a widget to test.
- Enter text in the text field.
- Ensure tapping a button adds the todo.
- Ensure swipe-to-dismiss removes the todo.
1. Create a widget to test
For this example, create a basic todo app that tests three features:
- Entering text into a
TextField
. - Tapping a
FloatingActionButton
to add the text to a list of todos. - Swiping-to-dismiss to remove the item from the list.
To keep the focus on testing, this recipe won’t provide a detailed guide on how to build the todo app. To learn more about how this app is built, see the relevant recipes:
class TodoList extends StatefulWidget {
const TodoList({super.key});
@override
State<TodoList> createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
static const _appTitle = 'Todo List';
final todos = <String>[];
final controller = TextEditingController();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _appTitle,
home: Scaffold(
appBar: AppBar(
title: const Text(_appTitle),
),
body: Column(
children: [
TextField(
controller: controller,
),
Expanded(
child: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return Dismissible(
key: Key('$todo$index'),
onDismissed: (direction) => todos.removeAt(index),
background: Container(color: Colors.red),
child: ListTile(title: Text(todo)),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
todos.add(controller.text);
controller.clear();
});
},
child: const Icon(Icons.add),
),
),
);
}
}
2. Enter text in the text field
Now that you have a todo app, begin writing the test.
Start by entering text into the TextField
.
Accomplish this task by:
- Building the widget in the test environment.
- Using the
enterText()
method from theWidgetTester
.
testWidgets('Add and remove a todo', (tester) async {
// Build the widget
await tester.pumpWidget(const TodoList());
// Enter 'hi' into the TextField.
await tester.enterText(find.byType(TextField), 'hi');
});
3. Ensure tapping a button adds the todo
After entering text into the TextField
, ensure that tapping
the FloatingActionButton
adds the item to the list.
This involves three steps:
- Tap the add button using the
tap()
method. - Rebuild the widget after the state has changed using the
pump()
method. - Ensure that the list item appears on screen.
testWidgets('Add and remove a todo', (tester) async {
// Enter text code...
// Tap the add button.
await tester.tap(find.byType(FloatingActionButton));
// Rebuild the widget after the state has changed.
await tester.pump();
// Expect to find the item on screen.
expect(find.text('hi'), findsOneWidget);
});
4. Ensure swipe-to-dismiss removes the todo
Finally, ensure that performing a swipe-to-dismiss action on the todo item removes it from the list. This involves three steps:
- Use the
drag()
method to perform a swipe-to-dismiss action. - Use the
pumpAndSettle()
method to continually rebuild the widget tree until the dismiss animation is complete. - Ensure that the item no longer appears on screen.
testWidgets('Add and remove a todo', (tester) async {
// Enter text and add the item...
// Swipe the item to dismiss it.
await tester.drag(find.byType(Dismissible), const Offset(500.0, 0.0));
// Build the widget until the dismiss animation ends.
await tester.pumpAndSettle();
// Ensure that the item is no longer on screen.
expect(find.text('hi'), findsNothing);
});
Complete example
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Add and remove a todo', (tester) async {
// Build the widget.
await tester.pumpWidget(const TodoList());
// Enter 'hi' into the TextField.
await tester.enterText(find.byType(TextField), 'hi');
// Tap the add button.
await tester.tap(find.byType(FloatingActionButton));
// Rebuild the widget with the new item.
await tester.pump();
// Expect to find the item on screen.
expect(find.text('hi'), findsOneWidget);
// Swipe the item to dismiss it.
await tester.drag(find.byType(Dismissible), const Offset(500.0, 0.0));
// Build the widget until the dismiss animation ends.
await tester.pumpAndSettle();
// Ensure that the item is no longer on screen.
expect(find.text('hi'), findsNothing);
});
}
class TodoList extends StatefulWidget {
const TodoList({super.key});
@override
State<TodoList> createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
static const _appTitle = 'Todo List';
final todos = <String>[];
final controller = TextEditingController();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _appTitle,
home: Scaffold(
appBar: AppBar(
title: const Text(_appTitle),
),
body: Column(
children: [
TextField(
controller: controller,
),
Expanded(
child: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return Dismissible(
key: Key('$todo$index'),
onDismissed: (direction) => todos.removeAt(index),
background: Container(color: Colors.red),
child: ListTile(title: Text(todo)),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
todos.add(controller.text);
controller.clear();
});
},
child: const Icon(Icons.add),
),
),
);
}
}