main
md 154 lines 6.23 KB
Rendered Raw
1 ---
2 name: flutter-add-widget-test
3 description: Implement a component-level test using `WidgetTester` to verify UI rendering and user interactions (tapping, scrolling, entering text). Use when validating that a specific widget displays correct data and responds to events as expected.
4 metadata:
5 model: models/gemini-3.1-pro-preview
6 last_modified: Tue, 21 Apr 2026 21:15:41 GMT
7 ---
8 # Writing Flutter Widget Tests
9
10 ## Contents
11 - [Setup & Configuration](#setup--configuration)
12 - [Core Components](#core-components)
13 - [Workflow: Implementing a Widget Test](#workflow-implementing-a-widget-test)
14 - [Interaction & State Management](#interaction--state-management)
15 - [Examples](#examples)
16
17 ## Setup & Configuration
18
19 Ensure the testing environment is properly configured before authoring widget tests.
20
21 1. Add the `flutter_test` dependency to the `dev_dependencies` section of `pubspec.yaml`.
22 2. Place all test files in the `test/` directory at the root of the project.
23 3. Suffix all test file names with `_test.dart` (e.g., `widget_test.dart`).
24
25 ## Core Components
26
27 Utilize the following `flutter_test` components to interact with and validate the widget tree:
28
29 * **`WidgetTester`**: The primary interface for building and interacting with widgets in the test environment. Provided automatically by the `testWidgets()` function.
30 * **`Finder`**: Locates widgets in the test environment (e.g., `find.text('Submit')`, `find.byType(TextField)`, `find.byKey(Key('submit_btn'))`).
31 * **`Matcher`**: Verifies the presence or state of widgets located by a `Finder` (e.g., `findsOneWidget`, `findsNothing`, `findsNWidgets(2)`, `matchesGoldenFile`).
32
33 ## Workflow: Implementing a Widget Test
34
35 Copy the following checklist to track progress when implementing a new widget test.
36
37 ### Task Progress
38 - [ ] **Step 1: Define the test.** Use `testWidgets('description', (WidgetTester tester) async { ... })`.
39 - [ ] **Step 2: Build the widget.** Call `await tester.pumpWidget(MyWidget())` to render the UI. Wrap the widget in a `MaterialApp` or `Directionality` widget if it requires inherited directional or theme data.
40 - [ ] **Step 3: Locate elements.** Instantiate `Finder` objects for the target widgets.
41 - [ ] **Step 4: Verify initial state.** Use `expect(finder, matcher)` to validate the initial render.
42 - [ ] **Step 5: Simulate interactions.** Execute gestures or inputs (e.g., `await tester.tap(buttonFinder)`).
43 - [ ] **Step 6: Rebuild the tree.** Call `await tester.pump()` or `await tester.pumpAndSettle()` to process state changes.
44 - [ ] **Step 7: Verify updated state.** Use `expect()` to validate the UI after the interaction.
45 - [ ] **Step 8: Run and validate.** Execute `flutter test test/your_test_file_test.dart`.
46 - [ ] **Step 9: Feedback Loop.** Review test output -> identify failing matchers -> adjust widget logic or test assertions -> re-run until passing.
47
48 ## Interaction & State Management
49
50 Apply the following conditional logic based on the type of interaction or state change being tested:
51
52 * **If testing static rendering:** Call `await tester.pumpWidget()` once, then immediately run `expect()` assertions.
53 * **If testing standard state changes (e.g., button taps):**
54 1. Call `await tester.tap(finder)`.
55 2. Call `await tester.pump()` to trigger a single frame rebuild.
56 * **If testing animations, transitions, or asynchronous UI updates:**
57 1. Trigger the action (e.g., `await tester.drag(finder, Offset(500, 0))`).
58 2. Call `await tester.pumpAndSettle()` to repeatedly pump frames until no more frames are scheduled (animation completes).
59 * **If testing text input:** Call `await tester.enterText(textFieldFinder, 'Input string')`.
60 * **If testing items in a dynamic or long list:** Call `await tester.scrollUntilVisible(itemFinder, 500.0, scrollable: listFinder)` to ensure the target widget is rendered before interacting with it.
61
62 ## Examples
63
64 ### High-Fidelity Widget Test Implementation
65
66 **Target Widget (`lib/todo_list.dart`):**
67 ```dart
68 import 'package:flutter/material.dart';
69
70 class TodoList extends StatefulWidget {
71 const TodoList({super.key});
72
73 @override
74 State<TodoList> createState() => _TodoListState();
75 }
76
77 class _TodoListState extends State<TodoList> {
78 final todos = <String>[];
79 final controller = TextEditingController();
80
81 @override
82 Widget build(BuildContext context) {
83 return MaterialApp(
84 home: Scaffold(
85 body: Column(
86 children: [
87 TextField(controller: controller),
88 Expanded(
89 child: ListView.builder(
90 itemCount: todos.length,
91 itemBuilder: (context, index) {
92 final todo = todos[index];
93 return Dismissible(
94 key: Key('$todo$index'),
95 onDismissed: (_) => setState(() => todos.removeAt(index)),
96 child: ListTile(title: Text(todo)),
97 );
98 },
99 ),
100 ),
101 ],
102 ),
103 floatingActionButton: FloatingActionButton(
104 onPressed: () {
105 setState(() {
106 todos.add(controller.text);
107 controller.clear();
108 });
109 },
110 child: const Icon(Icons.add),
111 ),
112 ),
113 );
114 }
115 }
116 ```
117
118 **Test Implementation (`test/todo_list_test.dart`):**
119 ```dart
120 import 'package:flutter/material.dart';
121 import 'package:flutter_test/flutter_test.dart';
122 import 'package:my_app/todo_list.dart';
123
124 void main() {
125 testWidgets('Add and remove a todo item', (WidgetTester tester) async {
126 // 1. Build the widget
127 await tester.pumpWidget(const TodoList());
128
129 // 2. Verify initial state
130 expect(find.byType(ListTile), findsNothing);
131
132 // 3. Enter text into the TextField
133 await tester.enterText(find.byType(TextField), 'Buy groceries');
134
135 // 4. Tap the add button
136 await tester.tap(find.byType(FloatingActionButton));
137
138 // 5. Rebuild the widget to reflect the new state
139 await tester.pump();
140
141 // 6. Verify the item was added
142 expect(find.text('Buy groceries'), findsOneWidget);
143
144 // 7. Swipe the item to dismiss it
145 await tester.drag(find.byType(Dismissible), const Offset(500, 0));
146
147 // 8. Build the widget until the dismiss animation ends
148 await tester.pumpAndSettle();
149
150 // 9. Verify the item was removed
151 expect(find.text('Buy groceries'), findsNothing);
152 });
153 }
154 ```