skills/flutter/flutter-add-widget-test/SKILL.md
Write widget tests using WidgetTester with pump patterns, finder APIs, and key-based targeting. Use when testing UI components, user interactions, or verifying widget rendering and state changes.
npx skillsauth add dhruvanbhalara/skills flutter-add-widget-testInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
flutter_test is an SDK dependency, so no pub add is needed.test/<mirror_path>/<widget>_test.dart.import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
testWidgets('description', (WidgetTester tester) async { ... }) for all widget tests.MaterialApp to provide MediaQuery, Theme, and Navigator context.| API | Purpose |
|---|---|
| tester.pumpWidget(widget) | Render widget into test environment |
| tester.pump() | Trigger a single frame |
| tester.pump(Duration(...)) | Advance by specific duration |
| tester.pumpAndSettle() | Wait for all animations to complete |
| tester.tap(finder) | Simulate tap gesture |
| tester.longPress(finder) | Simulate long press |
| tester.enterText(finder, 'text') | Type into text field |
| tester.drag(finder, Offset(dx, dy)) | Simulate drag gesture |
| tester.scrollUntilVisible(finder, delta) | Scroll until widget is visible |
Use finders to locate widgets in the test tree. Prefer Key-based finders for stability.
find.byKey(const ValueKey('login_button')) — Preferred. Most stable across refactors.find.byType(ElevatedButton) — By widget type. Fails if multiple instances exist.find.text('Submit') — By displayed text. Avoid with localized strings.find.byIcon(Icons.add) — By icon data.find.descendant(of: parentFinder, matching: childFinder) — Nested lookup.find.ancestor(of: childFinder, matching: parentFinder) — Reverse lookup.Key Naming Convention: Use Key('feature_action_id') format on interactive widgets.
// Production code
ElevatedButton(
key: const Key('login_submit_button'),
onPressed: _onSubmit,
child: const Text('Login'),
)
// Test code
final submitButton = find.byKey(const Key('login_submit_button'));
await tester.tap(submitButton);
testWidgets('increments counter on tap', (tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byKey(const Key('increment_button')));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
testWidgets('validates email field', (tester) async {
await tester.pumpWidget(const MaterialApp(home: LoginForm()));
await tester.enterText(find.byKey(const Key('email_field')), 'invalid');
await tester.tap(find.byKey(const Key('submit_button')));
await tester.pumpAndSettle();
expect(find.text('Enter a valid email'), findsOneWidget);
});
testWidgets('finds item in long list', (tester) async {
await tester.pumpWidget(const MaterialApp(home: ItemListPage()));
final listFinder = find.byType(Scrollable);
final itemFinder = find.byKey(const Key('item_99'));
await tester.scrollUntilVisible(itemFinder, 500.0, scrollable: listFinder);
expect(itemFinder, findsOneWidget);
});
Choose the right pump method based on your scenario:
| Scenario | Method | Why |
|---|---|---|
| Simple state change | pump() | Single frame is enough |
| Animation completes | pumpAndSettle() | Waits for all frames |
| Timed animation | pump(Duration(milliseconds: 300)) | Advance specific time |
| Infinite animation (e.g., CircularProgressIndicator) | pump() | pumpAndSettle() will timeout |
| Debounced input | pump(Duration(milliseconds: 500)) | Wait for debounce period |
WARNING: pumpAndSettle() throws PumpAndSettleTimedOutException on infinite animations. Use pump() instead when testing loading states.
When testing widgets that depend on BLoC/Cubit:
testWidgets('shows user name from BLoC', (tester) async {
final mockBloc = MockUserBloc();
whenListen(
mockBloc,
Stream.fromIterable([UserLoaded(User(name: 'Alice'))]),
initialState: UserInitial(),
);
await tester.pumpWidget(
MaterialApp(
home: BlocProvider<UserBloc>.value(
value: mockBloc,
child: const UserProfilePage(),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Alice'), findsOneWidget);
});
MockBloc / MockCubit from bloc_test package.whenListen() to stub state stream responses.BlocProvider.value() to inject mock into the widget tree.| Error | Cause | Fix |
|---|---|---|
| No MediaQuery widget ancestor | Missing MaterialApp wrapper | Wrap in MaterialApp(home: ...) |
| A RenderFlex overflowed | Widget exceeds test viewport | Constrain with SizedBox or Expanded |
| Vertical viewport was given unbounded height | ListView without height constraint | Wrap in SizedBox(height: 600) |
| Widget not found after navigation | Missing pumpAndSettle() | Add await tester.pumpAndSettle() after navigation |
| PumpAndSettleTimedOutException | Infinite animation running | Use pump() instead of pumpAndSettle() |
Keys to interactive widgets in production code.test/<mirror_path>/<widget>_test.dart.MaterialApp and call tester.pumpWidget().byKey preferred).tap, enterText, drag).expect(finder, findsOneWidget) or state checks.flutter-testing).flutter test test/path/to/widget_test.dart.import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/features/counter/counter_page.dart';
void main() {
group('$CounterPage', () {
testWidgets('renders initial counter value', (tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
expect(find.text('0'), findsOneWidget);
expect(find.byType(FloatingActionButton), findsOneWidget);
});
testWidgets('increments counter when FAB is tapped', (tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
await tester.tap(find.byType(FloatingActionButton));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
});
}
development
Perform REST API networking operations (GET, POST, PUT, DELETE) using the lightweight and robust standard `http` package, including platform configurations and background parsing models.
development
Configure internationalization and localization support using Flutter's built-in l10n system, App Resource Bundle (ARB) files, and ICU formatting syntax.
development
Create model classes with fromJson/toJson using dart:convert and Dart 3 pattern matching. Use when manually mapping JSON to classes, parsing HTTP responses, or choosing between manual and code-generated serialization.
data-ai
Diagnose and fix Flutter layout constraint violations (RenderFlex overflow, unbounded height/width, ParentData misuse). Use when encountering layout exceptions, yellow-black overflow stripes, or red error screens.