Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43fb2a594e |
+24
-181
@@ -4,22 +4,11 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:sharedinbox/data/db/database.dart';
|
import 'package:sharedinbox/data/db/database.dart';
|
||||||
import 'package:sqlite3/sqlite3.dart' as sqlite;
|
import 'package:sqlite3/sqlite3.dart' as sqlite;
|
||||||
|
|
||||||
/// Reads all column names for [tableName] from [db].
|
|
||||||
Future<List<String>> _tableColumns(AppDatabase db, String tableName) async {
|
|
||||||
final rows = await db.customSelect('PRAGMA table_info($tableName)').get();
|
|
||||||
return rows.map((r) => r.read<String>('name')).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('Migration', () {
|
group('Migration', () {
|
||||||
test('schemaVersion matches expected value', () async {
|
test('upgrade from v1 to latest', () async {
|
||||||
final db = AppDatabase(NativeDatabase.memory());
|
// 1. Create a V1 database using raw sqlite3.
|
||||||
expect(db.schemaVersion, 24);
|
final dbFile = File('test_migration.db');
|
||||||
await db.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('upgrade from v1 to latest checks all added columns', () async {
|
|
||||||
final dbFile = File('test_migration_v1.db');
|
|
||||||
if (dbFile.existsSync()) dbFile.deleteSync();
|
if (dbFile.existsSync()) dbFile.deleteSync();
|
||||||
|
|
||||||
final rawDb = sqlite.sqlite3.open(dbFile.path);
|
final rawDb = sqlite.sqlite3.open(dbFile.path);
|
||||||
@@ -78,187 +67,41 @@ void main() {
|
|||||||
rawDb.execute('PRAGMA user_version = 1;');
|
rawDb.execute('PRAGMA user_version = 1;');
|
||||||
rawDb.close();
|
rawDb.close();
|
||||||
|
|
||||||
|
// 2. Open it with AppDatabase (v22).
|
||||||
final db = AppDatabase(NativeDatabase(dbFile));
|
final db = AppDatabase(NativeDatabase(dbFile));
|
||||||
|
|
||||||
// Trigger migration by performing a query.
|
// Trigger migration by performing a simple query.
|
||||||
final accs = await db.select(db.accounts).get();
|
final accs = await db.select(db.accounts).get();
|
||||||
expect(accs, hasLength(1));
|
expect(accs, hasLength(1));
|
||||||
expect(accs.first.displayName, 'Alice');
|
expect(accs.first.displayName, 'Alice');
|
||||||
expect(accs.first.accountType, 'imap');
|
expect(accs.first.accountType, 'imap'); // default value
|
||||||
|
|
||||||
// v2–v3: accounts columns.
|
// 3. Verify that all columns exist.
|
||||||
final accountColumns = await _tableColumns(db, 'accounts');
|
// If migration failed, it would have thrown an exception during opening or query.
|
||||||
expect(
|
final tableInfo =
|
||||||
accountColumns,
|
await db.customSelect('PRAGMA table_info(emails)').get();
|
||||||
containsAll(['account_type', 'jmap_url', 'username']),
|
final columns = tableInfo.map((r) => r.read<String>('name')).toList();
|
||||||
);
|
|
||||||
|
expect(columns, contains('thread_id'));
|
||||||
|
expect(columns, contains('snoozed_until'));
|
||||||
|
expect(columns, contains('snoozed_from_mailbox_path'));
|
||||||
|
|
||||||
|
final accountsInfo =
|
||||||
|
await db.customSelect('PRAGMA table_info(accounts)').get();
|
||||||
|
final accountColumns =
|
||||||
|
accountsInfo.map((r) => r.read<String>('name')).toList();
|
||||||
|
expect(accountColumns, contains('account_type'));
|
||||||
|
expect(accountColumns, contains('username'));
|
||||||
expect(accountColumns, contains('manage_sieve_host'));
|
expect(accountColumns, contains('manage_sieve_host'));
|
||||||
|
|
||||||
// v14: threading columns.
|
|
||||||
final emailColumns = await _tableColumns(db, 'emails');
|
|
||||||
expect(
|
|
||||||
emailColumns,
|
|
||||||
containsAll(['thread_id', 'message_id', 'in_reply_to', 'references']),
|
|
||||||
);
|
|
||||||
|
|
||||||
// v22: snooze columns.
|
|
||||||
expect(
|
|
||||||
emailColumns,
|
|
||||||
containsAll(['snoozed_until', 'snoozed_from_mailbox_path']),
|
|
||||||
);
|
|
||||||
|
|
||||||
// v23: list-unsubscribe header column.
|
|
||||||
expect(emailColumns, contains('list_unsubscribe_header'));
|
|
||||||
|
|
||||||
// v8: mailboxes role column.
|
|
||||||
final mailboxColumns = await _tableColumns(db, 'mailboxes');
|
|
||||||
expect(mailboxColumns, contains('role'));
|
|
||||||
|
|
||||||
// v9: email_bodies cached_at column.
|
|
||||||
final bodyColumns = await _tableColumns(db, 'email_bodies');
|
|
||||||
expect(bodyColumns, contains('cached_at'));
|
|
||||||
expect(bodyColumns, contains('headers_json'));
|
|
||||||
|
|
||||||
// v4: drafts table with v24 imap_server_id column.
|
|
||||||
final draftColumns = await _tableColumns(db, 'drafts');
|
|
||||||
expect(draftColumns, contains('imap_server_id'));
|
|
||||||
|
|
||||||
// v5, v6, v7, v12, v17, v19, v21: new tables.
|
|
||||||
final allTables = await db
|
|
||||||
.customSelect("SELECT name FROM sqlite_master WHERE type='table'")
|
|
||||||
.get();
|
|
||||||
final tableNames = allTables.map((r) => r.read<String>('name')).toList();
|
|
||||||
expect(
|
|
||||||
tableNames,
|
|
||||||
containsAll([
|
|
||||||
'sync_states', // v5
|
|
||||||
'pending_changes', // v6
|
|
||||||
'sync_logs', // v7
|
|
||||||
'sync_log_mailboxes', // v12
|
|
||||||
'threads', // v17
|
|
||||||
'sync_health', // v19
|
|
||||||
'undo_actions', // v21
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.close();
|
await db.close();
|
||||||
if (dbFile.existsSync()) dbFile.deleteSync();
|
if (dbFile.existsSync()) dbFile.deleteSync();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test('fresh install (v22) works', () async {
|
||||||
'upgrade from v22 to latest adds list_unsubscribe_header and imap_server_id',
|
|
||||||
() async {
|
|
||||||
final dbFile = File('test_migration_v22.db');
|
|
||||||
if (dbFile.existsSync()) dbFile.deleteSync();
|
|
||||||
|
|
||||||
// Build a v22 database schema directly with raw SQL.
|
|
||||||
final rawDb = sqlite.sqlite3.open(dbFile.path);
|
|
||||||
rawDb.execute('''
|
|
||||||
CREATE TABLE accounts (
|
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
|
||||||
display_name TEXT NOT NULL,
|
|
||||||
email TEXT NOT NULL,
|
|
||||||
imap_host TEXT NOT NULL,
|
|
||||||
imap_port INTEGER NOT NULL DEFAULT 993,
|
|
||||||
imap_ssl INTEGER NOT NULL DEFAULT 1 CHECK ("imap_ssl" IN (0, 1)),
|
|
||||||
smtp_host TEXT NOT NULL DEFAULT '',
|
|
||||||
smtp_port INTEGER NOT NULL DEFAULT 465,
|
|
||||||
smtp_ssl INTEGER NOT NULL DEFAULT 1 CHECK ("smtp_ssl" IN (0, 1)),
|
|
||||||
account_type TEXT NOT NULL DEFAULT 'imap',
|
|
||||||
jmap_url TEXT NULL,
|
|
||||||
username TEXT NULL,
|
|
||||||
manage_sieve_host TEXT NULL,
|
|
||||||
manage_sieve_port INTEGER NULL,
|
|
||||||
manage_sieve_ssl INTEGER NULL,
|
|
||||||
manage_sieve_available INTEGER NOT NULL DEFAULT 0 CHECK ("manage_sieve_available" IN (0, 1)),
|
|
||||||
verbose INTEGER NOT NULL DEFAULT 0 CHECK ("verbose" IN (0, 1))
|
|
||||||
);
|
|
||||||
''');
|
|
||||||
rawDb.execute('''
|
|
||||||
CREATE TABLE drafts (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
account_id TEXT NULL,
|
|
||||||
reply_to_email_id TEXT NULL,
|
|
||||||
to_text TEXT NOT NULL DEFAULT '',
|
|
||||||
cc_text TEXT NOT NULL DEFAULT '',
|
|
||||||
subject_text TEXT NOT NULL DEFAULT '',
|
|
||||||
body_text TEXT NOT NULL DEFAULT '',
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
''');
|
|
||||||
rawDb.execute('''
|
|
||||||
CREATE TABLE emails (
|
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
|
||||||
account_id TEXT NOT NULL,
|
|
||||||
mailbox_path TEXT NOT NULL,
|
|
||||||
uid INTEGER NOT NULL,
|
|
||||||
subject TEXT NULL,
|
|
||||||
sent_at INTEGER NULL,
|
|
||||||
received_at INTEGER NOT NULL,
|
|
||||||
from_json TEXT NOT NULL DEFAULT '[]',
|
|
||||||
to_addresses TEXT NOT NULL DEFAULT '[]',
|
|
||||||
cc_json TEXT NOT NULL DEFAULT '[]',
|
|
||||||
preview TEXT NULL,
|
|
||||||
is_seen INTEGER NOT NULL DEFAULT 0 CHECK ("is_seen" IN (0, 1)),
|
|
||||||
is_flagged INTEGER NOT NULL DEFAULT 0 CHECK ("is_flagged" IN (0, 1)),
|
|
||||||
has_attachment INTEGER NOT NULL DEFAULT 0 CHECK ("has_attachment" IN (0, 1)),
|
|
||||||
thread_id TEXT NULL,
|
|
||||||
message_id TEXT NULL,
|
|
||||||
in_reply_to TEXT NULL,
|
|
||||||
"references" TEXT NULL,
|
|
||||||
snoozed_until INTEGER NULL,
|
|
||||||
snoozed_from_mailbox_path TEXT NULL
|
|
||||||
);
|
|
||||||
''');
|
|
||||||
rawDb.execute('PRAGMA user_version = 22;');
|
|
||||||
rawDb.close();
|
|
||||||
|
|
||||||
final db = AppDatabase(NativeDatabase(dbFile));
|
|
||||||
// Trigger migration.
|
|
||||||
await db.select(db.accounts).get();
|
|
||||||
|
|
||||||
final emailColumns = await _tableColumns(db, 'emails');
|
|
||||||
expect(emailColumns, contains('list_unsubscribe_header'));
|
|
||||||
|
|
||||||
final draftColumns = await _tableColumns(db, 'drafts');
|
|
||||||
expect(draftColumns, contains('imap_server_id'));
|
|
||||||
|
|
||||||
await db.close();
|
|
||||||
if (dbFile.existsSync()) dbFile.deleteSync();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fresh install creates all tables at schemaVersion 24', () async {
|
|
||||||
final db = AppDatabase(NativeDatabase.memory());
|
final db = AppDatabase(NativeDatabase.memory());
|
||||||
|
// Just ensure we can create everything and query.
|
||||||
await db.select(db.accounts).get();
|
await db.select(db.accounts).get();
|
||||||
|
|
||||||
final allTables = await db
|
|
||||||
.customSelect("SELECT name FROM sqlite_master WHERE type='table'")
|
|
||||||
.get();
|
|
||||||
final tableNames = allTables.map((r) => r.read<String>('name')).toSet();
|
|
||||||
expect(
|
|
||||||
tableNames,
|
|
||||||
containsAll([
|
|
||||||
'accounts',
|
|
||||||
'mailboxes',
|
|
||||||
'emails',
|
|
||||||
'email_bodies',
|
|
||||||
'drafts',
|
|
||||||
'sync_states',
|
|
||||||
'pending_changes',
|
|
||||||
'sync_logs',
|
|
||||||
'sync_log_mailboxes',
|
|
||||||
'threads',
|
|
||||||
'sync_health',
|
|
||||||
'undo_actions',
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
final emailColumns = await _tableColumns(db, 'emails');
|
|
||||||
expect(emailColumns, contains('list_unsubscribe_header'));
|
|
||||||
|
|
||||||
final draftColumns = await _tableColumns(db, 'drafts');
|
|
||||||
expect(draftColumns, contains('imap_server_id'));
|
|
||||||
|
|
||||||
await db.close();
|
await db.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import 'package:sharedinbox/ui/screens/email_detail_screen.dart';
|
|||||||
import 'package:sharedinbox/ui/screens/email_list_screen.dart';
|
import 'package:sharedinbox/ui/screens/email_list_screen.dart';
|
||||||
import 'package:sharedinbox/ui/screens/mailbox_list_screen.dart';
|
import 'package:sharedinbox/ui/screens/mailbox_list_screen.dart';
|
||||||
import 'package:sharedinbox/ui/screens/search_screen.dart';
|
import 'package:sharedinbox/ui/screens/search_screen.dart';
|
||||||
|
import 'package:sharedinbox/ui/screens/thread_detail_screen.dart';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Fake repositories
|
// Fake repositories
|
||||||
@@ -381,6 +382,18 @@ Widget buildApp({
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: ':mailboxPath/threads/:threadId',
|
||||||
|
builder: (ctx, state) => ThreadDetailScreen(
|
||||||
|
accountId: state.pathParameters['accountId']!,
|
||||||
|
mailboxPath: Uri.decodeComponent(
|
||||||
|
state.pathParameters['mailboxPath']!,
|
||||||
|
),
|
||||||
|
threadId: Uri.decodeComponent(
|
||||||
|
state.pathParameters['threadId']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:sharedinbox/core/models/mailbox.dart';
|
||||||
|
import 'package:sharedinbox/di.dart';
|
||||||
|
|
||||||
|
import 'helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('SearchScreen', () {
|
||||||
|
testWidgets('shows placeholder hint text when empty', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/search',
|
||||||
|
overrides: [
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository(),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Type 3+ characters to search'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('typing fewer than 3 characters does not trigger search', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/search',
|
||||||
|
overrides: [
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository(),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), 'hi');
|
||||||
|
await tester.pump(const Duration(milliseconds: 400));
|
||||||
|
|
||||||
|
expect(find.text('Type 3+ characters to search'), findsOneWidget);
|
||||||
|
expect(find.text('No results'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows "No results" when search returns nothing', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/search',
|
||||||
|
overrides: [
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository(),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), 'xyz');
|
||||||
|
await tester.pump(const Duration(milliseconds: 400));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('No results'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows email results under "Messages" section', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final email = testEmail(subject: 'Invoice Q3');
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/search',
|
||||||
|
overrides: [
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository(),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(
|
||||||
|
FakeEmailRepository(searchResults: [email]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), 'inv');
|
||||||
|
await tester.pump(const Duration(milliseconds: 400));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Messages'), findsOneWidget);
|
||||||
|
expect(find.text('Invoice Q3'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows folder results under "Folders" section', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
const archiveMailbox = Mailbox(
|
||||||
|
id: 'acc-1:Archive',
|
||||||
|
accountId: 'acc-1',
|
||||||
|
path: 'Archive',
|
||||||
|
name: 'Archive',
|
||||||
|
unreadCount: 0,
|
||||||
|
totalCount: 5,
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/search',
|
||||||
|
overrides: [
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository([archiveMailbox]),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), 'arc');
|
||||||
|
await tester.pump(const Duration(milliseconds: 400));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Folders'), findsOneWidget);
|
||||||
|
expect(find.text('Archive'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('tapping clear button resets results to placeholder', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final email = testEmail(subject: 'Found email');
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/search',
|
||||||
|
overrides: [
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository(),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(
|
||||||
|
FakeEmailRepository(searchResults: [email]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), 'found');
|
||||||
|
await tester.pump(const Duration(milliseconds: 400));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Found email'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byIcon(Icons.clear));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Found email'), findsNothing);
|
||||||
|
expect(find.text('Type 3+ characters to search'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:sharedinbox/core/models/email.dart';
|
||||||
|
import 'package:sharedinbox/di.dart';
|
||||||
|
|
||||||
|
import 'helpers.dart';
|
||||||
|
|
||||||
|
Email _threadEmail({
|
||||||
|
String id = 'acc-1:10',
|
||||||
|
bool isFlagged = false,
|
||||||
|
bool isSeen = true,
|
||||||
|
}) =>
|
||||||
|
Email(
|
||||||
|
id: id,
|
||||||
|
accountId: 'acc-1',
|
||||||
|
mailboxPath: 'INBOX',
|
||||||
|
uid: 10,
|
||||||
|
threadId: 'thread-1',
|
||||||
|
subject: 'Project update',
|
||||||
|
receivedAt: DateTime(2024, 6),
|
||||||
|
sentAt: DateTime(2024, 6, 1, 9),
|
||||||
|
from: const [EmailAddress(name: 'Bob', email: 'bob@example.com')],
|
||||||
|
to: const [EmailAddress(email: 'alice@example.com')],
|
||||||
|
cc: const [],
|
||||||
|
isSeen: isSeen,
|
||||||
|
isFlagged: isFlagged,
|
||||||
|
hasAttachment: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('ThreadDetailScreen', () {
|
||||||
|
testWidgets('shows "Thread not found or empty" when thread is empty', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/mailboxes/INBOX/threads/thread-1',
|
||||||
|
overrides: [
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository(),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Thread not found or empty'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows sender name for email in thread', (tester) async {
|
||||||
|
final email = _threadEmail();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/mailboxes/INBOX/threads/thread-1',
|
||||||
|
overrides: [
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository(),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(
|
||||||
|
FakeEmailRepository(emails: [email]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.textContaining('Bob'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('last email in thread is expanded by default', (tester) async {
|
||||||
|
final email = _threadEmail();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/mailboxes/INBOX/threads/thread-1',
|
||||||
|
overrides: [
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository(),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(
|
||||||
|
FakeEmailRepository(
|
||||||
|
emails: [email],
|
||||||
|
emailBody: const EmailBody(
|
||||||
|
emailId: 'acc-1:10',
|
||||||
|
textBody: 'Hello body text',
|
||||||
|
attachments: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Reply and delete buttons are visible for the expanded card.
|
||||||
|
expect(find.byIcon(Icons.reply), findsOneWidget);
|
||||||
|
expect(find.byIcon(Icons.delete_outline), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('tapping an expanded card collapses it', (tester) async {
|
||||||
|
final email = _threadEmail();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/mailboxes/INBOX/threads/thread-1',
|
||||||
|
overrides: [
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository(),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(
|
||||||
|
FakeEmailRepository(
|
||||||
|
emails: [email],
|
||||||
|
emailBody: const EmailBody(
|
||||||
|
emailId: 'acc-1:10',
|
||||||
|
textBody: 'Hello body text',
|
||||||
|
attachments: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Tap the expand_less icon to collapse.
|
||||||
|
await tester.tap(find.byIcon(Icons.expand_less));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byIcon(Icons.reply), findsNothing);
|
||||||
|
expect(find.byIcon(Icons.expand_more), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('flagged email shows star icon', (tester) async {
|
||||||
|
final email = _threadEmail(isFlagged: true);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/mailboxes/INBOX/threads/thread-1',
|
||||||
|
overrides: [
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository(),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(
|
||||||
|
FakeEmailRepository(emails: [email]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byIcon(Icons.star), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('expanded card shows plain text body', (tester) async {
|
||||||
|
final email = _threadEmail();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
initialLocation: '/accounts/acc-1/mailboxes/INBOX/threads/thread-1',
|
||||||
|
overrides: [
|
||||||
|
accountRepositoryProvider.overrideWithValue(
|
||||||
|
FakeAccountRepository([kTestAccount]),
|
||||||
|
),
|
||||||
|
mailboxRepositoryProvider.overrideWithValue(
|
||||||
|
FakeMailboxRepository(),
|
||||||
|
),
|
||||||
|
emailRepositoryProvider.overrideWithValue(
|
||||||
|
FakeEmailRepository(
|
||||||
|
emails: [email],
|
||||||
|
emailBody: const EmailBody(
|
||||||
|
emailId: 'acc-1:10',
|
||||||
|
textBody: 'Body content here',
|
||||||
|
attachments: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Body content here'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user