fix: close Raw Email dialog automatically after download (#114)
After a successful download, Navigator.pop is called so the dialog dismisses without requiring a manual close. Adds a widget test that verifies this using a fake PathProviderPlatform and IOOverrides so the entire async chain runs as pure microtasks inside the Flutter test zone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
651110b389
commit
e327b42312
@@ -495,7 +495,10 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
|
||||
child: const Text('Copy'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => unawaited(_downloadRaw(ctx, header, raw)),
|
||||
onPressed: () async {
|
||||
await _downloadRaw(ctx, header, raw);
|
||||
if (ctx.mounted) Navigator.pop(ctx);
|
||||
},
|
||||
child: const Text('Download'),
|
||||
),
|
||||
TextButton(
|
||||
|
||||
@@ -72,6 +72,7 @@ dev_dependencies:
|
||||
test: ^1.25.0
|
||||
mockito: ^5.4.4
|
||||
fake_async: ^1.3.1
|
||||
path_provider_platform_interface: ^2.1.2
|
||||
sqlite3: any # used directly in test/unit/db_test_helper.dart
|
||||
url_launcher_platform_interface: ^2.3.2
|
||||
plugin_platform_interface: ^2.1.8
|
||||
|
||||
@@ -1,14 +1,53 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
||||
|
||||
import 'package:sharedinbox/core/models/email.dart';
|
||||
import 'package:sharedinbox/di.dart';
|
||||
|
||||
import 'helpers.dart';
|
||||
|
||||
// Fake PathProviderPlatform so _downloadRaw resolves getDownloadsDirectory /
|
||||
// getTemporaryDirectory via pure microtasks instead of calling xdg-user-dir.
|
||||
class _FakePathProviderPlatform extends PathProviderPlatform {
|
||||
@override
|
||||
Future<String?> getTemporaryPath() async => '/tmp';
|
||||
|
||||
@override
|
||||
Future<String?> getDownloadsPath() async => '/tmp';
|
||||
}
|
||||
|
||||
// IOOverrides subclass that stubs File creation so _downloadRaw completes
|
||||
// without real dart:io — writeAsString becomes a no-op microtask.
|
||||
base class _FakeIOOverrides extends IOOverrides {
|
||||
@override
|
||||
File createFile(String path) => _FakeFile(path);
|
||||
}
|
||||
|
||||
// Fake File whose writeAsString is a no-op so _downloadRaw completes without
|
||||
// real I/O. Other methods are unused and left to Fake's noSuchMethod handler.
|
||||
class _FakeFile extends Fake implements File {
|
||||
_FakeFile(this._path);
|
||||
final String _path;
|
||||
|
||||
@override
|
||||
String get path => _path;
|
||||
|
||||
@override
|
||||
Future<File> writeAsString(
|
||||
String contents, {
|
||||
FileMode mode = FileMode.write,
|
||||
Encoding encoding = utf8,
|
||||
bool flush = false,
|
||||
}) async =>
|
||||
this;
|
||||
}
|
||||
|
||||
// Shared overrides for email detail tests.
|
||||
List<Override> _overrides({required EmailBody body, Email? email}) => [
|
||||
accountRepositoryProvider.overrideWithValue(
|
||||
@@ -178,6 +217,61 @@ void main() {
|
||||
expect(find.text('2.0 KB'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Download Raw Email closes dialog after download', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
buildApp(
|
||||
initialLocation: '/accounts/acc-1/mailboxes/INBOX/emails/acc-1%3A42',
|
||||
overrides: [
|
||||
accountRepositoryProvider.overrideWithValue(
|
||||
FakeAccountRepository([kTestAccount]),
|
||||
),
|
||||
mailboxRepositoryProvider
|
||||
.overrideWithValue(FakeMailboxRepository()),
|
||||
emailRepositoryProvider.overrideWithValue(
|
||||
FakeEmailRepository(
|
||||
emailDetail: testEmail(),
|
||||
emailBody:
|
||||
const EmailBody(emailId: 'acc-1:42', attachments: []),
|
||||
rawRfc822: 'Subject: test\r\n\r\nBody',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(PopupMenuButton<String>));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Show Raw Email'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Raw Email'), findsOneWidget);
|
||||
|
||||
// Replace path_provider and File I/O with pure-microtask fakes so the
|
||||
// entire _downloadRaw → Navigator.pop chain completes within pump loops.
|
||||
final prevPathProvider = PathProviderPlatform.instance;
|
||||
PathProviderPlatform.instance = _FakePathProviderPlatform();
|
||||
IOOverrides.global = _FakeIOOverrides();
|
||||
addTearDown(() {
|
||||
PathProviderPlatform.instance = prevPathProvider;
|
||||
IOOverrides.global = null;
|
||||
});
|
||||
|
||||
await tester.tap(find.text('Download'));
|
||||
// Each pump drains one microtask level: getDownloadsDirectory, then
|
||||
// writeAsString, then _downloadRaw return, then Navigator.pop.
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await tester.pump(Duration.zero);
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Dialog must be dismissed after download completes.
|
||||
expect(find.text('Raw Email'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Show Mail Structure opens dialog with MIME parts', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
Reference in New Issue
Block a user