| 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 | ``` |