Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a18784a76 | ||
|
|
a723380560 |
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@@ -60,20 +61,27 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
|
|||||||
tooltip: 'Reply',
|
tooltip: 'Reply',
|
||||||
onPressed: header == null
|
onPressed: header == null
|
||||||
? null
|
? null
|
||||||
: () => _reply(context, header, body, replyAll: false),
|
: () {
|
||||||
|
unawaited(_reply(context, header, body, replyAll: false));
|
||||||
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.reply_all),
|
icon: const Icon(Icons.reply_all),
|
||||||
tooltip: 'Reply all',
|
tooltip: 'Reply all',
|
||||||
onPressed: header == null
|
onPressed: header == null
|
||||||
? null
|
? null
|
||||||
: () => _reply(context, header, body, replyAll: true),
|
: () {
|
||||||
|
unawaited(_reply(context, header, body, replyAll: true));
|
||||||
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.forward),
|
icon: const Icon(Icons.forward),
|
||||||
tooltip: 'Forward',
|
tooltip: 'Forward',
|
||||||
onPressed:
|
onPressed: header == null
|
||||||
header == null ? null : () => _forward(context, header, body),
|
? null
|
||||||
|
: () {
|
||||||
|
unawaited(_forward(context, header, body));
|
||||||
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.mark_email_unread_outlined),
|
icon: const Icon(Icons.mark_email_unread_outlined),
|
||||||
@@ -263,26 +271,31 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _quotedBody(Email header, EmailBody? body) {
|
Future<String> _quotedBody(Email header, EmailBody? body) async {
|
||||||
final date = header.sentAt != null ? _dateFmt.format(header.sentAt!) : '';
|
final date = header.sentAt != null ? _dateFmt.format(header.sentAt!) : '';
|
||||||
final from =
|
final from =
|
||||||
header.from.isNotEmpty ? header.from.first.toString() : '(unknown)';
|
header.from.isNotEmpty ? header.from.first.toString() : '(unknown)';
|
||||||
final text = body?.textBody ?? htmlToPlain(body?.htmlBody ?? '');
|
final rawText = body?.textBody;
|
||||||
|
final text = (rawText != null && rawText.isNotEmpty)
|
||||||
|
? rawText
|
||||||
|
: await compute(htmlToPlain, body?.htmlBody ?? '');
|
||||||
final quoted = text.trim().split('\n').map((l) => '> $l').join('\n');
|
final quoted = text.trim().split('\n').map((l) => '> $l').join('\n');
|
||||||
return '\n\n— On $date, $from wrote:\n$quoted';
|
return '\n\n— On $date, $from wrote:\n$quoted';
|
||||||
}
|
}
|
||||||
|
|
||||||
void _reply(
|
Future<void> _reply(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Email header,
|
Email header,
|
||||||
EmailBody? body, {
|
EmailBody? body, {
|
||||||
required bool replyAll,
|
required bool replyAll,
|
||||||
}) {
|
}) async {
|
||||||
final to = header.from.isNotEmpty ? header.from.first.email : '';
|
final to = header.from.isNotEmpty ? header.from.first.email : '';
|
||||||
final subject = (header.subject?.startsWith('Re:') ?? false)
|
final subject = (header.subject?.startsWith('Re:') ?? false)
|
||||||
? header.subject!
|
? header.subject!
|
||||||
: 'Re: ${header.subject ?? ''}';
|
: 'Re: ${header.subject ?? ''}';
|
||||||
final cc = replyAll ? header.to.map((a) => a.email).join(', ') : '';
|
final cc = replyAll ? header.to.map((a) => a.email).join(', ') : '';
|
||||||
|
final quoted = await _quotedBody(header, body);
|
||||||
|
if (!context.mounted) return;
|
||||||
unawaited(
|
unawaited(
|
||||||
context.push(
|
context.push(
|
||||||
'/compose',
|
'/compose',
|
||||||
@@ -290,23 +303,29 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
|
|||||||
'replyToEmailId': widget.emailId,
|
'replyToEmailId': widget.emailId,
|
||||||
'prefillTo': to,
|
'prefillTo': to,
|
||||||
'prefillSubject': subject,
|
'prefillSubject': subject,
|
||||||
'prefillBody': _quotedBody(header, body),
|
'prefillBody': quoted,
|
||||||
if (cc.isNotEmpty) 'prefillCc': cc,
|
if (cc.isNotEmpty) 'prefillCc': cc,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _forward(BuildContext context, Email header, EmailBody? body) {
|
Future<void> _forward(
|
||||||
|
BuildContext context,
|
||||||
|
Email header,
|
||||||
|
EmailBody? body,
|
||||||
|
) async {
|
||||||
final subject = (header.subject?.startsWith('Fwd:') ?? false)
|
final subject = (header.subject?.startsWith('Fwd:') ?? false)
|
||||||
? header.subject!
|
? header.subject!
|
||||||
: 'Fwd: ${header.subject ?? ''}';
|
: 'Fwd: ${header.subject ?? ''}';
|
||||||
|
final quoted = await _quotedBody(header, body);
|
||||||
|
if (!context.mounted) return;
|
||||||
unawaited(
|
unawaited(
|
||||||
context.push(
|
context.push(
|
||||||
'/compose',
|
'/compose',
|
||||||
extra: {
|
extra: {
|
||||||
'prefillSubject': subject,
|
'prefillSubject': subject,
|
||||||
'prefillBody': _quotedBody(header, body),
|
'prefillBody': quoted,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:sharedinbox/core/models/email.dart';
|
||||||
|
import 'package:sharedinbox/di.dart';
|
||||||
|
|
||||||
|
import 'helpers.dart';
|
||||||
|
|
||||||
|
// Fixed-date emails so golden files don't change day to day.
|
||||||
|
final _kDate = DateTime(2024, 6);
|
||||||
|
|
||||||
|
Email _email({
|
||||||
|
String id = 'acc-1:1',
|
||||||
|
String subject = 'Hello world',
|
||||||
|
bool isSeen = true,
|
||||||
|
bool isFlagged = false,
|
||||||
|
}) =>
|
||||||
|
Email(
|
||||||
|
id: id,
|
||||||
|
accountId: 'acc-1',
|
||||||
|
mailboxPath: 'INBOX',
|
||||||
|
uid: int.parse(id.split(':').last),
|
||||||
|
subject: subject,
|
||||||
|
receivedAt: _kDate,
|
||||||
|
sentAt: _kDate,
|
||||||
|
from: const [EmailAddress(name: 'Bob', email: 'bob@example.com')],
|
||||||
|
to: const [EmailAddress(email: 'alice@example.com')],
|
||||||
|
cc: const [],
|
||||||
|
isSeen: isSeen,
|
||||||
|
isFlagged: isFlagged,
|
||||||
|
hasAttachment: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Override> _overrides({
|
||||||
|
List<Email> emails = const [],
|
||||||
|
List<Email> searchResults = const [],
|
||||||
|
String? syncError,
|
||||||
|
}) =>
|
||||||
|
[
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository([kTestMailbox]),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(
|
||||||
|
FakeEmailRepository(emails: emails, searchResults: searchResults),
|
||||||
|
),
|
||||||
|
draftRepositoryProvider.overrideWithValue(FakeDraftRepository()),
|
||||||
|
searchHistoryRepositoryProvider.overrideWithValue(
|
||||||
|
FakeSearchHistoryRepository(),
|
||||||
|
),
|
||||||
|
syncLastErrorProvider.overrideWith(
|
||||||
|
(ref, _) => Stream.value(syncError),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('EmailListScreen goldens', () {
|
||||||
|
testWidgets('golden: empty state', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/mailboxes/INBOX/emails',
|
||||||
|
overrides: _overrides(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byType(MaterialApp),
|
||||||
|
matchesGoldenFile('goldens/email_list_empty.png'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('golden: list with emails', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/mailboxes/INBOX/emails',
|
||||||
|
overrides: _overrides(
|
||||||
|
emails: [
|
||||||
|
_email(subject: 'Team standup notes', isSeen: false),
|
||||||
|
_email(id: 'acc-1:2', subject: 'Q3 review', isFlagged: true),
|
||||||
|
_email(id: 'acc-1:3', subject: 'Welcome to the project'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byType(MaterialApp),
|
||||||
|
matchesGoldenFile('goldens/email_list_with_emails.png'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('golden: selection mode', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/mailboxes/INBOX/emails',
|
||||||
|
overrides: _overrides(
|
||||||
|
emails: [
|
||||||
|
_email(subject: 'Team standup notes', isSeen: false),
|
||||||
|
_email(id: 'acc-1:2', subject: 'Q3 review'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.longPress(find.text('Team standup notes'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byType(MaterialApp),
|
||||||
|
matchesGoldenFile('goldens/email_list_selection.png'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('golden: search with results', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/mailboxes/INBOX/emails',
|
||||||
|
overrides: _overrides(
|
||||||
|
searchResults: [
|
||||||
|
_email(id: 'acc-1:5', subject: 'Project proposal'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(SearchBar), 'project');
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.search);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byType(MaterialApp),
|
||||||
|
matchesGoldenFile('goldens/email_list_search_results.png'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('golden: error banner', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/mailboxes/INBOX/emails',
|
||||||
|
overrides: _overrides(syncError: 'Connection refused'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byType(MaterialApp),
|
||||||
|
matchesGoldenFile('goldens/email_list_error_banner.png'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Reference in New Issue
Block a user