From 43fb2a594e509349f0edcb114f90978e2e7d5e10 Mon Sep 17 00:00:00 2001 From: Thomas SharedInbox Date: Thu, 14 May 2026 04:54:32 +0200 Subject: [PATCH] test(T2): add widget tests for ThreadDetailScreen and SearchScreen Six tests for ThreadDetailScreen: - empty thread shows placeholder message - sender name rendered in card - last email is expanded by default (reply/delete buttons visible) - tapping expanded card collapses it - flagged email shows star icon - expanded card renders plain text body Six tests for SearchScreen: - placeholder shown when field is empty - typing fewer than 3 characters keeps placeholder (no search fires) - search returning empty shows "No results" - email results shown under "Messages" section - folder results shown under "Folders" section - clear button resets results to placeholder Also adds ThreadDetailScreen route to the shared test router in helpers.dart so tests can navigate to it by URL. Co-Authored-By: Claude Sonnet 4.6 --- test/widget/helpers.dart | 13 ++ test/widget/search_screen_test.dart | 182 +++++++++++++++++++ test/widget/thread_detail_screen_test.dart | 198 +++++++++++++++++++++ 3 files changed, 393 insertions(+) create mode 100644 test/widget/search_screen_test.dart create mode 100644 test/widget/thread_detail_screen_test.dart diff --git a/test/widget/helpers.dart b/test/widget/helpers.dart index 0cf3c0b..149634a 100644 --- a/test/widget/helpers.dart +++ b/test/widget/helpers.dart @@ -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/mailbox_list_screen.dart'; import 'package:sharedinbox/ui/screens/search_screen.dart'; +import 'package:sharedinbox/ui/screens/thread_detail_screen.dart'; // --------------------------------------------------------------------------- // 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']!, + ), + ), + ), ], ), ], diff --git a/test/widget/search_screen_test.dart b/test/widget/search_screen_test.dart new file mode 100644 index 0000000..281a944 --- /dev/null +++ b/test/widget/search_screen_test.dart @@ -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); + }); + }); +} diff --git a/test/widget/thread_detail_screen_test.dart b/test/widget/thread_detail_screen_test.dart new file mode 100644 index 0000000..44fd8f3 --- /dev/null +++ b/test/widget/thread_detail_screen_test.dart @@ -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); + }); + }); +}