diff --git a/lib/screens/settings/atmos_diagnostics_screen.dart b/lib/screens/settings/atmos_diagnostics_screen.dart index 8ecb7e9a..4935bb18 100644 --- a/lib/screens/settings/atmos_diagnostics_screen.dart +++ b/lib/screens/settings/atmos_diagnostics_screen.dart @@ -29,6 +29,7 @@ class _AtmosDiagnosticsScreenState extends State { Timer? _poll; Map _status = const {}; String? _activeMode; + bool _stopping = false; @override void initState() { @@ -40,7 +41,7 @@ class _AtmosDiagnosticsScreenState extends State { @override void dispose() { _poll?.cancel(); - _channel.invokeMethod('stop').ignore(); + if (!_stopping) _channel.invokeMethod('stop').ignore(); super.dispose(); } @@ -70,9 +71,18 @@ class _AtmosDiagnosticsScreenState extends State { } Future _stop() async { - await _channel.invokeMethod('stop'); - setState(() => _activeMode = null); - await _refreshStatus(); + if (_stopping) return; + _stopping = true; + try { + await _channel.invokeMethod('stop'); + if (!mounted) return; + setState(() => _activeMode = null); + await _refreshStatus(); + } on PlatformException catch (e) { + if (mounted) showErrorSnackBar(context, e.message ?? e.code); + } finally { + _stopping = false; + } } Widget _testTile({required String mode, required IconData icon, required String title, required String subtitle}) { diff --git a/test/screens/settings/atmos_diagnostics_screen_test.dart b/test/screens/settings/atmos_diagnostics_screen_test.dart new file mode 100644 index 00000000..16e6aa7f --- /dev/null +++ b/test/screens/settings/atmos_diagnostics_screen_test.dart @@ -0,0 +1,80 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/i18n/strings.g.dart'; +import 'package:plezy/screens/settings/atmos_diagnostics_screen.dart'; +import 'package:plezy/services/settings_service.dart'; +import 'package:plezy/theme/mono_theme.dart'; + +import '../../test_helpers/prefs.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const channel = MethodChannel('plezy/atmos_probe'); + final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + + setUp(() async { + resetSharedPreferencesForTest(); + SettingsService.resetForTesting(); + await SettingsService.getInstance(); + LocaleSettings.setLocaleSync(AppLocale.en); + }); + + tearDown(() { + messenger.setMockMethodCallHandler(channel, null); + }); + + Future pumpScreen(WidgetTester tester) async { + await tester.pumpWidget(MaterialApp(theme: monoTheme(dark: true), home: const AtmosDiagnosticsScreen())); + await tester.pump(); + await tester.ensureVisible(find.text(t.settings.atmosTestStop)); + } + + testWidgets('unmounting during stop does not set state or stop twice', (tester) async { + final stopCompleter = Completer(); + var stopCalls = 0; + messenger.setMockMethodCallHandler(channel, (call) async { + if (call.method == 'stop') { + stopCalls++; + await stopCompleter.future; + } + return call.method == 'getStatus' ? {} : null; + }); + + await pumpScreen(tester); + await tester.tap(find.text(t.settings.atmosTestStop)); + await tester.pump(); + expect(stopCalls, 1); + + await tester.pumpWidget(const MaterialApp(home: SizedBox())); + expect(stopCalls, 1); + + stopCompleter.complete(); + await tester.pump(); + + expect(tester.takeException(), isNull); + expect(stopCalls, 1); + }); + + testWidgets('native stop failure is shown without escaping', (tester) async { + var stopCalls = 0; + messenger.setMockMethodCallHandler(channel, (call) async { + if (call.method == 'stop') { + stopCalls++; + throw PlatformException(code: 'stop_failed', message: 'Native stop failed'); + } + return call.method == 'getStatus' ? {} : null; + }); + + await pumpScreen(tester); + await tester.tap(find.text(t.settings.atmosTestStop)); + await tester.pump(); + + expect(stopCalls, 1); + expect(find.text('Native stop failed'), findsOneWidget); + expect(tester.takeException(), isNull); + }); +}