Files
sharedinbox/test/widget/email_thread_list_controller_test.dart
T
Claude CodeandClaude Opus 4.7 ee14b88bc4 refactor(ui): unify email-list code across folder, combined inbox, search
Closes #533.

Pull selection, swipe, pagination and batch actions out of three
near-duplicate screens (EmailListScreen, CombinedInboxScreen,
AddressEmailsScreen) into a single shared widget. Folder view, combined
inbox, in-folder search results and by-address lists now share one tile
renderer, one selection controller and one batch-action bottom bar.

- New EmailThreadList widget + EmailThreadListController own the
  list rendering, selection set, optional swipe-to-archive/delete and
  optional pagination. Hosts listen to the controller to swap between
  their normal AppBar/drawer/FAB and the shared selection AppBar /
  BottomAppBar (buildSelectionAppBar, buildSelectionBottomBar).
- Batch actions (batchArchive, batchDelete, batchMarkSpam,
  batchMove, batchSnooze) and swipeDismissThread move to
  email_action_helpers.dart and group threads by account so multi-
  account selections produce correctly scoped repository calls and undo
  actions. The combined inbox now supports the full action set (was
  archive + delete only).
- The duplicate EmailThreadTile widget is removed; ThreadTile is the
  single tile used everywhere. Search results now render with the same
  unread/flag icons as the inbox list.
- AddressEmailsScreen adopts the shared list, gaining selection +
  batch actions for free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 12:59:08 +00:00

108 lines
3.3 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:sharedinbox/core/models/email.dart';
import 'package:sharedinbox/ui/widgets/email_thread_list.dart';
EmailThread _t(String id, {String accountId = 'acc-1'}) => EmailThread(
threadId: id,
subject: id,
participants: const [],
latestDate: DateTime(2024, 6),
messageCount: 1,
hasUnread: false,
isFlagged: false,
latestEmailId: id,
emailIds: [id],
accountId: accountId,
mailboxPath: 'INBOX',
);
void main() {
group('EmailThreadListController', () {
test('toggle adds then removes a thread id and fires notifications', () {
final ctrl = EmailThreadListController()
..updateThreads([_t('a'), _t('b')]);
var notifications = 0;
ctrl.addListener(() => notifications++);
expect(ctrl.isSelecting, isFalse);
ctrl.toggle(_t('a'));
expect(ctrl.isSelecting, isTrue);
expect(ctrl.selectionCount, 1);
expect(ctrl.isSelected(_t('a')), isTrue);
expect(notifications, 1);
ctrl.toggle(_t('a'));
expect(ctrl.isSelecting, isFalse);
expect(ctrl.selectionCount, 0);
expect(notifications, 2);
});
test('selectAll selects every visible thread', () {
final ctrl = EmailThreadListController()
..updateThreads([_t('a'), _t('b'), _t('c')]);
ctrl.selectAll();
expect(ctrl.selectionCount, 3);
expect(ctrl.selectedIds, {'a', 'b', 'c'});
});
test('clear empties the selection and notifies once', () {
final ctrl = EmailThreadListController()
..updateThreads([_t('a'), _t('b')])
..toggle(_t('a'))
..toggle(_t('b'));
var notifications = 0;
ctrl.addListener(() => notifications++);
ctrl.clear();
expect(ctrl.isSelecting, isFalse);
expect(notifications, 1);
// Clearing an already-empty selection does not notify again.
ctrl.clear();
expect(notifications, 1);
});
test('updateThreads drops selections that are no longer visible', () {
final ctrl = EmailThreadListController()
..updateThreads([_t('a'), _t('b'), _t('c')])
..toggle(_t('a'))
..toggle(_t('c'));
expect(ctrl.selectionCount, 2);
ctrl.updateThreads([_t('a'), _t('b')]);
// 'c' is no longer visible, so it gets dropped.
expect(ctrl.selectionCount, 1);
expect(ctrl.selectedIds, {'a'});
});
test('selectedThreads preserves the visible-list order', () {
final a = _t('a');
final b = _t('b');
final c = _t('c');
final ctrl = EmailThreadListController()
..updateThreads([a, b, c])
..toggle(c)
..toggle(a);
// Selection order is insertion (c, a), but selectedThreads must follow
// the visible-list order (a, c).
expect(ctrl.selectedThreads.map((t) => t.threadId), ['a', 'c']);
});
test('multi-account threads are kept independent in the selection', () {
final ctrl = EmailThreadListController()
..updateThreads([
_t('a', accountId: 'acc-1'),
_t('b', accountId: 'acc-2'),
]);
ctrl.selectAll();
final byAccount = <String, int>{};
for (final t in ctrl.selectedThreads) {
byAccount[t.accountId] = (byAccount[t.accountId] ?? 0) + 1;
}
expect(byAccount, {'acc-1': 1, 'acc-2': 1});
});
});
}