From 6894b2ea2279b5f73259e95e2111142a61849a69 Mon Sep 17 00:00:00 2001 From: Peter Ombodi Date: Tue, 4 Aug 2026 12:00:03 +0300 Subject: [PATCH] fix(mobile): handle file-backed view intent uploads -Route materialized view intents through share-intent upload, replace the synthetic viewer asset after sync, and manage temporary file cleanup. --- mobile/lib/domain/services/asset.service.dart | 4 + .../presentation/actions/upload.action.dart | 19 +- .../asset_viewer/asset_viewer.page.dart | 13 +- .../asset_upload_coordinator.provider.dart | 117 +++++++++ .../view_intent_asset_resolver.service.dart | 1 + .../asset_viewer_timeline_reload_test.dart | 106 ++++++++ ...sset_upload_coordinator_provider_test.dart | 243 ++++++++++++++++++ .../actions/upload_action_test.dart | 3 +- 8 files changed, 493 insertions(+), 13 deletions(-) create mode 100644 mobile/lib/providers/asset_upload_coordinator.provider.dart create mode 100644 mobile/test/presentation/widgets/asset_viewer/asset_viewer_timeline_reload_test.dart create mode 100644 mobile/test/providers/asset_upload_coordinator_provider_test.dart diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index 1f7f77df18..05a879973f 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -52,6 +52,10 @@ class AssetService { }); } + Stream watchRemoteAsset(String id) { + return _remoteRepository.watch(id); + } + Future> getLocalAssetsByChecksum(String checksum) { return _localRepository.getByChecksum(checksum); } diff --git a/mobile/lib/presentation/actions/upload.action.dart b/mobile/lib/presentation/actions/upload.action.dart index ceb35c8786..5b9427866c 100644 --- a/mobile/lib/presentation/actions/upload.action.dart +++ b/mobile/lib/presentation/actions/upload.action.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/asset_upload_coordinator.provider.dart'; import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; @@ -36,7 +37,7 @@ class UploadAction extends AssetActionBuilder { Future _upload(BuildContext context, WidgetRef ref, List assets) async { try { if (!showProgress) { - await uploadAssets(context, ref, assets); + await uploadAssets(context, ref, assets, source: source); return; } @@ -51,7 +52,7 @@ class UploadAction extends AssetActionBuilder { ).whenComplete(() => isDialogOpen = false), ); - await uploadAssets(context, ref, assets); + await uploadAssets(context, ref, assets, source: source); if (isDialogOpen && context.mounted) { Navigator.of(context, rootNavigator: true).pop(); @@ -63,9 +64,14 @@ class UploadAction extends AssetActionBuilder { } @visibleForTesting -Future uploadAssets(BuildContext context, WidgetRef ref, List assets) async { +Future uploadAssets( + BuildContext context, + WidgetRef ref, + List assets, { + required ActionSource source, +}) async { final progress = ref.read(assetUploadProgressProvider.notifier); - final uploads = ref.read(foregroundUploadServiceProvider); + final uploads = ref.read(assetUploadCoordinatorProvider); final toastService = ref.read(toastServiceProvider); final errorMessage = context.t.scaffold_body_error_occurred; @@ -79,8 +85,9 @@ Future uploadAssets(BuildContext context, WidgetRef ref, List } try { - await uploads.uploadManual( - assets, + await uploads.upload( + source: source, + assets: assets, cancelToken: cancelToken, callbacks: UploadCallbacks( onProgress: (id, _, bytes, total) => progress.setProgress(id, total > 0 ? bytes / total : 0.0), diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index d7b48cf0dd..b24e96ce98 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -283,16 +283,17 @@ class _AssetViewerState extends ConsumerState { if (index != _currentPage) { _pageController.jumpToPage(index); unawaited(_onAssetChanged(index)); - } else if (currentAsset is RemoteAsset && - currentAsset.stackId != null && + } else if (currentAsset != null && assetIndex == null && !_shouldIgnoreMissingAssetOnTimelineReload(currentAsset, timelineService)) { - final timelineAsset = timelineService.getAssetSafe(index); - if (timelineAsset is! RemoteAsset || currentAsset.stackId != timelineAsset.stackId) { + if (currentAsset is RemoteAsset && currentAsset.stackId != null) { + final timelineAsset = timelineService.getAssetSafe(index); + if (timelineAsset is! RemoteAsset || currentAsset.stackId != timelineAsset.stackId) { + unawaited(_onAssetChanged(index)); + } + } else { unawaited(_onAssetChanged(index)); } - } else if (currentAsset != null && assetIndex == null) { - unawaited(_onAssetChanged(index)); } if (_totalAssets != totalAssets) { diff --git a/mobile/lib/providers/asset_upload_coordinator.provider.dart b/mobile/lib/providers/asset_upload_coordinator.provider.dart new file mode 100644 index 0000000000..827c77eeb0 --- /dev/null +++ b/mobile/lib/providers/asset_upload_coordinator.provider.dart @@ -0,0 +1,117 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/view_intent/view_intent_file_path.provider.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/services/view_intent.service.dart'; +import 'package:logging/logging.dart'; + +final assetUploadCoordinatorProvider = Provider(AssetUploadCoordinator.new); + +class AssetUploadCoordinator { + AssetUploadCoordinator(this._ref); + + final Ref _ref; + static final Logger _logger = Logger('AssetUploadCoordinator'); + + Future upload({ + required ActionSource source, + required List assets, + required Completer cancelToken, + required UploadCallbacks callbacks, + }) async { + final viewIntentFilePath = source == ActionSource.viewer ? _ref.read(viewIntentFilePathProvider) : null; + if (viewIntentFilePath == null) { + await _ref + .read(foregroundUploadServiceProvider) + .uploadManual(assets, cancelToken: cancelToken, callbacks: callbacks); + return; + } + + if (assets.length != 1) { + throw StateError('A file-backed viewer upload requires exactly one asset.'); + } + + _logger.fine('Using file-backed upload for view intent'); + await _uploadViewIntentFile( + asset: assets.single, + path: viewIntentFilePath, + cancelToken: cancelToken, + callbacks: callbacks, + ); + } + + Future _uploadViewIntentFile({ + required LocalAsset asset, + required String path, + required Completer cancelToken, + required UploadCallbacks callbacks, + }) async { + final viewIntentService = _ref.read(viewIntentServiceProvider); + String? remoteAssetId; + viewIntentService.markUploadActive(path); + + try { + await _ref + .read(foregroundUploadServiceProvider) + .uploadShareIntent( + [File(path)], + cancelToken: cancelToken, + onProgress: (_, bytes, total) => callbacks.onProgress?.call(asset.id, asset.name, bytes, total), + onSuccess: (_, remoteId) { + remoteAssetId = remoteId; + callbacks.onSuccess?.call(asset.id, remoteId); + }, + onError: (_, error) => callbacks.onError?.call(asset.id, error), + ); + + final uploadedRemoteAssetId = remoteAssetId; + if (cancelToken.isCompleted || uploadedRemoteAssetId == null) { + return; + } + + final remoteAsset = await _waitForRemoteAsset(uploadedRemoteAssetId); + if (remoteAsset == null || !_isCurrentUpload(asset, path)) { + return; + } + + _ref.read(assetViewerProvider.notifier).setAsset(remoteAsset); + _ref.read(viewIntentFilePathProvider.notifier).clearIfMatch(path); + await viewIntentService.cleanupManagedTempFileIfCurrent(path); + } finally { + await viewIntentService.markUploadInactive(path); + } + } + + Future _waitForRemoteAsset(String remoteAssetId) async { + try { + return await _ref + .read(assetServiceProvider) + .watchRemoteAsset(remoteAssetId) + .where((asset) => asset != null) + .cast() + .first + .timeout(const Duration(seconds: 15)); + } on TimeoutException { + final asset = await _ref.read(assetServiceProvider).getRemoteAsset(remoteAssetId); + _logger.warning( + 'Timed out waiting for uploaded asset $remoteAssetId; direct lookup ${asset == null ? 'failed' : 'succeeded'}', + ); + return asset; + } + } + + bool _isCurrentUpload(LocalAsset asset, String path) { + if (_ref.read(viewIntentFilePathProvider) != path) { + return false; + } + + final currentAsset = _ref.read(assetViewerProvider).currentAsset; + return currentAsset != null && currentAsset.refersToSameAsset(asset); + } +} diff --git a/mobile/lib/services/view_intent_asset_resolver.service.dart b/mobile/lib/services/view_intent_asset_resolver.service.dart index fe5cb78f6f..e44839c2d2 100644 --- a/mobile/lib/services/view_intent_asset_resolver.service.dart +++ b/mobile/lib/services/view_intent_asset_resolver.service.dart @@ -128,6 +128,7 @@ class ViewIntentAssetResolver { LocalAsset _toTransientAsset(ViewIntentPayload attachment, String? checksum) { final now = DateTime.now(); + // A FileBackedAsset could model the path more explicitly, but would require broader changes to the asset hierarchy. return LocalAsset( id: attachment.localAssetId ?? '-${attachment.path!.hashCode.abs()}', name: attachment.fileName, diff --git a/mobile/test/presentation/widgets/asset_viewer/asset_viewer_timeline_reload_test.dart b/mobile/test/presentation/widgets/asset_viewer/asset_viewer_timeline_reload_test.dart new file mode 100644 index 0000000000..fa33657ce3 --- /dev/null +++ b/mobile/test/presentation/widgets/asset_viewer/asset_viewer_timeline_reload_test.dart @@ -0,0 +1,106 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/locales.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/events.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/generated/codegen_loader.g.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../fixtures/asset.stub.dart'; +import '../../../unit/presentation/presentation_context.dart'; + +final _uploadedAsset = RemoteAsset( + id: 'remote-id', + name: 'uploaded.jpg', + ownerId: 'owner-id', + checksum: 'remote-checksum', + type: AssetType.image, + createdAt: DateTime(2026), + updatedAt: DateTime(2026), + isEdited: false, +); + +class _UploadedAssetViewerNotifier extends AssetViewerStateNotifier { + @override + AssetViewerState build() { + super.build(); + return AssetViewerState(currentAsset: _uploadedAsset); + } +} + +TimelineService _viewIntentTimeline() { + return TimelineService(( + assetSource: (_, __) async => [LocalAssetStub.image1], + bucketSource: () => Stream.value(const [Bucket(assetCount: 1)]), + origin: TimelineOrigin.deepLink, + )); +} + +void main() { + late PresentationContext presentationContext; + + setUp(() async { + await initializeDateFormatting(); + presentationContext = await PresentationContext.create(); + when(() => presentationContext.service.asset.service.watchAsset(any())).thenAnswer((_) => const Stream.empty()); + }); + + tearDown(() async { + await presentationContext.dispose(); + }); + + testWidgets('keeps uploaded remote asset when it is missing from a deep-link timeline reload', (tester) async { + final timeline = _viewIntentTimeline(); + addTearDown(timeline.dispose); + + late ProviderContainer container; + await tester.pumpWidget( + EasyLocalization( + supportedLocales: locales.values.toList(), + path: translationsPath, + startLocale: locales.values.first, + fallbackLocale: locales.values.first, + saveLocale: false, + useFallbackTranslations: true, + assetLoader: const CodegenLoader(), + child: ProviderScope( + overrides: [ + ...presentationContext.overrides, + timelineServiceProvider.overrideWithValue(timeline), + assetViewerProvider.overrideWith(_UploadedAssetViewerNotifier.new), + ], + child: Builder( + builder: (context) { + container = ProviderScope.containerOf(context); + return MaterialApp( + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + home: const Material(child: AssetViewer(initialIndex: 0)), + ); + }, + ), + ), + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 600)); + tester.takeException(); + + EventStream.shared.emit(const TimelineReloadEvent()); + await tester.pump(); + await tester.pump(); + tester.takeException(); + + expect(container.read(assetViewerProvider).currentAsset, same(_uploadedAsset)); + }); +} diff --git a/mobile/test/providers/asset_upload_coordinator_provider_test.dart b/mobile/test/providers/asset_upload_coordinator_provider_test.dart new file mode 100644 index 0000000000..494c1a65b7 --- /dev/null +++ b/mobile/test/providers/asset_upload_coordinator_provider_test.dart @@ -0,0 +1,243 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/providers/asset_upload_coordinator.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/view_intent/view_intent_file_path.provider.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/services/view_intent.service.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../service.mocks.dart'; +import '../unit/factories/local_asset_factory.dart'; +import '../unit/factories/remote_asset_factory.dart'; + +class MockViewIntentService extends Mock implements ViewIntentService {} + +void main() { + late ProviderContainer container; + late MockForegroundUploadService uploadService; + late MockAssetService assetService; + late MockViewIntentService viewIntentService; + + setUpAll(() { + registerFallbackValue(LocalAssetFactory.create()); + registerFallbackValue(const UploadCallbacks()); + }); + + setUp(() { + uploadService = MockForegroundUploadService(); + assetService = MockAssetService(); + viewIntentService = MockViewIntentService(); + + when(() => assetService.watchAsset(any())).thenAnswer((_) => const Stream.empty()); + + container = ProviderContainer( + overrides: [ + foregroundUploadServiceProvider.overrideWithValue(uploadService), + assetServiceProvider.overrideWithValue(assetService), + viewIntentServiceProvider.overrideWithValue(viewIntentService), + ], + ); + addTearDown(container.dispose); + }); + + test('uploads a path-only viewer asset as a file and replaces it with the synchronized remote asset', () async { + const path = 'C:/cache/view_intent_1.jpg'; + final localAsset = LocalAssetFactory.create(id: '-1'); + final remoteAsset = RemoteAssetFactory.create(id: 'remote-1'); + final progress = <(String, int, int)>[]; + final succeeded = <(String, String)>[]; + + container.read(viewIntentFilePathProvider.notifier).setPath(path); + container.read(assetViewerProvider.notifier).setAsset(localAsset); + + when(() => viewIntentService.markUploadActive(path)).thenReturn(null); + when(() => viewIntentService.cleanupManagedTempFileIfCurrent(path)).thenAnswer((_) async {}); + when(() => viewIntentService.markUploadInactive(path)).thenAnswer((_) async {}); + when(() => assetService.watchRemoteAsset('remote-1')).thenAnswer((_) => Stream.value(remoteAsset)); + when( + () => uploadService.uploadShareIntent( + any(), + cancelToken: any(named: 'cancelToken'), + onProgress: any(named: 'onProgress'), + onSuccess: any(named: 'onSuccess'), + onError: any(named: 'onError'), + ), + ).thenAnswer((invocation) async { + final onProgress = invocation.namedArguments[#onProgress] as void Function(String, int, int)?; + final onSuccess = invocation.namedArguments[#onSuccess] as void Function(String, String)?; + onProgress?.call('file-id', 5, 10); + onSuccess?.call('file-id', 'remote-1'); + }); + + await container + .read(assetUploadCoordinatorProvider) + .upload( + source: ActionSource.viewer, + assets: [localAsset], + cancelToken: Completer(), + callbacks: UploadCallbacks( + onProgress: (id, _, bytes, total) => progress.add((id, bytes, total)), + onSuccess: (localId, remoteId) => succeeded.add((localId, remoteId)), + ), + ); + + final files = + verify( + () => uploadService.uploadShareIntent( + captureAny(), + cancelToken: any(named: 'cancelToken'), + onProgress: any(named: 'onProgress'), + onSuccess: any(named: 'onSuccess'), + onError: any(named: 'onError'), + ), + ).captured.single + as List; + expect(files.single.path, path); + expect(progress, [(localAsset.id, 5, 10)]); + expect(succeeded, [(localAsset.id, remoteAsset.id)]); + expect(container.read(assetViewerProvider).currentAsset, remoteAsset); + expect(container.read(viewIntentFilePathProvider), isNull); + verify(() => viewIntentService.markUploadActive(path)).called(1); + verify(() => viewIntentService.cleanupManagedTempFileIfCurrent(path)).called(1); + verify(() => viewIntentService.markUploadInactive(path)).called(1); + verifyNever( + () => uploadService.uploadManual( + any(), + cancelToken: any(named: 'cancelToken'), + callbacks: any(named: 'callbacks'), + ), + ); + }); + + test('keeps the path-only asset current when the upload is cancelled', () async { + const path = 'C:/cache/view_intent_cancelled.jpg'; + final localAsset = LocalAssetFactory.create(id: '-2'); + final cancelToken = Completer(); + + container.read(viewIntentFilePathProvider.notifier).setPath(path); + container.read(assetViewerProvider.notifier).setAsset(localAsset); + when(() => viewIntentService.markUploadActive(path)).thenReturn(null); + when(() => viewIntentService.markUploadInactive(path)).thenAnswer((_) async {}); + when( + () => uploadService.uploadShareIntent( + any(), + cancelToken: cancelToken, + onProgress: any(named: 'onProgress'), + onSuccess: any(named: 'onSuccess'), + onError: any(named: 'onError'), + ), + ).thenAnswer((invocation) async { + cancelToken.complete(); + final onSuccess = invocation.namedArguments[#onSuccess] as void Function(String, String)?; + onSuccess?.call('file-id', 'remote-cancelled'); + }); + + await container + .read(assetUploadCoordinatorProvider) + .upload( + source: ActionSource.viewer, + assets: [localAsset], + cancelToken: cancelToken, + callbacks: const UploadCallbacks(), + ); + + expect(container.read(assetViewerProvider).currentAsset, localAsset); + expect(container.read(viewIntentFilePathProvider), path); + verifyNever(() => assetService.watchRemoteAsset('remote-cancelled')); + verifyNever(() => viewIntentService.cleanupManagedTempFileIfCurrent(path)); + verify(() => viewIntentService.markUploadInactive(path)).called(1); + }); + + test('reports a file upload error under the synthetic asset id and keeps the source file', () async { + const path = 'C:/cache/view_intent_failed.jpg'; + final localAsset = LocalAssetFactory.create(id: '-5'); + final errors = <(String, String)>[]; + + container.read(viewIntentFilePathProvider.notifier).setPath(path); + container.read(assetViewerProvider.notifier).setAsset(localAsset); + when(() => viewIntentService.markUploadActive(path)).thenReturn(null); + when(() => viewIntentService.markUploadInactive(path)).thenAnswer((_) async {}); + when( + () => uploadService.uploadShareIntent( + any(), + cancelToken: any(named: 'cancelToken'), + onProgress: any(named: 'onProgress'), + onSuccess: any(named: 'onSuccess'), + onError: any(named: 'onError'), + ), + ).thenAnswer((invocation) async { + final onError = invocation.namedArguments[#onError] as void Function(String, String)?; + onError?.call('file-id', 'boom'); + }); + + await container + .read(assetUploadCoordinatorProvider) + .upload( + source: ActionSource.viewer, + assets: [localAsset], + cancelToken: Completer(), + callbacks: UploadCallbacks(onError: (id, error) => errors.add((id, error))), + ); + + expect(errors, [(localAsset.id, 'boom')]); + expect(container.read(assetViewerProvider).currentAsset, localAsset); + expect(container.read(viewIntentFilePathProvider), path); + verifyNever(() => viewIntentService.cleanupManagedTempFileIfCurrent(path)); + verify(() => viewIntentService.markUploadInactive(path)).called(1); + }); + + test('does not let an older upload replace a newer view intent', () async { + const oldPath = 'C:/cache/view_intent_old.jpg'; + const newPath = 'C:/cache/view_intent_new.jpg'; + final oldAsset = LocalAssetFactory.create(id: '-3'); + final newAsset = LocalAssetFactory.create(id: '-4'); + final uploadedRemote = RemoteAssetFactory.create(id: 'remote-old'); + final remoteController = StreamController.broadcast(); + addTearDown(remoteController.close); + + container.read(viewIntentFilePathProvider.notifier).setPath(oldPath); + container.read(assetViewerProvider.notifier).setAsset(oldAsset); + when(() => viewIntentService.markUploadActive(oldPath)).thenReturn(null); + when(() => viewIntentService.markUploadInactive(oldPath)).thenAnswer((_) async {}); + when(() => assetService.watchRemoteAsset(uploadedRemote.id)).thenAnswer((_) => remoteController.stream); + when( + () => uploadService.uploadShareIntent( + any(), + cancelToken: any(named: 'cancelToken'), + onProgress: any(named: 'onProgress'), + onSuccess: any(named: 'onSuccess'), + onError: any(named: 'onError'), + ), + ).thenAnswer((invocation) async { + final onSuccess = invocation.namedArguments[#onSuccess] as void Function(String, String)?; + onSuccess?.call('file-id', uploadedRemote.id); + }); + + final upload = container + .read(assetUploadCoordinatorProvider) + .upload( + source: ActionSource.viewer, + assets: [oldAsset], + cancelToken: Completer(), + callbacks: const UploadCallbacks(), + ); + await pumpEventQueue(); + + container.read(viewIntentFilePathProvider.notifier).setPath(newPath); + container.read(assetViewerProvider.notifier).setAsset(newAsset); + remoteController.add(uploadedRemote); + await upload; + + expect(container.read(assetViewerProvider).currentAsset, newAsset); + expect(container.read(viewIntentFilePathProvider), newPath); + verifyNever(() => viewIntentService.cleanupManagedTempFileIfCurrent(oldPath)); + verify(() => viewIntentService.markUploadInactive(oldPath)).called(1); + }); +} diff --git a/mobile/test/unit/presentation/actions/upload_action_test.dart b/mobile/test/unit/presentation/actions/upload_action_test.dart index 8aeb7a915f..253a7fb56d 100644 --- a/mobile/test/unit/presentation/actions/upload_action_test.dart +++ b/mobile/test/unit/presentation/actions/upload_action_test.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; @@ -203,7 +204,7 @@ void main() { overrides: uploadOverrides(), ); - await uploadAssets(tester.element(find.byType(SizedBox)), capturedRef, [asset]); + await uploadAssets(tester.element(find.byType(SizedBox)), capturedRef, [asset], source: ActionSource.timeline); await settleUpload(tester); expect(capturedRef.read(assetUploadProgressProvider), isEmpty);