mirror of
https://github.com/ouchadam/small-talk.git
synced 2024-12-25 01:02:11 +01:00
adding tests around the messaging view model
This commit is contained in:
parent
ccecfb08e0
commit
fcfabcda27
@ -71,7 +71,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
||||
|
||||
private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db")
|
||||
private val database = DapkDb(driver)
|
||||
|
||||
private val clock = Clock.systemUTC()
|
||||
private val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
|
||||
|
||||
val storeModule = unsafeLazy {
|
||||
@ -116,6 +116,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
||||
context,
|
||||
buildMeta,
|
||||
coroutineDispatchers,
|
||||
clock,
|
||||
)
|
||||
}
|
||||
|
||||
@ -128,6 +129,7 @@ internal class FeatureModules internal constructor(
|
||||
context: Context,
|
||||
buildMeta: BuildMeta,
|
||||
coroutineDispatchers: CoroutineDispatchers,
|
||||
clock: Clock,
|
||||
) {
|
||||
|
||||
val directoryModule by unsafeLazy {
|
||||
@ -155,6 +157,7 @@ internal class FeatureModules internal constructor(
|
||||
matrixModules.room,
|
||||
storeModule.value.credentialsStore(),
|
||||
storeModule.value.roomStore(),
|
||||
clock
|
||||
)
|
||||
}
|
||||
val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile) }
|
||||
|
@ -3,7 +3,7 @@ package test
|
||||
import io.mockk.MockKMatcherScope
|
||||
import io.mockk.MockKVerificationScope
|
||||
import io.mockk.coJustRun
|
||||
import io.mockk.coVerifyAll
|
||||
import io.mockk.coVerify
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@ -15,13 +15,15 @@ fun runExpectTest(testBody: suspend ExpectTestScope.() -> Unit) {
|
||||
|
||||
class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestScope {
|
||||
|
||||
private val expects = mutableListOf<suspend MockKVerificationScope.() -> Unit>()
|
||||
private val expects = mutableListOf<Pair<Int, suspend MockKVerificationScope.() -> Unit>>()
|
||||
|
||||
override fun verifyExpects() = coVerifyAll { expects.forEach { it.invoke(this@coVerifyAll) } }
|
||||
override fun verifyExpects() = expects.forEach { (times, block) ->
|
||||
coVerify(exactly = times) { block.invoke(this) }
|
||||
}
|
||||
|
||||
override fun <T> T.expectUnit(block: suspend MockKMatcherScope.(T) -> Unit) {
|
||||
override fun <T> T.expectUnit(times: Int, block: suspend MockKMatcherScope.(T) -> Unit) {
|
||||
coJustRun { block(this@expectUnit) }.ignore()
|
||||
expects.add { block(this@expectUnit) }
|
||||
expects.add(times to { block(this@expectUnit) })
|
||||
}
|
||||
|
||||
}
|
||||
@ -30,5 +32,5 @@ private fun Any.ignore() = Unit
|
||||
|
||||
interface ExpectTestScope : CoroutineScope {
|
||||
fun verifyExpects()
|
||||
fun <T> T.expectUnit(block: suspend MockKMatcherScope.(T) -> Unit)
|
||||
fun <T> T.expectUnit(times: Int = 1, block: suspend MockKMatcherScope.(T) -> Unit)
|
||||
}
|
@ -11,4 +11,13 @@ dependencies {
|
||||
implementation project(":features:navigator")
|
||||
implementation project(":design-library")
|
||||
implementation("io.coil-kt:coil-compose:1.4.0")
|
||||
|
||||
kotlinTest(it)
|
||||
|
||||
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
|
||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package app.dapk.st.messenger
|
||||
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.sync.MessageMeta
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
|
||||
class LocalEchoMapper {
|
||||
|
||||
fun MessageService.LocalEcho.toMessage(message: MessageService.Message.TextMessage, member: RoomMember): RoomEvent.Message {
|
||||
return RoomEvent.Message(
|
||||
eventId = this.eventId ?: EventId(this.localId),
|
||||
content = message.content.body,
|
||||
author = member,
|
||||
utcTimestamp = message.timestampUtc,
|
||||
meta = this.toMeta()
|
||||
)
|
||||
}
|
||||
|
||||
fun RoomEvent.mergeWith(echo: MessageService.LocalEcho) = when (this) {
|
||||
is RoomEvent.Message -> this.copy(meta = echo.toMeta())
|
||||
is RoomEvent.Reply -> this.copy(message = this.message.copy(meta = echo.toMeta()))
|
||||
}
|
||||
|
||||
private fun MessageService.LocalEcho.toMeta() = MessageMeta.LocalEcho(
|
||||
echoId = this.localId,
|
||||
state = when (val localEchoState = this.state) {
|
||||
MessageService.LocalEcho.State.Sending -> MessageMeta.LocalEcho.State.Sending
|
||||
MessageService.LocalEcho.State.Sent -> MessageMeta.LocalEcho.State.Sent
|
||||
is MessageService.LocalEcho.State.Error -> MessageMeta.LocalEcho.State.Error(
|
||||
localEchoState.message,
|
||||
type = MessageMeta.LocalEcho.State.Error.Type.UNKNOWN,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package app.dapk.st.messenger
|
||||
|
||||
import java.util.*
|
||||
|
||||
internal class LocalIdFactory {
|
||||
fun create() = "local.${UUID.randomUUID()}"
|
||||
}
|
@ -3,63 +3,50 @@ package app.dapk.st.messenger
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.sync.MessageMeta
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
import app.dapk.st.matrix.sync.RoomState
|
||||
|
||||
internal typealias MergeWithLocalEchosUseCase = (RoomState, RoomMember, List<MessageService.LocalEcho>) -> RoomState
|
||||
|
||||
internal class MergeWithLocalEchosUseCaseImpl : MergeWithLocalEchosUseCase {
|
||||
internal class MergeWithLocalEchosUseCaseImpl(
|
||||
private val localEventMapper: LocalEchoMapper,
|
||||
) : MergeWithLocalEchosUseCase {
|
||||
|
||||
override fun invoke(roomState: RoomState, member: RoomMember, echos: List<MessageService.LocalEcho>): RoomState {
|
||||
val echosByEventId = echos.associateBy { it.eventId }
|
||||
val stateByEventId = roomState.events.associateBy { it.eventId }
|
||||
|
||||
val uniqueEchos = echos.filter { echo ->
|
||||
echo.eventId == null || stateByEventId[echo.eventId] == null
|
||||
}.map { localEcho ->
|
||||
when (val message = localEcho.message) {
|
||||
is MessageService.Message.TextMessage -> {
|
||||
createMessage(localEcho, message, member)
|
||||
}
|
||||
}
|
||||
}
|
||||
val uniqueEchos = uniqueEchos(echos, stateByEventId, member)
|
||||
val existingWithEcho = updateExistingEventsWithLocalEchoMeta(roomState, echosByEventId)
|
||||
|
||||
val existingWithEcho = roomState.events.map {
|
||||
when (val echo = echosByEventId[it.eventId]) {
|
||||
null -> it
|
||||
else -> when (it) {
|
||||
is RoomEvent.Message -> it.copy(
|
||||
meta = echo.toMeta()
|
||||
)
|
||||
is RoomEvent.Reply -> it.copy(message = it.message.copy(meta = echo.toMeta()))
|
||||
}
|
||||
}
|
||||
}
|
||||
val sortedEvents = (existingWithEcho + uniqueEchos)
|
||||
.sortedByDescending { if (it is RoomEvent.Message) it.utcTimestamp else null }
|
||||
.distinctBy { it.eventId }
|
||||
return roomState.copy(events = sortedEvents)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun createMessage(localEcho: MessageService.LocalEcho, message: MessageService.Message.TextMessage, member: RoomMember) = RoomEvent.Message(
|
||||
eventId = localEcho.eventId ?: EventId(localEcho.localId),
|
||||
content = message.content.body,
|
||||
author = member,
|
||||
utcTimestamp = message.timestampUtc,
|
||||
meta = localEcho.toMeta()
|
||||
)
|
||||
|
||||
private fun MessageService.LocalEcho.toMeta() = MessageMeta.LocalEcho(
|
||||
echoId = this.localId,
|
||||
state = when (val localEchoState = this.state) {
|
||||
MessageService.LocalEcho.State.Sending -> MessageMeta.LocalEcho.State.Sending
|
||||
MessageService.LocalEcho.State.Sent -> MessageMeta.LocalEcho.State.Sent
|
||||
is MessageService.LocalEcho.State.Error -> MessageMeta.LocalEcho.State.Error(
|
||||
localEchoState.message,
|
||||
type = MessageMeta.LocalEcho.State.Error.Type.UNKNOWN,
|
||||
)
|
||||
private fun uniqueEchos(echos: List<MessageService.LocalEcho>, stateByEventId: Map<EventId, RoomEvent>, member: RoomMember): List<RoomEvent.Message> {
|
||||
return with(localEventMapper) {
|
||||
echos
|
||||
.filter { echo -> echo.eventId == null || stateByEventId[echo.eventId] == null }
|
||||
.map { localEcho ->
|
||||
when (val message = localEcho.message) {
|
||||
is MessageService.Message.TextMessage -> {
|
||||
localEcho.toMessage(message, member)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
private fun updateExistingEventsWithLocalEchoMeta(roomState: RoomState, echosByEventId: Map<EventId?, MessageService.LocalEcho>): List<RoomEvent> {
|
||||
return with(localEventMapper) {
|
||||
roomState.events.map { roomEvent ->
|
||||
when (val echo = echosByEventId[roomEvent.eventId]) {
|
||||
null -> roomEvent
|
||||
else -> roomEvent.mergeWith(echo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.room.RoomService
|
||||
import app.dapk.st.matrix.sync.RoomStore
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import java.time.Clock
|
||||
|
||||
class MessengerModule(
|
||||
private val syncService: SyncService,
|
||||
@ -13,11 +14,12 @@ class MessengerModule(
|
||||
private val roomService: RoomService,
|
||||
private val credentialsStore: CredentialsStore,
|
||||
private val roomStore: RoomStore,
|
||||
private val clock: Clock,
|
||||
) : ProvidableModule {
|
||||
|
||||
internal fun messengerViewModel(): MessengerViewModel {
|
||||
return MessengerViewModel(messageService, roomService, roomStore, credentialsStore, timelineUseCase())
|
||||
return MessengerViewModel(messageService, roomService, roomStore, credentialsStore, timelineUseCase(), LocalIdFactory(), clock)
|
||||
}
|
||||
|
||||
private fun timelineUseCase() = TimelineUseCase(syncService, messageService, roomService, MergeWithLocalEchosUseCaseImpl())
|
||||
private fun timelineUseCase() = TimelineUseCaseImpl(syncService, messageService, roomService, MergeWithLocalEchosUseCaseImpl(LocalEchoMapper()))
|
||||
}
|
@ -12,23 +12,30 @@ import app.dapk.st.matrix.room.RoomService
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
import app.dapk.st.matrix.sync.RoomStore
|
||||
import app.dapk.st.viewmodel.DapkViewModel
|
||||
import app.dapk.st.viewmodel.MutableStateFactory
|
||||
import app.dapk.st.viewmodel.defaultStateFactory
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.time.Clock
|
||||
|
||||
internal class MessengerViewModel(
|
||||
private val messageService: MessageService,
|
||||
private val roomService: RoomService,
|
||||
private val roomStore: RoomStore,
|
||||
private val credentialsStore: CredentialsStore,
|
||||
private val useCase: TimelineUseCase,
|
||||
private val observeTimeline: ObserveTimelineUseCase,
|
||||
private val localIdFactory: LocalIdFactory,
|
||||
private val clock: Clock,
|
||||
factory: MutableStateFactory<MessengerScreenState> = defaultStateFactory(),
|
||||
) : DapkViewModel<MessengerScreenState, MessengerEvent>(
|
||||
initialState = MessengerScreenState(
|
||||
roomId = null,
|
||||
roomState = Lce.Loading(),
|
||||
composerState = ComposerState.Text(value = "")
|
||||
)
|
||||
),
|
||||
factory = factory,
|
||||
) {
|
||||
|
||||
private var syncJob: Job? = null
|
||||
@ -49,7 +56,7 @@ internal class MessengerViewModel(
|
||||
|
||||
val credentials = credentialsStore.credentials()!!
|
||||
var lastKnownReadEvent: EventId? = null
|
||||
useCase.state(action.roomId, credentials.userId).distinctUntilChanged().onEach { state ->
|
||||
observeTimeline.invoke(action.roomId, credentials.userId).distinctUntilChanged().onEach { state ->
|
||||
state.lastestMessageEventFromOthers(self = credentials.userId)?.let {
|
||||
if (lastKnownReadEvent != it) {
|
||||
updateRoomReadStateAsync(latestReadEvent = it, state)
|
||||
@ -82,6 +89,8 @@ internal class MessengerViewModel(
|
||||
MessageService.Message.Content.TextContent(body = copy.value),
|
||||
roomId = roomState.roomOverview.roomId,
|
||||
sendEncrypted = roomState.roomOverview.isEncrypted,
|
||||
localId = localIdFactory.create(),
|
||||
timestampUtc = clock.millis(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -10,20 +10,16 @@ import app.dapk.st.matrix.sync.SyncService
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
data class MessengerState(
|
||||
val self: UserId,
|
||||
val roomState: RoomState,
|
||||
val typing: SyncService.SyncEvent.Typing?
|
||||
)
|
||||
internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow<MessengerState>
|
||||
|
||||
internal class TimelineUseCase(
|
||||
internal class TimelineUseCaseImpl(
|
||||
private val syncService: SyncService,
|
||||
private val messageService: MessageService,
|
||||
private val roomService: RoomService,
|
||||
private val mergeWithLocalEchosUseCase: MergeWithLocalEchosUseCase
|
||||
) {
|
||||
) : ObserveTimelineUseCase {
|
||||
|
||||
suspend fun state(roomId: RoomId, userId: UserId): Flow<MessengerState> {
|
||||
override fun invoke(roomId: RoomId, userId: UserId): Flow<MessengerState> {
|
||||
return combine(
|
||||
syncService.startSyncing(),
|
||||
syncService.room(roomId),
|
||||
@ -50,3 +46,9 @@ internal class TimelineUseCase(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class MessengerState(
|
||||
val self: UserId,
|
||||
val roomState: RoomState,
|
||||
val typing: SyncService.SyncEvent.Typing?
|
||||
)
|
||||
|
@ -0,0 +1,10 @@
|
||||
package app.dapk.st.messenger
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import test.delegateReturn
|
||||
|
||||
internal class FakeLocalIdFactory {
|
||||
val instance = mockk<LocalIdFactory>()
|
||||
fun givenCreate() = every { instance.create() }.delegateReturn()
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package app.dapk.st.messenger
|
||||
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.MessageType
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import fixture.*
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
|
||||
private val A_ROOM_MESSAGE_EVENT = aRoomMessageEvent(eventId = anEventId("1"))
|
||||
private val A_LOCAL_ECHO_EVENT_ID = anEventId("2")
|
||||
private const val A_LOCAL_ECHO_BODY = "body"
|
||||
private val A_ROOM_MEMBER = aRoomMember()
|
||||
private val ANOTHER_ROOM_MESSAGE_EVENT = A_ROOM_MESSAGE_EVENT.copy(eventId = anEventId("a second room event"))
|
||||
|
||||
class MergeWithLocalEchosUseCaseTest {
|
||||
|
||||
private val fakeLocalEchoMapper = FakeLocalEventMapper()
|
||||
private val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(fakeLocalEchoMapper.instance)
|
||||
|
||||
@Test
|
||||
fun `given no local echos, when merging, then returns original state`() {
|
||||
val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT))
|
||||
|
||||
val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, emptyList())
|
||||
|
||||
result shouldBeEqualTo roomState
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given local echo with sending state, when merging then maps to room event with local echo state`() {
|
||||
val second = createLocalEcho(A_LOCAL_ECHO_EVENT_ID, A_LOCAL_ECHO_BODY, state = MessageService.LocalEcho.State.Sending)
|
||||
fakeLocalEchoMapper.givenMapping(second, aTextMessage(aTextContent(A_LOCAL_ECHO_BODY)), A_ROOM_MEMBER).returns(ANOTHER_ROOM_MESSAGE_EVENT)
|
||||
val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT))
|
||||
|
||||
val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, listOf(second))
|
||||
|
||||
result shouldBeEqualTo roomState.copy(
|
||||
events = listOf(
|
||||
A_ROOM_MESSAGE_EVENT,
|
||||
ANOTHER_ROOM_MESSAGE_EVENT,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createLocalEcho(eventId: EventId, body: String, state: MessageService.LocalEcho.State) = aLocalEcho(
|
||||
eventId,
|
||||
aTextMessage(aTextContent(body)),
|
||||
state,
|
||||
)
|
||||
}
|
||||
|
||||
fun aLocalEcho(
|
||||
eventId: EventId? = anEventId(),
|
||||
message: MessageService.Message = aTextMessage(),
|
||||
state: MessageService.LocalEcho.State = MessageService.LocalEcho.State.Sending,
|
||||
) = MessageService.LocalEcho(eventId, message, state)
|
||||
|
||||
|
||||
fun aTextMessage(
|
||||
content: MessageService.Message.Content.TextContent = aTextContent(),
|
||||
sendEncrypted: Boolean = false,
|
||||
roomId: RoomId = aRoomId(),
|
||||
localId: String = "a-local-id",
|
||||
timestampUtc: Long = 0,
|
||||
) = MessageService.Message.TextMessage(content, sendEncrypted, roomId, localId, timestampUtc)
|
||||
|
||||
fun aTextContent(
|
||||
body: String = "text content body",
|
||||
type: String = MessageType.TEXT.value,
|
||||
) = MessageService.Message.Content.TextContent(body, type)
|
||||
|
||||
class FakeLocalEventMapper {
|
||||
val instance = mockk<LocalEchoMapper>()
|
||||
fun givenMapping(echo: MessageService.LocalEcho, event: MessageService.Message.TextMessage, roomMember: RoomMember) = every {
|
||||
with(instance) { echo.toMessage(event, roomMember) }
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package app.dapk.st.messenger
|
||||
|
||||
import ViewModelTest
|
||||
import app.dapk.st.core.Lce
|
||||
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.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.room.RoomService
|
||||
import app.dapk.st.matrix.sync.RoomState
|
||||
import fake.FakeCredentialsStore
|
||||
import fake.FakeRoomStore
|
||||
import fixture.*
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.junit.Test
|
||||
import test.delegateReturn
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
private const val A_CURRENT_TIMESTAMP = 10000L
|
||||
private val A_ROOM_ID = aRoomId("messenger state room id")
|
||||
private const val A_MESSAGE_CONTENT = "message content"
|
||||
private const val A_LOCAL_ID = "local.1111-2222-3333"
|
||||
private val AN_EVENT_ID = anEventId("state event")
|
||||
private val A_SELF_ID = aUserId("self")
|
||||
|
||||
class MessengerViewModelTest {
|
||||
|
||||
private val runViewModelTest = ViewModelTest()
|
||||
|
||||
private val fakeMessageService = FakeMessageService()
|
||||
private val fakeRoomService = FakeRoomService()
|
||||
private val fakeRoomStore = FakeRoomStore()
|
||||
private val fakeCredentialsStore = FakeCredentialsStore().also { it.givenCredentials().returns(aUserCredentials(userId = A_SELF_ID)) }
|
||||
private val fakeObserveTimelineUseCase = FakeObserveTimelineUseCase()
|
||||
|
||||
private val viewModel = MessengerViewModel(
|
||||
fakeMessageService,
|
||||
fakeRoomService,
|
||||
fakeRoomStore,
|
||||
fakeCredentialsStore,
|
||||
fakeObserveTimelineUseCase,
|
||||
localIdFactory = FakeLocalIdFactory().also { it.givenCreate().returns(A_LOCAL_ID) }.instance,
|
||||
clock = fixedClock(A_CURRENT_TIMESTAMP),
|
||||
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 = "")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given timeline emits state, when starting, then updates state and marks room and events as read`() = runViewModelTest {
|
||||
fakeRoomStore.expectUnit(times = 2) { it.markRead(A_ROOM_ID) }
|
||||
fakeRoomService.expectUnit { it.markFullyRead(A_ROOM_ID, AN_EVENT_ID) }
|
||||
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
||||
fakeObserveTimelineUseCase.given(A_ROOM_ID, A_SELF_ID).returns(flowOf(state))
|
||||
|
||||
viewModel.test().post(MessengerAction.OnMessengerVisible(A_ROOM_ID))
|
||||
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given composer message state when posting send text, then resets composer state and sends message`() = runViewModelTest {
|
||||
fakeMessageService.expectUnit { it.scheduleMessage(expectEncryptedMessage(A_ROOM_ID, A_LOCAL_ID, A_CURRENT_TIMESTAMP, A_MESSAGE_CONTENT)) }
|
||||
|
||||
viewModel.test(initialState = initialStateWithComposerMessage(A_ROOM_ID, A_MESSAGE_CONTENT)).post(MessengerAction.ComposerSendText)
|
||||
|
||||
assertStates<MessengerScreenState>({ copy(composerState = ComposerState.Text("")) })
|
||||
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 expectEncryptedMessage(roomId: RoomId, localId: String, timestamp: Long, messageContent: String): MessageService.Message.TextMessage {
|
||||
val content = MessageService.Message.Content.TextContent(body = messageContent)
|
||||
return MessageService.Message.TextMessage(content, sendEncrypted = true, roomId, localId, timestamp)
|
||||
}
|
||||
|
||||
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: MessengerState, messageContent: String?) = MessengerScreenState(
|
||||
roomId = roomId,
|
||||
roomState = Lce.Content(roomState),
|
||||
composerState = ComposerState.Text(value = messageContent ?: "")
|
||||
)
|
||||
|
||||
fun aMessengerState(
|
||||
self: UserId = aUserId(),
|
||||
roomState: RoomState,
|
||||
) = MessengerState(self, roomState, typing = null)
|
||||
|
||||
class FakeObserveTimelineUseCase : ObserveTimelineUseCase by mockk() {
|
||||
fun given(roomId: RoomId, selfId: UserId) = coEvery { this@FakeObserveTimelineUseCase.invoke(roomId, selfId) }.delegateReturn()
|
||||
}
|
||||
|
||||
class FakeMessageService : MessageService by mockk()
|
||||
class FakeRoomService : RoomService by mockk()
|
||||
|
||||
fun fixedClock(timestamp: Long = 0) = Clock.fixed(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC)
|
@ -42,8 +42,8 @@ interface MessageService : MatrixService {
|
||||
@SerialName("content") val content: Content.TextContent,
|
||||
@SerialName("send_encrypted") val sendEncrypted: Boolean,
|
||||
@SerialName("room_id") val roomId: RoomId,
|
||||
@SerialName("local_id") val localId: String = "local.${UUID.randomUUID()}",
|
||||
@SerialName("timestamp") val timestampUtc: Long = System.currentTimeMillis(),
|
||||
@SerialName("local_id") val localId: String,
|
||||
@SerialName("timestamp") val timestampUtc: Long,
|
||||
) : Message()
|
||||
|
||||
@Serializable
|
||||
|
@ -21,9 +21,9 @@ interface SyncService : MatrixService {
|
||||
|
||||
suspend fun invites(): Flow<InviteState>
|
||||
suspend fun overview(): Flow<OverviewState>
|
||||
suspend fun room(roomId: RoomId): Flow<RoomState>
|
||||
fun room(roomId: RoomId): Flow<RoomState>
|
||||
fun startSyncing(): Flow<Unit>
|
||||
suspend fun events(): Flow<List<SyncEvent>>
|
||||
fun events(): Flow<List<SyncEvent>>
|
||||
suspend fun observeEvent(eventId: EventId): Flow<EventId>
|
||||
suspend fun forceManualRefresh(roomIds: List<RoomId>)
|
||||
|
||||
|
@ -101,8 +101,8 @@ internal class DefaultSyncService(
|
||||
override fun startSyncing() = syncFlow
|
||||
override suspend fun invites() = overviewStore.latestInvites()
|
||||
override suspend fun overview() = overviewStore.latest()
|
||||
override suspend fun room(roomId: RoomId) = roomStore.latest(roomId)
|
||||
override suspend fun events() = syncEventsFlow
|
||||
override fun room(roomId: RoomId) = roomStore.latest(roomId)
|
||||
override fun events() = syncEventsFlow
|
||||
override suspend fun observeEvent(eventId: EventId) = roomStore.observeEvent(eventId)
|
||||
override suspend fun forceManualRefresh(roomIds: List<RoomId>) {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -0,0 +1,10 @@
|
||||
package fixture
|
||||
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
import app.dapk.st.matrix.sync.RoomOverview
|
||||
import app.dapk.st.matrix.sync.RoomState
|
||||
|
||||
fun aRoomState(
|
||||
roomOverview: RoomOverview = aRoomOverview(),
|
||||
events: List<RoomEvent> = listOf(aRoomMessageEvent()),
|
||||
) = RoomState(roomOverview, events)
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.fail
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import java.util.*
|
||||
|
||||
fun flowTest(block: suspend MatrixTestScope.() -> Unit) {
|
||||
runTest {
|
||||
@ -129,6 +130,8 @@ class MatrixTestScope(private val testScope: TestScope) {
|
||||
content = MessageService.Message.Content.TextContent(body = content),
|
||||
roomId = roomId,
|
||||
sendEncrypted = true,
|
||||
localId = "local.${UUID.randomUUID()}",
|
||||
timestampUtc = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user