Flutter Visual Testing
Platform: mobile-flutter
Applicable Recipes: flutter-firebase
Primary Tools: Flutter Golden Tests, Alchemist, Widgetbook, Fastlane
🔄 Tight Loop (Default)
Goal: Catch UI regressions immediately via goldens before doing full device screenshot runs.
Start Small:
- •Widgets: Add/update goldens for 1–3 widgets/screens changed in this story
- •Themes: Cover light + dark if the app supports both
- •Sizes: One "phone-sized" constraint first; add tablet only if layout changes
Run Order:
- •Run goldens:
flutter test test/goldens/ - •If failures: Fix UI or update baselines (only when change is intended)
- •Only then (optional): Capture emulator/simulator screenshots for app-store visuals
Expand Only When:
- •Story explicitly covers tablet layouts
- •Need app store screenshots (Fastlane)
- •Epic validation requires device matrix
🧪 Golden Tests
Flutter's golden testing captures widget renders as images and compares against baselines.
Basic Golden Test
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/button.dart';
void main() {
testWidgets('MyButton golden', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MyButton(label: 'Click me'),
),
),
);
await expectLater(
find.byType(MyButton),
matchesGoldenFile('goldens/my_button.png'),
);
});
}
Running Golden Tests
# Run golden tests flutter test test/goldens/ # Update goldens when changes are intentional flutter test --update-goldens test/goldens/
🎨 Theme Variation Testing
Test both light and dark themes:
void main() {
for (final brightness in [Brightness.light, Brightness.dark]) {
testWidgets('MyWidget ${brightness.name} theme', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(brightness: brightness),
home: MyWidget(),
),
);
await expectLater(
find.byType(MyWidget),
matchesGoldenFile('goldens/my_widget_${brightness.name}.png'),
);
});
}
}
📱 Device Size Constraints
Test different screen sizes:
void main() {
final devices = {
'phone': Size(375, 667),
'tablet': Size(768, 1024),
};
for (final entry in devices.entries) {
testWidgets('Screen on ${entry.key}', (tester) async {
tester.view.physicalSize = entry.value;
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
await tester.pumpWidget(MyApp());
await expectLater(
find.byType(MyApp),
matchesGoldenFile('goldens/screen_${entry.key}.png'),
);
});
}
}
⚗️ Alchemist (Advanced Golden Testing)
Alchemist provides more sophisticated golden testing with scenarios:
import 'package:alchemist/alchemist.dart';
void main() {
goldenTest(
'MyButton states',
fileName: 'my_button_states',
builder: () => GoldenTestGroup(
children: [
GoldenTestScenario(
name: 'default',
child: MyButton(label: 'Default'),
),
GoldenTestScenario(
name: 'disabled',
child: MyButton(label: 'Disabled', enabled: false),
),
GoldenTestScenario(
name: 'loading',
child: MyButton(label: 'Loading', loading: true),
),
],
),
);
}
📲 Emulator/Simulator Screenshots
For app store or integration testing, capture from real devices:
iOS Simulator
# List available simulators xcrun simctl list devices # Boot a simulator xcrun simctl boot "iPhone 15 Pro" # Take screenshot xcrun simctl io booted screenshot ~/Desktop/screenshot.png # With status bar cleaned up xcrun simctl status_bar booted override --time "9:41" --batteryState charged --batteryLevel 100 xcrun simctl io booted screenshot screenshot.png
Android Emulator
# List available emulators emulator -list-avds # Start emulator emulator -avd Pixel_8_API_34 & # Take screenshot adb exec-out screencap -p > screenshot.png # Set demo mode for clean status bar adb shell settings put global sysui_demo_allowed 1 adb shell am broadcast -a com.android.systemui.demo -e command clock -e hhmm 0941 adb exec-out screencap -p > screenshot.png
📸 Fastlane Screenshots
For automated app store screenshots:
iOS (Fastlane Snapshot)
# Snapfile
devices([
"iPhone 15 Pro Max",
"iPhone SE (3rd generation)",
"iPad Pro (12.9-inch)"
])
languages(["en-US"])
scheme("MyApp")
output_directory("./screenshots")
clear_previous_screenshots(true)
fastlane snapshot
Android (Fastlane Screengrab)
# Screengrabfile
locales(['en-US'])
app_package_name('com.example.myapp')
app_apk_path('build/app/outputs/apk/debug/app-debug.apk')
tests_apk_path('build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk')
fastlane screengrab
✅ Validation Checklist
Verify these for each validation:
- • Golden tests pass for changed widgets
- • Light and dark themes tested
- • Key screen sizes covered (phone at minimum)
- • No golden file regressions
- • Widget states tested (default, disabled, loading, error)
- • Text renders correctly (no overflow)
- • Colors match design system tokens
📁 Golden File Organization
test/ ├── goldens/ │ ├── widgets/ │ │ ├── button_light.png │ │ ├── button_dark.png │ │ └── button_states.png │ ├── screens/ │ │ ├── home_phone.png │ │ ├── home_tablet.png │ │ └── settings.png │ └── components/ │ └── header.png
🔗 Widgetbook Integration
If using Widgetbook for component documentation:
# Run Widgetbook flutter run -d chrome -t lib/widgetbook.dart
Capture screenshots of catalogued components for comprehensive coverage.