diff --git a/mobile/lib/features/settings/settings_page.dart b/mobile/lib/features/settings/settings_page.dart index d9ab3fe073..8fd32af987 100644 --- a/mobile/lib/features/settings/settings_page.dart +++ b/mobile/lib/features/settings/settings_page.dart @@ -150,15 +150,41 @@ class _VersionFooter extends StatelessWidget { @override Widget build(BuildContext context) { + final label = buildNumber.isEmpty + ? 'v$version' + : 'v$version ($buildNumber)'; return SafeArea( top: false, - child: Padding( - padding: const EdgeInsets.only(bottom: Grid.xs, top: Grid.xxs), - child: Center( - child: Text( - buildNumber.isEmpty ? 'v$version' : 'v$version ($buildNumber)', - style: context.textTheme.bodySmall?.copyWith( - color: context.colors.onSurfaceVariant.withValues(alpha: 0.6), + child: Center( + child: Semantics( + button: true, + onTapHint: 'Copy version', + child: InkWell( + borderRadius: BorderRadius.circular(Grid.xs), + onTap: () => + copyToClipboard(context, label, message: 'Version copied'), + // Keep the text where it was and grow the tap target downward + // to the platform minimum. + child: ConstrainedBox( + constraints: const BoxConstraints( + minHeight: kMinInteractiveDimension, + ), + child: Padding( + padding: const EdgeInsets.fromLTRB( + Grid.xs, + Grid.xxs, + Grid.xs, + Grid.xs, + ), + child: Text( + label, + style: context.textTheme.bodySmall?.copyWith( + color: context.colors.onSurfaceVariant.withValues( + alpha: 0.6, + ), + ), + ), + ), ), ), ), diff --git a/mobile/test/features/settings/settings_page_test.dart b/mobile/test/features/settings/settings_page_test.dart index fe57d9be35..6f741d25ad 100644 --- a/mobile/test/features/settings/settings_page_test.dart +++ b/mobile/test/features/settings/settings_page_test.dart @@ -92,6 +92,79 @@ void main() { }); } + for (final viaKeyboard in [false, true]) { + testWidgets( + '${viaKeyboard ? 'keyboard activation of' : 'tapping'} the version copies the whole version string', + (tester) async { + String? copied; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + (call) async { + if (call.method == 'Clipboard.setData') { + copied = (call.arguments as Map)['text'] as String?; + } + return null; + }, + ); + addTearDown( + () => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + null, + ), + ); + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + await tester.pumpWidget( + ProviderScope( + overrides: [savedPrefsProvider.overrideWithValue(prefs)], + child: MaterialApp( + theme: AppTheme.light(), + home: SettingsPage( + profileHeader: const SizedBox.shrink(), + identityRecoveryPageBuilder: (_) => const SizedBox.shrink(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + final version = find.text('v0.16.0 (432)'); + await tester.scrollUntilVisible(version, 200); + await tester.pumpAndSettle(); + final target = find.ancestor( + of: version, + matching: find.byType(InkWell), + ); + expect( + tester.getSize(target).height, + greaterThanOrEqualTo(kMinInteractiveDimension), + ); + + if (viaKeyboard) { + final versionFocus = Focus.of(tester.element(version)); + var tabs = 0; + while (!versionFocus.hasPrimaryFocus) { + expect( + tabs++, + lessThan(50), + reason: 'version is not reachable by Tab', + ); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pump(); + } + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + } else { + await tester.tap(version); + } + await tester.pump(); + + expect(copied, 'v0.16.0 (432)'); + expect(find.text('Version copied'), findsOneWidget); + await tester.pump(const Duration(seconds: 5)); + await tester.pumpAndSettle(); + }, + ); + } + for (final outcome in ['absent', 'error']) { testWidgets('hides notifications while capability loads and is $outcome', ( tester,