点击、拖动和输入文本

许多小部件不仅显示信息,还可以响应用户交互。 这包括可以点击的按钮,以及 TextField 用于输入文本。

要测试这些交互,您需要一种在测试环境中模拟它们的方法。 为此,请使用 WidgetTester 库。

WidgetTester 提供了用于输入文本、点击和拖动的函数。

在许多情况下,用户交互会更新应用程序的状态。 在测试环境中,Flutter 不会在状态更改时自动重建小部件。 为了确保在模拟用户交互后重建小部件树,请调用 pump()pumpAndSettle() 函数,这些函数由 WidgetTester 提供。 此食谱使用以下步骤

  1. 创建一个要测试的小部件。
  2. 在文本字段中输入文本。
  3. 确保点击按钮会添加待办事项。
  4. 确保滑动删除会删除待办事项。

1. 创建一个要测试的小部件

#

对于此示例,创建一个基本的待办事项应用程序,该应用程序测试三个功能

  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. 在测试环境中构建小部件。
  2. 使用 enterText() 函数从 WidgetTester 中。
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. 使用 tap() 函数点击添加按钮。
  2. 在状态更改后使用 pump() 函数重建小部件。
  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. 使用 drag() 方法执行滑动删除操作。
  2. 使用 pumpAndSettle() 方法持续重建小部件树,直到删除动画完成。
  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),
        ),
      ),
    );
  }
}