Fix and write tests

This commit is contained in:
Benoit Marty 2025-01-21 10:57:43 +01:00 committed by Benoit Marty
parent 13defbbcc0
commit f21aeea980
16 changed files with 952 additions and 266 deletions

View file

@ -123,6 +123,7 @@ class EventItemFactory @Inject constructor(
duration = null, duration = null,
), ),
mediaSource = type.source, mediaSource = type.source,
// TODO We may want to add a thumbnailSource and set it to type.info?.thumbnailSource
) )
is ImageMessageType -> MediaItem.Image( is ImageMessageType -> MediaItem.Image(
id = currentTimelineItem.uniqueId, id = currentTimelineItem.uniqueId,
@ -142,7 +143,7 @@ class EventItemFactory @Inject constructor(
duration = null, duration = null,
), ),
mediaSource = type.source, mediaSource = type.source,
thumbnailSource = null, thumbnailSource = type.info?.thumbnailSource,
) )
is StickerMessageType -> MediaItem.Image( is StickerMessageType -> MediaItem.Image(
id = currentTimelineItem.uniqueId, id = currentTimelineItem.uniqueId,
@ -162,7 +163,7 @@ class EventItemFactory @Inject constructor(
duration = null, duration = null,
), ),
mediaSource = type.source, mediaSource = type.source,
thumbnailSource = null, thumbnailSource = type.info?.thumbnailSource,
) )
is VideoMessageType -> MediaItem.Video( is VideoMessageType -> MediaItem.Video(
id = currentTimelineItem.uniqueId, id = currentTimelineItem.uniqueId,

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.mediaviewer.impl.gallery 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.architecture.AsyncData
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
@ -35,6 +36,7 @@ interface MediaGalleryDataSource {
} }
@SingleIn(RoomScope::class) @SingleIn(RoomScope::class)
@ContributesBinding(RoomScope::class)
class TimelineMediaGalleryDataSource @Inject constructor( class TimelineMediaGalleryDataSource @Inject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
private val timelineMediaItemsFactory: TimelineMediaItemsFactory, private val timelineMediaItemsFactory: TimelineMediaItemsFactory,
@ -62,7 +64,9 @@ class TimelineMediaGalleryDataSource @Inject constructor(
timeline = it timeline = it
emit(it) emit(it)
}, },
{ groupedMediaItemsFlow.emit(AsyncData.Failure(it)) }, {
groupedMediaItemsFlow.emit(AsyncData.Failure(it))
},
) )
}.flatMapLatest { timeline -> }.flatMapLatest { timeline ->
timeline.timelineItems.onEach { timeline.timelineItems.onEach {

View file

@ -40,7 +40,7 @@ import kotlinx.coroutines.launch
class MediaGalleryPresenter @AssistedInject constructor( class MediaGalleryPresenter @AssistedInject constructor(
@Assisted private val navigator: MediaGalleryNavigator, @Assisted private val navigator: MediaGalleryNavigator,
private val room: MatrixRoom, private val room: MatrixRoom,
private val mediaGalleryDataSource: TimelineMediaGalleryDataSource, private val mediaGalleryDataSource: MediaGalleryDataSource,
private val localMediaFactory: LocalMediaFactory, private val localMediaFactory: LocalMediaFactory,
private val mediaLoader: MatrixMediaLoader, private val mediaLoader: MatrixMediaLoader,
private val localMediaActions: LocalMediaActions, private val localMediaActions: LocalMediaActions,

View file

@ -18,6 +18,7 @@ fun aMediaItemImage(
id: UniqueId = UniqueId("imageId"), id: UniqueId = UniqueId("imageId"),
eventId: EventId? = null, eventId: EventId? = null,
senderId: UserId? = null, senderId: UserId? = null,
mediaSourceUrl: String = "",
): MediaItem.Image { ): MediaItem.Image {
return MediaItem.Image( return MediaItem.Image(
id = id, id = id,
@ -25,7 +26,7 @@ fun aMediaItemImage(
mediaInfo = anImageMediaInfo( mediaInfo = anImageMediaInfo(
senderId = senderId, senderId = senderId,
), ),
mediaSource = MediaSource(""), mediaSource = MediaSource(mediaSourceUrl),
thumbnailSource = null, thumbnailSource = null,
) )
} }

View file

@ -41,7 +41,6 @@ class MediaViewerDataSource(
private val mediaLoader: MatrixMediaLoader, private val mediaLoader: MatrixMediaLoader,
private val localMediaFactory: LocalMediaFactory, private val localMediaFactory: LocalMediaFactory,
) { ) {
// List of media files that are currently being loaded // List of media files that are currently being loaded
private val mediaFiles: MutableList<MediaFile> = mutableListOf() private val mediaFiles: MutableList<MediaFile> = mutableListOf()
@ -145,6 +144,4 @@ class MediaViewerDataSource(
localMediaState.value = AsyncData.Failure(it) localMediaState.value = AsyncData.Failure(it)
} }
} }
} }

View file

@ -266,7 +266,6 @@ private fun MediaViewerPage(
onShowOverlayChange: (Boolean) -> Unit, onShowOverlayChange: (Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val currentShowOverlay by rememberUpdatedState(showOverlay) val currentShowOverlay by rememberUpdatedState(showOverlay)
val currentOnShowOverlayChange by rememberUpdatedState(onShowOverlayChange) val currentOnShowOverlayChange by rememberUpdatedState(onShowOverlayChange)
val flickState = rememberFlickToDismissState(dismissThresholdRatio = 0.1f, rotateOnDrag = false) val flickState = rememberFlickToDismissState(dismissThresholdRatio = 0.1f, rotateOnDrag = false)
@ -489,7 +488,7 @@ private fun MediaViewerTopBar(
) { ) {
Icon( Icon(
imageVector = CompoundIcons.Info(), imageVector = CompoundIcons.Info(),
contentDescription = null, contentDescription = stringResource(id = CommonStrings.a11y_view_details),
) )
} }
} }

