fix: do not show snackbar for stale undo actions on startup (#113)

Actions persisted to the database triggered a snackbar when the app
restarted. Added a 30-second recency check so only actions created in
the current session show the snackbar; added widget tests covering both
cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas SharedInbox
2026-05-16 09:23:49 +02:00
co-authored by Claude Sonnet 4.6
parent 0611323cfa
commit 651110b389
2 changed files with 104 additions and 1 deletions
+6 -1
View File
@@ -13,7 +13,12 @@ class UndoShell extends ConsumerWidget {
ref.listen<List<UndoAction>>(undoServiceProvider, (previous, next) {
if (next.isNotEmpty &&
(previous == null || previous.length < next.length)) {
_showUndoSnackbar(context, ref, next.last);
final action = next.last;
// Don't show a snackbar for actions loaded from persistence on app
// startup — only for actions pushed in this session.
if (DateTime.now().difference(action.timestamp).inSeconds < 30) {
_showUndoSnackbar(context, ref, action);
}
}
});
+98
View File
@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:sharedinbox/core/models/undo_action.dart';
import 'package:sharedinbox/di.dart';
import 'package:sharedinbox/ui/widgets/undo_shell.dart';
import '../unit/undo_service_test.mocks.dart';
void main() {
late MockUndoRepository mockUndoRepo;
setUp(() {
mockUndoRepo = MockUndoRepository();
when(mockUndoRepo.saveAction(any)).thenAnswer((_) async {});
when(mockUndoRepo.deleteAction(any)).thenAnswer((_) async {});
when(mockUndoRepo.clearHistory()).thenAnswer((_) async {});
});
Widget buildShell(MockUndoRepository repo) {
return ProviderScope(
overrides: [undoRepositoryProvider.overrideWithValue(repo)],
child: const MaterialApp(
home: UndoShell(child: Scaffold(body: Text('content'))),
),
);
}
testWidgets(
'does not show snackbar for stale action loaded from persistence on startup',
(tester) async {
final staleAction = UndoAction(
id: '1',
accountId: 'acc1',
type: UndoType.move,
emailIds: ['e1'],
sourceMailboxPath: 'INBOX',
timestamp: DateTime.now().subtract(const Duration(hours: 1)),
);
when(mockUndoRepo.getHistory(limit: anyNamed('limit')))
.thenAnswer((_) async => [staleAction]);
await tester.pumpWidget(buildShell(mockUndoRepo));
await tester.pumpAndSettle();
expect(find.text('1 email(s) moved'), findsNothing);
},
);
testWidgets('shows snackbar for fresh action pushed in current session',
(tester) async {
when(mockUndoRepo.getHistory(limit: anyNamed('limit')))
.thenAnswer((_) async => []);
await tester.pumpWidget(buildShell(mockUndoRepo));
await tester.pumpAndSettle();
final context = tester.element(find.byType(UndoShell));
final freshAction = UndoAction(
id: '1',
accountId: 'acc1',
type: UndoType.move,
emailIds: ['e1'],
sourceMailboxPath: 'INBOX',
);
await ProviderScope.containerOf(context)
.read(undoServiceProvider.notifier)
.pushAction(freshAction);
await tester.pumpAndSettle();
expect(find.text('1 email(s) moved'), findsOneWidget);
});
testWidgets('shows correct text for delete action (moved to Trash)',
(tester) async {
when(mockUndoRepo.getHistory(limit: anyNamed('limit')))
.thenAnswer((_) async => []);
await tester.pumpWidget(buildShell(mockUndoRepo));
await tester.pumpAndSettle();
final context = tester.element(find.byType(UndoShell));
final deleteAction = UndoAction(
id: '2',
accountId: 'acc1',
type: UndoType.delete,
emailIds: ['e1', 'e2'],
sourceMailboxPath: 'INBOX',
);
await ProviderScope.containerOf(context)
.read(undoServiceProvider.notifier)
.pushAction(deleteAction);
await tester.pumpAndSettle();
expect(find.text('2 email(s) moved to Trash'), findsOneWidget);
});
}