mirror of
https://github.com/we-promise/sure.git
synced 2026-07-20 00:35:22 +00:00
* fix(mobile): own the API-key dialog controller in its own State
_showApiKeyDialog created a TextEditingController in the parent State and
only disposed it in the Cancel/Sign-In handlers, so it leaked whenever the
dialog was dismissed via a barrier tap or the system back button.
Disposing it right after `await showDialog` (an earlier attempt) instead
risks disposing the controller while the dialog's TextField is still
mounted during the route's exit transition ("TextEditingController was
used after being disposed").
Extract the dialog into a small StatefulWidget (_ApiKeyLoginDialog) that
owns the controller and disposes it in State.dispose(), tying the
controller's lifecycle to the dialog's widget tree. The dialog pops
true/false/null and the screen shows the error snackbar on a failed
attempt.
flutter analyze: no new issues; flutter test: all green.
* test(mobile): cover ApiKeyLoginDialog controller disposal
Per review: add a widget test for the lifecycle contract that is the core
of this change. Exposes the dialog as `ApiKeyLoginDialog` (@visibleForTesting)
with an injectable controller, and asserts the controller is disposed on
all three dismissal paths — Cancel button, barrier tap, and system back —
by checking that using the controller afterward throws (a disposed
ChangeNotifier throws on reuse).
119 tests pass; flutter analyze: no new issues.
* test(mobile): derive the barrier-tap point from dialog geometry
Per review: replace the hard-coded Offset(10, 10) in the barrier-dismissal
test. Tapping the ModalBarrier widget directly hits the dialog that
occludes its centre, so instead tap halfway between the screen corner and
the dialog's top-left — a point derived from the dialog's real geometry,
resilient to layout changes, and always on the dismissible barrier.
* fix(mobile): keyboard submit + disable Sign In when API key is empty
- Add onSubmitted to the API key TextField so the keyboard Done/Enter key
submits the form (no need to tap the button).
- Wire a controller listener to rebuild the dialog state and disable the
Sign In ElevatedButton while the field is blank, giving clear feedback
before any network call is made.
76 lines
2.6 KiB
Dart
76 lines
2.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:sure_mobile/l10n/app_localizations.dart';
|
|
import 'package:sure_mobile/screens/login_screen.dart';
|
|
|
|
void main() {
|
|
// Opens the dialog with an injected controller and returns it so the test can
|
|
// assert the dialog disposed it. A disposed ChangeNotifier throws when used
|
|
// again, which is how we verify disposal.
|
|
Future<TextEditingController> openDialog(WidgetTester tester) async {
|
|
final controller = TextEditingController();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(
|
|
body: Builder(
|
|
builder: (context) => ElevatedButton(
|
|
onPressed: () => showDialog<bool>(
|
|
context: context,
|
|
builder: (_) => ApiKeyLoginDialog(controller: controller),
|
|
),
|
|
child: const Text('open'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.text('open'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('API Key Login'), findsOneWidget);
|
|
return controller;
|
|
}
|
|
|
|
void expectDisposed(TextEditingController controller) {
|
|
expect(() => controller.addListener(() {}), throwsA(isA<FlutterError>()));
|
|
}
|
|
|
|
testWidgets('disposes its controller when cancelled', (tester) async {
|
|
final controller = await openDialog(tester);
|
|
|
|
await tester.tap(find.text('Cancel'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('API Key Login'), findsNothing);
|
|
expectDisposed(controller);
|
|
});
|
|
|
|
testWidgets('disposes its controller when dismissed by a barrier tap',
|
|
(tester) async {
|
|
final controller = await openDialog(tester);
|
|
|
|
// The barrier fills the screen but its centre is occluded by the dialog, so
|
|
// tap halfway between the screen corner and the dialog's top-left — a point
|
|
// derived from the dialog's real geometry (not a fixed coordinate) that is
|
|
// always on the dismissible barrier.
|
|
final dialogTopLeft = tester.getTopLeft(find.byType(AlertDialog));
|
|
await tester.tapAt(dialogTopLeft / 2);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('API Key Login'), findsNothing);
|
|
expectDisposed(controller);
|
|
});
|
|
|
|
testWidgets('disposes its controller when dismissed by the system back button',
|
|
(tester) async {
|
|
final controller = await openDialog(tester);
|
|
|
|
await tester.binding.handlePopRoute();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('API Key Login'), findsNothing);
|
|
expectDisposed(controller);
|
|
});
|
|
}
|