diff --git a/lib/ui/screens/email_list_screen.dart b/lib/ui/screens/email_list_screen.dart index b51d5d5..60a0aba 100644 --- a/lib/ui/screens/email_list_screen.dart +++ b/lib/ui/screens/email_list_screen.dart @@ -55,6 +55,10 @@ class _EmailListScreenState extends ConsumerState { // generation has advanced (prevents out-of-order IMAP responses from // overwriting fresh results with results for an older query). int _searchGeneration = 0; + // The query whose results are currently settled in _searchResults. + // Used to skip redundant re-runs when the user presses Enter on an + // already-settled search (issue #473). + String? _lastSettledQuery; bool get _selecting => _selectedThreadIds.isNotEmpty || _selectedSearchIds.isNotEmpty; @@ -66,6 +70,7 @@ class _EmailListScreenState extends ConsumerState { setState(() { _searchResults = null; _searchLoading = false; + _lastSettledQuery = null; }); } }); @@ -122,8 +127,17 @@ class _EmailListScreenState extends ConsumerState { } Future _runSearch(String query) async { - if (query.trim().isEmpty) { - setState(() => _searchResults = null); + final q = query.trim(); + if (q.isEmpty) { + setState(() { + _searchResults = null; + _lastSettledQuery = null; + }); + return; + } + // Skip if results are already settled for this exact query — prevents the + // Enter key from re-triggering an IMAP search that already completed. + if (_searchResults != null && !_searchLoading && q == _lastSettledQuery) { return; } final generation = ++_searchGeneration; @@ -131,9 +145,12 @@ class _EmailListScreenState extends ConsumerState { try { final results = await ref .read(emailRepositoryProvider) - .searchEmails(widget.accountId, widget.mailboxPath, query.trim()); + .searchEmails(widget.accountId, widget.mailboxPath, q); if (mounted && generation == _searchGeneration) { - setState(() => _searchResults = results); + setState(() { + _searchResults = results; + _lastSettledQuery = q; + }); } } finally { if (mounted && generation == _searchGeneration) { diff --git a/test/widget/email_list_screen_test.dart b/test/widget/email_list_screen_test.dart index 05c0633..24f019e 100644 --- a/test/widget/email_list_screen_test.dart +++ b/test/widget/email_list_screen_test.dart @@ -554,6 +554,69 @@ void main() { }, ); + testWidgets( + 'pressing Enter on already-settled search does not re-run search (issue #473)', + (tester) async { + final email1 = testEmail(id: 'acc-1:1', subject: 'Alpha Match'); + final email2 = testEmail(id: 'acc-1:2', subject: 'Beta Match'); + + var searchCallCount = 0; + + await tester.pumpWidget( + buildApp( + initialLocation: '/accounts/acc-1/mailboxes/INBOX/emails', + overrides: [ + accountRepositoryProvider.overrideWithValue( + FakeAccountRepository([kTestAccount]), + ), + mailboxRepositoryProvider.overrideWithValue( + FakeMailboxRepository(), + ), + emailRepositoryProvider.overrideWithValue( + FakeEmailRepository( + onSearch: (_) async { + searchCallCount++; + return [email1, email2]; + }, + emailBody: const EmailBody(emailId: '', attachments: []), + ), + ), + ], + ), + ); + await tester.pumpAndSettle(); + + // Run the initial search. + await tester.enterText(find.byType(TextField), 'Match'); + await tester.testTextInput.receiveAction(TextInputAction.search); + await tester.pumpAndSettle(); + + expect(find.text('Alpha Match'), findsOneWidget); + expect(find.text('Beta Match'), findsOneWidget); + + final countAfterFirstSearch = searchCallCount; + + // Re-focus the search bar (simulates user tapping back into the field + // with the keyboard still visible) and press Enter again on the same, + // already-settled query. + await tester.tap(find.byType(TextField)); + await tester.pump(); + await tester.testTextInput.receiveAction(TextInputAction.search); + await tester.pumpAndSettle(); + + // The search must NOT re-run; call count must not increase. + expect( + searchCallCount, + countAfterFirstSearch, + reason: + 'Enter on settled results must not re-run the search (issue #473)', + ); + // Results must still be visible — no loading spinner. + expect(find.byType(CircularProgressIndicator), findsNothing); + expect(find.text('Alpha Match'), findsOneWidget); + }, + ); + testWidgets('deleting all search results pops back to previous screen', ( tester, ) async {