diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt index 99fcb790c4..c80e287c30 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.core.preview.loremIpsum +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo @@ -30,12 +31,13 @@ class MediaItemFileProvider : PreviewParameterProvider { fun aMediaItemFile( id: UniqueId = UniqueId("fileId"), + eventId: EventId? = null, filename: String = "filename", caption: String? = null, ): MediaItem.File { return MediaItem.File( id = id, - eventId = null, + eventId = eventId, mediaInfo = aPdfMediaInfo( filename = filename, caption = caption, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemLoadingIndicatorProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemLoadingIndicatorProvider.kt index 546d2f0127..2c78898325 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemLoadingIndicatorProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemLoadingIndicatorProvider.kt @@ -13,10 +13,11 @@ import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem fun aMediaItemLoadingIndicator( id: UniqueId = UniqueId("loadingId"), + direction: Timeline.PaginationDirection = Timeline.PaginationDirection.BACKWARDS, ): MediaItem.LoadingIndicator { return MediaItem.LoadingIndicator( id = id, - direction = Timeline.PaginationDirection.BACKWARDS, + direction = direction, timestamp = 123, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt index 89c3fe943d..d03b003440 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.State @@ -63,7 +64,8 @@ class MediaViewerDataSource( return remember { dataFlow() }.collectAsState(initialData()) } - private fun dataFlow(): Flow> { + @VisibleForTesting + fun dataFlow(): Flow> { return galleryDataSource.groupedMediaItemsFlow() .map { groupedItems -> val mediaItems = groupedItems.dataOrNull()?.getItems(galleryMode).orEmpty() @@ -104,7 +106,7 @@ class MediaViewerDataSource( } } if (isEmpty()) { - MediaViewerPageData.Loading(Timeline.PaginationDirection.BACKWARDS) + add(MediaViewerPageData.Loading(Timeline.PaginationDirection.BACKWARDS)) } }.toPersistentList() diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt new file mode 100644 index 0000000000..092467a63d --- /dev/null +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt @@ -0,0 +1,275 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.viewer + +import android.net.Uri +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader +import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory +import io.element.android.libraries.mediaviewer.impl.gallery.FakeMediaGalleryDataSource +import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryDataSource +import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryMode +import io.element.android.libraries.mediaviewer.impl.gallery.aGroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemDateSeparator +import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile +import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage +import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator +import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.testCoroutineDispatchers +import io.mockk.mockk +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MediaViewerDataSourceTest { + private val mockMediaUrl: Uri = mockk("localMediaUri") + + @Test + fun `setup should start the gallery data source`() = runTest { + val startLambda = lambdaRecorder { } + val galleryDataSource = FakeMediaGalleryDataSource( + startLambda = startLambda + ) + val sut = createMediaViewerDataSource( + galleryDataSource = galleryDataSource, + ) + sut.setup() + startLambda.assertions().isCalledOnce() + } + + @Test + fun `test dispose`() = runTest { + val sut = createMediaViewerDataSource() + sut.dispose() + } + + @Test + fun `test dataFlow uninitialized, loading and error`() = runTest { + val galleryDataSource = FakeMediaGalleryDataSource() + val sut = createMediaViewerDataSource( + galleryDataSource = galleryDataSource, + ) + sut.dataFlow().test { + galleryDataSource.emitGroupedMediaItems(AsyncData.Uninitialized) + assertThat(awaitItem().first()).isInstanceOf(MediaViewerPageData.Loading::class.java) + galleryDataSource.emitGroupedMediaItems(AsyncData.Loading()) + assertThat(awaitItem().first()).isInstanceOf(MediaViewerPageData.Loading::class.java) + galleryDataSource.emitGroupedMediaItems(AsyncData.Failure(AN_EXCEPTION)) + // TODO Add an error screen in the ui + assertThat(awaitItem().first()).isInstanceOf(MediaViewerPageData.Loading::class.java) + } + } + + @Test + fun `test dataFlow empty`() = runTest { + val galleryDataSource = FakeMediaGalleryDataSource() + val sut = createMediaViewerDataSource( + galleryDataSource = galleryDataSource, + ) + sut.dataFlow().test { + galleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + aGroupedMediaItems( + imageAndVideoItems = listOf(), + fileItems = listOf(), + ) + ) + ) + val result = awaitItem() + assertThat(result).hasSize(1) + assertThat(result.first()).isEqualTo(MediaViewerPageData.Loading(Timeline.PaginationDirection.BACKWARDS)) + } + } + + @Test + fun `test dataFlow loading items`() = runTest { + val galleryDataSource = FakeMediaGalleryDataSource() + val sut = createMediaViewerDataSource( + galleryDataSource = galleryDataSource, + ) + sut.dataFlow().test { + galleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + aGroupedMediaItems( + imageAndVideoItems = listOf( + aMediaItemLoadingIndicator( + direction = Timeline.PaginationDirection.BACKWARDS, + ), + aMediaItemLoadingIndicator( + direction = Timeline.PaginationDirection.FORWARDS, + ), + ), + fileItems = listOf(), + ) + ) + ) + val result = awaitItem() + assertThat(result).containsExactly( + MediaViewerPageData.Loading(Timeline.PaginationDirection.BACKWARDS), + MediaViewerPageData.Loading(Timeline.PaginationDirection.FORWARDS), + ) + } + } + + @Test + fun `test dataFlow with data galleryMode image`() = runTest { + val galleryDataSource = FakeMediaGalleryDataSource() + val sut = createMediaViewerDataSource( + galleryMode = MediaGalleryMode.Images, + galleryDataSource = galleryDataSource, + ) + sut.dataFlow().test { + galleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + aGroupedMediaItems( + imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)), + fileItems = listOf(aMediaItemFile(eventId = AN_EVENT_ID_2)), + ) + ) + ) + val result = awaitItem() + assertThat(result).hasSize(1) + assertThat((result.first() as MediaViewerPageData.MediaViewerData).eventId).isEqualTo(AN_EVENT_ID) + } + } + + @Test + fun `test dataFlow with data galleryMode files`() = runTest { + val galleryDataSource = FakeMediaGalleryDataSource() + val sut = createMediaViewerDataSource( + galleryMode = MediaGalleryMode.Files, + galleryDataSource = galleryDataSource, + ) + sut.dataFlow().test { + galleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + aGroupedMediaItems( + imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)), + fileItems = listOf(aMediaItemFile(eventId = AN_EVENT_ID_2)), + ) + ) + ) + val result = awaitItem() + assertThat(result).hasSize(1) + assertThat((result.first() as MediaViewerPageData.MediaViewerData).eventId).isEqualTo(AN_EVENT_ID_2) + } + } + + @Test + fun `test dataFlow - date separator are filtered out`() = runTest { + val galleryDataSource = FakeMediaGalleryDataSource() + val sut = createMediaViewerDataSource( + galleryDataSource = galleryDataSource, + ) + sut.dataFlow().test { + galleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + aGroupedMediaItems( + imageAndVideoItems = listOf(aMediaItemDateSeparator(), aMediaItemImage(), aMediaItemDateSeparator()), + fileItems = emptyList(), + ) + ) + ) + val result = awaitItem() + assertThat(result).hasSize(1) + } + } + + @Test + fun `loadMore invokes the gallery data source loadMore`() = runTest { + val loadMoreLambda = lambdaRecorder { } + val galleryDataSource = FakeMediaGalleryDataSource( + loadMoreLambda = loadMoreLambda + ) + val sut = createMediaViewerDataSource( + galleryDataSource = galleryDataSource, + ) + sut.loadMore(Timeline.PaginationDirection.BACKWARDS) + loadMoreLambda.assertions().isCalledOnce().with(value(Timeline.PaginationDirection.BACKWARDS)) + } + + @Test + fun `test dataFlow with data galleryMode image and load media`() = runTest { + val galleryDataSource = FakeMediaGalleryDataSource() + val sut = createMediaViewerDataSource( + galleryDataSource = galleryDataSource, + ) + sut.dataFlow().test { + galleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + aGroupedMediaItems( + imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)), + ) + ) + ) + val result = awaitItem() + val mediaViewerData = result.first() as MediaViewerPageData.MediaViewerData + assertThat(mediaViewerData.downloadedMedia.value).isEqualTo(AsyncData.Uninitialized) + sut.loadMedia(mediaViewerData) + assertThat(mediaViewerData.downloadedMedia.value.isSuccess()).isTrue() + } + } + + @Test + fun `test dataFlow with data galleryMode image and load media with failure then success`() = runTest { + val galleryDataSource = FakeMediaGalleryDataSource() + val mediaLoader = FakeMatrixMediaLoader() + val sut = createMediaViewerDataSource( + galleryDataSource = galleryDataSource, + mediaLoader = mediaLoader, + ) + sut.dataFlow().test { + galleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + aGroupedMediaItems( + imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)), + ) + ) + ) + val result = awaitItem() + val mediaViewerData = result.first() as MediaViewerPageData.MediaViewerData + assertThat(mediaViewerData.downloadedMedia.value).isEqualTo(AsyncData.Uninitialized) + mediaLoader.shouldFail = true + sut.loadMedia(mediaViewerData) + assertThat(mediaViewerData.downloadedMedia.value.isFailure()).isTrue() + // clear the error + sut.clearLoadingError(mediaViewerData) + assertThat(mediaViewerData.downloadedMedia.value).isEqualTo(AsyncData.Uninitialized) + // load again with success + mediaLoader.shouldFail = false + sut.loadMedia(mediaViewerData) + assertThat(mediaViewerData.downloadedMedia.value.isSuccess()).isTrue() + } + } + + @Test + fun clearLoadingError() { + } + + private fun TestScope.createMediaViewerDataSource( + galleryMode: MediaGalleryMode = MediaGalleryMode.Images, + galleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(), + mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(), + localMediaFactory: LocalMediaFactory = FakeLocalMediaFactory(mockMediaUrl), + ) = MediaViewerDataSource( + galleryMode = galleryMode, + dispatcher = testCoroutineDispatchers().computation, + galleryDataSource = galleryDataSource, + mediaLoader = mediaLoader, + localMediaFactory = localMediaFactory, + ) +}