View file

@ -165,6 +165,7 @@ class DefaultEventItemFactoryTest {
dateSent = "0 Day false", dateSent = "0 Day false",
dateSentFull = "0 Full false", dateSentFull = "0 Full false",
waveform = null, waveform = null,
duration = null,
), ),
mediaSource = MediaSource(""), mediaSource = MediaSource(""),
) )
@ -214,6 +215,7 @@ class DefaultEventItemFactoryTest {
dateSent = "0 Day false", dateSent = "0 Day false",
dateSentFull = "0 Full false", dateSentFull = "0 Full false",
waveform = null, waveform = null,
duration = null,
), ),
mediaSource = MediaSource(""), mediaSource = MediaSource(""),
thumbnailSource = null, thumbnailSource = null,
@ -260,6 +262,7 @@ class DefaultEventItemFactoryTest {
dateSent = "0 Day false", dateSent = "0 Day false",
dateSentFull = "0 Full false", dateSentFull = "0 Full false",
waveform = null, waveform = null,
duration = null,
), ),
mediaSource = MediaSource(""), mediaSource = MediaSource(""),
) )
@ -310,10 +313,10 @@ class DefaultEventItemFactoryTest {
dateSent = "0 Day false", dateSent = "0 Day false",
dateSentFull = "0 Full false", dateSentFull = "0 Full false",
waveform = null, waveform = null,
duration = "2:03",
), ),
mediaSource = MediaSource(""), mediaSource = MediaSource(""),
thumbnailSource = null, thumbnailSource = null,
duration = "2:03",
) )
) )
} }
@ -361,10 +364,9 @@ class DefaultEventItemFactoryTest {
dateSent = "0 Day false", dateSent = "0 Day false",
dateSentFull = "0 Full false", dateSentFull = "0 Full false",
waveform = listOf(1f, 2f).toImmutableList(), waveform = listOf(1f, 2f).toImmutableList(),
duration = "7:36",
), ),
mediaSource = MediaSource(""), mediaSource = MediaSource(""),
duration = "7:36",
waveform = listOf(1f, 2f).toImmutableList(),
) )
) )
} }
@ -412,6 +414,7 @@ class DefaultEventItemFactoryTest {
dateSent = "0 Day false", dateSent = "0 Day false",
dateSentFull = "0 Full false", dateSentFull = "0 Full false",
waveform = null, waveform = null,
duration = null,
), ),
mediaSource = MediaSource(""), mediaSource = MediaSource(""),
thumbnailSource = null, thumbnailSource = null,

View file

@ -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)
}
}

View file

