How to Test Tutorial Walkthrough on Flutter (Complete Guide)
Testing the tutorial walkthrough of a Flutter app is not a nice‑to‑have checkbox; it directly influences first‑time user activation, retention, and the perception of product quality. When a tutorial f
How to Test Tutorial Walkthrough on Flutter (Complete Guide): Why It Matters
Testing the tutorial walkthrough of a Flutter app is not a nice‑to‑have checkbox; it directly influences first‑time user activation, retention, and the perception of product quality. When a tutorial fails—whether by skipping a step, showing stale assets, or blocking interaction—users abandon the app before they ever experience core value. In production, tutorial bugs surface as spikes in day‑one churn, increased support tickets about “I can’t get past the intro,” and negative reviews that mention “confusing onboarding.” Because tutorials often combine animations, asset loading, conditional logic (e.g., show‑only‑once flags), and platform‑specific overlays, they are a fragile integration point that unit tests rarely catch. A dedicated test strategy that covers happy paths, error conditions, accessibility, and persona‑driven exploration is therefore essential for any Flutter team that ships to real users.
How to Test Tutorial Walkthrough on Flutter (Complete Guide): Building a Test Matrix
A comprehensive test matrix ensures you exercise every tutorial walkthroughs helps you prioritize effort and avoid blind spots. Below is a table that maps test categories to specific conditions, expected outcomes, and the tooling best suited to verify each.
| Test Category | Sub‑condition / Example | Expected Outcome | Recommended Verification Method |
|---|---|---|---|
| Happy Path | User taps “Next” through all slides, sees final “Get Started” button, taps it to enter main app | All slides display correctly, animations finish, navigation to home screen succeeds | Widget test for each slide + integration test for full flow |
| Error Paths | Network failure while loading tutorial image, asset missing, sudden orientation change, low‑memory kill | Graceful fallback (placeholder image or retry), UI remains usable, tutorial resumes after recovery | Integration test with mocked network, simulation of orientation change via WidgetTester.binding.window.devicePixelRatioTest, low‑memory simulation using TestAsyncUtils |
| Edge Cases | User interrupts tutorial with system notification, switches apps, enables TalkBack mid‑tutorial, device language changes while tutorial is showing | Tutorial pauses/resumes correctly, accessibility announcements are accurate, language updates without restart | Manual observation + accessibility scanner (e.g., flutter_lints + axe_core via web view), interruption simulation using integration_test with await Future.delayed |
| Accessibility (WCAG) | Contrast ratio of text on background, touch target size ≥48dp, screen reader labels, logical reading order | All WCAG AA criteria met for tutorial screens | Automated audit with flutter_launcher_icons + accessibility_test package, manual TalkBack verification |
| Security / Privacy | Tutorial shows personal data (e.g., pre‑filled email from debug build), logs tutorial steps to analytics without consent | No PII exposed, analytics opt‑in respected | Static analysis with flutter pub pub publish --dry-run, runtime check using SharedPreferences flag for consent |
| Performance | Frame drop >16ms during animation, excessive GPU usage, jank on low‑end devices | Smooth 60fps (or device‑specific target) animation, CPU <30% during tutorial | flutter profile + flutter_driver timeline, benchmark_test package |
| Localization | Tutorial text overflows in languages with longer strings (German, Finnish), right‑to‑left layout for Arabic | Text wraps, layout mirrors correctly, no clipped content | Golden tests per locale, flutter_localizations with Locale overrides |
| Device Fragmentation | Tutorial runs on foldable (multi‑window), tablet with split‑screen, Android TV (remote input) | Layout adapts, controls reachable, navigation works with D‑pad or remote | Test on emulators/tablets, use MediaQuery to verify breakpoints, remote input simulation via android.inputmethodservice |
The matrix above gives you a concrete checklist you can translate into test cases, automation scripts, or exploratory charters. Each row can be expanded into a sub‑matrix of variations (e.g., three network states: online, offline, flaky).
How to Test Tutorial Walkthrough on Flutter (Complete Guide): Manual Approach Step‑by‑Step
Even with strong automation, manual verification remains valuable for catching UX nuances that scripts ignore—such as the feel of an animation, the clarity of microcopy, or the reaction to an unexpected system event. Follow this step‑by‑step procedure on a clean emulator or physical device to ensure you don’t miss regressions.
1. Prepare a pristine environment
- Flutter version:
flutter --version≥ 3.13 (stable). - Create a new emulator:
avdmanager create avd -n flutter_test -k "system-images;android-34;google_apis;x86_64"(or use a real device with developer options enabled). - Clear app data:
adb uninstall com.example.tutorialapp; adb shell pm clear com.example.tutorialapp. - Disable instant run / hot reload to force a full rebuild:
flutter run --release.
2. Capture the intended tutorial flow
- Open the app and note each screen’s purpose (welcome, permission request, feature highlight, CTA).
- Take screenshots or record a short video (
adb shell screenrecord /sdcard/tutorial.mp4). - Write down the expected UI identifiers (e.g.,
Key('tutorial_page_1'),Key('next_button')).
3. Walk the happy path
- Tap through each “Next” button, verifying that:
- The page indicator updates correctly.
- Any animation (e.g., fade, slide) completes before the next tap is accepted.
- No UI element is obscured by the system bar or keyboard.
- On the final screen, tap “Get Started” and confirm navigation to the home route (
Navigator.of(context).pushNamedAndRemoveUntil('/home', (route) => false)).
4. Inject common error conditions
- Network loss: Enable airplane mode after the first slide loads; observe whether the app shows a placeholder or retry button and whether tapping “Next” still works once connectivity returns.
- Asset failure: Rename an image asset in the
assets/folder, rebuild, and confirm the app falls back to a colored box or error icon without crashing. - Orientation change: Rotate the device mid‑tutorial; ensure the layout re‑flows, the current slide index is preserved, and animations restart smoothly.
5. Test interruption and resumption
- Pull down the notification shade while on slide 3, tap a notification to open another app, then return via recent‑apps. The tutorial should resume on slide 3, not reset to slide 1.
- Lock the screen, wait 10 seconds, unlock; verify the tutorial state is retained (usually via
SharedPreferencesor a local flag).
6. Validate accessibility
- Enable TalkBack (Android) or VoiceOver (iOS). Swipe through each tutorial slide and listen for:
- Descriptive labels for images (
contentDescription). - Correct reading order (title → body → button).
- Announcement of state changes (e.g., “Page 2 of 5”).
- Use the Accessibility Scanner app to capture contrast and touch‑target issues.
7. Check security/privacy flags
- If the tutorial shows a debug email or token, confirm it is absent in a release build (
flutter build apk --release). - Verify that any analytics call (
FirebaseAnalytics.instance.logEvent) is gated by a user‑consent flag that defaults to false until the user explicitly opts in.
8. Log observations
Create a simple markdown log:
| Step | Action | Result | Notes |
|------|--------|--------|-------|
| 1 | Launch app | Tutorial shows slide 1 | Asset loaded correctly |
| 2 | Tap Next | Slide 2 appears with slide‑in animation | Animation duration 300ms |
| … | … | … | … |
Attach screenshots or video clips for any deviation. This log becomes the baseline for future regression checks.
How to Test Tutorial Walkthrough on Flutter (Complete Guide): Automated Approaches and Tooling
Automation turns the manual checklist into repeatable CI gatekeepers. Flutter offers a layered testing pyramid: unit, widget, and integration tests. For tutorial walkthroughs, widget tests verify individual slide logic, while integration tests validate the full end‑to‑end flow, including navigation and persistence.
Unit and Widget Tests for Individual Slides
Each tutorial slide is often a stateless widget that receives a SlideData object (title, image, action). Write a widget test that pumps the slide with a TestWidgetsFlutterBinding and asserts the presence of key elements.
import 'package:flutter_test/flutter_test.dart';
import 'package:tutorial_app/tutorial_slide.dart';
void main() {
testWidgets('Slide displays title and image', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: TutorialSlide(
data: SlideData(
title: 'Welcome',
imageAsset: 'assets/images/welcome.png',
onNext: () {},
),
),
),
);
expect(find.text('Welcome'), findsOneWidget);
expect(find.byKey(Key('welcome_image')), findsOneWidget);
// Ensure placeholder appears if asset missing
await tester.binding.window.devicePixelRatioTest = 1.0;
await tester.pump();
expect(find.byType(Icon), findsOneWidget); // fallback icon
});
}
Run with flutter test test/tutorial_slide_test.dart.
Integration Test for Full Tutorial Flow
Use the integration_test package to drive a real emulator or device. The test below walks the tutorial, simulates a network failure, and checks that the app recovers.
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:tutorial_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Tutorial Walkthrough', () {
testWidgets('completes happy path and handles network loss', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
// Verify first slide
expect(find.text('Welcome'), findsOneWidget);
await tester.tap(find.byKey(Key('next_button')));
await tester.pumpAndSettle();
// Simulate network loss
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
'<io.flutter.embedding.engine.systemchannels.platform>',
const StandardMethodCodec().encodeMethodCall(
MethodCall('setNetworkStatus', <String, bool>{'connected': false}),
),
(ByteData? data) {},
);
// Expect placeholder or retry UI
expect(find.text('Retry'), findsOneWidget);
await tester.tap(find.byKey(Key('retry_button')));
await tester.pumpAndSettle();
// Continue through rest of slides
for (int i = 2; i <= 5; i++) {
await tester.tap(find.byKey(Key('next_button')));
await tester.pumpAndSettle();
expect(find.textContaining('Slide $i'), findsOneWidget);
}
// Final CTA
await tester.tap(find.byKey(Key('get_started_button')));
await tester.pumpAndSettle();
expect(find.text('Home Screen'), findsOneWidget);
});
});
}
Run via flutter drive --target=integration_test/tutorial_test.dart.
Golden Tests for Visual Regression
Because tutorials rely heavily on layout and imagery, golden tests catch subtle rendering regressions (font changes, padding drift).
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
void main() {
testGoldens('Tutorial slide renders correctly', (tester) async {
await tester.pumpWidgetBuilder(
MaterialApp(
home: TutorialSlide(
data: SlideData(
title: 'Explore',
imageAsset: 'assets/images/explore.png',
onNext: () {},
),
),
),
);
await screenMatchesGolden(tester, 'tutorial_slide_explore');
});
}
Execute with flutter test --update-goldens to regenerate baselines.
Performance Benchmarking
Use the benchmark_test package to measure frame‑time during tutorial animations.
import 'package:benchmark_harness/benchmark_harness.dart';
import 'package:flutter_test/flutter_test.dart';
class TutorialAnimationBenchmark extends BenchmarkBase {
TutorialAnimationBenchmark() : super('Tutorial Animation');
@override
void run() {
final tester = WidgetTester(const WidgetsFlutterBinding());
tester.runAsync(() async {
await tester.pumpWidget(const MaterialApp(
home: TutorialSlide(
data: SlideData(
title: 'Animate',
imageAsset: 'assets/images/animate.png',
onNext: () {},
),
),
));
// Trigger animation by tapping next
await tester.tap(find.byKey(Key('next_button')));
await tester.pumpAndSettle(const Duration(milliseconds: 500));
});
}
}
void main() {
TutorialAnimationBenchmark()..report();
}
Run with flutter run -d chrome --dart-define=FLUTTER_WEB_AUTO_DETECT=true benchmark/tutorial_animation_benchmark.dart.
Tooling Comparison Table
| Approach | Scope | Setup Effort | Flakiness | Best For |
|---|---|---|---|---|
| Unit test | Pure Dart logic (e.g., state transitions) | Low | Very low | Business rules, navigation guards |
| Widget test | UI of a single slide, mocking dependencies | Medium | Low (if deterministic) | Layout, accessibility, asset loading |
| Integration test | Full tutorial flow, device/emulator | High | Medium (timing, device state) | End‑to‑end validation, interruption handling |
| Golden test | Pixel‑perfect rendering | Medium | Low (if environment stable) | Visual regression, branding |
| Benchmark | Performance metrics (fps, CPU) | High | Medium | Animation smoothness, resource usage |
| Manual exploratory | UX feel, edge‑case discovery | Low | N/A (human) | Unexpected gestures, accessibility feel |
Pick the combination that matches your risk tolerance and CI capacity. Many teams run unit/widget tests on every push, integration tests on nightly runs, and goldens on release branches.
Persona‑Driven Autonomous Exploration with SUSA
While scripted tests verify known paths, autonomous, persona‑driven exploration can surface bugs that no one thought to script—such as a tutorial that breaks when a power user repeatedly taps fast, or when an elderly user holds a button longer than the expected gesture threshold. SUSA (susatest.com) is an autonomous QA platform that explores an uploaded APK or web URL using a library of behavioral personas. Each persona models a distinct interaction style (curious, impatient, novice, adversarial, elderly, accessibility‑focused, power user, etc.) and drives the app through taps, swipes, text entry, and system dialog handling without any test code.
How SUSA Works for Tutorial Testing
- Upload – Provide the Flutter APK (
flutter build apk --release) or a test‑flight build URL. - Select Personas – Choose the subset relevant to onboarding:
- *Novice* – expects clear affordances, may miss subtle icons.
- *Impatient* – taps rapidly, may skip animation waits.
- *Accessibility* – enables TalkBack, checks labels and reading order.
- *Adversarial* – attempts to break the flow by tapping outside bounds, rotating rapidly, or invoking system overlays.
- Explore – SUSA launches the app on a cloud‑based Android device farm, lets each persona roam freely for a configurable time (e.g., 5 minutes per persona), and records every screen transition, crash, ANR, and accessibility violation.
- Analyze – The platform groups findings by screen and by persona, highlighting:
- Dead ends (buttons that lead nowhere).
- Unhandled exceptions (e.g.,
NullCheckOperatorwhen an image fails to load). - WCAG contrast failures specific to tutorial screens.
- UX friction metrics (time to complete tutorial, number of mis‑taps).
- Feedback Loop – SUSA generates regression scripts (Appium for Android, Playwright for Web) that you can download and commit to your repo, ensuring the discovered bug stays caught in future CI runs.
Configuring Personas for Tutorial Scenarios
In the SUSA dashboard, under “Persona Settings,” you can tune parameters:
| Persona | Tap Delay (ms) | Scroll Speed | Text Input Probability | Accessibility Enabled |
|---|---|---|---|---|
| Novice | 300 | 0.5x | 0.1 | false |
| Impatient | 50 | 2.0x | 0.0 | false |
| Elderly | 600 | 0.3x | 0.0 | true (TalkBack) |
| Accessibility | 200 | 0.5x | 0.0 | true (TalkBack + Switch) |
| Adversarial | 10 | 1.0x | 0.0 | false (but injects system intents) |
Running the exploration with these settings often reveals issues such as:
- The “Next” button becomes unresponsive when tapped <100 ms after the previous tap (animation not yet finished).
- TalkBack reads an image as “image” instead of the intended description because
semanticLabelis missing. - Rotating the device while an animation is in progress throws a
setState called after disposeerror.
Interpreting Results and Closing the Loop
After a SUSA run, download the JSON report and look for entries under the tutorial screen tag. Prioritize:
- Crashes/ANRs – Fix immediately; they block all users.
- Accessibility violations – Add missing
semanticLabelor adjust contrast. - Unhandled exceptions – Guard async image loads with
try/catchand show a placeholder. - UX friction – If the impatient persona averages 2 seconds per slide while the novice needs 6 seconds, consider adding a skip‑after‑timeout feature or making the animation skippable.
Integrate the generated Appium test into your CI pipeline (flutter drive --target=test_data/susa_generated_test.dart) to ensure the regression stays caught.
Code Samples: Tutorial Walkthrough Implementation and Tests
Below is a minimal but complete example of a tutorial built with a PageView and a set of widget/integration tests that demonstrate the concepts discussed.
1. Tutorial Widget (lib/tutorial/tutorial_page_view.dart)
import 'package:flutter/material.dart';
class TutorialPageView extends StatefulWidget {
final List<SlideData> slides;
final VoidCallback onCompleted;
const TutorialPageView({
Key? key,
required this.slides,
required this.onCompleted,
}) : super(key: key);
@override
State<TutorialPageView> createState() => _TutorialPageViewState();
}
class _TutorialPageViewState extends State<TutorialPageView> {
final PageController _pageController = PageController();
int _currentIndex = 0;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
void _onNext() {
if (_currentIndex < widget.slides.length - 1) {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
} else {
widget.onCompleted();
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: PageView.builder(
controller: _pageController,
itemCount: widget.slides.length,
onPageChanged: (index) => setState(() => _currentIndex = index),
itemBuilder: (context, index) {
final slide = widget.slides[index];
return Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
slide.imageAsset,
width: 200,
height: 200,
errorBuilder: (_, __, ___) => Icon(Icons.broken_image, size: 80),
),
const SizedBox(height: 24),
Text(
slide.title,
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
slide.body,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
),
);
},
),
),
// Dots indicator
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
widget.slides.length,
(index) => Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
width: _currentIndex == index ? 12 : 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _currentIndex == index
? Theme.of(context).primaryColor
: Colors.grey.shade300,
),
),
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _onNext,
child: Text(_currentIndex == widget.slides.length - 1
? 'Get Started'
: 'Next'),
),
],
);
}
}
class SlideData {
final String title;
final String body;
final String imageAsset;
SlideData({
required this.title,
required this.body,
required this.imageAsset,
});
}
2. Widget Test for Slide Navigation (test/tutorial_page_view_test.dart)
import 'package:flutter_test/flutter_test.dart';
import 'package:tutorial_app/tutorial/tutorial_page_view.dart';
void main() {
List<SlideData> fakeSlides = [
const SlideData(title: 'First', body: 'Body 1', imageAsset: 'assets/img1.png'),
const SlideData(title: 'Second', body: 'Body 2', imageAsset: 'assets/img2.png'),
const SlideData(title: 'Third', body: 'Body 3', imageAsset: 'assets/img3.png'),
];
testWidgets('taps next move through pages', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: TutorialPageView(
slides: fakeSlides,
onCompleted: () {},
),
),
);
// Start on first slide
expect(find.text('First'), findsOneWidget);
expect(find.byType(PageView), findsOneWidget);
// Tap moves to second slide
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.text('Second'), findsOneWidget);
// Third slide
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.text('Third'), findsOneWidget);
// Completed callback fired
expect(find.text('Get Started'), findsOneWidget);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// Verify onCompleted triggered via a State flag (omitted for brevity)
});
testWidgets('shows placeholder when image missing', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: TutorialPageView(
slides: [
const SlideData(
title: 'Missing',
body: 'No image',
imageAsset: 'assets/does_not_exist.png',
),
],
onCompleted: () {},
),
),
);
await tester.pumpAndSettle();
// The errorBuilder should display an Icon
expect(find.icon(Icons.broken_image), findsOneWidget);
});
}
3. Integration Test for Full Flow with Network Failure (integration_test/tutorial_flow_test.dart)
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:tutorial_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('End‑to‑End Tutorial', () {
testWidgets('completes tutorial and survives simulated network loss', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
// Verify first slide
expect(find.text('Welcome to DemoApp'), findsOneWidget);
await tester.tap(find.byKey(Key('next_button')));
await tester.pumpAndSettle();
// Inject network loss via platform channel
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
'<io.flutter.embedding.engine.systemchannels.platform>',
const StandardMethodCodec().encodeMethodCall(
MethodCall('setNetworkStatus', <String, bool>{'connected': false}),
),
(_) {},
);
// Expect fallback UI
expect(find.text('Loading…'), findsOneWidget);
await tester.tap(find.byKey(Key('retry_button')));
await tester.pumpAndSettle();
// Continue through remaining slides (assume 4 total)
for (int i = 2; i <= 4; i++) {
await tester.tap(find.byKey(Key('next_button')));
await tester.pumpAndSettle();
expect(find.textContaining('Slide $i'), findsOneWidget);
}
// Final CTA
await tester.tap(find.byKey(Key('get_started_button')));
await tester.pumpAndSettle();
expect(find.text('Home Screen'), findsOneWidget);
});
});
}
Run with:
flutter drive --target=integration_test/tutorial_flow_test.dart -d android-emulator
These snippets illustrate how you can unit‑test logic, widget‑test UI and error handling, and integration‑test the full tutorial journey, including simulated fault injection.
Edge Cases That Only Appear in Production
Even the most thorough test suite can miss issues that manifest only under real‑world conditions. Below are several production‑only pitfalls that have repeatedly surfaced in Flutter tutorial walkthroughs, along with concrete detection strategies.
1. Network‑Dependent Asset Loading with Flaky Connections
In CI, the emulator usually has perfect Wi‑Fi. In the field, users may enter a tunnel or experience LTE handoff while the tutorial is still fetching images. If your code uses precacheImage or NetworkImage without a timeout, the UI can hang indefinitely, leaving the user stuck on a blank screen.
Detection:
- Use
integration_testto throttle the network (adb shell netem add delay 200ms loss 5%). - Assert that a placeholder appears within a bounded time (e.g., 2 seconds).
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
'<io.flutter.embedding.engine.systemchannels.platform>',
const StandardMethodCodec().encodeMethodCall(
MethodCall('setNetworkParams', <String, dynamic>{
'delayMs': 200,
'lossPercent': 5,
}),
),
(_) {},
);
await tester.pump(const Duration(seconds: 3));
expect(find.byType(CircularProgressIndicator), findsNothing);
expect(find.text('Retry'), findsOneWidget);
2. Orientation Change Mid‑Animation
A tutorial that relies on AnimationController may incorrectly dispose the controller when the device rotates, leading to a setState called after dispose error. This only appears when the user rotates while an animation is still running.
Detection:
- In an integration test, start an animation, then trigger an orientation change via
MediaQueryData.fromWindow(window).copyWith(orientation: Orientation.landscape). - Verify no error appears in the console and the UI settles.
await tester.tap(find.byKey(Key('next_button')));
await tester.pump(const Duration(milliseconds: 150)); // mid‑animation
await tester.binding.window.physicalSizeTester(
Size(window.physicalSize.height, window.physicalSize.width),
);
await tester.pumpAndSettle();
expect(find.textContaining('Error'), findsNothing);
3. Low‑Memory Kill During Asset Decode
On low‑end devices, the system may kill the background isolate that decodes large PNG assets, causing the image to appear as a broken icon after the user returns from a background task.
Detection:
- Simulate a memory pressure event using
adb shell am send-trim-memory com.example.app MODERATE. - After returning to the app, check that the image placeholder is shown and the app does not crash.
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
'<io.flutter.embedding.engine.systemchannels.platform>',
const StandardMethodCodec().encodeMethodCall(
MethodCall('simulateMemoryTrim', <String, String>{'level': 'MODERATE'}),
),
(_) {},
);
await tester.pumpAndSettle();
expect(find.byIcon(Icons.broken_image), findsOneWidget);
4. System UI Overlays Intercepting Gestures
When a notification heads‑up appears, or the user pulls down the quick‑settings shade, the tutorial’s gesture recognizers may still receive the tap, causing unintended navigation (e.g., skipping a slide).
Detection:
- Overlay a system alert using
adb shell service call notification 1 ...(or useflutter_test’sSurfaceAndroidViewControllerto simulate a system window). - Confirm that taps on the tutorial area are ignored while the overlay is active.
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
'<io.flutter.embedding.engine.systemchannels.platform>',
const StandardMethodCodec().encodeMethodCall(
MethodCall('showSystemOverlay', <String, bool>{'show': true}),
),
(_) {},
);
await tester.tap(find.byKey(Key('next_button')));
await tester.pump(); // Should NOT change page
expect(find.text('Slide 1'), findsOneWidget);
5. Accessibility Services Changing Touch Timing
TalkBack adds a delay between a tap and the actual onPressed callback to allow users to explore the screen. If your tutorial relies on rapid double‑taps to skip, TalkBack users may never trigger the skip.
Detection:
- Enable TalkBack in the emulator (`adb shell settings put secure accessibility_enabled
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free