點擊、拖曳與輸入文字
如何測試元件(Widget)的使用者互動。
許多元件 (Widget) 不僅用來顯示資訊,也會回應使用者的互動。這包括可點擊的按鈕,以及用於輸入文字的 TextField。
為了測試這些互動行為,你需要在測試環境中模擬這些操作。為此,請使用 WidgetTester
函式庫。
WidgetTester 提供了輸入文字、點擊以及拖曳等方法。
在許多情況下,使用者互動會更新應用程式的狀態 (state)。在測試環境中,Flutter 並不會在狀態變更時自動重建元件。為了確保在模擬使用者互動後元件樹會被重建,請呼叫 WidgetTester
所提供的 pump()
或 pumpAndSettle()
方法。
本教學將採用以下步驟:
- 建立要測試的元件 (Widget)。
- 在文字欄位 (text field) 中輸入文字。
- 確認點擊按鈕會新增待辦事項。
- 確認滑動刪除會移除待辦事項。
1. 建立要測試的元件(Widget)
#在這個範例中, 我們會建立一個基本的待辦事項 (todo) 應用程式,並測試三個功能:
- 在
TextField中輸入文字。 - 點擊
FloatingActionButton,將文字新增到待辦清單中。 - 透過滑動刪除,將項目從清單中移除。
為了聚焦於測試, 本教學不會詳細說明如何建置這個待辦事項應用程式。 若想進一步了解此應用程式的建構方式, 請參考以下相關教學:
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. 在文字欄位 (text field) 輸入文字
#
現在你已經有了一個待辦事項應用程式,開始撰寫測試吧。
首先,將文字輸入到 TextField 中。
你可以透過以下步驟完成這個任務:
- 在測試環境中建置元件 (Widget)。
-
使用
WidgetTester的enterText()方法。
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. 確認點擊按鈕會新增待辦事項
#在將文字輸入到 TextField 之後,請確認點擊 FloatingActionButton 會將項目新增到清單中。
這包含三個步驟:
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. 確認滑動刪除能移除待辦事項
#最後,請確認對待辦事項項目執行滑動刪除(swipe-to-dismiss)操作時,該項目會從清單中移除。這包含三個步驟:
-
使用
drag()方法來執行滑動刪除操作。 -
使用
pumpAndSettle()方法,持續重建元件樹,直到刪除動畫完成。 - 確認該項目已不再顯示於螢幕上。
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));
// 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);
});
完整範例
#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));
// 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),
),
),
);
}
}
Unless stated otherwise, the documentation on this site reflects Flutter 3.44.0. Page last updated on 2026-06-14. View source or report an issue.