Compare commits

...
Author SHA1 Message Date
Bot of Thomas Güttler bcac327f0e Merge branch 'main' into issue-473-search-result-reorder 2026-06-07 04:27:13 +02:00
Thomas SharedInbox d64a33f7ef Merge branch 'main' into issue-473-search-result-reorder 2026-06-07 00:30:44 +02:00
Thomas SharedInbox 9eea81632a Merge branch 'main' into issue-473-search-result-reorder 2026-06-07 00:15:40 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 1e4911d323 fix(ci): forward SSH tunnel directly to dagger engine socket
Eliminates the socat bridge dependency by using OpenSSH's built-in
Unix socket forwarding (-L port:socket_path). The dagger user already
owns /run/dagger/engine.sock so no intermediate TCP listener is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 23:43:18 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 5ae555b51e fix: prevent Enter key from re-running a settled search (#473)
When the user typed a query, onChanged already fired _runSearch and
results settled. Pressing Enter then triggered onSubmitted → a second
IMAP search whose response could arrive in a different order, silently
reordering the visible list so the tile at position 0 no longer
corresponded to the email the user was about to tap.

Fix: onSubmitted now skips _runSearch when results are already present
(_searchResults != null) or a search is in flight (_searchLoading).
Adds a regression test that verifies the list order is unchanged after
pressing Enter on an already-settled search.

Closes #473

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 21:51:20 +02:00
2 changed files with 69 additions and 1 deletions
+8 -1
View File
@@ -278,7 +278,14 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
),
],
onChanged: _onSearchChanged,
onSubmitted: _runSearch,
onSubmitted: (value) {
// Only run the search if results haven't settled yet via
// onChanged — prevents a second IMAP round-trip from reordering
// the already-visible results when the user presses Enter.
if (_searchResults == null && !_searchLoading) {
unawaited(_runSearch(value));
}
},
textInputAction: TextInputAction.search,
),
),
+61
View File
@@ -798,6 +798,67 @@ void main() {
},
);
testWidgets(
'pressing Enter after search settles does not reorder results',
(tester) async {
// Reproduces: user types a query → onChanged fires → results settle.
// Then user presses Enter → onSubmitted fires a second search → the
// second IMAP response may return results in a different order, so the
// tile the user is about to tap is no longer the email they expect.
final email1 = testEmail(id: 'acc-1:1', subject: 'Alpha Foo');
final email2 = testEmail(id: 'acc-1:2', subject: 'Beta Foo');
var callCount = 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 {
callCount++;
// First call: [Alpha, Beta]. Second call: reversed.
return callCount == 1 ? [email1, email2] : [email2, email1];
},
emailBody: const EmailBody(emailId: '', attachments: []),
),
),
],
),
);
await tester.pumpAndSettle();
// Typing triggers onChanged → first search → results settle.
await tester.enterText(find.byType(TextField), 'foo');
await tester.pumpAndSettle();
expect(find.text('Alpha Foo'), findsOneWidget);
expect(find.text('Beta Foo'), findsOneWidget);
// Alpha must appear above Beta (it is first in the list).
expect(
tester.getTopLeft(find.text('Alpha Foo')).dy,
lessThan(tester.getTopLeft(find.text('Beta Foo')).dy),
);
// Pressing Enter triggers onSubmitted — must NOT re-run the search.
await tester.testTextInput.receiveAction(TextInputAction.search);
await tester.pumpAndSettle();
// Order must be unchanged: pressing Enter must not reorder results.
expect(find.text('Alpha Foo'), findsOneWidget);
expect(find.text('Beta Foo'), findsOneWidget);
expect(
tester.getTopLeft(find.text('Alpha Foo')).dy,
lessThan(tester.getTopLeft(find.text('Beta Foo')).dy),
);
},
);
testWidgets('shows preview snippet when email has preview', (tester) async {
final email = Email(
id: 'acc-1:99',