port messageviewmodel tests to the reducer domain and add more
This commit is contained in:
parent
a5b7ede2d8
commit
b9dda51112
|
@ -10,8 +10,6 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.amshove.kluent.internal.assertEquals
|
import org.amshove.kluent.internal.assertEquals
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import test.ExpectTest
|
|
||||||
import test.ExpectTestScope
|
|
||||||
|
|
||||||
interface ReducerTest<S, E> {
|
interface ReducerTest<S, E> {
|
||||||
operator fun invoke(block: suspend ReducerTestScope<S, E>.() -> Unit)
|
operator fun invoke(block: suspend ReducerTestScope<S, E>.() -> Unit)
|
||||||
|
@ -63,16 +61,31 @@ class ReducerTestScope<S, E>(
|
||||||
manualState = state
|
manualState = state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setState(block: (S) -> S) {
|
||||||
|
manualState = block(reducerScope.getState())
|
||||||
|
}
|
||||||
|
|
||||||
fun assertInitialState(expected: S) {
|
fun assertInitialState(expected: S) {
|
||||||
reducerFactory.initialState() shouldBeEqualTo expected
|
reducerFactory.initialState() shouldBeEqualTo expected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun assertEvents(events: List<E>) {
|
||||||
|
fakeEventSource.assertEvents(events)
|
||||||
|
}
|
||||||
|
|
||||||
fun assertOnlyStateChange(expected: S) {
|
fun assertOnlyStateChange(expected: S) {
|
||||||
assertStateChange(expected)
|
assertStateChange(expected)
|
||||||
assertNoDispatches()
|
assertNoDispatches()
|
||||||
fakeEventSource.assertNoEvents()
|
fakeEventSource.assertNoEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun assertOnlyStateChange(block: (S) -> S) {
|
||||||
|
val expected = block(reducerScope.getState())
|
||||||
|
assertStateChange(expected)
|
||||||
|
assertNoDispatches()
|
||||||
|
fakeEventSource.assertNoEvents()
|
||||||
|
}
|
||||||
|
|
||||||
fun assertStateChange(expected: S) {
|
fun assertStateChange(expected: S) {
|
||||||
capturedResult shouldBeEqualTo expected
|
capturedResult shouldBeEqualTo expected
|
||||||
}
|
}
|
||||||
|
@ -86,7 +99,11 @@ class ReducerTestScope<S, E>(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertNoStateChange() {
|
fun assertNoStateChange() {
|
||||||
assertEquals(reducerFactory.initialState(), capturedResult)
|
assertEquals(reducerScope.getState(), capturedResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertNoEvents() {
|
||||||
|
fakeEventSource.assertNoEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertOnlyDispatches(expected: List<Action>) {
|
fun assertOnlyDispatches(expected: List<Action>) {
|
||||||
|
@ -103,7 +120,7 @@ class ReducerTestScope<S, E>(
|
||||||
|
|
||||||
fun assertNoChanges() {
|
fun assertNoChanges() {
|
||||||
assertNoStateChange()
|
assertNoStateChange()
|
||||||
fakeEventSource.assertNoEvents()
|
assertNoEvents()
|
||||||
assertNoDispatches()
|
assertNoDispatches()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,6 +9,7 @@ import app.dapk.st.core.extensions.takeIfContent
|
||||||
import app.dapk.st.design.components.BubbleModel
|
import app.dapk.st.design.components.BubbleModel
|
||||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.engine.ChatEngine
|
import app.dapk.st.engine.ChatEngine
|
||||||
|
import app.dapk.st.engine.MessengerPageState
|
||||||
import app.dapk.st.engine.RoomEvent
|
import app.dapk.st.engine.RoomEvent
|
||||||
import app.dapk.st.engine.SendMessage
|
import app.dapk.st.engine.SendMessage
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
@ -33,7 +34,7 @@ internal fun messengerReducer(
|
||||||
initialState = MessengerScreenState(
|
initialState = MessengerScreenState(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
roomState = Lce.Loading(),
|
roomState = Lce.Loading(),
|
||||||
composerState = initialAttachments?.let { ComposerState.Attachments(it, null) } ?: ComposerState.Text(value = "", reply = null),
|
composerState = initialComposerState(initialAttachments),
|
||||||
viewerState = null,
|
viewerState = null,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -120,32 +121,12 @@ internal fun messengerReducer(
|
||||||
is ComposerState.Text -> {
|
is ComposerState.Text -> {
|
||||||
dispatch(ComposerStateChange.Clear)
|
dispatch(ComposerStateChange.Clear)
|
||||||
state.roomState.takeIfContent()?.let { content ->
|
state.roomState.takeIfContent()?.let { content ->
|
||||||
val roomState = content.roomState
|
chatEngine.sendTextMessage(content, composerState)
|
||||||
chatEngine.send(
|
|
||||||
message = SendMessage.TextMessage(
|
|
||||||
content = composerState.value,
|
|
||||||
reply = composerState.reply?.let {
|
|
||||||
SendMessage.TextMessage.Reply(
|
|
||||||
author = it.author,
|
|
||||||
originalMessage = when (it) {
|
|
||||||
is RoomEvent.Image -> TODO()
|
|
||||||
is RoomEvent.Reply -> TODO()
|
|
||||||
is RoomEvent.Message -> it.content.asString()
|
|
||||||
is RoomEvent.Encrypted -> error("Should never happen")
|
|
||||||
},
|
|
||||||
eventId = it.eventId,
|
|
||||||
timestampUtc = it.utcTimestamp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
room = roomState.roomOverview,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ComposerState.Attachments -> {
|
is ComposerState.Attachments -> {
|
||||||
dispatch(ComposerStateChange.Clear)
|
dispatch(ComposerStateChange.Clear)
|
||||||
|
|
||||||
state.roomState.takeIfContent()?.let { content ->
|
state.roomState.takeIfContent()?.let { content ->
|
||||||
val roomState = content.roomState
|
val roomState = content.roomState
|
||||||
chatEngine.send(SendMessage.ImageMessage(uri = composerState.values.first().uri.value), roomState.roomOverview)
|
chatEngine.send(SendMessage.ImageMessage(uri = composerState.values.first().uri.value), roomState.roomOverview)
|
||||||
|
@ -156,6 +137,33 @@ internal fun messengerReducer(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun ChatEngine.sendTextMessage(content: MessengerPageState, composerState: ComposerState.Text) {
|
||||||
|
val roomState = content.roomState
|
||||||
|
val message = SendMessage.TextMessage(
|
||||||
|
content = composerState.value,
|
||||||
|
reply = composerState.reply?.toSendMessageReply(),
|
||||||
|
)
|
||||||
|
this.send(message = message, room = roomState.roomOverview)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomEvent.toSendMessageReply() = SendMessage.TextMessage.Reply(
|
||||||
|
author = this.author,
|
||||||
|
originalMessage = when (this) {
|
||||||
|
is RoomEvent.Image -> TODO()
|
||||||
|
is RoomEvent.Reply -> TODO()
|
||||||
|
is RoomEvent.Message -> this.content.asString()
|
||||||
|
is RoomEvent.Encrypted -> error("Should never happen")
|
||||||
|
},
|
||||||
|
eventId = this.eventId,
|
||||||
|
timestampUtc = this.utcTimestamp,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
private fun initialComposerState(initialAttachments: List<MessageAttachment>?) = initialAttachments
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
?.let { ComposerState.Attachments(it, null) }
|
||||||
|
?: ComposerState.Text(value = "", reply = null)
|
||||||
|
|
||||||
private fun BubbleModel.findCopyableContent(): CopyableResult = when (this) {
|
private fun BubbleModel.findCopyableContent(): CopyableResult = when (this) {
|
||||||
is BubbleModel.Encrypted -> CopyableResult.NothingToCopy
|
is BubbleModel.Encrypted -> CopyableResult.NothingToCopy
|
||||||
is BubbleModel.Image -> CopyableResult.NothingToCopy
|
is BubbleModel.Image -> CopyableResult.NothingToCopy
|
||||||
|
|
|
@ -0,0 +1,362 @@
|
||||||
|
package app.dapk.st.messenger
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import app.dapk.st.core.*
|
||||||
|
import app.dapk.st.design.components.BubbleModel
|
||||||
|
import app.dapk.st.engine.RoomEvent
|
||||||
|
import app.dapk.st.engine.RoomState
|
||||||
|
import app.dapk.st.engine.SendMessage
|
||||||
|
import app.dapk.st.matrix.common.EventId
|
||||||
|
import app.dapk.st.matrix.common.UserId
|
||||||
|
import app.dapk.st.matrix.common.asString
|
||||||
|
import app.dapk.st.messenger.state.*
|
||||||
|
import app.dapk.st.navigator.MessageAttachment
|
||||||
|
import fake.FakeChatEngine
|
||||||
|
import fake.FakeMessageOptionsStore
|
||||||
|
import fixture.*
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import org.junit.Test
|
||||||
|
import test.ReducerTestScope
|
||||||
|
import test.delegateReturn
|
||||||
|
import test.expect
|
||||||
|
import test.testReducer
|
||||||
|
|
||||||
|
private const val READ_RECEIPTS_ARE_DISABLED = true
|
||||||
|
private val A_ROOM_ID = aRoomId("messenger state room id")
|
||||||
|
private const val A_MESSAGE_CONTENT = "message content"
|
||||||
|
private val AN_EVENT_ID = anEventId("state event")
|
||||||
|
private val A_SELF_ID = aUserId("self")
|
||||||
|
private val A_MESSENGER_PAGE_STATE = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
||||||
|
private val A_MESSAGE_ATTACHMENT = MessageAttachment(AndroidUri("a-uri"), MimeType.Image)
|
||||||
|
private val A_REPLY = aRoomReplyMessageEvent()
|
||||||
|
private val AN_IMAGE_BUBBLE = BubbleModel.Image(
|
||||||
|
BubbleModel.Image.ImageContent(100, 200, "a-url"),
|
||||||
|
mockk(),
|
||||||
|
BubbleModel.Event("author-id", "author-name", edited = false, time = "10:27")
|
||||||
|
)
|
||||||
|
|
||||||
|
private val A_TEXT_BUBBLE = BubbleModel.Text(
|
||||||
|
content = RichText(listOf(RichText.Part.Normal(A_MESSAGE_CONTENT))),
|
||||||
|
BubbleModel.Event("author-id", "author-name", edited = false, time = "10:27")
|
||||||
|
)
|
||||||
|
|
||||||
|
class MessengerReducerTest {
|
||||||
|
|
||||||
|
private val fakeMessageOptionsStore = FakeMessageOptionsStore()
|
||||||
|
private val fakeChatEngine = FakeChatEngine()
|
||||||
|
private val fakeCopyToClipboard = FakeCopyToClipboard()
|
||||||
|
private val fakeDeviceMeta = FakeDeviceMeta()
|
||||||
|
private val fakeJobBag = FakeJobBag()
|
||||||
|
|
||||||
|
private val runReducerTest = testReducer { fakeEventSource ->
|
||||||
|
messengerReducer(
|
||||||
|
fakeJobBag.instance,
|
||||||
|
fakeChatEngine,
|
||||||
|
fakeCopyToClipboard.instance,
|
||||||
|
fakeDeviceMeta.instance,
|
||||||
|
fakeMessageOptionsStore.instance,
|
||||||
|
A_ROOM_ID,
|
||||||
|
emptyList(),
|
||||||
|
fakeEventSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given empty initial attachments, then initial state is loading with text composer`() = reducerWithInitialState(initialAttachments = emptyList()) {
|
||||||
|
assertInitialState(
|
||||||
|
MessengerScreenState(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
roomState = Lce.Loading(),
|
||||||
|
composerState = ComposerState.Text(value = "", reply = null),
|
||||||
|
viewerState = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given null initial attachments, then initial state is loading with text composer`() = reducerWithInitialState(initialAttachments = null) {
|
||||||
|
assertInitialState(
|
||||||
|
MessengerScreenState(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
roomState = Lce.Loading(),
|
||||||
|
composerState = ComposerState.Text(value = "", reply = null),
|
||||||
|
viewerState = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given initial attachments, then initial state is loading attachment composer`() = reducerWithInitialState(listOf(A_MESSAGE_ATTACHMENT)) {
|
||||||
|
assertInitialState(
|
||||||
|
MessengerScreenState(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
roomState = Lce.Loading(),
|
||||||
|
composerState = ComposerState.Attachments(listOf(A_MESSAGE_ATTACHMENT), reply = null),
|
||||||
|
viewerState = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given messages emits state, when Visible, then dispatches content`() = runReducerTest {
|
||||||
|
fakeJobBag.instance.expect { it.add("messages", any()) }
|
||||||
|
fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(READ_RECEIPTS_ARE_DISABLED)
|
||||||
|
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
||||||
|
fakeChatEngine.givenMessages(A_ROOM_ID, READ_RECEIPTS_ARE_DISABLED).returns(flowOf(state))
|
||||||
|
|
||||||
|
reduce(ComponentLifecycle.Visible)
|
||||||
|
|
||||||
|
assertOnlyDispatches(listOf(MessagesStateChange.Content(state)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when Gone, then cancels sync job`() = runReducerTest {
|
||||||
|
fakeJobBag.instance.expect { it.cancel("messages") }
|
||||||
|
|
||||||
|
reduce(ComponentLifecycle.Gone)
|
||||||
|
|
||||||
|
assertNoChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when Content StateChange, then updates room state`() = runReducerTest {
|
||||||
|
reduce(MessagesStateChange.Content(A_MESSENGER_PAGE_STATE))
|
||||||
|
|
||||||
|
assertOnlyStateChange { previous ->
|
||||||
|
previous.copy(roomState = Lce.Content(A_MESSENGER_PAGE_STATE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when SelectAttachmentToSend, then updates composer state`() = runReducerTest {
|
||||||
|
reduce(ComposerStateChange.SelectAttachmentToSend(A_MESSAGE_ATTACHMENT))
|
||||||
|
|
||||||
|
assertOnlyStateChange { previous ->
|
||||||
|
previous.copy(composerState = ComposerState.Attachments(listOf(A_MESSAGE_ATTACHMENT), reply = null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when Show ImagePreview, then updates viewer state`() = runReducerTest {
|
||||||
|
reduce(ComposerStateChange.ImagePreview.Show(AN_IMAGE_BUBBLE))
|
||||||
|
|
||||||
|
assertOnlyStateChange { previous ->
|
||||||
|
previous.copy(viewerState = ViewerState(AN_IMAGE_BUBBLE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when Hide ImagePreview, then updates viewer state`() = runReducerTest {
|
||||||
|
reduce(ComposerStateChange.ImagePreview.Hide)
|
||||||
|
|
||||||
|
assertOnlyStateChange { previous ->
|
||||||
|
previous.copy(viewerState = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when TextUpdate StateChange, then updates composer state`() = runReducerTest {
|
||||||
|
reduce(ComposerStateChange.TextUpdate(A_MESSAGE_CONTENT))
|
||||||
|
|
||||||
|
assertOnlyStateChange { previous ->
|
||||||
|
previous.copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when Clear ComposerStateChange, then clear composer state`() = runReducerTest {
|
||||||
|
setState { it.copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = null)) }
|
||||||
|
|
||||||
|
reduce(ComposerStateChange.Clear)
|
||||||
|
|
||||||
|
assertOnlyStateChange { previous ->
|
||||||
|
previous.copy(composerState = ComposerState.Text(value = "", reply = null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given text composer, when Enter ReplyMode, then updates composer state with reply`() = runReducerTest {
|
||||||
|
setState { it.copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = null)) }
|
||||||
|
|
||||||
|
reduce(ComposerStateChange.ReplyMode.Enter(A_REPLY))
|
||||||
|
|
||||||
|
assertOnlyStateChange { previous ->
|
||||||
|
previous.copy(composerState = (previous.composerState as ComposerState.Text).copy(reply = A_REPLY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given text composer, when Exit ReplyMode, then updates composer state`() = runReducerTest {
|
||||||
|
setState { it.copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = A_REPLY)) }
|
||||||
|
|
||||||
|
reduce(ComposerStateChange.ReplyMode.Exit)
|
||||||
|
|
||||||
|
assertOnlyStateChange { previous ->
|
||||||
|
previous.copy(composerState = (previous.composerState as ComposerState.Text).copy(reply = null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given attachment composer, when Enter ReplyMode, then updates composer state with reply`() = runReducerTest {
|
||||||
|
setState { it.copy(composerState = ComposerState.Attachments(listOf(A_MESSAGE_ATTACHMENT), reply = null)) }
|
||||||
|
|
||||||
|
reduce(ComposerStateChange.ReplyMode.Enter(A_REPLY))
|
||||||
|
|
||||||
|
assertOnlyStateChange { previous ->
|
||||||
|
previous.copy(composerState = (previous.composerState as ComposerState.Attachments).copy(reply = A_REPLY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given attachment composer, when Exit ReplyMode, then updates composer state`() = runReducerTest {
|
||||||
|
setState { it.copy(composerState = ComposerState.Attachments(listOf(A_MESSAGE_ATTACHMENT), reply = A_REPLY)) }
|
||||||
|
|
||||||
|
reduce(ComposerStateChange.ReplyMode.Exit)
|
||||||
|
|
||||||
|
assertOnlyStateChange { previous ->
|
||||||
|
previous.copy(composerState = (previous.composerState as ComposerState.Attachments).copy(reply = null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when OpenGalleryPicker, then emits event`() = runReducerTest {
|
||||||
|
reduce(ScreenAction.OpenGalleryPicker)
|
||||||
|
|
||||||
|
assertOnlyEvents(listOf(MessengerEvent.SelectImageAttachment))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given android api is lower than S_v2 and has text content, when CopyToClipboard, then copies to system and toasts`() = runReducerTest {
|
||||||
|
fakeDeviceMeta.givenApiVersion().returns(Build.VERSION_CODES.S)
|
||||||
|
fakeCopyToClipboard.instance.expect { it.copy(CopyToClipboard.Copyable.Text(A_MESSAGE_CONTENT)) }
|
||||||
|
|
||||||
|
reduce(ScreenAction.CopyToClipboard(A_TEXT_BUBBLE))
|
||||||
|
|
||||||
|
assertEvents(listOf(MessengerEvent.Toast("Copied to clipboard")))
|
||||||
|
assertNoDispatches()
|
||||||
|
assertNoStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given android api is higher than S_v2 and has text content, when CopyToClipboard, then copies to system and does not toast`() = runReducerTest {
|
||||||
|
fakeDeviceMeta.givenApiVersion().returns(Build.VERSION_CODES.TIRAMISU)
|
||||||
|
fakeCopyToClipboard.instance.expect { it.copy(CopyToClipboard.Copyable.Text(A_MESSAGE_CONTENT)) }
|
||||||
|
|
||||||
|
reduce(ScreenAction.CopyToClipboard(A_TEXT_BUBBLE))
|
||||||
|
|
||||||
|
assertNoChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given image content, when CopyToClipboard, then toasts nothing to copy`() = runReducerTest {
|
||||||
|
reduce(ScreenAction.CopyToClipboard(AN_IMAGE_BUBBLE))
|
||||||
|
|
||||||
|
assertEvents(listOf(MessengerEvent.Toast("Nothing to copy")))
|
||||||
|
assertNoDispatches()
|
||||||
|
assertNoStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given text composer, when SendMessage, then clear composer and sends text message`() = runReducerTest {
|
||||||
|
setState { it.copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = null), roomState = Lce.Content(A_MESSENGER_PAGE_STATE)) }
|
||||||
|
fakeChatEngine.expectUnit { it.send(expectTextMessage(A_MESSAGE_CONTENT), A_MESSENGER_PAGE_STATE.roomState.roomOverview) }
|
||||||
|
|
||||||
|
reduce(ScreenAction.SendMessage)
|
||||||
|
|
||||||
|
assertDispatches(listOf(ComposerStateChange.Clear))
|
||||||
|
assertNoEvents()
|
||||||
|
assertNoStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given text composer with reply, when SendMessage, then clear composer and sends text message`() = runReducerTest {
|
||||||
|
setState { it.copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = A_REPLY.message), roomState = Lce.Content(A_MESSENGER_PAGE_STATE)) }
|
||||||
|
fakeChatEngine.expectUnit { it.send(expectTextMessage(A_MESSAGE_CONTENT, reply = A_REPLY.message), A_MESSENGER_PAGE_STATE.roomState.roomOverview) }
|
||||||
|
|
||||||
|
reduce(ScreenAction.SendMessage)
|
||||||
|
|
||||||
|
assertDispatches(listOf(ComposerStateChange.Clear))
|
||||||
|
assertNoEvents()
|
||||||
|
assertNoStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given attachment composer, when SendMessage, then clear composer and sends image message`() = runReducerTest {
|
||||||
|
setState {
|
||||||
|
it.copy(
|
||||||
|
composerState = ComposerState.Attachments(listOf(A_MESSAGE_ATTACHMENT), reply = null),
|
||||||
|
roomState = Lce.Content(A_MESSENGER_PAGE_STATE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fakeChatEngine.expectUnit { it.send(expectImageMessage(A_MESSAGE_ATTACHMENT.uri), A_MESSENGER_PAGE_STATE.roomState.roomOverview) }
|
||||||
|
|
||||||
|
reduce(ScreenAction.SendMessage)
|
||||||
|
|
||||||
|
assertDispatches(listOf(ComposerStateChange.Clear))
|
||||||
|
assertNoEvents()
|
||||||
|
assertNoStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expectTextMessage(messageContent: String, reply: RoomEvent? = null): SendMessage.TextMessage {
|
||||||
|
return SendMessage.TextMessage(messageContent, reply = reply?.toSendMessageReply())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expectImageMessage(uri: AndroidUri): SendMessage.ImageMessage {
|
||||||
|
return SendMessage.ImageMessage(uri.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomEvent.toSendMessageReply() = SendMessage.TextMessage.Reply(
|
||||||
|
author = this.author,
|
||||||
|
originalMessage = when (this) {
|
||||||
|
is RoomEvent.Image -> TODO()
|
||||||
|
is RoomEvent.Reply -> TODO()
|
||||||
|
is RoomEvent.Message -> this.content.asString()
|
||||||
|
is RoomEvent.Encrypted -> error("Should never happen")
|
||||||
|
},
|
||||||
|
eventId = this.eventId,
|
||||||
|
timestampUtc = this.utcTimestamp,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun reducerWithInitialState(
|
||||||
|
initialAttachments: List<MessageAttachment>?,
|
||||||
|
block: suspend ReducerTestScope<MessengerScreenState, MessengerEvent>.() -> Unit
|
||||||
|
) = testReducer { fakeEventSource ->
|
||||||
|
messengerReducer(
|
||||||
|
fakeJobBag.instance,
|
||||||
|
fakeChatEngine,
|
||||||
|
fakeCopyToClipboard.instance,
|
||||||
|
fakeDeviceMeta.instance,
|
||||||
|
fakeMessageOptionsStore.instance,
|
||||||
|
A_ROOM_ID,
|
||||||
|
initialAttachments = initialAttachments,
|
||||||
|
fakeEventSource,
|
||||||
|
)
|
||||||
|
}(block)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aMessengerStateWithEvent(eventId: EventId, selfId: UserId) = aRoomStateWithEventId(eventId).toMessengerState(selfId)
|
||||||
|
|
||||||
|
private fun aRoomStateWithEventId(eventId: EventId): RoomState {
|
||||||
|
val element = anEncryptedRoomMessageEvent(eventId = eventId, utcTimestamp = 1)
|
||||||
|
return RoomState(aRoomOverview(roomId = A_ROOM_ID, isEncrypted = true), listOf(element))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomState.toMessengerState(selfId: UserId) = aMessengerState(self = selfId, roomState = this)
|
||||||
|
|
||||||
|
class FakeCopyToClipboard {
|
||||||
|
val instance = mockk<CopyToClipboard>()
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeJobBag {
|
||||||
|
val instance = mockk<JobBag>()
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeDeviceMeta {
|
||||||
|
val instance = mockk<DeviceMeta>()
|
||||||
|
|
||||||
|
fun givenApiVersion() = every { instance.apiVersion }.delegateReturn()
|
||||||
|
}
|
|
@ -1,126 +0,0 @@
|
||||||
package app.dapk.st.messenger
|
|
||||||
|
|
||||||
import ViewModelTest
|
|
||||||
import app.dapk.st.core.DeviceMeta
|
|
||||||
import app.dapk.st.core.Lce
|
|
||||||
import app.dapk.st.core.extensions.takeIfContent
|
|
||||||
import app.dapk.st.engine.MessengerPageState
|
|
||||||
import app.dapk.st.engine.RoomState
|
|
||||||
import app.dapk.st.engine.SendMessage
|
|
||||||
import app.dapk.st.matrix.common.EventId
|
|
||||||
import app.dapk.st.matrix.common.RoomId
|
|
||||||
import app.dapk.st.matrix.common.UserId
|
|
||||||
import app.dapk.st.messenger.state.ComposerState
|
|
||||||
import app.dapk.st.messenger.state.MessengerScreenState
|
|
||||||
import fake.FakeChatEngine
|
|
||||||
import fake.FakeMessageOptionsStore
|
|
||||||
import fixture.*
|
|
||||||
import io.mockk.mockk
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
private const val READ_RECEIPTS_ARE_DISABLED = true
|
|
||||||
private val A_ROOM_ID = aRoomId("messenger state room id")
|
|
||||||
private const val A_MESSAGE_CONTENT = "message content"
|
|
||||||
private val AN_EVENT_ID = anEventId("state event")
|
|
||||||
private val A_SELF_ID = aUserId("self")
|
|
||||||
|
|
||||||
class MessengerViewModelTest {
|
|
||||||
|
|
||||||
private val runViewModelTest = ViewModelTest()
|
|
||||||
|
|
||||||
private val fakeMessageOptionsStore = FakeMessageOptionsStore()
|
|
||||||
private val fakeChatEngine = FakeChatEngine()
|
|
||||||
private val fakeCopyToClipboard = FakeCopyToClipboard()
|
|
||||||
private val deviceMeta = DeviceMeta(26)
|
|
||||||
|
|
||||||
private val viewModel = MessengerViewModel(
|
|
||||||
fakeChatEngine,
|
|
||||||
fakeMessageOptionsStore.instance,
|
|
||||||
fakeCopyToClipboard.instance,
|
|
||||||
deviceMeta,
|
|
||||||
factory = runViewModelTest.testMutableStateFactory(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when creating view model, then initial state is loading room state`() = runViewModelTest {
|
|
||||||
viewModel.test()
|
|
||||||
|
|
||||||
assertInitialState(
|
|
||||||
MessengerScreenState(
|
|
||||||
roomId = null,
|
|
||||||
roomState = Lce.Loading(),
|
|
||||||
composerState = ComposerState.Text(value = "", reply = null),
|
|
||||||
viewerState = null,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `given timeline emits state, when starting, then updates state and marks room and events as read`() = runViewModelTest {
|
|
||||||
fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(READ_RECEIPTS_ARE_DISABLED)
|
|
||||||
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
|
||||||
fakeChatEngine.givenMessages(A_ROOM_ID, READ_RECEIPTS_ARE_DISABLED).returns(flowOf(state))
|
|
||||||
|
|
||||||
viewModel.test().post(MessengerAction.OnMessengerVisible(A_ROOM_ID, attachments = null))
|
|
||||||
|
|
||||||
assertStates<MessengerScreenState>(
|
|
||||||
{ copy(roomId = A_ROOM_ID) },
|
|
||||||
{ copy(roomState = Lce.Content(state)) },
|
|
||||||
)
|
|
||||||
verifyExpects()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when posting composer update, then updates state`() = runViewModelTest {
|
|
||||||
viewModel.test().post(MessengerAction.ComposerTextUpdate(A_MESSAGE_CONTENT))
|
|
||||||
|
|
||||||
assertStates<MessengerScreenState>({
|
|
||||||
copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = null))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `given composer message state when posting send text, then resets composer state and sends message`() = runViewModelTest {
|
|
||||||
val initialState = initialStateWithComposerMessage(A_ROOM_ID, A_MESSAGE_CONTENT)
|
|
||||||
fakeChatEngine.expectUnit { it.send(expectTextMessage(A_MESSAGE_CONTENT), initialState.roomState.takeIfContent()!!.roomState.roomOverview) }
|
|
||||||
|
|
||||||
viewModel.test(initialState = initialState).post(MessengerAction.ComposerSendText)
|
|
||||||
|
|
||||||
assertStates<MessengerScreenState>({ copy(composerState = ComposerState.Text("", reply = null)) })
|
|
||||||
verifyExpects()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initialStateWithComposerMessage(roomId: RoomId, messageContent: String): MessengerScreenState {
|
|
||||||
val roomState = RoomState(
|
|
||||||
aRoomOverview(roomId = roomId, isEncrypted = true),
|
|
||||||
listOf(anEncryptedRoomMessageEvent(utcTimestamp = 1))
|
|
||||||
)
|
|
||||||
return aMessageScreenState(roomId, aMessengerState(roomState = roomState), messageContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun expectTextMessage(messageContent: String): SendMessage.TextMessage {
|
|
||||||
return SendMessage.TextMessage(messageContent, reply = null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun aMessengerStateWithEvent(eventId: EventId, selfId: UserId) = aRoomStateWithEventId(eventId).toMessengerState(selfId)
|
|
||||||
|
|
||||||
private fun RoomState.toMessengerState(selfId: UserId) = aMessengerState(self = selfId, roomState = this)
|
|
||||||
|
|
||||||
private fun aRoomStateWithEventId(eventId: EventId): RoomState {
|
|
||||||
val element = anEncryptedRoomMessageEvent(eventId = eventId, utcTimestamp = 1)
|
|
||||||
return RoomState(aRoomOverview(roomId = A_ROOM_ID, isEncrypted = true), listOf(element))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun aMessageScreenState(roomId: RoomId = aRoomId(), roomState: MessengerPageState, messageContent: String?) = MessengerScreenState(
|
|
||||||
roomId = roomId,
|
|
||||||
roomState = Lce.Content(roomState),
|
|
||||||
composerState = ComposerState.Text(value = messageContent ?: "", reply = null),
|
|
||||||
viewerState = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
class FakeCopyToClipboard {
|
|
||||||
val instance = mockk<CopyToClipboard>()
|
|
||||||
}
|
|
Loading…
Reference in New Issue