From 651110b389d801935b0d5bd9cdc8760d76d2ac3d Mon Sep 17 00:00:00 2001 From: Thomas SharedInbox Date: Sat, 16 May 2026 09:23:49 +0200 Subject: [PATCH] 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 --- lib/ui/widgets/undo_shell.dart | 7 ++- test/widget/undo_shell_test.dart | 98 ++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 test/widget/undo_shell_test.dart diff --git a/lib/ui/widgets/undo_shell.dart b/lib/ui/widgets/undo_shell.dart index 2042927..015be67 100644 --- a/lib/ui/widgets/undo_shell.dart +++ b/lib/ui/widgets/undo_shell.dart @@ -13,7 +13,12 @@ class UndoShell extends ConsumerWidget { ref.listen>(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); + } } }); diff --git a/test/widget/undo_shell_test.dart b/test/widget/undo_shell_test.dart new file mode 100644 index 0000000..4b9ce7d --- /dev/null +++ b/test/widget/undo_shell_test.dart @@ -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); + }); +}