diff --git a/domains/state/src/testFixtures/kotlin/test/ReducerTest.kt b/domains/state/src/testFixtures/kotlin/test/ReducerTest.kt index a8acc00..3fdf94a 100644 --- a/domains/state/src/testFixtures/kotlin/test/ReducerTest.kt +++ b/domains/state/src/testFixtures/kotlin/test/ReducerTest.kt @@ -10,8 +10,6 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.shouldBeEqualTo -import test.ExpectTest -import test.ExpectTestScope interface ReducerTest { operator fun invoke(block: suspend ReducerTestScope.() -> Unit) @@ -63,16 +61,31 @@ class ReducerTestScope( manualState = state } + fun setState(block: (S) -> S) { + manualState = block(reducerScope.getState()) + } + fun assertInitialState(expected: S) { reducerFactory.initialState() shouldBeEqualTo expected } + fun assertEvents(events: List) { + fakeEventSource.assertEvents(events) + } + fun assertOnlyStateChange(expected: S) { assertStateChange(expected) assertNoDispatches() fakeEventSource.assertNoEvents() } + fun assertOnlyStateChange(block: (S) -> S) { + val expected = block(reducerScope.getState()) + assertStateChange(expected) + assertNoDispatches() + fakeEventSource.assertNoEvents() + } + fun assertStateChange(expected: S) { capturedResult shouldBeEqualTo expected } @@ -86,7 +99,11 @@ class ReducerTestScope( } fun assertNoStateChange() { - assertEquals(reducerFactory.initialState(), capturedResult) + assertEquals(reducerScope.getState(), capturedResult) + } + + fun assertNoEvents() { + fakeEventSource.assertNoEvents() } fun assertOnlyDispatches(expected: List) { @@ -103,7 +120,7 @@ class ReducerTestScope( fun assertNoChanges() { assertNoStateChange() - fakeEventSource.assertNoEvents() + assertNoEvents() assertNoDispatches() } } \ No newline at end of file diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerReducer.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerReducer.kt index e94a1ab..ef65982 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerReducer.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerReducer.kt @@ -9,6 +9,7 @@ import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.design.components.BubbleModel import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.engine.ChatEngine +import app.dapk.st.engine.MessengerPageState import app.dapk.st.engine.RoomEvent import app.dapk.st.engine.SendMessage import app.dapk.st.matrix.common.RoomId @@ -33,7 +34,7 @@ internal fun messengerReducer( initialState = MessengerScreenState( roomId = roomId, roomState = Lce.Loading(), - composerState = initialAttachments?.let { ComposerState.Attachments(it, null) } ?: ComposerState.Text(value = "", reply = null), + composerState = initialComposerState(initialAttachments), viewerState = null, ), @@ -120,32 +121,12 @@ internal fun messengerReducer( is ComposerState.Text -> { dispatch(ComposerStateChange.Clear) state.roomState.takeIfContent()?.let { content -> - val roomState = content.roomState - 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, - ) + chatEngine.sendTextMessage(content, composerState) } } is ComposerState.Attachments -> { dispatch(ComposerStateChange.Clear) - state.roomState.takeIfContent()?.let { content -> val roomState = content.roomState 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?) = initialAttachments + ?.takeIf { it.isNotEmpty() } + ?.let { ComposerState.Attachments(it, null) } + ?: ComposerState.Text(value = "", reply = null) + private fun BubbleModel.findCopyableContent(): CopyableResult = when (this) { is BubbleModel.Encrypted -> CopyableResult.NothingToCopy is BubbleModel.Image -> CopyableResult.NothingToCopy diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerReducerTest.kt b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerReducerTest.kt new file mode 100644 index 0000000..d061d32 --- /dev/null +++ b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerReducerTest.kt @@ -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?, + block: suspend ReducerTestScope.() -> 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() +} + +class FakeJobBag { + val instance = mockk() +} + +class FakeDeviceMeta { + val instance = mockk() + + fun givenApiVersion() = every { instance.apiVersion }.delegateReturn() +} \ No newline at end of file diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt deleted file mode 100644 index 59e7efb..0000000 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt +++ /dev/null @@ -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( - { 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({ - 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({ 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() -}