From 2bb2a93f636df3be181269c345fcf0d07aeeca44 Mon Sep 17 00:00:00 2001 From: Thomas SharedInbox Date: Mon, 25 May 2026 08:12:35 +0200 Subject: [PATCH] feat: show app version as link on crash screen and in MD report (#236) When a git hash is available, the crash screen now displays the app version number as a tappable link (pointing to the Codeberg commit page) above the existing git-hash link, and the clipboard markdown report formats the App Version line as a markdown link in the same way the About screen already does. Co-Authored-By: Claude Sonnet 4.6 --- lib/ui/screens/crash_screen.dart | 34 +++++++++- test/widget/crash_screen_test.dart | 104 +++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/lib/ui/screens/crash_screen.dart b/lib/ui/screens/crash_screen.dart index 0780c25..0573f8b 100644 --- a/lib/ui/screens/crash_screen.dart +++ b/lib/ui/screens/crash_screen.dart @@ -37,11 +37,14 @@ class CrashScreen extends StatelessWidget { final version = await _fetchVersion(); final platform = '${Platform.operatingSystem} ${Platform.operatingSystemVersion}'; + final versionDisplay = gitHash.isNotEmpty + ? '[$version](https://codeberg.org/guettli/sharedinbox/commit/$gitHash)' + : version; final gitLine = gitHash.isNotEmpty ? 'Git Commit: [$gitHash](https://codeberg.org/guettli/sharedinbox/commit/$gitHash)\n' : ''; final timestamp = DateTime.now().toUtc().toIso8601String(); - return 'App Version: $version\n' + return 'App Version: $versionDisplay\n' 'Build Mode: $_buildMode\n' '$gitLine' 'Platform: $platform\n' @@ -86,6 +89,35 @@ class CrashScreen extends StatelessWidget { ), if (gitHash.isNotEmpty) ...[ const SizedBox(height: 8), + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (_, snapshot) { + if (!snapshot.hasData) return const SizedBox.shrink(); + final version = + '${snapshot.data!.version}+${snapshot.data!.buildNumber}'; + return GestureDetector( + onTap: () async { + final url = Uri.parse( + 'https://codeberg.org/guettli/sharedinbox/commit/$gitHash', + ); + await launchUrl( + url, + mode: LaunchMode.externalApplication, + ); + }, + child: Text( + 'App Version: $version', + style: const TextStyle( + fontSize: 12, + color: Colors.blue, + decoration: TextDecoration.underline, + ), + textAlign: TextAlign.center, + ), + ); + }, + ), + const SizedBox(height: 4), GestureDetector( onTap: () async { final url = Uri.parse( diff --git a/test/widget/crash_screen_test.dart b/test/widget/crash_screen_test.dart index 3925dbb..f191220 100644 --- a/test/widget/crash_screen_test.dart +++ b/test/widget/crash_screen_test.dart @@ -147,6 +147,7 @@ void main() { gitHash: testHash, ), ); + await tester.pumpAndSettle(); // Git hash link should be present final gitLinkFinder = find.textContaining('Git Commit: abc1234'); @@ -199,6 +200,109 @@ void main() { }, ); + testWidgets( + 'CrashScreen shows app version as clickable link when git hash is set', + (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: version link test'; + final stackTrace = StackTrace.current; + const testHash = 'abc1234'; + + await tester.pumpWidget( + CrashScreen( + exception: exception, + stackTrace: stackTrace, + gitHash: testHash, + ), + ); + await tester.pumpAndSettle(); + + // App version link should be present (mocked as 1.0.0+42) + final versionLinkFinder = find.textContaining('App Version: 1.0.0+42'); + expect(versionLinkFinder, findsOneWidget); + + // It must appear above the git hash link + final gitLinkFinder = find.textContaining('Git Commit: abc1234'); + expect( + tester.getTopLeft(versionLinkFinder).dy, + lessThan(tester.getTopLeft(gitLinkFinder).dy), + ); + + // Tapping it should open the Codeberg commit URL + await tester.tap(versionLinkFinder); + await tester.pumpAndSettle(); + + expect( + mock.launchedUrl, + equals('https://codeberg.org/guettli/sharedinbox/commit/abc1234'), + ); + }, + ); + + testWidgets( + 'CrashScreen copy-to-clipboard includes app version as markdown link when git hash is set', + (tester) async { + tester.view.physicalSize = const Size(800, 1200); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + String? clipboardText; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + (MethodCall call) async { + if (call.method == 'Clipboard.setData') { + clipboardText = + (call.arguments as Map)['text'] as String?; + } + return null; + }, + ); + addTearDown( + () => tester.binding.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null), + ); + + const exception = 'TestException: version link clipboard test'; + final stackTrace = StackTrace.current; + const testHash = 'abc1234'; + + await tester.pumpWidget( + CrashScreen( + exception: exception, + stackTrace: stackTrace, + gitHash: testHash, + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Copy to Clipboard')); + await tester.pump(); + await tester.pump(); + await tester.pumpAndSettle(); + + expect(clipboardText, isNotNull); + // App Version must be a markdown link pointing to the commit + expect( + clipboardText, + contains( + 'App Version: [1.0.0+42](https://codeberg.org/guettli/sharedinbox/commit/abc1234)', + ), + ); + expect( + clipboardText, + contains( + 'Git Commit: [abc1234](https://codeberg.org/guettli/sharedinbox/commit/abc1234)', + ), + ); + }, + ); + testWidgets( 'CrashScreen used as root widget — buttons work without ScaffoldMessenger crash', (tester) async {