@ -8,12 +8,12 @@
package io.element.android.libraries.mediaviewer.impl.gallery package io.element.android.libraries.mediaviewer.impl.gallery
import android.net.Uri import android.net.Uri
import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat 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.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.api.core.EventId 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.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.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_USER_ID 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.impl.gallery.ui.aMediaItemImage
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory 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.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import io.mockk.mockk import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -47,49 +43,37 @@ class MediaGalleryPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val onViewInTimelineClickLambda = lambdaRecorder<EventId, Unit> { } val startLambda = lambdaRecorder<Unit> { }
val navigator = FakeMediaGalleryNavigator(
onViewInTimelineClickLambda = onViewInTimelineClickLambda,
)
val presenter = createMediaGalleryPresenter( val presenter = createMediaGalleryPresenter(
navigator = navigator, mediaGalleryDataSource = FakeMediaGalleryDataSource(
startLambda = startLambda,
),
room = FakeMatrixRoom( room = FakeMatrixRoom(
displayName = A_ROOM_NAME, displayName = A_ROOM_NAME,
mediaTimelineResult = { Result.success(FakeTimeline()) }, mediaTimelineResult = { Result.success(FakeTimeline()) },
) )
) )
presenter.test { presenter.test {
skipItems(2) val initialState = awaitFirstItem()
val initialState = awaitItem()
assertThat(initialState.mode).isEqualTo(MediaGalleryMode.Images) assertThat(initialState.mode).isEqualTo(MediaGalleryMode.Images)
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
assertThat(initialState.roomName).isEqualTo(A_ROOM_NAME) assertThat(initialState.roomName).isEqualTo(A_ROOM_NAME)
assertThat(initialState.groupedMediaItems.dataOrNull()).isEqualTo( assertThat(initialState.groupedMediaItems.isUninitialized()).isTrue()
GroupedMediaItems(
imageAndVideoItems = persistentListOf(),
fileItems = persistentListOf(),
)
)
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
} }
startLambda.assertions().isCalledOnce()
} }
@Test @Test
fun `present - change mode`() = runTest { fun `present - change mode`() = runTest {
val onViewInTimelineClickLambda = lambdaRecorder<EventId, Unit> { }
val navigator = FakeMediaGalleryNavigator(
onViewInTimelineClickLambda = onViewInTimelineClickLambda,
)
val presenter = createMediaGalleryPresenter( val presenter = createMediaGalleryPresenter(
navigator = navigator,
room = FakeMatrixRoom( room = FakeMatrixRoom(
displayName = A_ROOM_NAME, displayName = A_ROOM_NAME,
mediaTimelineResult = { Result.success(FakeTimeline()) }, mediaTimelineResult = { Result.success(FakeTimeline()) },
) )
) )
presenter.test { presenter.test {
skipItems(2) val initialState = awaitFirstItem()
val initialState = awaitItem()
assertThat(initialState.mode).isEqualTo(MediaGalleryMode.Images) assertThat(initialState.mode).isEqualTo(MediaGalleryMode.Images)
initialState.eventSink(MediaGalleryEvents.ChangeMode(MediaGalleryMode.Files)) initialState.eventSink(MediaGalleryEvents.ChangeMode(MediaGalleryMode.Files))
val state = awaitItem() val state = awaitItem()
@ -110,7 +94,7 @@ class MediaGalleryPresenterTest {
`present - bottom sheet state - own message`(canDeleteOwn = false) `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( val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom( room = FakeMatrixRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
@ -120,8 +104,7 @@ class MediaGalleryPresenterTest {
) )
) )
presenter.test { presenter.test {
skipItems(2) val initialState = awaitFirstItem()
val initialState = awaitItem()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
val item = aMediaItemImage( val item = aMediaItemImage(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
@ -154,7 +137,7 @@ class MediaGalleryPresenterTest {
`present - bottom sheet state - other message`(canDeleteOther = false) `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( val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom( room = FakeMatrixRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
@ -164,8 +147,7 @@ class MediaGalleryPresenterTest {
) )
) )
presenter.test { presenter.test {
skipItems(2) val initialState = awaitFirstItem()
val initialState = awaitItem()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
val item = aMediaItemImage( val item = aMediaItemImage(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
@ -197,8 +179,7 @@ class MediaGalleryPresenterTest {
) )
) )
presenter.test { presenter.test {
skipItems(2) val initialState = awaitFirstItem()
val initialState = awaitItem()
// Delete bottom sheet // Delete bottom sheet
val item = aMediaItemImage() val item = aMediaItemImage()
initialState.eventSink(MediaGalleryEvents.ConfirmDelete(AN_EVENT_ID, item.mediaInfo, item.thumbnailSource)) 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 @Test
fun `present - view in timeline invokes the navigator`() = runTest { fun `present - view in timeline invokes the navigator`() = runTest {
val onViewInTimelineClickLambda = lambdaRecorder<EventId, Unit> { } val onViewInTimelineClickLambda = lambdaRecorder<EventId, Unit> { }
@ -230,15 +247,37 @@ class MediaGalleryPresenterTest {
navigator = navigator, navigator = navigator,
) )
presenter.test { presenter.test {
skipItems(2) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink(MediaGalleryEvents.ViewInTimeline(AN_EVENT_ID)) initialState.eventSink(MediaGalleryEvents.ViewInTimeline(AN_EVENT_ID))
onViewInTimelineClickLambda.assertions().isCalledOnce().with(value(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(), matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(),
mediaGalleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(
startLambda = { },
),
localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(), localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
navigator: MediaGalleryNavigator = FakeMediaGalleryNavigator(), navigator: MediaGalleryNavigator = FakeMediaGalleryNavigator(),
@ -249,22 +288,11 @@ class MediaGalleryPresenterTest {
return MediaGalleryPresenter( return MediaGalleryPresenter(
navigator = navigator, navigator = navigator,
room = room, room = room,
timelineMediaItemsFactory = TimelineMediaItemsFactory( mediaGalleryDataSource = mediaGalleryDataSource,
dispatchers = testCoroutineDispatchers(),
virtualItemFactory = VirtualItemFactory(
dateFormatter = FakeDateFormatter(),
),
eventItemFactory = EventItemFactory(
fileSizeFormatter = FakeFileSizeFormatter(),
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
dateFormatter = FakeDateFormatter(),
),
),
localMediaFactory = localMediaFactory, localMediaFactory = localMediaFactory,
mediaLoader = matrixMediaLoader, mediaLoader = matrixMediaLoader,
localMediaActions = localMediaActions, localMediaActions = localMediaActions,
snackbarDispatcher = snackbarDispatcher, snackbarDispatcher = snackbarDispatcher,
mediaItemsPostProcessor = MediaItemsPostProcessor(),
) )
} }
} }

View file

@ -8,9 +8,7 @@
package io.element.android.libraries.mediaviewer.impl.gallery package io.element.android.libraries.mediaviewer.impl.gallery
import com.google.common.truth.Truth.assertThat 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.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.aMediaItemAudio
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemDateSeparator 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.aMediaItemFile
@ -42,27 +40,6 @@ class MediaItemsPostProcessorTest {
private val date3 = aMediaItemDateSeparator(id = UniqueId("3")) private val date3 = aMediaItemDateSeparator(id = UniqueId("3"))
private val loading1 = aMediaItemLoadingIndicator(id = UniqueId("1")) 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 @Test
fun `process Empty`() { fun `process Empty`() {
test( test(
@ -215,19 +192,16 @@ class MediaItemsPostProcessorTest {
expectedFileItems: List<MediaItem>, expectedFileItems: List<MediaItem>,
) { ) {
val sut = MediaItemsPostProcessor() val sut = MediaItemsPostProcessor()
val result = sut.process(AsyncData.Success(mediaItems.toImmutableList())) val result = sut.process(mediaItems.toImmutableList())
val data = result.dataOrNull()!!
// Compare the lists to have better failure info // Compare the lists to have better failure info
assertThat(data.imageAndVideoItems.toList()).isEqualTo(expectedImageAndVideoItems) assertThat(result.imageAndVideoItems.toList()).isEqualTo(expectedImageAndVideoItems)
assertThat(data.fileItems.toList()).isEqualTo(expectedFileItems) assertThat(result.fileItems.toList()).isEqualTo(expectedFileItems)
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
AsyncData.Success( GroupedMediaItems(
GroupedMediaItems( imageAndVideoItems = expectedImageAndVideoItems.toImmutableList(),
imageAndVideoItems = expectedImageAndVideoItems.toImmutableList(), fileItems = expectedFileItems.toImmutableList(),
fileItems = expectedFileItems.toImmutableList(),
)
) )
) )
} }

View file

@ -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(),
)
}
}

View file

@ -50,6 +50,7 @@ class AndroidLocalMediaFactoryTest {
dateSent = "12:34", dateSent = "12:34",
dateSentFull = "full", dateSentFull = "full",
waveform = null, waveform = null,
duration = null,
) )
) )
} }

View file

@ -10,14 +10,14 @@
package io.element.android.libraries.mediaviewer.impl.viewer package io.element.android.libraries.mediaviewer.impl.viewer
import android.net.Uri import android.net.Uri
import app.cash.molecule.RecompositionMode import app.cash.turbine.ReceiveTurbine
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher 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.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.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.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId 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_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.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState 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.FakeLocalMediaActions
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import io.mockk.mockk import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -52,6 +60,7 @@ class MediaViewerPresenterTest {
private val mockMediaUri: Uri = mockk("localMediaUri") private val mockMediaUri: Uri = mockk("localMediaUri")
private val localMediaFactory = FakeLocalMediaFactory(mockMediaUri) private val localMediaFactory = FakeLocalMediaFactory(mockMediaUri)
private val aUrl = "aUrl"
@Test @Test
fun `present - initial state null Event`() = runTest { fun `present - initial state null Event`() = runTest {
@ -61,9 +70,9 @@ class MediaViewerPresenterTest {
) )
) )
presenter.test { presenter.test {
skipItems(2) val initialState = awaitFirstItem()
val initialState = awaitItem() assertThat(initialState.listData).isEmpty()
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) assertThat(initialState.currentIndex).isEqualTo(0)
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.canShowInfo).isTrue() assertThat(initialState.canShowInfo).isTrue()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
@ -79,9 +88,9 @@ class MediaViewerPresenterTest {
) )
) )
presenter.test { presenter.test {
skipItems(2) val initialState = awaitFirstItem()
val initialState = awaitItem() assertThat(initialState.listData).isEmpty()
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) assertThat(initialState.currentIndex).isEqualTo(0)
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.canShowInfo).isFalse() assertThat(initialState.canShowInfo).isFalse()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
@ -97,9 +106,9 @@ class MediaViewerPresenterTest {
) )
) )
presenter.test { presenter.test {
skipItems(2) val initialState = awaitFirstItem()
val initialState = awaitItem() assertThat(initialState.listData).isEmpty()
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) assertThat(initialState.currentIndex).isEqualTo(0)
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.canShowInfo).isTrue() assertThat(initialState.canShowInfo).isTrue()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
@ -116,9 +125,9 @@ class MediaViewerPresenterTest {
) )
) )
presenter.test { presenter.test {
skipItems(2) val initialState = awaitFirstItem()
val initialState = awaitItem() assertThat(initialState.listData).isEmpty()
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) assertThat(initialState.currentIndex).isEqualTo(0)
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.canShowInfo).isTrue() assertThat(initialState.canShowInfo).isTrue()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
@ -126,114 +135,280 @@ class MediaViewerPresenterTest {
} }
@Test @Test
fun `present - download media success scenario`() = runTest { fun `present - data source update`() = runTest {
val presenter = createMediaViewerPresenter( val mediaGalleryDataSource = FakeMediaGalleryDataSource(
room = FakeMatrixRoom( startLambda = { },
canRedactOwnResult = { Result.success(true) },
)
) )
moleculeFlow(RecompositionMode.Immediate) { val presenter = createMediaViewerPresenter(
presenter.present() mediaGalleryDataSource = mediaGalleryDataSource,
}.test { )
var state = awaitItem() val anImage = aMediaItemImage()
assertThat(state.downloadedMedia).isEqualTo(AsyncData.Uninitialized) presenter.test {
assertThat(state.mediaInfo).isEqualTo(TESTED_MEDIA_INFO) val initialState = awaitFirstItem()
state = awaitItem() assertThat(initialState.listData).isEmpty()
assertThat(state.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java) mediaGalleryDataSource.emitGroupedMediaItems(
state = awaitItem() AsyncData.Success(
val successData = state.downloadedMedia.dataOrNull() GroupedMediaItems(
assertThat(state.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) imageAndVideoItems = persistentListOf(anImage),
assertThat(successData).isNotNull() 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 @Test
fun `present - check all actions`() = runTest { fun `present - load media`() = runTest {
val mediaActions = FakeLocalMediaActions() val mediaGalleryDataSource = FakeMediaGalleryDataSource(
val snackbarDispatcher = SnackbarDispatcher() startLambda = { },
val presenter = createMediaViewerPresenter(
localMediaActions = mediaActions,
snackbarDispatcher = snackbarDispatcher,
room = FakeMatrixRoom(
canRedactOwnResult = { Result.success(true) },
)
) )
moleculeFlow(RecompositionMode.Immediate) { val presenter = createMediaViewerPresenter(
presenter.present() mediaGalleryDataSource = mediaGalleryDataSource,
}.test { )
var state = awaitItem() val anImage = aMediaItemImage(
assertThat(state.downloadedMedia).isEqualTo(AsyncData.Uninitialized) mediaSourceUrl = aUrl,
state = awaitItem() )
assertThat(state.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java) presenter.test {
// no state changes while media is loading awaitFirstItem()
state.eventSink(MediaViewerEvents.OpenWith) mediaGalleryDataSource.emitGroupedMediaItems(
state.eventSink(MediaViewerEvents.Share) AsyncData.Success(
state.eventSink(MediaViewerEvents.SaveOnDisk) GroupedMediaItems(
state = awaitItem() imageAndVideoItems = persistentListOf(anImage),
assertThat(state.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) fileItems = persistentListOf(),
// Should succeed without change of state )
state.eventSink(MediaViewerEvents.OpenWith) )
// Should succeed without change of state )
state.eventSink(MediaViewerEvents.Share) val updatedState = awaitItem()
state.eventSink(MediaViewerEvents.SaveOnDisk) updatedState.eventSink(
state = awaitItem() MediaViewerEvents.LoadMedia(
assertThat(state.snackbarMessage).isNotNull() aMediaViewerPageData(
snackbarDispatcher.clear() mediaSource = MediaSource(aUrl)
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()
} }
} }
@Test @Test
fun `present - download media failure then retry with success scenario`() = runTest { fun `present - open info`() = runTest {
val matrixMediaLoader = FakeMatrixMediaLoader() val mediaGalleryDataSource = FakeMediaGalleryDataSource(
startLambda = { },
)
val presenter = createMediaViewerPresenter( val presenter = createMediaViewerPresenter(
matrixMediaLoader = matrixMediaLoader, mediaGalleryDataSource = mediaGalleryDataSource,
room = FakeMatrixRoom( room = FakeMatrixRoom(
canRedactOwnResult = { Result.success(true) }, canRedactOwnResult = { Result.success(true) },
) )
) )
moleculeFlow(RecompositionMode.Immediate) { val anImage = aMediaItemImage(
presenter.present() mediaSourceUrl = aUrl,
}.test { )
matrixMediaLoader.shouldFail = true presenter.test {
val initialState = awaitItem() awaitFirstItem()
assertThat(initialState.downloadedMedia).isEqualTo(AsyncData.Uninitialized) mediaGalleryDataSource.emitGroupedMediaItems(
assertThat(initialState.mediaInfo).isEqualTo(TESTED_MEDIA_INFO) AsyncData.Success(
val loadingState = awaitItem() GroupedMediaItems(
assertThat(loadingState.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java) imageAndVideoItems = persistentListOf(anImage),
val failureState = awaitItem() fileItems = persistentListOf(),
assertThat(failureState.downloadedMedia).isInstanceOf(AsyncData.Failure::class.java) )
matrixMediaLoader.shouldFail = false )
failureState.eventSink(MediaViewerEvents.RetryLoading) )
// There is one recomposition because of the retry mechanism val updatedState = awaitItem()
skipItems(1) updatedState.eventSink(
val retryLoadingState = awaitItem() MediaViewerEvents.OpenInfo(
assertThat(retryLoadingState.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java) aMediaViewerPageData(
val successState = awaitItem() mediaSource = MediaSource(aUrl)
val successData = successState.downloadedMedia.dataOrNull() )
assertThat(successState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) )
assertThat(successData).isNotNull() )
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 @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>> { _, _ -> val redactEventLambda = lambdaRecorder<EventOrTransactionId, String?, Result<Unit>> { _, _ ->
Result.success(Unit) Result.success(Unit)
} }
@ -241,26 +416,51 @@ class MediaViewerPresenterTest {
this.redactEventLambda = redactEventLambda this.redactEventLambda = redactEventLambda
} }
val onItemDeletedLambda = lambdaRecorder<Unit> { } val onItemDeletedLambda = lambdaRecorder<Unit> { }
val navigator = FakeMediaViewerNavigator( val mediaGalleryDataSource = FakeMediaGalleryDataSource(
onItemDeletedLambda = onItemDeletedLambda, startLambda = { },
) )
val presenter = createMediaViewerPresenter( val presenter = createMediaViewerPresenter(
room = FakeMatrixRoom( room = FakeMatrixRoom(
liveTimeline = timeline, liveTimeline = timeline,
canRedactOwnResult = { Result.success(true) }, canRedactOwnResult = { Result.success(true) },
), ),
mediaViewerNavigator = navigator, mediaGalleryDataSource = mediaGalleryDataSource,
mediaViewerNavigator = FakeMediaViewerNavigator(
onItemDeletedLambda = onItemDeletedLambda
)
)
val anImage = aMediaItemImage(
eventId = AN_EVENT_ID,
mediaSourceUrl = aUrl,
) )
presenter.test { presenter.test {
val initialState = awaitItem() awaitFirstItem()
assertThat(initialState.downloadedMedia).isEqualTo(AsyncData.Uninitialized) mediaGalleryDataSource.emitGroupedMediaItems(
assertThat(initialState.mediaInfo).isEqualTo(TESTED_MEDIA_INFO) AsyncData.Success(
val loadingState = awaitItem() GroupedMediaItems(
assertThat(loadingState.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java) imageAndVideoItems = persistentListOf(anImage),
val successState = awaitItem() fileItems = persistentListOf(),
assertThat(successState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) )
successState.eventSink(MediaViewerEvents.Delete(AN_EVENT_ID)) )
)
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() redactEventLambda.assertions()
.isCalledOnce() .isCalledOnce()
.with( .with(
@ -272,7 +472,71 @@ class MediaViewerPresenterTest {
} }
@Test @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 onViewInTimelineClickLambda = lambdaRecorder<EventId, Unit> { }
val navigator = FakeMediaViewerNavigator( val navigator = FakeMediaViewerNavigator(
onViewInTimelineClickLambda = onViewInTimelineClickLambda, onViewInTimelineClickLambda = onViewInTimelineClickLambda,
@ -285,22 +549,28 @@ class MediaViewerPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.downloadedMedia).isEqualTo(AsyncData.Uninitialized) initialState.eventSink(MediaViewerEvents.OpenInfo(aMediaViewerPageData()))
assertThat(initialState.mediaInfo).isEqualTo(TESTED_MEDIA_INFO) val withBottomSheetState = awaitItem()
val loadingState = awaitItem() assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java)
assertThat(loadingState.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java) initialState.eventSink(MediaViewerEvents.ViewInTimeline(AN_EVENT_ID))
val successState = awaitItem() val finalState = awaitItem()
assertThat(successState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
successState.eventSink(MediaViewerEvents.ViewInTimeline(AN_EVENT_ID))
onViewInTimelineClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID)) 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, eventId: EventId? = null,
matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(), matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(),
localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(), localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
mediaGalleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(
startLambda = { },
),
canShowInfo: Boolean = true, canShowInfo: Boolean = true,
mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(), mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(),
room: MatrixRoom = FakeMatrixRoom( room: MatrixRoom = FakeMatrixRoom(
@ -309,18 +579,24 @@ class MediaViewerPresenterTest {
): MediaViewerPresenter { ): MediaViewerPresenter {
return MediaViewerPresenter( return MediaViewerPresenter(
inputs = MediaViewerEntryPoint.Params( inputs = MediaViewerEntryPoint.Params(
mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
eventId = eventId, eventId = eventId,
mediaInfo = TESTED_MEDIA_INFO, mediaInfo = TESTED_MEDIA_INFO,
mediaSource = aMediaSource(), mediaSource = aMediaSource(),
thumbnailSource = null, thumbnailSource = null,
canShowInfo = canShowInfo, canShowInfo = canShowInfo,
), ),
localMediaFactory = localMediaFactory, navigator = mediaViewerNavigator,
mediaLoader = matrixMediaLoader, dataSource = MediaViewerDataSource(
galleryMode = MediaGalleryMode.Images,
dispatcher = testCoroutineDispatchers().computation,
galleryDataSource = mediaGalleryDataSource,
mediaLoader = matrixMediaLoader,
localMediaFactory = localMediaFactory,
),
room = room,
localMediaActions = localMediaActions, localMediaActions = localMediaActions,
snackbarDispatcher = snackbarDispatcher, snackbarDispatcher = snackbarDispatcher,
navigator = mediaViewerNavigator,
room = room,
) )
} }
} }

View file

@ -18,15 +18,15 @@ import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown import androidx.compose.ui.test.swipeDown
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncData 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.impl.details.aMediaDetailsBottomSheetState
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.mockk.mockk
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule import org.junit.rules.TestRule
@ -36,78 +36,127 @@ import org.junit.runner.RunWith
class MediaViewerViewTest { class MediaViewerViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>() @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
private val mockMediaUrl: Uri = mockk("localMediaUri")
@Test @Test
fun `clicking on back invokes expected callback`() { fun `clicking on back invokes expected callback`() {
val eventsRecorder = EventsRecorder<MediaViewerEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<MediaViewerEvents>()
val state = aMediaViewerState(
eventSink = eventsRecorder
)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setMediaViewerView( rule.setMediaViewerView(
aMediaViewerState( state = state,
eventSink = eventsRecorder
),
onBackClick = callback, onBackClick = callback,
) )
rule.pressBack() rule.pressBack()
} }
eventsRecorder.assertList(
listOf(
MediaViewerEvents.OnNavigateTo(0),
MediaViewerEvents.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData),
)
)
} }
@Test @Test
fun `clicking on open emit expected Event`() { 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>() val eventsRecorder = EventsRecorder<MediaViewerEvents>()
rule.setMediaViewerView( rule.setMediaViewerView(
aMediaViewerState( aMediaViewerState(
downloadedMedia = AsyncData.Success( listData = listOf(data),
LocalMedia(Uri.EMPTY, anImageMediaInfo())
),
mediaInfo = anImageMediaInfo(),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
val contentDescription = rule.activity.getString(contentDescriptionRes) val contentDescription = rule.activity.getString(contentDescriptionRes)
rule.onNodeWithContentDescription(contentDescription).performClick() rule.onNodeWithContentDescription(contentDescription).performClick()
eventsRecorder.assertSingle(expectedEvent) eventsRecorder.assertList(
listOf(
MediaViewerEvents.OnNavigateTo(0),
MediaViewerEvents.LoadMedia(data),
expectedEvent,
)
)
} }
@Test @Test
fun `clicking on save emit expected Event`() { 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 @Test
fun `clicking on share emit expected Event`() { 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>() val eventsRecorder = EventsRecorder<MediaViewerEvents>()
rule.setMediaViewerView( rule.setMediaViewerView(
aMediaViewerState( aMediaViewerState(
downloadedMedia = AsyncData.Success( listData = listOf(data),
LocalMedia(Uri.EMPTY, anImageMediaInfo())
),
mediaInfo = anImageMediaInfo(),
mediaBottomSheetState = aMediaDetailsBottomSheetState(), mediaBottomSheetState = aMediaDetailsBottomSheetState(),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(contentDescriptionRes) rule.clickOn(contentDescriptionRes)
eventsRecorder.assertSingle(expectedEvent) eventsRecorder.assertList(
listOf(
MediaViewerEvents.OnNavigateTo(0),
MediaViewerEvents.LoadMedia(data),
expectedEvent,
)
)
} }
@Test @Test
fun `clicking on image hides the overlay`() { fun `clicking on image hides the overlay`() {
val eventsRecorder = EventsRecorder<MediaViewerEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<MediaViewerEvents>()
val state = aMediaViewerState(
eventSink = eventsRecorder
)
rule.setMediaViewerView( rule.setMediaViewerView(
aMediaViewerState( state = state,
downloadedMedia = AsyncData.Success(
LocalMedia(Uri.EMPTY, anImageMediaInfo())
),
mediaInfo = anImageMediaInfo(),
eventSink = eventsRecorder
),
) )
// Ensure that the action are visible // Ensure that the action are visible
val contentDescription = rule.activity.getString(CommonStrings.action_open_with) val contentDescription = rule.activity.getString(CommonStrings.action_open_with)
@ -120,54 +169,79 @@ class MediaViewerViewTest {
rule.mainClock.advanceTimeBy(1_000) rule.mainClock.advanceTimeBy(1_000)
rule.onNodeWithContentDescription(contentDescription) rule.onNodeWithContentDescription(contentDescription)
.assertDoesNotExist() .assertDoesNotExist()
eventsRecorder.assertList(
listOf(
MediaViewerEvents.OnNavigateTo(0),
MediaViewerEvents.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData),
)
)
} }
@Test @Test
fun `clicking swipe on the image invokes the expected callback`() { 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 -> ensureCalledOnce { callback ->
rule.setMediaViewerView( rule.setMediaViewerView(
aMediaViewerState( state = state,
downloadedMedia = AsyncData.Success(
LocalMedia(Uri.EMPTY, anImageMediaInfo())
),
mediaInfo = anImageMediaInfo(),
eventSink = eventsRecorder
),
onBackClick = callback, onBackClick = callback,
) )
val imageContentDescription = rule.activity.getString(CommonStrings.common_image) val imageContentDescription = rule.activity.getString(CommonStrings.common_image)
rule.onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown(startY = centerY) } rule.onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown(startY = centerY) }
rule.mainClock.advanceTimeBy(1_000) rule.mainClock.advanceTimeBy(1_000)
} }
eventsRecorder.assertList(
listOf(
MediaViewerEvents.OnNavigateTo(0),
MediaViewerEvents.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData),
)
)
} }
@Test @Test
fun `error case, click on retry emits the expected Event`() { fun `error case, click on retry emits the expected Event`() {
val eventsRecorder = EventsRecorder<MediaViewerEvents>() val eventsRecorder = EventsRecorder<MediaViewerEvents>()
val data = aMediaViewerPageData(
downloadedMedia = AsyncData.Failure(IllegalStateException("error")),
)
rule.setMediaViewerView( rule.setMediaViewerView(
aMediaViewerState( aMediaViewerState(
downloadedMedia = AsyncData.Failure(IllegalStateException("error")), listData = listOf(data),
mediaInfo = anImageMediaInfo(),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_retry) rule.clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(MediaViewerEvents.RetryLoading) eventsRecorder.assertList(
listOf(
MediaViewerEvents.OnNavigateTo(0),
MediaViewerEvents.LoadMedia(data),
MediaViewerEvents.LoadMedia(data),
)
)
} }
@Test @Test
fun `error case, click on cancel emits the expected Event`() { fun `error case, click on cancel emits the expected Event`() {
val eventsRecorder = EventsRecorder<MediaViewerEvents>() val eventsRecorder = EventsRecorder<MediaViewerEvents>()
val data = aMediaViewerPageData(
downloadedMedia = AsyncData.Failure(IllegalStateException("error")),
)
rule.setMediaViewerView( rule.setMediaViewerView(
aMediaViewerState( aMediaViewerState(
downloadedMedia = AsyncData.Failure(IllegalStateException("error")), listData = listOf(data),
mediaInfo = anImageMediaInfo(),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_cancel) rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(MediaViewerEvents.ClearLoadingError) eventsRecorder.assertList(
listOf(
MediaViewerEvents.OnNavigateTo(0),
MediaViewerEvents.LoadMedia(data),
MediaViewerEvents.ClearLoadingError(data)
)
)
} }
} }

View file

@ -43,6 +43,7 @@ class FakeLocalMediaFactory(
dateSent = null, dateSent = null,
dateSentFull = null, dateSentFull = null,
waveform = null, waveform = null,
duration = null,
) )
return aLocalMedia(uri, mediaInfo) return aLocalMedia(uri, mediaInfo)
} }

View file

@ -29,6 +29,7 @@
<string name="a11y_show_password">"Show password"</string> <string name="a11y_show_password">"Show password"</string>
<string name="a11y_start_call">"Start a call"</string> <string name="a11y_start_call">"Start a call"</string>
<string name="a11y_user_menu">"User menu"</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_record">"Record voice message."</string>
<string name="a11y_voice_message_stop_recording">"Stop recording"</string> <string name="a11y_voice_message_stop_recording">"Stop recording"</string>
<string name="action_accept">"Accept"</string> <string name="action_accept">"Accept"</string>