fix(#28): guard settings onChanged persisters against silent failures (panes)
Settings onChanged/onTap callbacks fire-and-forgot AppSettingsService Future<void> setters, so a persist failure rejected unhandled and was swallowed. Add a shared persistSetting(context, action) helper that awaits the setter in a try/catch and shows a dismissible error SnackBar, capturing the messenger before the await so it survives a picker-dialog pop. Wrap the 27 onChanged/onTap setter sites in app_settings_view + message_settings_view; setAppDebugLogEnabled and setNotificationsEnabled (which await + show their own confirmation) get an inline try/catch instead. Covers the two onChanged panes; the settings_screen action flows (GPX export, location dialogs, version-info mounted guard) follow in a second pass, so #28 stays open. Error strings are English for now (separate settings l10n sweep). Adds a persistSetting widget test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>pull/60/head
parent
5227507810
commit
d2cca1da82
@ -0,0 +1,36 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Runs a settings-persist [action] and surfaces a dismissible error SnackBar if
|
||||||
|
/// it throws, instead of dropping the returned Future and swallowing the error
|
||||||
|
/// (no silent failures — #28).
|
||||||
|
///
|
||||||
|
/// Call sites intentionally don't await the returned Future: this function never
|
||||||
|
/// rethrows (it catches internally), so dropping it is safe. The
|
||||||
|
/// [ScaffoldMessenger] is captured before the await, so the error still shows
|
||||||
|
/// even when the triggering widget (e.g. a picker dialog) is popped immediately
|
||||||
|
/// after.
|
||||||
|
///
|
||||||
|
/// [message] is English-only for now; localizing it is part of the separate
|
||||||
|
/// settings l10n sweep noted on #28.
|
||||||
|
Future<void> persistSetting(
|
||||||
|
BuildContext context,
|
||||||
|
Future<void> Function() action, {
|
||||||
|
String message = 'Could not save that setting. Please try again.',
|
||||||
|
}) async {
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
} catch (_) {
|
||||||
|
messenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: GestureDetector(
|
||||||
|
onTap: () => messenger.hideCurrentSnackBar(),
|
||||||
|
child: Text(message),
|
||||||
|
),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
dismissDirection: DismissDirection.down,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:meshcore_open/helpers/settings_persist.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
Widget harness(Future<void> Function() action) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Builder(
|
||||||
|
builder: (context) => ElevatedButton(
|
||||||
|
onPressed: () =>
|
||||||
|
persistSetting(context, action, message: 'Save failed'),
|
||||||
|
child: const Text('go'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('shows an error SnackBar when the persist action throws', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
harness(() async {
|
||||||
|
throw Exception('boom');
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('go'));
|
||||||
|
await tester.pump(); // run persistSetting (catch + schedule SnackBar)
|
||||||
|
await tester.pump(); // build the SnackBar
|
||||||
|
|
||||||
|
expect(find.text('Save failed'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows no SnackBar when the persist action succeeds', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(harness(() async {}));
|
||||||
|
|
||||||
|
await tester.tap(find.text('go'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.byType(SnackBar), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in new issue