diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 39c30fe..1aa05eb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -38,6 +38,10 @@ In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. --> + + + + diff --git a/lib/ui/screens/crash_screen.dart b/lib/ui/screens/crash_screen.dart index 432ee6d..20575c3 100644 --- a/lib/ui/screens/crash_screen.dart +++ b/lib/ui/screens/crash_screen.dart @@ -90,11 +90,33 @@ class CrashScreen extends StatelessWidget { const SizedBox(height: 16), OutlinedButton.icon( onPressed: () async { - final url = Uri.parse( - 'https://codeberg.org/guettli/sharedinbox/issues/new', + final title = Uri.encodeComponent( + 'Crash: ${exception.toString().split('\n').first}', ); - if (await canLaunchUrl(url)) { - await launchUrl(url, mode: LaunchMode.externalApplication); + final body = Uri.encodeComponent( + 'Error: $exception\n\nStack Trace:\n$stackTrace', + ); + final url = Uri.parse( + 'https://codeberg.org/guettli/sharedinbox/issues/new?title=$title&body=$body', + ); + try { + final launched = await launchUrl( + url, + mode: LaunchMode.externalApplication, + ); + if (!launched && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Could not open browser.'), + ), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } } }, icon: const Icon(Icons.bug_report), diff --git a/pubspec.yaml b/pubspec.yaml index 0e5d035..3587141 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,8 @@ dev_dependencies: mockito: ^5.4.4 fake_async: ^1.3.1 sqlite3: any # used directly in test/unit/db_test_helper.dart + url_launcher_platform_interface: ^2.3.2 + plugin_platform_interface: ^2.1.8 flutter: uses-material-design: true diff --git a/test/widget/crash_screen_test.dart b/test/widget/crash_screen_test.dart new file mode 100644 index 0000000..7f61030 --- /dev/null +++ b/test/widget/crash_screen_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:sharedinbox/ui/screens/crash_screen.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +class MockUrlLauncher extends Mock + with MockPlatformInterfaceMixin + implements UrlLauncherPlatform { + String? launchedUrl; + LaunchOptions? launchOptions; + + @override + Future canLaunch(String? url) async => true; + + @override + Future launchUrl(String? url, LaunchOptions? options) async { + launchedUrl = url; + launchOptions = options; + return true; + } +} + +void main() { + testWidgets('CrashScreen shows error details and has a report button', + (tester) async { + tester.view.physicalSize = const Size(800, 1200); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + final mock = MockUrlLauncher(); + UrlLauncherPlatform.instance = mock; + + const exception = 'TestException: something broke'; + final stackTrace = StackTrace.current; + + await tester.pumpWidget( + MaterialApp( + home: CrashScreen(exception: exception, stackTrace: stackTrace), + ), + ); + + expect(find.textContaining('TestException'), findsOneWidget); + expect(find.text('Report Issue on Codeberg'), findsOneWidget); + + await tester.tap(find.text('Report Issue on Codeberg')); + await tester.pumpAndSettle(); + + expect( + mock.launchedUrl, + contains('https://codeberg.org/guettli/sharedinbox/issues/new'), + ); + expect( + mock.launchedUrl, + contains('title=Crash%3A%20TestException%3A%20something%20broke'), + ); + expect( + mock.launchedUrl, + contains('body=Error%3A%20TestException%3A%20something%20broke'), + ); + }); +}