Install
$ agentstack add skill-dhruvanbhalara-skills-flutter-add-widget-test ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Contents
- [Setup](#setup)
- [Core APIs](#core-apis)
- [Finder Patterns](#finder-patterns)
- [Interaction Patterns](#interaction-patterns)
- [Pump Strategies](#pump-strategies)
- [Testing with BLoC](#testing-with-bloc)
- [Common Pitfalls](#common-pitfalls)
- [Workflow: Adding a Widget Test](#workflow-adding-a-widget-test)
- [Examples](#examples)
Setup
flutter_testis an SDK dependency, so nopub addis needed.- Test file naming:
test//_test.dart. - Every test file starts with:
``dart import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; ``
- Use
testWidgets('description', (WidgetTester tester) async { ... })for all widget tests. - Always wrap the widget under test in
MaterialAppto provideMediaQuery,Theme, andNavigatorcontext.
Core APIs
| 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 |
Finder Patterns
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);
Interaction Patterns
Tap and Verify State Change
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);
});
Enter Text and Validate Form
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);
});
Scroll to Off-Screen Widget
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);
});
Pump Strategies
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.
Testing with BLoC
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.value(
value: mockBloc,
child: const UserProfilePage(),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Alice'), findsOneWidget);
});
- Use
MockBloc/MockCubitfrombloc_testpackage. - Use
whenListen()to stub state stream responses. - Use
BlocProvider.value()to inject mock into the widget tree.
Common Pitfalls
| 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() |
Workflow: Adding a Widget Test
Task Progress
- [ ] Step 1: Add
Keys to interactive widgets in production code. - [ ] Step 2: Create test file at
test//_test.dart. - [ ] Step 3: Wrap widget in
MaterialAppand calltester.pumpWidget(). - [ ] Step 4: Use appropriate finder (
byKeypreferred). - [ ] Step 5: Simulate interactions (
tap,enterText,drag). - [ ] Step 6: Assert with
expect(finder, findsOneWidget)or state checks. - [ ] Step 7: Apply Golden Variant / State Matrix / Interaction Contract pattern (see
flutter-testing). - [ ] Step 8: Run
flutter test test/path/to/widget_test.dart. - [ ] Step 9: Feedback Loop — fix failures → re-run until green.
Examples
Minimal Widget Test
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);
});
});
}
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: dhruvanbhalara
- Source: dhruvanbhalara/skills
- License: MIT
- Homepage: https://dhruvanbhalara.github.io/skills/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.