Media: add more tests
This commit is contained in:
parent
6fd16e9414
commit
db90b5051c
15 changed files with 354 additions and 53 deletions
|
|
@ -18,10 +18,12 @@ package io.element.android.features.messages.impl.media.local
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
|
|
@ -29,8 +31,13 @@ class AndroidLocalMediaFactory @Inject constructor(
|
||||||
@ApplicationContext private val context: Context
|
@ApplicationContext private val context: Context
|
||||||
) : LocalMediaFactory {
|
) : LocalMediaFactory {
|
||||||
|
|
||||||
override fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? {
|
override fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia {
|
||||||
if (uri == null) return null
|
val resolvedMimeType = mimeType ?: MimeTypes.OctetStream
|
||||||
|
val uri = mediaFile.path().toUri()
|
||||||
|
return LocalMedia(uri, resolvedMimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFromUri(uri: Uri, mimeType: String?): LocalMedia {
|
||||||
val resolvedMimeType = mimeType ?: context.contentResolver.getType(uri) ?: MimeTypes.OctetStream
|
val resolvedMimeType = mimeType ?: context.contentResolver.getType(uri) ?: MimeTypes.OctetStream
|
||||||
return LocalMedia(uri, resolvedMimeType)
|
return LocalMedia(uri, resolvedMimeType)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,20 @@
|
||||||
package io.element.android.features.messages.impl.media.local
|
package io.element.android.features.messages.impl.media.local
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
|
|
||||||
interface LocalMediaFactory {
|
interface LocalMediaFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will create a [LocalMedia] with the given [MediaFile] and [mimeType]
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method will create a [LocalMedia] with the given [uri] and [mimeType]
|
* This method will create a [LocalMedia] with the given [uri] and [mimeType]
|
||||||
* If the [mimeType] is null, it'll try to read it from the content.
|
* If the [mimeType] is null, it'll try to read it from the content.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia?
|
fun createFromUri(uri: Uri, mimeType: String?): LocalMedia
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.net.toUri
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
@ -32,7 +31,7 @@ import io.element.android.features.messages.impl.media.local.LocalMedia
|
||||||
import io.element.android.features.messages.impl.media.local.LocalMediaFactory
|
import io.element.android.features.messages.impl.media.local.LocalMediaFactory
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||||
import io.element.android.libraries.matrix.api.media.MediaFile
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -40,7 +39,7 @@ import kotlinx.coroutines.launch
|
||||||
class MediaViewerPresenter @AssistedInject constructor(
|
class MediaViewerPresenter @AssistedInject constructor(
|
||||||
@Assisted private val inputs: MediaViewerNode.Inputs,
|
@Assisted private val inputs: MediaViewerNode.Inputs,
|
||||||
private val localMediaFactory: LocalMediaFactory,
|
private val localMediaFactory: LocalMediaFactory,
|
||||||
private val client: MatrixClient,
|
private val mediaLoader: MatrixMediaLoader,
|
||||||
) : Presenter<MediaViewerState> {
|
) : Presenter<MediaViewerState> {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -79,14 +78,12 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.loadMedia(mediaFile: MutableState<MediaFile?>, localMedia: MutableState<Async<LocalMedia>>) = launch {
|
private fun CoroutineScope.loadMedia(mediaFile: MutableState<MediaFile?>, localMedia: MutableState<Async<LocalMedia>>) = launch {
|
||||||
mediaFile.value = null
|
|
||||||
localMedia.value = Async.Loading()
|
localMedia.value = Async.Loading()
|
||||||
client.mediaLoader.loadMediaFile(inputs.mediaSource, inputs.mimeType)
|
mediaLoader.loadMediaFile(inputs.mediaSource, inputs.mimeType)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
mediaFile.value = it
|
mediaFile.value = it
|
||||||
}.mapCatching {
|
}.mapCatching {
|
||||||
val uri = it.path().toUri()
|
localMediaFactory.createFromMediaFile(it, inputs.mimeType)
|
||||||
localMediaFactory.createFromUri(uri, inputs.mimeType)!!
|
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
localMedia.value = Async.Success(it)
|
localMedia.value = Async.Success(it)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
|
|
|
||||||
|
|
@ -206,22 +206,22 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
mimeType: String? = null,
|
mimeType: String? = null,
|
||||||
compressIfPossible: Boolean = true,
|
compressIfPossible: Boolean = true,
|
||||||
) {
|
) {
|
||||||
|
if (uri == null) {
|
||||||
|
attachmentsState.value = AttachmentsState.None
|
||||||
|
return
|
||||||
|
}
|
||||||
val localMedia = localMediaFactory.createFromUri(uri, mimeType)
|
val localMedia = localMediaFactory.createFromUri(uri, mimeType)
|
||||||
attachmentsState.value = if (localMedia == null) {
|
val mediaAttachment = Attachment.Media(localMedia, compressIfPossible)
|
||||||
AttachmentsState.None
|
val isPreviewable = when {
|
||||||
|
MimeTypes.isImage(localMedia.mimeType) -> true
|
||||||
|
MimeTypes.isVideo(localMedia.mimeType) -> true
|
||||||
|
MimeTypes.isAudio(localMedia.mimeType) -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
attachmentsState.value = if (isPreviewable) {
|
||||||
|
AttachmentsState.Previewing(persistentListOf(mediaAttachment))
|
||||||
} else {
|
} else {
|
||||||
val mediaAttachment = Attachment.Media(localMedia, compressIfPossible)
|
AttachmentsState.Sending(persistentListOf(mediaAttachment))
|
||||||
val isPreviewable = when {
|
|
||||||
MimeTypes.isImage(localMedia.mimeType) -> true
|
|
||||||
MimeTypes.isVideo(localMedia.mimeType) -> true
|
|
||||||
MimeTypes.isAudio(localMedia.mimeType) -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
if (isPreviewable) {
|
|
||||||
AttachmentsState.Previewing(persistentListOf(mediaAttachment))
|
|
||||||
} else {
|
|
||||||
AttachmentsState.Sending(persistentListOf(mediaAttachment))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import io.element.android.features.messages.impl.MessagesEvents
|
||||||
import io.element.android.features.messages.impl.MessagesPresenter
|
import io.element.android.features.messages.impl.MessagesPresenter
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.media.local.FakeLocalMediaFactory
|
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.attachments
|
||||||
|
|
||||||
|
import androidx.media3.common.MimeTypes
|
||||||
|
import app.cash.molecule.RecompositionClock
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.messages.fixtures.aLocalMedia
|
||||||
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
|
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents
|
||||||
|
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter
|
||||||
|
import io.element.android.features.messages.impl.media.local.LocalMedia
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AttachmentsPreviewPresenterTest {
|
||||||
|
|
||||||
|
private val mediaPreProcessor = FakeMediaPreProcessor()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send media success scenario`() = runTest {
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val presenter = anAttachmentsPreviewPresenter(room = room)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(Async.Uninitialized)
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
val loadingState = awaitItem()
|
||||||
|
assertThat(loadingState.sendActionState).isEqualTo(Async.Loading<Unit>())
|
||||||
|
testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS)
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.sendActionState).isEqualTo(Async.Success(Unit))
|
||||||
|
assertThat(room.sendMediaCount).isEqualTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send media failure scenario`() = runTest {
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val failure = MediaPreProcessor.Failure(null)
|
||||||
|
room.givenSendMediaResult(Result.failure(failure))
|
||||||
|
val presenter = anAttachmentsPreviewPresenter(room = room)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(Async.Uninitialized)
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
val loadingState = awaitItem()
|
||||||
|
assertThat(loadingState.sendActionState).isEqualTo(Async.Loading<Unit>())
|
||||||
|
testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS)
|
||||||
|
val failureState = awaitItem()
|
||||||
|
assertThat(failureState.sendActionState).isEqualTo(Async.Failure<Unit>(failure))
|
||||||
|
assertThat(room.sendMediaCount).isEqualTo(0)
|
||||||
|
failureState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
||||||
|
val clearedState = awaitItem()
|
||||||
|
assertThat(clearedState.sendActionState).isEqualTo(Async.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun anAttachmentsPreviewPresenter(
|
||||||
|
localMedia: LocalMedia = aLocalMedia(mimeType = MimeTypes.IMAGE_JPEG),
|
||||||
|
room: MatrixRoom = FakeMatrixRoom()
|
||||||
|
): AttachmentsPreviewPresenter {
|
||||||
|
return AttachmentsPreviewPresenter(
|
||||||
|
attachment = Attachment.Media(localMedia, compressIfPossible = false),
|
||||||
|
mediaSender = MediaSender(mediaPreProcessor, room)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.fixtures
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
|
import io.element.android.features.messages.impl.media.local.LocalMedia
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
fun aLocalMedia(
|
||||||
|
uri: Uri = mockk("localMediaUri"),
|
||||||
|
mimeType: String
|
||||||
|
) = LocalMedia(
|
||||||
|
uri = uri,
|
||||||
|
mimeType = mimeType
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aMediaAttachment(localMedia: LocalMedia, compressIfPossible: Boolean = true) = Attachment.Media(
|
||||||
|
localMedia = localMedia,
|
||||||
|
compressIfPossible = compressIfPossible,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.media
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import io.element.android.features.messages.fixtures.aLocalMedia
|
||||||
|
import io.element.android.features.messages.impl.media.local.LocalMedia
|
||||||
|
import io.element.android.features.messages.impl.media.local.LocalMediaFactory
|
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
|
|
||||||
|
class FakeLocalMediaFactory() : LocalMediaFactory {
|
||||||
|
|
||||||
|
var fallbackMimeType: String = MimeTypes.OctetStream
|
||||||
|
|
||||||
|
override fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia {
|
||||||
|
return aLocalMedia(mimeType = mimeType ?: fallbackMimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFromUri(uri: Uri, mimeType: String?): LocalMedia {
|
||||||
|
return aLocalMedia(uri, mimeType ?: fallbackMimeType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.media.viewer
|
||||||
|
|
||||||
|
import androidx.media3.common.MimeTypes
|
||||||
|
import app.cash.molecule.RecompositionClock
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.messages.impl.media.viewer.MediaViewerEvents
|
||||||
|
import io.element.android.features.messages.impl.media.viewer.MediaViewerNode
|
||||||
|
import io.element.android.features.messages.impl.media.viewer.MediaViewerPresenter
|
||||||
|
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
|
||||||
|
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
|
||||||
|
import io.element.android.libraries.matrix.test.media.aMediaSource
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val TESTED_MIME_TYPE = MimeTypes.IMAGE_JPEG
|
||||||
|
private const val TESTED_MEDIA_NAME = "MediaName"
|
||||||
|
|
||||||
|
class MediaViewerPresenterTest {
|
||||||
|
|
||||||
|
private val localMediaFactory = FakeLocalMediaFactory()
|
||||||
|
private val mediaLoader = FakeMediaLoader()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - download media success scenario`() = runTest {
|
||||||
|
val presenter = aMediaViewerPresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.downloadedMedia).isEqualTo(Async.Uninitialized)
|
||||||
|
assertThat(initialState.name).isEqualTo(TESTED_MEDIA_NAME)
|
||||||
|
val loadingState = awaitItem()
|
||||||
|
assertThat(loadingState.downloadedMedia).isInstanceOf(Async.Loading::class.java)
|
||||||
|
testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS)
|
||||||
|
val successState = awaitItem()
|
||||||
|
val successData = successState.downloadedMedia.dataOrNull()
|
||||||
|
assertThat(successState.downloadedMedia).isInstanceOf(Async.Success::class.java)
|
||||||
|
assertThat(successData).isNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - download media failure then retry with success scenario`() = runTest {
|
||||||
|
val presenter = aMediaViewerPresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
mediaLoader.shouldFail = true
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.downloadedMedia).isEqualTo(Async.Uninitialized)
|
||||||
|
assertThat(initialState.name).isEqualTo(TESTED_MEDIA_NAME)
|
||||||
|
val loadingState = awaitItem()
|
||||||
|
assertThat(loadingState.downloadedMedia).isInstanceOf(Async.Loading::class.java)
|
||||||
|
testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS)
|
||||||
|
val failureState = awaitItem()
|
||||||
|
assertThat(failureState.downloadedMedia).isInstanceOf(Async.Failure::class.java)
|
||||||
|
mediaLoader.shouldFail = false
|
||||||
|
failureState.eventSink(MediaViewerEvents.RetryLoading)
|
||||||
|
//There is one recomposition because of the retry mechanism
|
||||||
|
skipItems(1)
|
||||||
|
val retryLoadingState = awaitItem()
|
||||||
|
assertThat(retryLoadingState.downloadedMedia).isInstanceOf(Async.Loading::class.java)
|
||||||
|
testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS)
|
||||||
|
val successState = awaitItem()
|
||||||
|
val successData = successState.downloadedMedia.dataOrNull()
|
||||||
|
assertThat(successState.downloadedMedia).isInstanceOf(Async.Success::class.java)
|
||||||
|
assertThat(successData).isNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aMediaViewerPresenter(mimeType: String = TESTED_MIME_TYPE): MediaViewerPresenter {
|
||||||
|
return MediaViewerPresenter(
|
||||||
|
inputs = MediaViewerNode.Inputs(
|
||||||
|
name = TESTED_MEDIA_NAME,
|
||||||
|
mediaSource = aMediaSource(),
|
||||||
|
mimeType = mimeType
|
||||||
|
),
|
||||||
|
localMediaFactory = localMediaFactory,
|
||||||
|
mediaLoader = mediaLoader
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.ReceiveTurbine
|
import app.cash.turbine.ReceiveTurbine
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.messages.impl.media.local.FakeLocalMediaFactory
|
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||||
|
|
@ -52,7 +52,6 @@ import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.test.runCurrent
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -392,7 +391,9 @@ class MessageComposerPresenterTest {
|
||||||
val sendingState = awaitItem()
|
val sendingState = awaitItem()
|
||||||
assertThat(sendingState.showAttachmentSourcePicker).isFalse()
|
assertThat(sendingState.showAttachmentSourcePicker).isFalse()
|
||||||
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java)
|
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java)
|
||||||
cancelAndIgnoreRemainingEvents()
|
val sentState = awaitItem()
|
||||||
|
assertThat(sentState.attachmentsState).isEqualTo(AttachmentsState.None)
|
||||||
|
assertThat(room.sendMediaCount).isEqualTo(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -440,7 +441,7 @@ class MessageComposerPresenterTest {
|
||||||
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
|
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
|
||||||
val sendingState = awaitItem()
|
val sendingState = awaitItem()
|
||||||
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java)
|
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java)
|
||||||
val finalState= awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.None::class.java)
|
assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.None::class.java)
|
||||||
snackbarDispatcher.snackbarMessage.test {
|
snackbarDispatcher.snackbarMessage.test {
|
||||||
// Assert error message received
|
// Assert error message received
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,16 @@ import dagger.Provides
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ContributesTo(SessionScope::class)
|
@ContributesTo(SessionScope::class)
|
||||||
object SessionMatrixModule {
|
object SessionMatrixModule {
|
||||||
@Provides
|
@Provides
|
||||||
@SingleIn(SessionScope::class)
|
@SingleIn(SessionScope::class)
|
||||||
fun providesRustSessionVerificationService(matrixClient: MatrixClient): SessionVerificationService {
|
fun providesSessionVerificationService(matrixClient: MatrixClient): SessionVerificationService {
|
||||||
return matrixClient.sessionVerificationService()
|
return matrixClient.sessionVerificationService()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,4 +40,10 @@ object SessionMatrixModule {
|
||||||
fun provideRoomMembershipObserver(matrixClient: MatrixClient): RoomMembershipObserver {
|
fun provideRoomMembershipObserver(matrixClient: MatrixClient): RoomMembershipObserver {
|
||||||
return matrixClient.roomMembershipObserver()
|
return matrixClient.roomMembershipObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@SingleIn(SessionScope::class)
|
||||||
|
fun provideMediaLoader(matrixClient: MatrixClient): MatrixMediaLoader {
|
||||||
|
return matrixClient.mediaLoader
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
import kotlin.time.toDuration
|
||||||
|
|
||||||
const val A_USER_NAME = "alice"
|
const val A_USER_NAME = "alice"
|
||||||
const val A_PASSWORD = "password"
|
const val A_PASSWORD = "password"
|
||||||
|
|
@ -52,6 +54,9 @@ val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, true, null)
|
||||||
const val AN_AVATAR_URL = "mxc://data"
|
const val AN_AVATAR_URL = "mxc://data"
|
||||||
|
|
||||||
const val A_FAILURE_REASON = "There has been a failure"
|
const val A_FAILURE_REASON = "There has been a failure"
|
||||||
|
|
||||||
|
const val FAKE_DELAY_IN_MS = 100L
|
||||||
|
|
||||||
val A_THROWABLE = Throwable(A_FAILURE_REASON)
|
val A_THROWABLE = Throwable(A_FAILURE_REASON)
|
||||||
val AN_EXCEPTION = Exception(A_FAILURE_REASON)
|
val AN_EXCEPTION = Exception(A_FAILURE_REASON)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,17 @@
|
||||||
package io.element.android.libraries.matrix.test.media
|
package io.element.android.libraries.matrix.test.media
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
|
||||||
import io.element.android.libraries.matrix.api.media.MediaFile
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
class FakeMediaLoader : MatrixMediaLoader {
|
class FakeMediaLoader : MatrixMediaLoader {
|
||||||
|
|
||||||
var shouldFail = false
|
var shouldFail = false
|
||||||
|
|
||||||
override suspend fun loadMediaContent(source: MediaSource): Result<ByteArray> {
|
override suspend fun loadMediaContent(source: MediaSource): Result<ByteArray> {
|
||||||
|
delay(FAKE_DELAY_IN_MS)
|
||||||
return if (shouldFail) {
|
return if (shouldFail) {
|
||||||
Result.failure(RuntimeException())
|
Result.failure(RuntimeException())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -33,6 +36,7 @@ class FakeMediaLoader : MatrixMediaLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result<ByteArray> {
|
override suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result<ByteArray> {
|
||||||
|
delay(FAKE_DELAY_IN_MS)
|
||||||
return if (shouldFail) {
|
return if (shouldFail) {
|
||||||
Result.failure(RuntimeException())
|
Result.failure(RuntimeException())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -41,6 +45,7 @@ class FakeMediaLoader : MatrixMediaLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadMediaFile(source: MediaSource, mimeType: String?): Result<MediaFile> {
|
override suspend fun loadMediaFile(source: MediaSource, mimeType: String?): Result<MediaFile> {
|
||||||
|
delay(FAKE_DELAY_IN_MS)
|
||||||
return if (shouldFail) {
|
return if (shouldFail) {
|
||||||
Result.failure(RuntimeException())
|
Result.failure(RuntimeException())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.media.local
|
package io.element.android.libraries.matrix.test.media
|
||||||
|
|
||||||
import android.net.Uri
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
|
||||||
|
|
||||||
class FakeLocalMediaFactory() : LocalMediaFactory {
|
fun aMediaSource(url: String = "") = MediaSource(
|
||||||
|
url = url,
|
||||||
var fallbackMimeType: String = MimeTypes.OctetStream
|
json = null
|
||||||
|
)
|
||||||
override fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? {
|
|
||||||
if (uri == null) return null
|
|
||||||
return LocalMedia(uri, mimeType ?: fallbackMimeType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -94,7 +95,7 @@ class FakeMatrixRoom(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendMessage(message: String): Result<Unit> {
|
override suspend fun sendMessage(message: String): Result<Unit> {
|
||||||
delay(100)
|
delay(FAKE_DELAY_IN_MS)
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,7 +104,7 @@ class FakeMatrixRoom(
|
||||||
|
|
||||||
override suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> {
|
override suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> {
|
||||||
editMessageParameter = message
|
editMessageParameter = message
|
||||||
delay(100)
|
delay(FAKE_DELAY_IN_MS)
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,7 +113,7 @@ class FakeMatrixRoom(
|
||||||
|
|
||||||
override suspend fun replyMessage(eventId: EventId, message: String): Result<Unit> {
|
override suspend fun replyMessage(eventId: EventId, message: String): Result<Unit> {
|
||||||
replyMessageParameter = message
|
replyMessageParameter = message
|
||||||
delay(100)
|
delay(FAKE_DELAY_IN_MS)
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +122,7 @@ class FakeMatrixRoom(
|
||||||
|
|
||||||
override suspend fun redactEvent(eventId: EventId, reason: String?): Result<Unit> {
|
override suspend fun redactEvent(eventId: EventId, reason: String?): Result<Unit> {
|
||||||
redactEventEventIdParam = eventId
|
redactEventEventIdParam = eventId
|
||||||
delay(100)
|
delay(FAKE_DELAY_IN_MS)
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,13 +137,20 @@ class FakeMatrixRoom(
|
||||||
return rejectInviteResult
|
return rejectInviteResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result<Unit> = sendMediaResult.also { sendMediaCount++ }
|
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result<Unit> = fakeSendMedia()
|
||||||
|
|
||||||
override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result<Unit> = sendMediaResult.also { sendMediaCount++ }
|
override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result<Unit> = fakeSendMedia()
|
||||||
|
|
||||||
override suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result<Unit> = sendMediaResult.also { sendMediaCount++ }
|
override suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result<Unit> = fakeSendMedia()
|
||||||
|
|
||||||
override suspend fun sendFile(file: File, fileInfo: FileInfo): Result<Unit> = sendMediaResult.also { sendMediaCount++ }
|
override suspend fun sendFile(file: File, fileInfo: FileInfo): Result<Unit> = fakeSendMedia()
|
||||||
|
|
||||||
|
private suspend fun fakeSendMedia(): Result<Unit> {
|
||||||
|
delay(FAKE_DELAY_IN_MS)
|
||||||
|
return sendMediaResult.onSuccess {
|
||||||
|
sendMediaCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() = Unit
|
override fun close() = Unit
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue