343 lines
11 KiB
Dart
343 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.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/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<bool> canLaunch(String? url) async => true;
|
|
|
|
@override
|
|
Future<bool> launchUrl(String? url, LaunchOptions? options) async {
|
|
launchedUrl = url;
|
|
launchOptions = options;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
setUpAll(() {
|
|
PackageInfo.setMockInitialValues(
|
|
appName: 'SharedInbox',
|
|
packageName: 'org.sharedinbox',
|
|
version: '1.0.0',
|
|
buildNumber: '42',
|
|
buildSignature: '',
|
|
);
|
|
});
|
|
|
|
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();
|
|
|
|
// Regression for #146: URL must contain only the title, NOT the full
|
|
// report body. Long stack traces caused "create issue failed" by
|
|
// exceeding browser URL-length limits. The report is copied to clipboard
|
|
// so the user can paste it into the issue body.
|
|
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, isNot(contains('&body=')));
|
|
expect(mock.launchedUrl, isNot(contains('App%20Version')));
|
|
expect(mock.launchedUrl, isNot(contains('Stack%20Trace')));
|
|
});
|
|
|
|
testWidgets(
|
|
'CrashScreen copy-to-clipboard includes version and platform info',
|
|
(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<dynamic, dynamic>)['text'] as String?;
|
|
}
|
|
return null;
|
|
},
|
|
);
|
|
addTearDown(
|
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
null,
|
|
),
|
|
);
|
|
|
|
const exception = 'TestException: clipboard test';
|
|
final stackTrace = StackTrace.current;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: CrashScreen(exception: exception, stackTrace: stackTrace),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('Copy to Clipboard'));
|
|
await tester.pump();
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(clipboardText, isNotNull);
|
|
expect(clipboardText, contains('App Version: 1.0.0+42'));
|
|
expect(clipboardText, contains('Build Mode:'));
|
|
expect(clipboardText, contains('Platform:'));
|
|
expect(clipboardText, contains('Dart:'));
|
|
expect(clipboardText, contains('Timestamp:'));
|
|
expect(clipboardText, contains('TestException: clipboard test'));
|
|
// GIT_HASH is empty in test builds — no Git Commit line expected
|
|
expect(clipboardText, isNot(contains('Git Commit:')));
|
|
},
|
|
);
|
|
|
|
testWidgets('CrashScreen shows git hash as clickable link above stacktrace', (
|
|
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: git hash test';
|
|
final stackTrace = StackTrace.current;
|
|
const testHash = 'abc1234';
|
|
|
|
await tester.pumpWidget(
|
|
CrashScreen(
|
|
exception: exception,
|
|
stackTrace: stackTrace,
|
|
gitHash: testHash,
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Git hash link should be present
|
|
final gitLinkFinder = find.textContaining('Git Commit: abc1234');
|
|
expect(gitLinkFinder, findsOneWidget);
|
|
|
|
// Link must appear above the stack trace
|
|
final stackTraceFinder = find.text('Stack Trace:');
|
|
expect(
|
|
tester.getTopLeft(gitLinkFinder).dy,
|
|
lessThan(tester.getTopLeft(stackTraceFinder).dy),
|
|
);
|
|
|
|
// Tapping the link should open the Codeberg commit URL
|
|
await tester.tap(gitLinkFinder);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
mock.launchedUrl,
|
|
equals('https://codeberg.org/guettli/sharedinbox/commit/abc1234'),
|
|
);
|
|
});
|
|
|
|
testWidgets('CrashScreen shows version, build mode, and platform in the UI', (
|
|
tester,
|
|
) async {
|
|
tester.view.physicalSize = const Size(800, 1200);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
const exception = 'TestException: info row test';
|
|
final stackTrace = StackTrace.current;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: CrashScreen(exception: exception, stackTrace: stackTrace),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Info row shows app version (from mock), build mode, and platform OS.
|
|
expect(find.textContaining('1.0.0+42'), findsWidgets);
|
|
// In test builds kDebugMode is true.
|
|
expect(find.textContaining('debug'), findsOneWidget);
|
|
// Platform OS is always present (linux in CI, android/ios on device).
|
|
expect(
|
|
find.textContaining(RegExp(r'linux|android|ios|windows|macos')),
|
|
findsWidgets,
|
|
);
|
|
});
|
|
|
|
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<dynamic, dynamic>)['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 {
|
|
// Regression test for: ScaffoldMessenger.of(context) null-crash when
|
|
// CrashScreen is the root widget (runApp path after startup crash).
|
|
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: startup crash';
|
|
final stackTrace = StackTrace.current;
|
|
|
|
// Pump CrashScreen directly as the root — no parent MaterialApp.
|
|
await tester.pumpWidget(
|
|
CrashScreen(exception: exception, stackTrace: stackTrace),
|
|
);
|
|
|
|
expect(find.textContaining('TestException'), findsOneWidget);
|
|
|
|
// Tapping 'Report Issue on Codeberg' must not crash. Previously
|
|
// ScaffoldMessenger.of(context) threw because context was above the
|
|
// MaterialApp that CrashScreen itself creates.
|
|
await tester.tap(find.text('Report Issue on Codeberg'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
mock.launchedUrl,
|
|
contains('https://codeberg.org/guettli/sharedinbox/issues/new'),
|
|
);
|
|
},
|
|
);
|
|
}
|