diff --git a/mobile/lib/presentation/actions/delete.action.dart b/mobile/lib/presentation/actions/delete.action.dart index 9f2ca092f..dd33db847 100644 --- a/mobile/lib/presentation/actions/delete.action.dart +++ b/mobile/lib/presentation/actions/delete.action.dart @@ -121,14 +121,14 @@ class DeleteAction extends AssetActionBuilder { required bool isLocalOnly, }) async { final assetService = ref.read(assetServiceProvider); + final message = context.t.trash_action_prompt(count: remoteIds.length); if (localIds.isNotEmpty) { - await _cleanupLocalAssets(context, ref, localIds, isLocalOnly: isLocalOnly); - if (!context.mounted) { + final count = await _cleanupLocalAssets(context, ref, localIds, isLocalOnly: isLocalOnly); + if (count <= 0) { return null; } } - final message = context.t.trash_action_prompt(count: remoteIds.length); await assetService.trash(remoteIds); return message; } diff --git a/mobile/test/unit/presentation/actions/delete_action_test.dart b/mobile/test/unit/presentation/actions/delete_action_test.dart index d949e2ae5..d32193396 100644 --- a/mobile/test/unit/presentation/actions/delete_action_test.dart +++ b/mobile/test/unit/presentation/actions/delete_action_test.dart @@ -97,6 +97,7 @@ void main() { }); testWidgets('trashes a merged asset and removes its device copy', (tester) async { + when(() => cleanupService.deleteLocalAssets(['local'])).thenAnswer((_) async => 1); final asset = owned(localId: 'local'); await pumpDelete(tester, {asset}); @@ -106,6 +107,65 @@ void main() { verify(() => assetService.trash([asset.id])).called(1); }); + testWidgets('does not trash a merged asset when the MANAGE_MEDIA prompt is cancelled', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + await StoreService.I.put(StoreKey.manageLocalMediaAndroid, true); + final asset = owned(localId: 'local'); + + await pumpDelete(tester, {asset}); + await respondToDialog(tester, confirm: false); + + verifyNever(() => cleanupService.deleteLocalAssets(any())); + verifyNever(() => assetService.trash(any())); + verifyNever(() => assetService.delete(any())); + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets('does not trash a merged asset when the Android system prompt is cancelled', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + when(context.repository.permission.getAndroidSdkVersion).thenAnswer((_) async => 30); + when(() => cleanupService.deleteLocalAssets(['local'])).thenAnswer((_) async => 0); + final asset = owned(localId: 'local'); + + await pumpDelete(tester, {asset}); + await tester.pumpAndSettle(); + + verify(() => cleanupService.deleteLocalAssets(['local'])).called(1); + verifyNever(() => assetService.trash(any())); + verifyNever(() => assetService.delete(any())); + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets('finishes trashing when the action context is disposed during local cleanup', (tester) async { + final asset = owned(localId: 'local'); + final cleanupResult = Completer(); + when(() => cleanupService.deleteLocalAssets(['local'])).thenAnswer((_) => cleanupResult.future); + var showAction = true; + late StateSetter updateHost; + + await tester.pumpTestWidget( + context, + StatefulBuilder( + builder: (_, setState) { + updateHost = setState; + return showAction + ? const ActionIconButton(action: DeleteAction(source: .timeline)) + : const SizedBox.shrink(); + }, + ), + overrides: context.selected({asset}), + ); + + await tester.tap(find.byType(ImmichIconButton)); + await tester.pump(); + updateHost(() => showAction = false); + await tester.pump(); + cleanupResult.complete(1); + await tester.pumpAndSettle(); + + verify(() => assetService.trash([asset.id])).called(1); + }); + testWidgets('offers an undo that restores the trashed assets', (tester) async { final asset = owned(); @@ -283,6 +343,7 @@ void main() { when(context.repository.permission.getAndroidSdkVersion).thenAnswer((_) async => 28); final backedUp = owned(localId: 'local'); final localOnly = LocalAssetFactory.create(); + when(() => cleanupService.deleteLocalAssets(['local', localOnly.id])).thenAnswer((_) async => 1); await pumpDelete(tester, {backedUp, localOnly}); await tester.pump(const Duration(milliseconds: 300));