跳到主內容

點選、拖動和輸入文字

如何測試使用者互動的 Widget。

許多 Widget 不僅顯示資訊,還會響應使用者互動。這包括可以點選的按鈕,以及 TextField 用於輸入文字。

要測試這些互動,您需要在測試環境中模擬它們。為此,請使用 WidgetTester 庫。

WidgetTester 提供了用於輸入文字、點選和拖動的函式。

在許多情況下,使用者互動會更新應用程式的狀態。在測試環境中,Flutter 不會自動在狀態更改時重建 Widget。為了確保在模擬使用者互動後重建 Widget 樹,請呼叫 WidgetTester 提供的 pump()pumpAndSettle() 函式。本教程使用以下步驟

  1. 建立一個用於測試的 Widget。
  2. 在文字欄位中輸入文字。
  3. 確保點選按鈕會新增待辦事項。
  4. 確保滑動刪除會移除待辦事項。

1. 建立一個用於測試的 Widget

#

對於這個例子,建立一個基本的待辦事項應用程式來測試三個功能

  1. 將文字輸入到 TextField 中。
  2. 點選 FloatingActionButton 將文字新增到待辦事項列表中。
  3. 滑動刪除從列表中移除專案。

為了專注於測試,本教程不會提供有關如何構建待辦事項應用程式的詳細指南。要了解有關此應用程式構建方式的更多資訊,請參閱相關教程

dart
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. 在文字欄位中輸入文字

#

現在您已經有了待辦事項應用程式,開始編寫測試。首先將文字輸入到 TextField 中。

透過以下方式完成此任務

  1. 在測試環境中構建 Widget。
  2. 使用 WidgetTesterenterText() 函式。
dart
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 會將專案新增到列表中。

這涉及三個步驟

  1. 使用 WidgetControllertap() 函式點選新增按鈕。
  2. 使用 WidgetTesterpump() 函式在狀態更改後重建 Widget。
  3. 確保列表項出現在螢幕上。
dart
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. 確保滑動刪除會移除待辦事項

#

最後,確保執行待辦事項項上的滑動刪除操作會將其從列表中移除。這涉及三個步驟

  1. 使用 WidgetControllerdrag() 函式執行滑動刪除操作。
  2. 使用 WidgetTesterpumpAndSettle() 函式持續重建 Widget 樹,直到完成刪除動畫。
  3. 確保該專案不再出現在螢幕上。
dart
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);
});

完整示例

#
dart
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),
        ),
      ),
    );
  }
}