How to Test Login Flow on Flutter (Complete Guide)
Login is the gatekeeper for most mobile experiences. When a user cannot sign in, they abandon the app instantly, and any downstream feature—payments, settings, social sharing—becomes unreachable. In F
Why Login Flow Testing Matters in Flutter Apps
Login is the gatekeeper for most mobile experiences. When a user cannot sign in, they abandon the app instantly, and any downstream feature—payments, settings, social sharing—becomes unreachable. In Flutter, the login UI is often built with a mix of stateful widgets, asynchronous calls to authentication back‑ends, and platform‑specific plugins (e.g., firebase_auth, google_sign_in). Because the framework recompiles UI on every frame, a subtle timing issue or a missed state update can cause the login button to stay disabled, a loading indicator to spin forever, or an error toast to never appear. These defects surface only under specific interaction patterns (rapid taps, network loss, background‑foreground switches) that manual exploratory testing catches but scripted tests often miss if they follow a single happy‑path script.
A well‑tested login flow therefore protects three critical dimensions:
- User retention – a smooth sign‑in reduces bounce and improves conversion metrics.
- Data integrity – correct handling of tokens, refresh cycles, and secure storage prevents credential leakage.
- Compliance – accessibility (WCAG) and privacy regulations (GDPR, CCPA) demand that login screens announce state changes, support screen readers, and avoid logging sensitive data.
Flutter’s hot‑reload encourages rapid iteration, but it also encourages developers to ship UI changes without re‑running the full login suite. A dedicated testing strategy—combining manual checks, automated unit/widget/integration tests, and occasional autonomous exploration—ensures that regressions are caught before they reach production.
---
Core Components of a Flutter Login Flow
Before designing tests, break the login screen into its constituent pieces. This decomposition guides where to place assertions and which failure modes to expect.
| Component | Typical Implementation | Responsibility |
|---|---|---|
| Form fields (email, password) | TextFormField with TextEditingController and Validator | Capture user input, provide inline validation |
| Submit button | ElevatedButton or CupertinoButton with onPressed callback | Trigger authentication request, manage loading state |
| Loading indicator | CircularProgressIndicator wrapped in Visibility or AnimatedSwitcher | Communicate asynchronous work, block further interaction |
| Error display | SnackBar, Dialog, or Text with TextStyle.color = Colors.red | Surface authentication failures (invalid credentials, network) |
| Success navigation | Navigator.pushReplacement or GoRouter redirect | Move user to home/dashboard after token acquisition |
| Social providers | Plugins like google_sign_in, facebook_login | Offer alternate credential pathways, handle OAuth callbacks |
| Password‑reset link | TextButton navigating to a reset page | Provide recovery flow without leaving login screen |
| Remember‑me / biometric toggle | Switch or Checkbox tied to SharedPreferences or local_auth | Persist credentials securely across sessions |
Each component can fail independently: a validator may reject a valid email, the button may stay disabled due to a state‑management bug, the loading spinner may never hide if the Future never resolves, and the error UI may be swallowed by a ScaffoldMessenger that is not yet mounted.
---
Test Matrix for Login Flow
The following table enumerates the scenarios that should be exercised for a robust login verification. Each row maps a test category to specific conditions, expected outcomes, and the testing technique best suited to uncover defects.
| Category | Sub‑scenario | Input / Condition | Expected Result | Recommended Test Type |
|---|---|---|---|---|
| Happy path | Valid credentials | Email: user@example.com, Password: Correct!23 | Successful token receipt, navigation to home, loading indicator disappears | Widget test (mock auth repo) + Integration test |
| Invalid email format | Malformed email | Email: userexample.com, Password: any | Inline error under email field, button stays disabled | Unit test of validator |
| Wrong password | Correct email, wrong password | Email: user@example.com, Password: bad | Error snackbar shows “Invalid credentials”, fields remain editable | Widget test with mock auth returning failure |
| **Empty fields | Email empty, password empty | Both fields blank | Both fields show required‑error, button disabled | Widget test |
| Network loss | No connectivity during request | Enable airplane mode after pressing login | Loading spinner shows, then error snackbar “No internet connection”, button re‑enabled | Integration test with dart:io network mock |
| Slow backend | Latency > 5 s | Use throttling proxy (e.g., toxiproxy) to delay response | Loading indicator persists for the duration, then either success or error appears | Integration test with artificial delay |
| Rapid double‑tap | User taps button twice within 200 ms | Two quick taps on submit | Only one authentication request sent, second tap ignored, no duplicate error messages | Widget test using tester.tap twice with await tester.pump() |
| Background‑foreground switch | App sent to background during auth | Press home button while spinner visible, then restore | Auth continues, UI updates correctly on return, no stale spinner | Integration test with appLifecycleState simulation |
| Social login – Google | Valid Google account | Tap Google button, complete OAuth flow | Token received, navigation to home, Google sign‑out clears session | Integration test using firebase_auth_mocks |
| Social login – revoked token | Previously granted token now revoked | Simulate revoked token response from backend | Error snackbar “Session expired”, option to re‑login presented | Integration test with mock auth returning 401 |
| Remember‑me toggle | Enable switch, close app, relaunch | Switch ON, kill process, reopen | Email field pre‑filled, password field empty (or biometric prompt appears) | Integration test with SharedPreferences mock |
| Biometric fallback | Device supports fingerprint, user opts in | Toggle biometric ON, attempt login with wrong password then use fingerprint | After failed password, biometric prompt appears; successful fingerprint yields token | Integration test with local_auth mock |
| Accessibility – TalkBack | Screen reader enabled | Navigate with TalkBack, focus on each element | All fields announce label+state, button announces “disabled” when appropriate, loading announces “logging in”, error announces message | Manual test + accessibility scanner (e.g., flutter_lints with a11y rules) |
| Internationalization – RTL | Language set to Arabic (ar) | Change locale, direction RTL | Layout mirrors correctly, text aligns right, icons flip where needed | Widget test with Locale('ar', 'AE') |
| Internationalization – Long strings | Language with lengthy validation messages (German) | Set locale de, trigger error | No overflow, text wraps or scrolls as designed | Widget test |
| Security – Clear‑text logging | Accidental print(password) in code | Run app with flutter run --verbose | No password appears in console output | Static analysis + manual log review |
| Security – Token storage | Token written to SharedPreferences unencrypted | Inspect stored data after login | Token should be encrypted or stored in flutter_secure_storage | Manual inspection + unit test of storage wrapper |
| Privacy – GDPR consent | Login screen shows consent checkbox | Consent unchecked, attempt login | Login blocked, consent reminder shown | Widget test of consent gating |
| Edge case – Password paste | User pastes password from clipboard | Long password (>100 chars) pasted | Field accepts, validation runs, submission works if within backend limits | Widget test simulating Clipboard.setData + tester.pump |
| Edge case – Whitespace trimming | Email with leading/trailing spaces | " user@example.com " | Spaces trimmed before validation, login succeeds if core email valid | Unit test of input sanitizer |
| Edge case – Maximum length | Email at server limit (254 chars) | Generate 254‑char valid email | Accepted, token returned | Widget test with generated string |
| Edge case – Special characters | Password containing Unicode emoji | Password: 😀🔑🚀 | Accepted, hashed correctly, login succeeds | Widget test |
| Edge case – Device rotation | Portrait → landscape during auth | Rotate device while spinner visible | Layout adapts, no state loss, spinner remains centered | Integration test with tester.binding.window.physicalSizeTestValue |
*Note:* The matrix is intentionally exhaustive; in practice you prioritize based on risk. However, covering at least the happy path, all error paths, accessibility, and security basics yields a high confidence level.
---
Manual Testing Approach (Step‑by‑Step)
Manual exploration remains indispensable for catching UX friction, unexpected gestures, and device‑specific quirks. Follow this procedure on a physical device or emulator for each build candidate.
- Setup
- Install the latest debug APK (
flutter build apk --debug) on a device with Google Play services (if using Firebase). - Enable Developer options → “Show taps” and “Pointer location” to visualize interaction points.
- Turn on TalkBack (Android) or VoiceOver (iOS) for accessibility checks.
- Baseline Happy Path
- Launch the app, locate the login screen.
- Enter a known good email and password.
- Observe: field labels remain visible, button enables only after both fields pass validation.
- Tap submit; verify loading spinner appears centered, then disappears within expected time (usually < 3 s).
- Confirm navigation to home screen and that a valid ID token is present in secure storage (use
adb shell run-asor a debugging tool).cat shared_prefs/...
- Error Paths
- Repeat steps 2‑3 with each invalid input from the matrix (malformed email, short password, etc.).
- Ensure inline errors appear *immediately* after losing focus or on submit, and that they disappear when the field regains a valid value.
- Press the submit button while an error is visible; confirm no request is sent (check network logs via
adb logcator Charles Proxy).
- Network Conditions
- Enable airplane mode, then tap submit. Verify the loading indicator shows, then an appropriate offline message appears after a timeout (typically 10‑15 s).
- Use a throttling proxy (e.g.,
toxiproxyor Chrome DevTools) to add 2‑second latency; ensure the spinner does not freeze the UI and that the user can still interact with other screen elements (e.g., toggle remember‑me).
- Gesture Stress
- Perform a double‑tap on the submit button as fast as possible. Monitor the console for duplicate
AuthService.logincalls (you can add a temporaryprintor use a mock that increments a counter). - Long‑press the button to see if any context menu appears erroneously.
- Lifecycle Interruption
- While the spinner is visible, press the Home button, wait 2 seconds, then restore the app via recents.
- The auth call should still be in flight; upon completion the UI must reflect the correct state (either success navigation or error snackbar).
- Repeat with a device rotation during the same window.
- Social Login
- Tap the Google/Facebook button. Complete the OAuth flow using a test account.
- After returning to the app, confirm token receipt and that the social button now shows a “Logged in as …” label or allows logout.
- Simulate a revoked token by clearing the Google account’s OAuth permissions via the Google account website, then retry login; expect a clear session‑expired message.
- Remember‑Me & Biometric
- Enable the remember‑me switch, log in, then force‑stop the app (
adb shell am force-stop). - Relaunch; the email field should be prepopulated (or biometric prompt should appear).
- Disable biometric, log in with wrong password, then use the fingerprint sensor (or emulate via
adb shell emu finger print). Verify successful login after biometric success.
- Accessibility Check
- With TalkBack enabled, swipe left/right to move focus. Each element should announce:
- Email field: “Email, edit text, empty, required”.
- Password field: “Password, secure edit text”.
- Button: “Sign in, button, disabled” (when invalid) → “Sign in, button” (when enabled).
- Loading spinner: “Loading, image” (if using an image) or “Progress indicator”.
- Error snackbar: “Invalid credentials, button”.
- Ensure double‑tap activates the intended action.
- Internationalization Spot‑Check
- Change device language to Arabic (right‑to‑left). Verify the entire layout mirrors: email label on right, input field aligns left, button icon flips if directional.
- Switch to German and trigger a validation error; confirm the long error message wraps without clipping.
- Security & Privacy Scan
- Look at Android Studio Logcat while logging in with a verbose build; ensure no
printstatements expose passwords or tokens. - Use
adb shell run-asto inspect stored preferences; tokens should be absent or encrypted.cat shared_prefs/ .xml - If using
flutter_secure_storage, verify that the stored value is not plain text by checking the encrypted file in/data/data/./files/
- Final Sign‑Off
- Run through the happy path one more time on a clean install (clear app data).
- Confirm that the app does not retain any stale UI state from previous runs.
By following this checklist, you exercise the majority of the matrix items manually. Document any deviations (e.g., a spinner that never hides) as bugs with reproduction steps, device model, OS version, and Flutter channel.
---
Automated Testing with Flutter Tools
Flutter ships with a testing pyramid that matches the manual matrix: unit tests for pure logic, widget tests for UI with mocked dependencies, and integration tests for end‑to‑end flows on real devices or emulators.
Unit Testing
Unit tests validate validation functions, input sanitizers, and any business logic decoupled from the widget tree.
// test/validators_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/login/email_validator.dart';
void main() {
group('EmailValidator', () {
final validator = EmailValidator();
test('accepts correctly formatted email', () {
expect(validator.call('user@example.com'), isNull);
});
test('rejects missing @', () {
expect(validator.call('userexample.com'), equals('Enter a valid email'));
});
test('rejects empty string', () {
expect(validator.call(''), equals('Email is required'));
});
test('trims whitespace before validation', () {
expect(validator.call(' user@example.com '), isNull);
});
test('accepts maximum length 254', () {
final longEmail = 'a' * 240 + '@example.com';
expect(validator.call(longEmail), isNull);
});
test('rejects >254 characters', () {
final tooLong = 'a' * 250 + '@' + 'b' * 5 + '.com';
expect(validator.call(tooLong), isNotNull);
});
});
}
Run with flutter test test/validators_test.dart. Aim for > 90 % coverage on validation and sanitizer modules.
Widget Testing
Widget tests render the login form in isolation, allowing you to pump user gestures and assert on the resulting state. Use mocks for the authentication repository so the test does not hit the network.
// test/widget/login_form_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/login/login_form.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:myapp/login/auth_repository.dart';
@GenerateMocks([AuthRepository])
void main() {
late MockAuthRepository mockRepo;
setUp(() {
mockRepo = MockAuthRepository();
});
testWidgets('shows error when email invalid', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: LoginForm(authRepository: mockRepo),
),
);
// Enter invalid email
await tester.enterText(find.byKey(const Key('emailField')), 'bademail');
await tester.enterText(find.byKey(const Key('passwordField')), 'ValidPass1!');
await tester.tap(find.byKey(const Key('submitButton')));
// Pump to let validation run
await tester.pump();
expect(find.text('Enter a valid email'), findsOneWidget);
verifyNever(mockRepo.login(any, any));
});
testWidgets('disables button while loading', (tester) async {
when(mockRepo.login(any, any))
.thenAnswer((_) async => Future.delayed(const Duration(seconds: 2),
=> AuthResult.success(token: 'dummy')));
await tester.pumpWidget(
MaterialApp(
home: LoginForm(authRepository: mockRepo),
),
);
await tester.enterText(find.byKey(const Key('emailField')), 'user@example.com');
await tester.enterText(find.byKey(const Key('passwordField')), 'ValidPass1!');
await tester.tap(find.byKey(const Key('submitButton')));
// Immediately after tap, button should be disabled
expect(find.byKey(const Key('submitButton')), findsOneWidget);
expect(find.byKey(const Key('submitButton')).evaluate().single.widget.enabled, isFalse);
// After delay, button re‑enables and navigation occurs
await tester.pump(const Duration(seconds: 3));
verify(mockRepo.login('user@example.com', 'ValidPass1!')).called(1);
// Assuming LoginForm pushes a route on success
expect(find.byType(HomePage), findsOneWidget);
});
}
Key points:
- Use
enterTextto simulate keyboard input. - Pump with a duration to observe async state changes (loading, error).
- Verify that the repository’s
loginmethod is called exactly once for a valid submission and never when validation fails.
Run widget tests via flutter test test/widget/.
Integration Testing
Integration tests run on a real device or emulator and exercise the full Flutter engine, including platform plugins. The recommended package is integration_test.
Add to dev_dependencies:
dev_dependencies:
integration_test:
sdk: flutter
Create integration_test/login_test.dart:
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:myapp/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Login Flow', () {
testWidgets('successful login navigates to home', (tester) async {
app.main();
await tester.pumpAndSettle();
// Fill in valid credentials
await tester.enterText(find.byKey(const Key('emailField')), 'user@example.com');
await tester.enterText(find.byKey(const Key('passwordField')), 'SuperSecret!23');
await tester.tap(find.byKey(const Key('submitButton')));
await tester.pumpAndSettle(); // Wait for navigation
// Expect home screen
expect(find.byType(HomePage), findsOneWidget);
});
testWidgets('shows network error when offline', (tester) async {
app.main();
await tester.pumpAndSettle();
// Enable airplane mode via platform channel (simple method)
const bool offline = true;
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
'<channel_for_network_toggle>',
offline.toString().codeUnits,
(_) => null,
);
await tester.enterText(find.byKey(const Key('emailField')), 'user@example.com');
await tester.enterText(find.byKey(const Key('passwordField')), 'WrongPass');
await tester.tap(find.byKey(const Key('submitButton')));
await tester.pump(const Duration(seconds: 2));
expect(find.textContaining('No internet connection'), findsOneWidget);
// Button should be re‑enabled
expect(find.byKey(const Key('submitButton')).evaluate().single.widget.enabled, isTrue);
});
testWidgets('social login Google flow', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('googleButton')));
// Assuming a mock Google sign‑in UI appears; we simulate success via a test plugin
await tester.pumpAndSettle();
// After returning, check for token or home navigation
expect(find.byType(HomePage), findsOneWidget);
});
});
}
Run with:
flutter drive \
--target=integration_test/login_test.dart \
--dry-run # (optional to see emitted steps)
flutter drive \
--target=integration_test/login_test.dart \
-d <device-id>
Integration tests catch timing issues, lifecycle interruptions, and plugin‑specific bugs that unit/widget tests cannot.
Using SUSA for Autonomous Exploration
While scripted tests give deterministic coverage, they rarely explore the combinatorial space of user behaviors (e.g., a curious user who taps every icon, an impatient user who repeatedly presses the button, or an elderly user who performs long presses). SUSA (susatest.com) can be pointed at a Flutter APK or a web URL and will autonomously exercise the login flow using a variety of personas.
To incorporate SUSA into your CI pipeline:
- Install the agent:
pip install susatest-agent. - Build an unsigned APK:
flutter build apk --release --no-tree-shake-icons. - Run the agent with a persona set:
susatest-agent run \
--app ./build/app/outputs/flutter-apk/app-release.apk \
--personas curious impatient elderly \
--output ./susa-report.json \
--max-depth 5 \
--timeout 300
SUSA will generate a JSON report highlighting:
- Crashes or ANRs encountered during login attempts.
- Dead buttons (e.g., the submit button that stays disabled after valid input due to a state‑management bug).
- Accessibility violations (missing labels, insufficient contrast).
- Security red flags (clear‑text logging of passwords detected via logcat scraping).
Because Susa’s exploration is guided by behavior models rather than hard‑coded scripts, it often discovers edge cases such as:
- A “power user” who pastes a 300‑character password, revealing a missing client‑side length check that leads to a backend 413 error.
- An “adversarial” user who rapidly toggles the remember‑me switch while the auth request is in flight, exposing a race condition that leaves the toggle stuck in an intermediate state.
Integrate SUSA runs as a nightly job; treat any new finding as a bug to be added to the regression suite (either as a new widget test or an integration test scenario).
---
Edge Cases That Only Show Up in Production
Even the most thorough test matrix can miss issues that appear only under real‑world conditions. Below are recurring production‑only patterns observed in Flutter login flows, along with detection strategies.
| Production Symptom | Root Cause | Detection / Mitigation |
|---|---|---|
| Intermittent “Login button stuck disabled” | State‑management library (e.g., Provider, Riverpod) not rebuilt after async validation due to Equatable misuse or missing notifyListeners. | Add widget test that forces a rebuild after a mocked validation Future completes; enable debugPrintBuildScope to watch for missing builds. |
| Random crash on Android 12 when using Google Sign‑In | Missing android:usesCleartextTraffic="true" in manifest for debug builds, or mismatched SHA‑1 fingerprint in Firebase console. | Use flutter build apk --release and test with Firebase App Distribution; enable firebase_crashlytics to capture stack traces. |
| Memory leak after repeated login/logout cycles | StreamSubscription from auth state changes not cancelled in dispose. | Use Dart DevTools memory view; add a widget test that repeatedly pushes/pops the login screen and asserts that the number of active subscriptions does not grow. |
| Biometric prompt appears on devices without fingerprint hardware | Plugin returns true for canCheckBiometrics on emulators; real device lacks sensor. | Guard UI with local_auth.isDeviceSupported() before showing the toggle; write an integration test that runs on a physical device without biometrics and verifies the toggle is hidden. |
| Login success screen flashes then returns to login | Token expiration handled incorrectly; backend returns 401 immediately after issuing token, causing a redirect loop. | Add an integration test that mocks the backend to return a valid token then a 401 on the subsequent API call; assert that the app shows a re‑login prompt rather than looping. |
| Clipboard paste triggers validation error on iOS | iOS UIPasteboard returns NSString with hidden newline characters; trimmed only on Android. | Normalize input in the TextFormField’s onChanged callback: value.trim().replaceAll('\n', ''). Test with a unit test that feeds a string containing \r\n. |
| TalkBack reads password characters aloud | obscureText set but semanticLabel missing, causing screen reader to read the raw value. | Provide semanticLabel: 'Password' and enable obscureText: true. Run an accessibility audit via flutter run --dart-define=FLUTTER_WEB_AUTO_DETECT=true and use the axe plugin. |
| App crashes when switching to landscape while keyboard is open | Layout overflow due to fixed height containers not responding to MediaQuery.of(context).size. | Use Expanded or Flexible within a Column; add an integration test that rotates the device while the keyboard is visible (tester.binding.window.physicalSizeTestValue = Size(...)). |
| Push notification token registration fails after login | Firebase initialization occurs lazily after login; race condition causes missing token on first launch. | Move Firebase init to main() before runApp; add a unit test that verifies FirebaseApp.instance is not null before any auth call. |
Detecting these issues requires a combination of:
- Continuous delivery pipelines that run on a matrix of real devices (via Firebase Test Lab or AWS Device Farm).
- Crash reporting (Firebase Crashlytics, Sentry) with custom keys for login flow stage (e.g.,
login_stage: submitting). - Performance monitoring (Firebase Performance) to track login latency across devices and network types.
When a production anomaly appears, reproduce it locally by extracting the exact device/OS version from the crash report, then add a targeted test (usually an integration test) that simulates the same conditions.
---
Accessibility and Internationalization Checks
Accessibility (a11y) and i18n are often afterthoughts, yet they directly affect login conversion.
Accessibility Checklist (Flutter‑specific)
| Item | Implementation | Test |
|---|---|---|
| Label association | Use FormFieldLabel or set labelText on TextFormField. Ensure semanticLabel is not duplicated. | With TalkBack enabled, swipe to field; verify spoken label matches visual label. |
| Contrast ratio | Text and icons must meet WCAG AA (≥4.5:1 for normal text). Use ThemeData.colorScheme with contrast factor. | Run flutter pub run flutter_lints with rules: [avoid_print, prefer_const_constructors] plus custom contrast lint, or use the axe extension in DevTools. |
| Touch target size | Minimum 48 dp height/width. Wrap buttons in SizedBox(height: 48, width: 48) or use minSize property of MaterialButton. | Enable “Show touch targets” in Developer options; visually confirm. |
| Focus order | Logical tab‑like order: email → password → remember‑me → submit → social links. Use FocusNode and FocusScope. | With a keyboard attached, press Tab and observe focus movement. |
| Error announcement | Show errors via SnackBar with action label, or use AlertDialog. Ensure SnackBarContent has semanticLabel. | Trigger an error; listen with TalkBack for the message. |
| Loading indicator accessibility | Replace bare CircularProgressIndicator with Semantics(label: 'Logging in', button: false, liveRegion: true) so screen readers announce changes. | Observe TalkBack announcing “Logging in” when spinner appears. |
| Reduced motion | Respect MediaQuery.of(context).disableAnimations. Wrap animations in if (!disableAnimations) .... | Turn on “Remove animations” in Accessibility settings; verify no spinners or transitions cause discomfort. |
| Screen reader navigation past modal | When showing a dialog (e.g., terms), use barrierDismissible: false and provide a clear semanticLabel for close action. | Open dialog; ensure TalkBack moves focus inside dialog and can exit via close button. |
Automate some of these checks with the flutter_launcher_icons package’s flutter pub run flutter_launcher_icons:main and the semantics_test package, which can assert on Semantics properties in widget tests.
Internationalization (i18n) Checklist
- Use
intlpackage with ARB files for all user‑visible strings. - Test layout direction: wrap the login screen in
Directionality(textDirection: Locale('ar', 'AE').languageCode == 'ar' ? TextDirection.rtl : TextDirection.ltr, child: ...). - Validate dynamic content: numbers, dates, and currency should be formatted via
NumberFormat,DateFormat. - Check for hard‑coded strings: run
flutter pub run intl_translation:extract_to_arb --output-dir=lib/l10nand ensure the generated ARB matches the keys used. - Test Right‑to‑Left (RTL) mirroring:
- Add a widget test that sets
Locale('ar', 'SA')and asserts thatfind.byIcon(Icons.arrow_back)appears on the right side of the AppBar. - Use
debugPaintSizeEnabled = trueto visually confirm padding and alignment flips.
- Length‑expansion testing:
- Pseudolocalize strings (e.g., replace each character with “XXXX”) to simulate 30 % expansion.
- Run the app with the pseudolocale and verify no overflow warnings in the console.
Automated i18n regression can be added to your CI:
flutter test --dart-define=FLUTTER_TEST=true # ensures intl initialization
flutter drive --target=integration_test/i18n_test.dart -d emulator-5554
---
Security and Privacy Considerations
Login flows are a prime target for credential theft and data leakage. Address the following areas explicitly in your test plan.
| Concern | Typical Flutter Implementation | Test / Mitigation | |
|---|---|---|---|
| Clear‑text logging | Accidental print(email) or print(password) in view‑model. | Enable flutter run --verbose and grep logs for password or token. Add a custom lint that bans print of fields matching regex `(?i)pass | token`. |
| Token storage | Storing raw JWT in SharedPreferences. | Use flutter_secure_storage or keychain/keystore. Write a unit test that attempts to read the stored value via adb shell run-as and asserts it is not plain text. | |
| Transport security | API calls over HTTP in debug builds. | Enforce Only use HTTPS via Dio interceptors that throw on http://. Add integration test that simulates a MITM proxy and verifies the request fails. | |
| Replay attacks | No nonce or timestamp in login payload. | Back‑end should reject duplicate nonces; test by capturing a valid login request (via Charles) and resending it; expect 401. | |
| Brute‑force protection | No rate limiting on login endpoint. | Client can show a CAPTCHA after N failures; test by attempting 5 rapid wrong passwords and asserting a delay or extra UI appears. |
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