Fix and write tests
This commit is contained in:
committed by
Benoit Marty
parent
13defbbcc0
commit
f21aeea980
@@ -123,6 +123,7 @@ class EventItemFactory @Inject constructor(
|
||||
duration = null,
|
||||
),
|
||||
mediaSource = type.source,
|
||||
// TODO We may want to add a thumbnailSource and set it to type.info?.thumbnailSource
|
||||
)
|
||||
is ImageMessageType -> MediaItem.Image(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
@@ -142,7 +143,7 @@ class EventItemFactory @Inject constructor(
|
||||
duration = null,
|
||||
),
|
||||
mediaSource = type.source,
|
||||
thumbnailSource = null,
|
||||
thumbnailSource = type.info?.thumbnailSource,
|
||||
)
|
||||
is StickerMessageType -> MediaItem.Image(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
@@ -162,7 +163,7 @@ class EventItemFactory @Inject constructor(
|
||||
duration = null,
|
||||
),
|
||||
mediaSource = type.source,
|
||||
thumbnailSource = null,
|
||||
thumbnailSource = type.info?.thumbnailSource,
|
||||
)
|
||||
is VideoMessageType -> MediaItem.Video(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
@@ -35,6 +36,7 @@ interface MediaGalleryDataSource {
|
||||
}
|
||||
|
||||
@SingleIn(RoomScope::class)
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class TimelineMediaGalleryDataSource @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val timelineMediaItemsFactory: TimelineMediaItemsFactory,
|
||||
@@ -62,7 +64,9 @@ class TimelineMediaGalleryDataSource @Inject constructor(
|
||||
timeline = it
|
||||
emit(it)
|
||||
},
|
||||
{ groupedMediaItemsFlow.emit(AsyncData.Failure(it)) },
|
||||
{
|
||||
groupedMediaItemsFlow.emit(AsyncData.Failure(it))
|
||||
},
|
||||
)
|
||||
}.flatMapLatest { timeline ->
|
||||
timeline.timelineItems.onEach {
|
||||
|
||||
@@ -40,7 +40,7 @@ import kotlinx.coroutines.launch
|
||||
class MediaGalleryPresenter @AssistedInject constructor(
|
||||
@Assisted private val navigator: MediaGalleryNavigator,
|
||||
private val room: MatrixRoom,
|
||||
private val mediaGalleryDataSource: TimelineMediaGalleryDataSource,
|
||||
private val mediaGalleryDataSource: MediaGalleryDataSource,
|
||||
private val localMediaFactory: LocalMediaFactory,
|
||||
private val mediaLoader: MatrixMediaLoader,
|
||||
private val localMediaActions: LocalMediaActions,
|
||||
|
||||
@@ -18,6 +18,7 @@ fun aMediaItemImage(
|
||||
id: UniqueId = UniqueId("imageId"),
|
||||
eventId: EventId? = null,
|
||||
senderId: UserId? = null,
|
||||
mediaSourceUrl: String = "",
|
||||
): MediaItem.Image {
|
||||
return MediaItem.Image(
|
||||
id = id,
|
||||
@@ -25,7 +26,7 @@ fun aMediaItemImage(
|
||||
mediaInfo = anImageMediaInfo(
|
||||
senderId = senderId,
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
mediaSource = MediaSource(mediaSourceUrl),
|
||||
thumbnailSource = null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ class MediaViewerDataSource(
|
||||
private val mediaLoader: MatrixMediaLoader,
|
||||
private val localMediaFactory: LocalMediaFactory,
|
||||
) {
|
||||
|
||||
// List of media files that are currently being loaded
|
||||
private val mediaFiles: MutableList<MediaFile> = mutableListOf()
|
||||
|
||||
@@ -145,6 +144,4 @@ class MediaViewerDataSource(
|
||||
localMediaState.value = AsyncData.Failure(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -266,7 +266,6 @@ private fun MediaViewerPage(
|
||||
onShowOverlayChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
||||
val currentShowOverlay by rememberUpdatedState(showOverlay)
|
||||
val currentOnShowOverlayChange by rememberUpdatedState(onShowOverlayChange)
|
||||
val flickState = rememberFlickToDismissState(dismissThresholdRatio = 0.1f, rotateOnDrag = false)
|
||||
@@ -489,7 +488,7 @@ private fun MediaViewerTopBar(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Info(),
|
||||
contentDescription = null,
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_view_details),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ class DefaultEventItemFactoryTest {
|
||||
dateSent = "0 Day false",
|
||||
dateSentFull = "0 Full false",
|
||||
waveform = null,
|
||||
duration = null,
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
)
|
||||
@@ -214,6 +215,7 @@ class DefaultEventItemFactoryTest {
|
||||
dateSent = "0 Day false",
|
||||
dateSentFull = "0 Full false",
|
||||
waveform = null,
|
||||
duration = null,
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
thumbnailSource = null,
|
||||
@@ -260,6 +262,7 @@ class DefaultEventItemFactoryTest {
|
||||
dateSent = "0 Day false",
|
||||
dateSentFull = "0 Full false",
|
||||
waveform = null,
|
||||
duration = null,
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
)
|
||||
@@ -310,10 +313,10 @@ class DefaultEventItemFactoryTest {
|
||||
dateSent = "0 Day false",
|
||||
dateSentFull = "0 Full false",
|
||||
waveform = null,
|
||||
duration = "2:03",
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
thumbnailSource = null,
|
||||
duration = "2:03",
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -361,10 +364,9 @@ class DefaultEventItemFactoryTest {
|
||||
dateSent = "0 Day false",
|
||||
dateSentFull = "0 Full false",
|
||||
waveform = listOf(1f, 2f).toImmutableList(),
|
||||
duration = "7:36",
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
duration = "7:36",
|
||||
waveform = listOf(1f, 2f).toImmutableList(),
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -412,6 +414,7 @@ class DefaultEventItemFactoryTest {
|
||||
dateSent = "0 Day false",
|
||||
dateSentFull = "0 Full false",
|
||||
waveform = null,
|
||||
duration = null,
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
thumbnailSource = null,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.gallery
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
class FakeMediaGalleryDataSource(
|
||||
private val startLambda: () -> Unit = { lambdaError() },
|
||||
private val loadMoreLambda: (Timeline.PaginationDirection) -> Unit = { lambdaError() },
|
||||
private val deleteItemLambda: (EventId) -> Unit = { lambdaError() },
|
||||
) : MediaGalleryDataSource {
|
||||
override fun start() = startLambda()
|
||||
|
||||
private val groupedMediaItemsFlow = MutableSharedFlow<AsyncData<GroupedMediaItems>>(
|
||||
replay = 1
|
||||
)
|
||||
|
||||
override fun groupedMediaItemsFlow(): Flow<AsyncData<GroupedMediaItems>> {
|
||||
return groupedMediaItemsFlow
|
||||
}
|
||||
|
||||
suspend fun emitGroupedMediaItems(groupedMediaItems: AsyncData<GroupedMediaItems>) {
|
||||
groupedMediaItemsFlow.emit(groupedMediaItems)
|
||||
}
|
||||
|
||||
override fun getLastData(): AsyncData<GroupedMediaItems> {
|
||||
return groupedMediaItemsFlow.replayCache.firstOrNull() ?: AsyncData.Uninitialized
|
||||
}
|
||||
|
||||
override suspend fun loadMore(direction: Timeline.PaginationDirection) {
|
||||
loadMoreLambda(direction)
|
||||
}
|
||||
|
||||
override suspend fun deleteItem(eventId: EventId) {
|
||||
deleteItemLambda(eventId)
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,12 @@
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery
|
||||
|
||||
import android.net.Uri
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
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.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
@@ -25,15 +25,11 @@ import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetSta
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage
|
||||
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions
|
||||
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
|
||||
import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -47,49 +43,37 @@ class MediaGalleryPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val onViewInTimelineClickLambda = lambdaRecorder<EventId, Unit> { }
|
||||
val navigator = FakeMediaGalleryNavigator(
|
||||
onViewInTimelineClickLambda = onViewInTimelineClickLambda,
|
||||
)
|
||||
val startLambda = lambdaRecorder<Unit> { }
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
navigator = navigator,
|
||||
mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = startLambda,
|
||||
),
|
||||
room = FakeMatrixRoom(
|
||||
displayName = A_ROOM_NAME,
|
||||
mediaTimelineResult = { Result.success(FakeTimeline()) },
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.mode).isEqualTo(MediaGalleryMode.Images)
|
||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
assertThat(initialState.roomName).isEqualTo(A_ROOM_NAME)
|
||||
assertThat(initialState.groupedMediaItems.dataOrNull()).isEqualTo(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
assertThat(initialState.groupedMediaItems.isUninitialized()).isTrue()
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
}
|
||||
startLambda.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - change mode`() = runTest {
|
||||
val onViewInTimelineClickLambda = lambdaRecorder<EventId, Unit> { }
|
||||
val navigator = FakeMediaGalleryNavigator(
|
||||
onViewInTimelineClickLambda = onViewInTimelineClickLambda,
|
||||
)
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
navigator = navigator,
|
||||
room = FakeMatrixRoom(
|
||||
displayName = A_ROOM_NAME,
|
||||
mediaTimelineResult = { Result.success(FakeTimeline()) },
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.mode).isEqualTo(MediaGalleryMode.Images)
|
||||
initialState.eventSink(MediaGalleryEvents.ChangeMode(MediaGalleryMode.Files))
|
||||
val state = awaitItem()
|
||||
@@ -110,7 +94,7 @@ class MediaGalleryPresenterTest {
|
||||
`present - bottom sheet state - own message`(canDeleteOwn = false)
|
||||
}
|
||||
|
||||
private suspend fun TestScope.`present - bottom sheet state - own message`(canDeleteOwn: Boolean) {
|
||||
private suspend fun `present - bottom sheet state - own message`(canDeleteOwn: Boolean) {
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
room = FakeMatrixRoom(
|
||||
sessionId = A_USER_ID,
|
||||
@@ -120,8 +104,7 @@ class MediaGalleryPresenterTest {
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
val item = aMediaItemImage(
|
||||
eventId = AN_EVENT_ID,
|
||||
@@ -154,7 +137,7 @@ class MediaGalleryPresenterTest {
|
||||
`present - bottom sheet state - other message`(canDeleteOther = false)
|
||||
}
|
||||
|
||||
private suspend fun TestScope.`present - bottom sheet state - other message`(canDeleteOther: Boolean) {
|
||||
private suspend fun `present - bottom sheet state - other message`(canDeleteOther: Boolean) {
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
room = FakeMatrixRoom(
|
||||
sessionId = A_USER_ID,
|
||||
@@ -164,8 +147,7 @@ class MediaGalleryPresenterTest {
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
val item = aMediaItemImage(
|
||||
eventId = AN_EVENT_ID,
|
||||
@@ -197,8 +179,7 @@ class MediaGalleryPresenterTest {
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitFirstItem()
|
||||
// Delete bottom sheet
|
||||
val item = aMediaItemImage()
|
||||
initialState.eventSink(MediaGalleryEvents.ConfirmDelete(AN_EVENT_ID, item.mediaInfo, item.thumbnailSource))
|
||||
@@ -217,6 +198,42 @@ class MediaGalleryPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - delete item`() = runTest {
|
||||
val deleteItemLambda = lambdaRecorder<EventId, Unit> { }
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
deleteItemLambda = deleteItemLambda,
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.Delete(AN_EVENT_ID))
|
||||
deleteItemLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - share item`() = runTest {
|
||||
val presenter = createMediaGalleryPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID))
|
||||
}
|
||||
// TODO Add more test on this part
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - save on disk`() = runTest {
|
||||
val presenter = createMediaGalleryPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID))
|
||||
}
|
||||
// TODO Add more test on this part
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - view in timeline invokes the navigator`() = runTest {
|
||||
val onViewInTimelineClickLambda = lambdaRecorder<EventId, Unit> { }
|
||||
@@ -230,15 +247,37 @@ class MediaGalleryPresenterTest {
|
||||
navigator = navigator,
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.ViewInTimeline(AN_EVENT_ID))
|
||||
onViewInTimelineClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createMediaGalleryPresenter(
|
||||
@Test
|
||||
fun `present - load more`() = runTest {
|
||||
val loadMoreLambda = lambdaRecorder<Timeline.PaginationDirection, Unit> { }
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
loadMoreLambda = loadMoreLambda,
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS))
|
||||
loadMoreLambda.assertions().isCalledOnce().with(value(Timeline.PaginationDirection.BACKWARDS))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun createMediaGalleryPresenter(
|
||||
matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(),
|
||||
mediaGalleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
),
|
||||
localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
navigator: MediaGalleryNavigator = FakeMediaGalleryNavigator(),
|
||||
@@ -249,22 +288,11 @@ class MediaGalleryPresenterTest {
|
||||
return MediaGalleryPresenter(
|
||||
navigator = navigator,
|
||||
room = room,
|
||||
timelineMediaItemsFactory = TimelineMediaItemsFactory(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
virtualItemFactory = VirtualItemFactory(
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
),
|
||||
eventItemFactory = EventItemFactory(
|
||||
fileSizeFormatter = FakeFileSizeFormatter(),
|
||||
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
),
|
||||
),
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaLoader = matrixMediaLoader,
|
||||
localMediaActions = localMediaActions,
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
mediaItemsPostProcessor = MediaItemsPostProcessor(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemAudio
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemDateSeparator
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile
|
||||
@@ -42,27 +40,6 @@ class MediaItemsPostProcessorTest {
|
||||
private val date3 = aMediaItemDateSeparator(id = UniqueId("3"))
|
||||
private val loading1 = aMediaItemLoadingIndicator(id = UniqueId("1"))
|
||||
|
||||
@Test
|
||||
fun `process Uninitialized`() {
|
||||
val sut = MediaItemsPostProcessor()
|
||||
val result = sut.process(AsyncData.Uninitialized)
|
||||
assertThat(result).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process Loading`() {
|
||||
val sut = MediaItemsPostProcessor()
|
||||
val result = sut.process(AsyncData.Loading())
|
||||
assertThat(result).isEqualTo(AsyncData.Loading<GroupedMediaItems>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process Failure`() {
|
||||
val sut = MediaItemsPostProcessor()
|
||||
val result = sut.process(AsyncData.Failure(AN_EXCEPTION))
|
||||
assertThat(result).isEqualTo(AsyncData.Failure<GroupedMediaItems>(AN_EXCEPTION))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process Empty`() {
|
||||
test(
|
||||
@@ -215,19 +192,16 @@ class MediaItemsPostProcessorTest {
|
||||
expectedFileItems: List<MediaItem>,
|
||||
) {
|
||||
val sut = MediaItemsPostProcessor()
|
||||
val result = sut.process(AsyncData.Success(mediaItems.toImmutableList()))
|
||||
val data = result.dataOrNull()!!
|
||||
val result = sut.process(mediaItems.toImmutableList())
|
||||
|
||||
// Compare the lists to have better failure info
|
||||
assertThat(data.imageAndVideoItems.toList()).isEqualTo(expectedImageAndVideoItems)
|
||||
assertThat(data.fileItems.toList()).isEqualTo(expectedFileItems)
|
||||
assertThat(result.imageAndVideoItems.toList()).isEqualTo(expectedImageAndVideoItems)
|
||||
assertThat(result.fileItems.toList()).isEqualTo(expectedFileItems)
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = expectedImageAndVideoItems.toImmutableList(),
|
||||
fileItems = expectedFileItems.toImmutableList(),
|
||||
)
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = expectedImageAndVideoItems.toImmutableList(),
|
||||
fileItems = expectedFileItems.toImmutableList(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
* 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.gallery
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.media.ThumbnailInfo
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
|
||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class TimelineMediaGalleryDataSourceTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `test - not started TimelineMediaGalleryDataSource emits no events`() {
|
||||
val fakeTimeline = FakeTimeline()
|
||||
runTest {
|
||||
val sut = createTimelineMediaGalleryDataSource(
|
||||
room = FakeMatrixRoom(
|
||||
mediaTimelineResult = { Result.success(fakeTimeline) },
|
||||
roomCoroutineScope = backgroundScope,
|
||||
)
|
||||
)
|
||||
sut.groupedMediaItemsFlow().test {
|
||||
// Also, loadMore and deleteItem should be no-op
|
||||
sut.loadMore(Timeline.PaginationDirection.BACKWARDS)
|
||||
sut.deleteItem(AN_EVENT_ID)
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - getLastData should return the previous emitted data`() {
|
||||
val fakeTimeline = FakeTimeline()
|
||||
runTest {
|
||||
val sut = createTimelineMediaGalleryDataSource(
|
||||
room = FakeMatrixRoom(
|
||||
mediaTimelineResult = { Result.success(fakeTimeline) },
|
||||
roomCoroutineScope = backgroundScope,
|
||||
)
|
||||
)
|
||||
sut.start()
|
||||
assertThat(sut.getLastData()).isEqualTo(AsyncData.Uninitialized)
|
||||
sut.groupedMediaItemsFlow().test {
|
||||
assertThat(awaitItem().isLoading()).isTrue()
|
||||
assertThat(sut.getLastData().isLoading()).isTrue()
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(sut.getLastData().isSuccess()).isTrue()
|
||||
// Also test that starting again should have no effect
|
||||
sut.start()
|
||||
}
|
||||
}
|
||||
// Ensure that the timeline has been closed on flow completion
|
||||
assertThat(fakeTimeline.closeCounter).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - load more should call the timeline paginate method`() = runTest {
|
||||
val paginateLambdaRecorder =
|
||||
lambdaRecorder<Timeline.PaginationDirection, Result<Boolean>> { _ ->
|
||||
Result.success(true)
|
||||
}
|
||||
val fakeTimeline = FakeTimeline().apply {
|
||||
paginateLambda = paginateLambdaRecorder
|
||||
}
|
||||
val sut = createTimelineMediaGalleryDataSource(
|
||||
room = FakeMatrixRoom(
|
||||
mediaTimelineResult = { Result.success(fakeTimeline) },
|
||||
roomCoroutineScope = backgroundScope,
|
||||
)
|
||||
)
|
||||
sut.start()
|
||||
sut.groupedMediaItemsFlow().test {
|
||||
skipItems(2)
|
||||
sut.loadMore(Timeline.PaginationDirection.BACKWARDS)
|
||||
paginateLambdaRecorder.assertions().isCalledOnce().with(value(Timeline.PaginationDirection.BACKWARDS))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - delete item should call the timeline redact method`() = runTest {
|
||||
val redactEventLambdaRecorder =
|
||||
lambdaRecorder<EventOrTransactionId, String?, Result<Unit>> { _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val fakeTimeline = FakeTimeline().apply {
|
||||
redactEventLambda = redactEventLambdaRecorder
|
||||
}
|
||||
val sut = createTimelineMediaGalleryDataSource(
|
||||
room = FakeMatrixRoom(
|
||||
mediaTimelineResult = { Result.success(fakeTimeline) },
|
||||
roomCoroutineScope = backgroundScope,
|
||||
)
|
||||
)
|
||||
sut.start()
|
||||
sut.groupedMediaItemsFlow().test {
|
||||
skipItems(2)
|
||||
sut.deleteItem(AN_EVENT_ID)
|
||||
redactEventLambdaRecorder.assertions().isCalledOnce().with(
|
||||
value(AN_EVENT_ID.toEventOrTransactionId()),
|
||||
value(null),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - failing to load timeline should emit an error`() = runTest {
|
||||
val sut = createTimelineMediaGalleryDataSource(
|
||||
room = FakeMatrixRoom(
|
||||
mediaTimelineResult = { Result.failure(AN_EXCEPTION) },
|
||||
roomCoroutineScope = backgroundScope,
|
||||
)
|
||||
)
|
||||
sut.start()
|
||||
sut.groupedMediaItemsFlow().test {
|
||||
assertThat(awaitItem().isLoading()).isTrue()
|
||||
assertThat(sut.getLastData().isLoading()).isTrue()
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
AsyncData.Failure<GroupedMediaItems>(AN_EXCEPTION)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - when timeline emits new data, the flow emits the data`() = runTest {
|
||||
val timelineItems = MutableStateFlow<List<MatrixTimelineItem>>(emptyList())
|
||||
val fakeTimeline = FakeTimeline(
|
||||
timelineItems = timelineItems,
|
||||
)
|
||||
val sut = createTimelineMediaGalleryDataSource(
|
||||
room = FakeMatrixRoom(
|
||||
mediaTimelineResult = { Result.success(fakeTimeline) },
|
||||
roomCoroutineScope = backgroundScope,
|
||||
)
|
||||
)
|
||||
sut.start()
|
||||
sut.groupedMediaItemsFlow().test {
|
||||
assertThat(awaitItem().isLoading()).isTrue()
|
||||
assertThat(sut.getLastData().isLoading()).isTrue()
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
timelineItems.emit(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(
|
||||
content = aMessageContent(
|
||||
messageType = ImageMessageType(
|
||||
filename = "body.jpg",
|
||||
caption = "body.jpg caption",
|
||||
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"),
|
||||
source = MediaSource("url"),
|
||||
info = ImageInfo(
|
||||
height = 10L,
|
||||
width = 5L,
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 888L,
|
||||
thumbnailInfo = ThumbnailInfo(
|
||||
height = 10L,
|
||||
width = 5L,
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 111L,
|
||||
),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
blurhash = A_BLUR_HASH,
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(
|
||||
MediaItem.Image(
|
||||
id = A_UNIQUE_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
mediaInfo = MediaInfo(
|
||||
filename = "body.jpg",
|
||||
caption = "body.jpg caption",
|
||||
mimeType = MimeTypes.Jpeg,
|
||||
formattedFileSize = "888 Bytes",
|
||||
fileExtension = "jpg",
|
||||
senderId = A_USER_ID,
|
||||
senderName = "alice",
|
||||
senderAvatar = null,
|
||||
dateSent = "0 Day false",
|
||||
dateSentFull = "0 Full false",
|
||||
waveform = null,
|
||||
duration = null
|
||||
),
|
||||
mediaSource = MediaSource("url"),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
)
|
||||
),
|
||||
fileItems = persistentListOf()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createTimelineMediaGalleryDataSource(
|
||||
room: MatrixRoom = FakeMatrixRoom(
|
||||
liveTimeline = FakeTimeline(),
|
||||
),
|
||||
): TimelineMediaGalleryDataSource {
|
||||
return TimelineMediaGalleryDataSource(
|
||||
room = room,
|
||||
timelineMediaItemsFactory = TimelineMediaItemsFactory(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
virtualItemFactory = VirtualItemFactory(
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
),
|
||||
eventItemFactory = EventItemFactory(
|
||||
fileSizeFormatter = FakeFileSizeFormatter(),
|
||||
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
),
|
||||
),
|
||||
mediaItemsPostProcessor = MediaItemsPostProcessor(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ class AndroidLocalMediaFactoryTest {
|
||||
dateSent = "12:34",
|
||||
dateSentFull = "full",
|
||||
waveform = null,
|
||||
duration = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
package io.element.android.libraries.mediaviewer.impl.viewer
|
||||
|
||||
import android.net.Uri
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
@@ -30,14 +30,22 @@ import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.FakeMediaGalleryDataSource
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.GroupedMediaItems
|
||||
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.ui.aMediaItemImage
|
||||
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions
|
||||
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -52,6 +60,7 @@ class MediaViewerPresenterTest {
|
||||
|
||||
private val mockMediaUri: Uri = mockk("localMediaUri")
|
||||
private val localMediaFactory = FakeLocalMediaFactory(mockMediaUri)
|
||||
private val aUrl = "aUrl"
|
||||
|
||||
@Test
|
||||
fun `present - initial state null Event`() = runTest {
|
||||
@@ -61,9 +70,9 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.listData).isEmpty()
|
||||
assertThat(initialState.currentIndex).isEqualTo(0)
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
assertThat(initialState.canShowInfo).isTrue()
|
||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
@@ -79,9 +88,9 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.listData).isEmpty()
|
||||
assertThat(initialState.currentIndex).isEqualTo(0)
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
assertThat(initialState.canShowInfo).isFalse()
|
||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
@@ -97,9 +106,9 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.listData).isEmpty()
|
||||
assertThat(initialState.currentIndex).isEqualTo(0)
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
assertThat(initialState.canShowInfo).isTrue()
|
||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
@@ -116,9 +125,9 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.listData).isEmpty()
|
||||
assertThat(initialState.currentIndex).isEqualTo(0)
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
assertThat(initialState.canShowInfo).isTrue()
|
||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
@@ -126,114 +135,280 @@ class MediaViewerPresenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - download media success scenario`() = runTest {
|
||||
val presenter = createMediaViewerPresenter(
|
||||
room = FakeMatrixRoom(
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
)
|
||||
fun `present - data source update`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
var state = awaitItem()
|
||||
assertThat(state.downloadedMedia).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(state.mediaInfo).isEqualTo(TESTED_MEDIA_INFO)
|
||||
state = awaitItem()
|
||||
assertThat(state.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java)
|
||||
state = awaitItem()
|
||||
val successData = state.downloadedMedia.dataOrNull()
|
||||
assertThat(state.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successData).isNotNull()
|
||||
val presenter = createMediaViewerPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage()
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.listData).isEmpty()
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(anImage),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val updatedState = awaitFirstItem()
|
||||
assertThat(updatedState.listData).hasSize(1)
|
||||
val item = updatedState.listData.first() as MediaViewerPageData.MediaViewerData
|
||||
assertThat(item.eventId).isNull()
|
||||
assertThat(item.mediaInfo).isEqualTo(anImage.mediaInfo)
|
||||
assertThat(item.mediaSource).isEqualTo(anImage.mediaSource)
|
||||
assertThat(item.thumbnailSource).isEqualTo(anImage.thumbnailSource)
|
||||
assertThat(item.downloadedMedia.value).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - check all actions`() = runTest {
|
||||
val mediaActions = FakeLocalMediaActions()
|
||||
val snackbarDispatcher = SnackbarDispatcher()
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaActions = mediaActions,
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
room = FakeMatrixRoom(
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
)
|
||||
fun `present - load media`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
var state = awaitItem()
|
||||
assertThat(state.downloadedMedia).isEqualTo(AsyncData.Uninitialized)
|
||||
state = awaitItem()
|
||||
assertThat(state.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java)
|
||||
// no state changes while media is loading
|
||||
state.eventSink(MediaViewerEvents.OpenWith)
|
||||
state.eventSink(MediaViewerEvents.Share)
|
||||
state.eventSink(MediaViewerEvents.SaveOnDisk)
|
||||
state = awaitItem()
|
||||
assertThat(state.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
// Should succeed without change of state
|
||||
state.eventSink(MediaViewerEvents.OpenWith)
|
||||
// Should succeed without change of state
|
||||
state.eventSink(MediaViewerEvents.Share)
|
||||
state.eventSink(MediaViewerEvents.SaveOnDisk)
|
||||
state = awaitItem()
|
||||
assertThat(state.snackbarMessage).isNotNull()
|
||||
snackbarDispatcher.clear()
|
||||
assertThat(awaitItem().snackbarMessage).isNull()
|
||||
|
||||
// Check failures
|
||||
mediaActions.shouldFail = true
|
||||
state.eventSink(MediaViewerEvents.OpenWith)
|
||||
state = awaitItem()
|
||||
assertThat(state.snackbarMessage).isNotNull()
|
||||
snackbarDispatcher.clear()
|
||||
assertThat(awaitItem().snackbarMessage).isNull()
|
||||
state.eventSink(MediaViewerEvents.Share)
|
||||
state = awaitItem()
|
||||
assertThat(state.snackbarMessage).isNotNull()
|
||||
snackbarDispatcher.clear()
|
||||
assertThat(awaitItem().snackbarMessage).isNull()
|
||||
state.eventSink(MediaViewerEvents.SaveOnDisk)
|
||||
state = awaitItem()
|
||||
assertThat(state.snackbarMessage).isNotNull()
|
||||
val presenter = createMediaViewerPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
mediaSourceUrl = aUrl,
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(anImage),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.LoadMedia(
|
||||
aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - download media failure then retry with success scenario`() = runTest {
|
||||
val matrixMediaLoader = FakeMatrixMediaLoader()
|
||||
fun `present - open info`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
matrixMediaLoader = matrixMediaLoader,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
room = FakeMatrixRoom(
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
matrixMediaLoader.shouldFail = true
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.downloadedMedia).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(initialState.mediaInfo).isEqualTo(TESTED_MEDIA_INFO)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val failureState = awaitItem()
|
||||
assertThat(failureState.downloadedMedia).isInstanceOf(AsyncData.Failure::class.java)
|
||||
matrixMediaLoader.shouldFail = false
|
||||
failureState.eventSink(MediaViewerEvents.RetryLoading)
|
||||
// There is one recomposition because of the retry mechanism
|
||||
skipItems(1)
|
||||
val retryLoadingState = awaitItem()
|
||||
assertThat(retryLoadingState.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
val successData = successState.downloadedMedia.dataOrNull()
|
||||
assertThat(successState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successData).isNotNull()
|
||||
val anImage = aMediaItemImage(
|
||||
mediaSourceUrl = aUrl,
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(anImage),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.OpenInfo(
|
||||
aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
)
|
||||
)
|
||||
val withInfoState = awaitItem()
|
||||
assertThat(withInfoState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java)
|
||||
withInfoState.eventSink(
|
||||
MediaViewerEvents.CloseBottomSheet
|
||||
)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - delete media success scenario`() = runTest {
|
||||
fun `present - clear loading error`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
mediaSourceUrl = aUrl,
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(anImage),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.ClearLoadingError(
|
||||
aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - share`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
mediaSourceUrl = aUrl,
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(anImage),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.Share(
|
||||
aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - save on disk`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
mediaSourceUrl = aUrl,
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(anImage),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.SaveOnDisk(
|
||||
aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - open with`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
mediaSourceUrl = aUrl,
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(anImage),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.OpenWith(
|
||||
aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - delete and cancel`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
mediaSourceUrl = aUrl,
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(anImage),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.ConfirmDelete(
|
||||
eventId = AN_EVENT_ID,
|
||||
data = aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
)
|
||||
)
|
||||
val withBottomSheetState = awaitItem()
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDeleteConfirmationState::class.java)
|
||||
withBottomSheetState.eventSink(
|
||||
MediaViewerEvents.CloseBottomSheet
|
||||
)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - delete`() = runTest {
|
||||
val redactEventLambda = lambdaRecorder<EventOrTransactionId, String?, Result<Unit>> { _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
@@ -241,26 +416,51 @@ class MediaViewerPresenterTest {
|
||||
this.redactEventLambda = redactEventLambda
|
||||
}
|
||||
val onItemDeletedLambda = lambdaRecorder<Unit> { }
|
||||
val navigator = FakeMediaViewerNavigator(
|
||||
onItemDeletedLambda = onItemDeletedLambda,
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
|
||||
val presenter = createMediaViewerPresenter(
|
||||
room = FakeMatrixRoom(
|
||||
liveTimeline = timeline,
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
),
|
||||
mediaViewerNavigator = navigator,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
mediaViewerNavigator = FakeMediaViewerNavigator(
|
||||
onItemDeletedLambda = onItemDeletedLambda
|
||||
)
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
eventId = AN_EVENT_ID,
|
||||
mediaSourceUrl = aUrl,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.downloadedMedia).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(initialState.mediaInfo).isEqualTo(TESTED_MEDIA_INFO)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
successState.eventSink(MediaViewerEvents.Delete(AN_EVENT_ID))
|
||||
awaitFirstItem()
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(anImage),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.ConfirmDelete(
|
||||
eventId = AN_EVENT_ID,
|
||||
data = aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
)
|
||||
)
|
||||
val withBottomSheetState = awaitItem()
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDeleteConfirmationState::class.java)
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.Delete(
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
redactEventLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
@@ -272,7 +472,71 @@ class MediaViewerPresenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - view in timeline invokes the navigator`() = runTest {
|
||||
fun `present - on navigate to`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
mediaSourceUrl = aUrl,
|
||||
)
|
||||
val anImage2 = aMediaItemImage(
|
||||
mediaSourceUrl = aUrl,
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(anImage, anImage2),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.OnNavigateTo(1)
|
||||
)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.currentIndex).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - load more`() = runTest {
|
||||
val loadMoreLambda = lambdaRecorder<Timeline.PaginationDirection, Unit> { }
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
loadMoreLambda = loadMoreLambda,
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
mediaSourceUrl = aUrl,
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem()
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(anImage),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS)
|
||||
)
|
||||
loadMoreLambda.assertions().isCalledOnce().with(value(Timeline.PaginationDirection.BACKWARDS))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - view in timeline hide the bottom sheet and invokes the navigator`() = runTest {
|
||||
val onViewInTimelineClickLambda = lambdaRecorder<EventId, Unit> { }
|
||||
val navigator = FakeMediaViewerNavigator(
|
||||
onViewInTimelineClickLambda = onViewInTimelineClickLambda,
|
||||
@@ -285,22 +549,28 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.downloadedMedia).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(initialState.mediaInfo).isEqualTo(TESTED_MEDIA_INFO)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
successState.eventSink(MediaViewerEvents.ViewInTimeline(AN_EVENT_ID))
|
||||
initialState.eventSink(MediaViewerEvents.OpenInfo(aMediaViewerPageData()))
|
||||
val withBottomSheetState = awaitItem()
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java)
|
||||
initialState.eventSink(MediaViewerEvents.ViewInTimeline(AN_EVENT_ID))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
onViewInTimelineClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMediaViewerPresenter(
|
||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun TestScope.createMediaViewerPresenter(
|
||||
eventId: EventId? = null,
|
||||
matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(),
|
||||
localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
mediaGalleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
),
|
||||
canShowInfo: Boolean = true,
|
||||
mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(),
|
||||
room: MatrixRoom = FakeMatrixRoom(
|
||||
@@ -309,18 +579,24 @@ class MediaViewerPresenterTest {
|
||||
): MediaViewerPresenter {
|
||||
return MediaViewerPresenter(
|
||||
inputs = MediaViewerEntryPoint.Params(
|
||||
mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
|
||||
eventId = eventId,
|
||||
mediaInfo = TESTED_MEDIA_INFO,
|
||||
mediaSource = aMediaSource(),
|
||||
thumbnailSource = null,
|
||||
canShowInfo = canShowInfo,
|
||||
),
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaLoader = matrixMediaLoader,
|
||||
navigator = mediaViewerNavigator,
|
||||
dataSource = MediaViewerDataSource(
|
||||
galleryMode = MediaGalleryMode.Images,
|
||||
dispatcher = testCoroutineDispatchers().computation,
|
||||
galleryDataSource = mediaGalleryDataSource,
|
||||
mediaLoader = matrixMediaLoader,
|
||||
localMediaFactory = localMediaFactory,
|
||||
),
|
||||
room = room,
|
||||
localMediaActions = localMediaActions,
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
navigator = mediaViewerNavigator,
|
||||
room = room,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,15 @@ import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.compose.ui.test.swipeDown
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
|
||||
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import io.mockk.mockk
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
@@ -36,78 +36,127 @@ import org.junit.runner.RunWith
|
||||
class MediaViewerViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
private val mockMediaUrl: Uri = mockk("localMediaUri")
|
||||
|
||||
@Test
|
||||
fun `clicking on back invokes expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>(expectEvents = false)
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
val state = aMediaViewerState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setMediaViewerView(
|
||||
aMediaViewerState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
state = state,
|
||||
onBackClick = callback,
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on open emit expected Event`() {
|
||||
testMenuAction(CommonStrings.action_open_with, MediaViewerEvents.OpenWith)
|
||||
val data = aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl)),
|
||||
)
|
||||
testMenuAction(
|
||||
data,
|
||||
CommonStrings.action_open_with,
|
||||
MediaViewerEvents.OpenWith(data),
|
||||
)
|
||||
}
|
||||
|
||||
private fun testMenuAction(contentDescriptionRes: Int, expectedEvent: MediaViewerEvents) {
|
||||
@Test
|
||||
fun `clicking on info emit expected Event`() {
|
||||
val data = aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl)),
|
||||
)
|
||||
testMenuAction(
|
||||
data,
|
||||
CommonStrings.a11y_view_details,
|
||||
MediaViewerEvents.OpenInfo(data),
|
||||
)
|
||||
}
|
||||
|
||||
private fun testMenuAction(
|
||||
data: MediaViewerPageData.MediaViewerData,
|
||||
contentDescriptionRes: Int,
|
||||
expectedEvent: MediaViewerEvents,
|
||||
) {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
rule.setMediaViewerView(
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, anImageMediaInfo())
|
||||
),
|
||||
mediaInfo = anImageMediaInfo(),
|
||||
listData = listOf(data),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
val contentDescription = rule.activity.getString(contentDescriptionRes)
|
||||
rule.onNodeWithContentDescription(contentDescription).performClick()
|
||||
eventsRecorder.assertSingle(expectedEvent)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(data),
|
||||
expectedEvent,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on save emit expected Event`() {
|
||||
testBottomSheetAction(CommonStrings.action_save, MediaViewerEvents.SaveOnDisk)
|
||||
val data = aMediaViewerPageData()
|
||||
testBottomSheetAction(
|
||||
data,
|
||||
CommonStrings.action_save,
|
||||
MediaViewerEvents.SaveOnDisk(data),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on share emit expected Event`() {
|
||||
testBottomSheetAction(CommonStrings.action_share, MediaViewerEvents.Share)
|
||||
val data = aMediaViewerPageData()
|
||||
testBottomSheetAction(
|
||||
data,
|
||||
CommonStrings.action_share,
|
||||
MediaViewerEvents.Share(data),
|
||||
)
|
||||
}
|
||||
|
||||
private fun testBottomSheetAction(contentDescriptionRes: Int, expectedEvent: MediaViewerEvents) {
|
||||
private fun testBottomSheetAction(
|
||||
data: MediaViewerPageData.MediaViewerData,
|
||||
contentDescriptionRes: Int,
|
||||
expectedEvent: MediaViewerEvents,
|
||||
) {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
rule.setMediaViewerView(
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, anImageMediaInfo())
|
||||
),
|
||||
mediaInfo = anImageMediaInfo(),
|
||||
listData = listOf(data),
|
||||
mediaBottomSheetState = aMediaDetailsBottomSheetState(),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(contentDescriptionRes)
|
||||
eventsRecorder.assertSingle(expectedEvent)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(data),
|
||||
expectedEvent,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on image hides the overlay`() {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>(expectEvents = false)
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
val state = aMediaViewerState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
rule.setMediaViewerView(
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, anImageMediaInfo())
|
||||
),
|
||||
mediaInfo = anImageMediaInfo(),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
state = state,
|
||||
)
|
||||
// Ensure that the action are visible
|
||||
val contentDescription = rule.activity.getString(CommonStrings.action_open_with)
|
||||
@@ -120,54 +169,79 @@ class MediaViewerViewTest {
|
||||
rule.mainClock.advanceTimeBy(1_000)
|
||||
rule.onNodeWithContentDescription(contentDescription)
|
||||
.assertDoesNotExist()
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking swipe on the image invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>(expectEvents = false)
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
val state = aMediaViewerState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setMediaViewerView(
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, anImageMediaInfo())
|
||||
),
|
||||
mediaInfo = anImageMediaInfo(),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
state = state,
|
||||
onBackClick = callback,
|
||||
)
|
||||
val imageContentDescription = rule.activity.getString(CommonStrings.common_image)
|
||||
rule.onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown(startY = centerY) }
|
||||
rule.mainClock.advanceTimeBy(1_000)
|
||||
}
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error case, click on retry emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
val data = aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Failure(IllegalStateException("error")),
|
||||
)
|
||||
rule.setMediaViewerView(
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Failure(IllegalStateException("error")),
|
||||
mediaInfo = anImageMediaInfo(),
|
||||
listData = listOf(data),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_retry)
|
||||
eventsRecorder.assertSingle(MediaViewerEvents.RetryLoading)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(data),
|
||||
MediaViewerEvents.LoadMedia(data),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error case, click on cancel emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
val data = aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Failure(IllegalStateException("error")),
|
||||
)
|
||||
rule.setMediaViewerView(
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Failure(IllegalStateException("error")),
|
||||
mediaInfo = anImageMediaInfo(),
|
||||
listData = listOf(data),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(MediaViewerEvents.ClearLoadingError)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(data),
|
||||
MediaViewerEvents.ClearLoadingError(data)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ class FakeLocalMediaFactory(
|
||||
dateSent = null,
|
||||
dateSentFull = null,
|
||||
waveform = null,
|
||||
duration = null,
|
||||
)
|
||||
return aLocalMedia(uri, mediaInfo)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<string name="a11y_show_password">"Show password"</string>
|
||||
<string name="a11y_start_call">"Start a call"</string>
|
||||
<string name="a11y_user_menu">"User menu"</string>
|
||||
<string name="a11y_view_details">"View details"</string>
|
||||
<string name="a11y_voice_message_record">"Record voice message."</string>
|
||||
<string name="a11y_voice_message_stop_recording">"Stop recording"</string>
|
||||
<string name="action_accept">"Accept"</string>
|
||||
|
||||
Reference in New Issue
Block a user