點選、拖拽事件和文字輸入
我們建立的大部分 widget 不僅僅需要展示訊息,還需要回應使用者互動。常見的互動有用戶點選按鈕、在螢幕上拖動元件和在 TextField
中輸入文字。
為了測試這些互動,我們需要在測試環境中模擬上述場景,可以使用 WidgetTester
函式庫。
WidgetTester
提供了文字輸入、點選、拖動的相關方法:
在很多情況下,使用者互動會更新應用狀態。在測試環境中,Flutter 在狀態發生改變的時候並不會自動重建 widget。為了保證模擬使用者互動實現後,widget 樹能重建,一定要呼叫 WidgetTester
提供的
pump()
或 pumpAndSettle()
。
步驟
-
建立待測 Widget
-
在文字區輸入文字
-
點選按鈕,增加待辦清單項
-
滑動刪除待辦清單項
1. 建立待測 Widget
在這個範例中,我們將會建立一個簡單的待辦清單應用。其中有三個主要的功能點需要測試:
-
往
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. 在文字區輸入文字
我們有了一個待辦清單項應用以後,就可以開始編寫測試用例了。在本範例中,我們會先測試在文字區輸入文字。
完成這項任務,需要做到:
-
在測試環境建立 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. 滑動刪除待辦清單項
最後,我們需要確保滑動刪除的操作能夠正常地從清單中移除清單項。這包含了三個步驟:
-
使用
drag()
方法模擬滑動刪除操作。 -
使用
pumpAndSettle()
方法使 widget 樹保持重建更新,直到消除的動畫完成。 -
確保上述清單項不會再出現在螢幕上
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),
),
),
);
}
}