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
Bot of Thomas Güttler 76f2635700 fix(search): sort search results by received date descending (#520) 2026-06-07 04:24:24 +02:00
Bot of Thomas Güttler e2bb299300 fix(ci): exclude chaos_monkey_test from regular CI (#518) 2026-06-07 04:24:10 +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
4 changed files with 74 additions and 3 deletions
+2 -2
View File
@@ -539,7 +539,7 @@ func (m *Ci) TestBackend(ctx context.Context) (string, error) {
return m.WithStalwart(m.setup(m.backendSrc())).
WithExec([]string{"/bin/bash", "-c",
`tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT; ` +
`flutter test --concurrency=1 --reporter expanded --no-pub test/backend >"$tmp" 2>&1 || { cat "$tmp"; exit 1; }; ` +
`flutter test --concurrency=1 --reporter expanded --no-pub --exclude-tags=nightly test/backend >"$tmp" 2>&1 || { cat "$tmp"; exit 1; }; ` +
`grep -E '^All [0-9]+ tests passed' "$tmp" || tail -1 "$tmp"`}).
Stdout(ctx)
}
@@ -570,7 +570,7 @@ func (m *Ci) ChaosMonkeyBackend(ctx context.Context) (string, error) {
return m.WithStalwart(m.setup(m.backendSrc())).
WithExec([]string{"/bin/bash", "-c",
`tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT; ` +
`flutter test test/backend/chaos_monkey_test.dart --reporter expanded --concurrency=1 --no-pub >"$tmp" 2>&1 || { cat "$tmp"; exit 1; }; ` +
`flutter test test/backend/chaos_monkey_test.dart --reporter expanded --concurrency=1 --no-pub --tags=nightly >"$tmp" 2>&1 || { cat "$tmp"; exit 1; }; ` +
`grep -E '^All [0-9]+ tests passed' "$tmp" || tail -1 "$tmp"`}).
Stdout(ctx)
}
+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,
),
),
+3
View File
@@ -10,6 +10,9 @@
// CHAOS_ROUNDS (default: 30) — number of random operations to perform
// CHAOS_SEED (default: current epoch ms) — seed for reproducibility
@Tags(['nightly'])
library;
import 'dart:io';
import 'dart:math';
+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',