Files
sharedinbox/test/widget/about_screen_test.dart
T
Thomas SharedInboxandClaude Sonnet 4.6 a0071c86f8 fix(tests): suppress ink_sparkle shader crash in about/crash screen tests
CrashScreen creates its own MaterialApp, so the NoSplash fix in helpers.dart
(from #486) did not cover tests that exercise CrashScreen directly or via a
wrapper MaterialApp. The inner MaterialApp loaded InkSparkle on first tap,
crashing with "Unsupported runtime stages format version".

Fixes:
- Add splashFactory: NoSplash.splashFactory to CrashScreen's own MaterialApp
  (appropriate for an error screen — no ripple effects needed).
- Add splashFactory: NoSplash.splashFactory to the bare MaterialApp in
  about_screen_test.dart's _buildScreen helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 22:52:20 +02:00

231 lines
7.4 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:sharedinbox/core/models/account.dart';
import 'package:sharedinbox/di.dart';
import 'package:sharedinbox/ui/screens/about_screen.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
import 'helpers.dart';
class MockUrlLauncher extends Mock
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {
String? launchedUrl;
@override
Future<bool> canLaunch(String? url) async => true;
@override
Future<bool> launchUrl(String? url, LaunchOptions? options) async {
launchedUrl = url;
return true;
}
}
class ThrowingUrlLauncher extends Mock
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {
@override
Future<bool> canLaunch(String? url) async => true;
@override
Future<bool> launchUrl(String? url, LaunchOptions? options) async {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel: '
'"dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.launchUrl".',
);
}
}
Widget _buildScreen({List<Account> accounts = const []}) {
return ProviderScope(
overrides: [
accountRepositoryProvider.overrideWithValue(
FakeAccountRepository(accounts),
),
],
child: MaterialApp(
theme: ThemeData(splashFactory: NoSplash.splashFactory),
home: const AboutScreen(),
),
);
}
void main() {
setUpAll(() {
PackageInfo.setMockInitialValues(
appName: 'SharedInbox',
packageName: 'org.sharedinbox',
version: '1.2.3',
buildNumber: '99',
buildSignature: '',
);
});
testWidgets('AboutScreen shows title and info table', (tester) async {
tester.view.physicalSize = const Size(800, 1200);
tester.view.devicePixelRatio = 2.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(_buildScreen());
await tester.pumpAndSettle();
expect(find.text('About'), findsOneWidget);
expect(find.textContaining('App Version'), findsWidgets);
expect(find.textContaining('1.2.3+99'), findsOneWidget);
expect(find.textContaining('Resolution'), findsWidgets);
expect(find.textContaining('Dark Mode'), findsWidgets);
expect(find.textContaining('IMAP Accounts'), findsWidgets);
expect(find.textContaining('JMAP Accounts'), findsWidgets);
expect(find.textContaining('Locale'), findsWidgets);
expect(find.textContaining('Text Scale'), findsWidgets);
expect(find.textContaining('DB Schema Version'), findsWidgets);
// Buttons are in the body, not in the AppBar actions
expect(find.byIcon(Icons.copy), findsOneWidget);
expect(find.byIcon(Icons.bug_report_outlined), findsOneWidget);
expect(find.byIcon(Icons.feedback_outlined), findsOneWidget);
expect(find.text('Copy info'), findsOneWidget);
expect(find.text('Public issue'), findsOneWidget);
expect(find.text('Report bug'), findsOneWidget);
});
testWidgets('AboutScreen shows correct IMAP and JMAP account counts', (
tester,
) async {
tester.view.physicalSize = const Size(800, 1200);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
_buildScreen(
accounts: [
const Account(
id: 'imap-1',
displayName: 'Alice',
email: 'alice@example.com',
imapHost: 'imap.example.com',
smtpHost: 'smtp.example.com',
),
const Account(
id: 'imap-2',
displayName: 'Bob',
email: 'bob@example.com',
imapHost: 'imap.example.com',
smtpHost: 'smtp.example.com',
),
const Account(
id: 'jmap-1',
displayName: 'Carol',
email: 'carol@example.com',
type: AccountType.jmap,
jmapUrl: 'https://jmap.example.com',
),
],
),
);
await tester.pumpAndSettle();
expect(find.textContaining('IMAP Accounts'), findsWidgets);
expect(find.textContaining('JMAP Accounts'), findsWidgets);
});
testWidgets('AboutScreen copy button puts markdown in clipboard', (
tester,
) async {
tester.view.physicalSize = const Size(800, 1200);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
String? clipboardText;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(MethodCall call) async {
if (call.method == 'Clipboard.setData') {
clipboardText =
(call.arguments as Map<dynamic, dynamic>)['text'] as String?;
}
return null;
},
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
),
);
await tester.pumpWidget(_buildScreen());
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.copy));
await tester.pump();
await tester.pump();
await tester.pumpAndSettle();
expect(clipboardText, isNotNull);
expect(clipboardText, contains('1.2.3+99'));
expect(clipboardText, contains('App Version'));
expect(clipboardText, contains('Resolution'));
expect(clipboardText, contains('Dark Mode'));
expect(clipboardText, contains('IMAP Accounts'));
expect(clipboardText, contains('JMAP Accounts'));
expect(clipboardText, contains('Locale'));
expect(clipboardText, contains('Text Scale'));
expect(clipboardText, contains('DB Schema Version'));
expect(clipboardText, contains('[sharedinbox.de](https://sharedinbox.de)'));
});
testWidgets('AboutScreen create-issue button opens Codeberg URL', (
tester,
) async {
tester.view.physicalSize = const Size(800, 1200);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final mock = MockUrlLauncher();
UrlLauncherPlatform.instance = mock;
await tester.pumpWidget(_buildScreen());
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.bug_report_outlined));
await tester.pumpAndSettle();
expect(
mock.launchedUrl,
contains('https://codeberg.org/guettli/sharedinbox/issues/new'),
);
expect(mock.launchedUrl, contains('1.2.3%2B99'));
});
testWidgets(
'AboutScreen link tap with failed url_launcher shows error snackbar',
(tester) async {
tester.view.physicalSize = const Size(800, 1200);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
UrlLauncherPlatform.instance = ThrowingUrlLauncher();
await tester.pumpWidget(_buildScreen());
await tester.pumpAndSettle();
await tester.tap(find.textContaining('sharedinbox.de').first);
await tester.pumpAndSettle();
expect(find.textContaining('Error:'), findsOneWidget);
},
);
}