diff --git a/changelog.d/6320.misc b/changelog.d/6320.misc new file mode 100644 index 0000000000..7cdd41f486 --- /dev/null +++ b/changelog.d/6320.misc @@ -0,0 +1 @@ +CreatePollViewModel unit tests diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt new file mode 100644 index 0000000000..0387fc8986 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2022 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 im.vector.app.features.poll.create + +import com.airbnb.mvrx.test.MvRxTestRule +import im.vector.app.features.poll.PollMode +import im.vector.app.test.fakes.FakeCreatePollViewStates.A_FAKE_OPTIONS +import im.vector.app.test.fakes.FakeCreatePollViewStates.A_FAKE_QUESTION +import im.vector.app.test.fakes.FakeCreatePollViewStates.A_FAKE_ROOM_ID +import im.vector.app.test.fakes.FakeCreatePollViewStates.A_POLL_START_TIMELINE_EVENT +import im.vector.app.test.fakes.FakeCreatePollViewStates.createPollArgs +import im.vector.app.test.fakes.FakeCreatePollViewStates.editPollArgs +import im.vector.app.test.fakes.FakeCreatePollViewStates.editedPollViewState +import im.vector.app.test.fakes.FakeCreatePollViewStates.initialCreatePollViewState +import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithOnlyQuestion +import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithQuestionAndEnoughOptions +import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithQuestionAndEnoughOptionsButDeletedLastOption +import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithQuestionAndMaxOptions +import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithQuestionAndNotEnoughOptions +import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithoutQuestionAndEnoughOptions +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.test +import io.mockk.unmockkAll +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.matrix.android.sdk.api.session.room.model.message.PollType + +class CreatePollViewModelTest { + + private val testDispatcher = UnconfinedTestDispatcher() + + @get:Rule + val mvRxTestRule = MvRxTestRule( + testDispatcher = testDispatcher // See https://github.com/airbnb/mavericks/issues/599 + ) + + private val fakeSession = FakeSession() + + private fun createPollViewModel(pollMode: PollMode): CreatePollViewModel { + return if (pollMode == PollMode.EDIT) { + CreatePollViewModel(CreatePollViewState(editPollArgs), fakeSession) + } else { + CreatePollViewModel(CreatePollViewState(createPollArgs), fakeSession) + } + } + + @Before + fun setup() { + fakeSession + .roomService() + .getRoom(A_FAKE_ROOM_ID) + .timelineService() + .givenTimelineEvent(A_POLL_START_TIMELINE_EVENT) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given the view model is initialized then poll cannot be created and more options can be added`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + + test + .assertLatestState(initialCreatePollViewState) + .finish() + } + + @Test + fun `given there is not any options when the question is added then poll cannot be created and more options can be added`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) + + test + .assertLatestState(pollViewStateWithOnlyQuestion) + .finish() + } + + @Test + fun `given there is not enough options when the question is added then poll cannot be created and more options can be added`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) + repeat(CreatePollViewModel.MIN_OPTIONS_COUNT - 1) { + createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) + } + + test + .assertLatestState(pollViewStateWithQuestionAndNotEnoughOptions) + .finish() + } + + @Test + fun `given there is not a question when enough options are added then poll cannot be created and more options can be added`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + + repeat(CreatePollViewModel.MIN_OPTIONS_COUNT) { + createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) + } + + test + .assertLatestState(pollViewStateWithoutQuestionAndEnoughOptions) + .finish() + } + + @Test + fun `given there is a question when enough options are added then poll can be created and more options can be added`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) + repeat(CreatePollViewModel.MIN_OPTIONS_COUNT) { + createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) + } + + test + .assertLatestState(pollViewStateWithQuestionAndEnoughOptions) + .finish() + } + + @Test + fun `given there is a question when max number of options are added then poll can be created and more options cannot be added`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) + repeat(CreatePollViewModel.MAX_OPTIONS_COUNT) { + if (it >= CreatePollViewModel.MIN_OPTIONS_COUNT) { + createPollViewModel.handle(CreatePollAction.OnAddOption) + } + createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) + } + + test + .assertLatestState(pollViewStateWithQuestionAndMaxOptions) + .finish() + } + + @Test + fun `given an initial poll state when poll type is changed then view state is updated accordingly`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + + createPollViewModel.handle(CreatePollAction.OnPollTypeChanged(PollType.UNDISCLOSED)) + createPollViewModel.handle(CreatePollAction.OnPollTypeChanged(PollType.DISCLOSED)) + + test + .assertStatesChanges( + initialCreatePollViewState, + { copy(pollType = PollType.UNDISCLOSED) }, + { copy(pollType = PollType.DISCLOSED) }, + ) + .finish() + } + + @Test + fun `given there is not a question and enough options when create poll is requested then error view events are post`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + + createPollViewModel.handle(CreatePollAction.OnCreatePoll) + + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) + createPollViewModel.handle(CreatePollAction.OnCreatePoll) + + createPollViewModel.handle(CreatePollAction.OnOptionChanged(0, A_FAKE_OPTIONS[0])) + createPollViewModel.handle(CreatePollAction.OnCreatePoll) + + test + .assertEvents( + CreatePollViewEvents.EmptyQuestionError, + CreatePollViewEvents.NotEnoughOptionsError(requiredOptionsCount = CreatePollViewModel.MIN_OPTIONS_COUNT), + CreatePollViewEvents.NotEnoughOptionsError(requiredOptionsCount = CreatePollViewModel.MIN_OPTIONS_COUNT), + ) + } + + @Test + fun `given there is a question and enough options when create poll is requested then success view event is post`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(0, A_FAKE_OPTIONS[0])) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(1, A_FAKE_OPTIONS[1])) + createPollViewModel.handle(CreatePollAction.OnCreatePoll) + + test + .assertEvents( + CreatePollViewEvents.Success, + ) + } + + @Test + fun `given there is a question and enough options when the last option is deleted then view state should be updated accordingly`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(0, A_FAKE_OPTIONS[0])) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(1, A_FAKE_OPTIONS[1])) + createPollViewModel.handle(CreatePollAction.OnDeleteOption(1)) + + test.assertLatestState(pollViewStateWithQuestionAndEnoughOptionsButDeletedLastOption) + } + + @Test + fun `given an edited poll event when question and options are changed then view state is updated accordingly`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.EDIT) + val test = createPollViewModel.test() + + test + .assertState(editedPollViewState) + .finish() + } + + @Test + fun `given an edited poll event then able to be edited`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.EDIT) + val test = createPollViewModel.test() + + createPollViewModel.handle(CreatePollAction.OnCreatePoll) + + test + .assertEvents( + CreatePollViewEvents.Success, + ) + } +} diff --git a/vector/src/test/java/im/vector/app/test/Extensions.kt b/vector/src/test/java/im/vector/app/test/Extensions.kt index e5d5af2ece..5ac17cc5ff 100644 --- a/vector/src/test/java/im/vector/app/test/Extensions.kt +++ b/vector/src/test/java/im/vector/app/test/Extensions.kt @@ -86,6 +86,11 @@ class ViewModelTest( return this } + fun assertLatestState(expected: S): ViewModelTest { + states.assertLatestValue(expected) + return this + } + fun finish() { states.finish() viewEvents.finish() diff --git a/vector/src/test/java/im/vector/app/test/FlowTestObserver.kt b/vector/src/test/java/im/vector/app/test/FlowTestObserver.kt index 37f5f118c1..db828be232 100644 --- a/vector/src/test/java/im/vector/app/test/FlowTestObserver.kt +++ b/vector/src/test/java/im/vector/app/test/FlowTestObserver.kt @@ -47,6 +47,10 @@ class FlowTestObserver( return this } + fun assertLatestValue(value: T) { + assertTrue(values.last() == value) + } + fun assertValues(values: List): FlowTestObserver { assertEquals(values, this.values) return this diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt new file mode 100644 index 0000000000..04f3526602 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2022 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 im.vector.app.test.fakes + +import im.vector.app.features.poll.PollMode +import im.vector.app.features.poll.create.CreatePollArgs +import im.vector.app.features.poll.create.CreatePollViewModel +import im.vector.app.features.poll.create.CreatePollViewState +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.PollAnswer +import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo +import org.matrix.android.sdk.api.session.room.model.message.PollQuestion +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import kotlin.random.Random + +object FakeCreatePollViewStates { + + const val A_FAKE_ROOM_ID = "fakeRoomId" + private const val A_FAKE_EVENT_ID = "fakeEventId" + private const val A_FAKE_USER_ID = "fakeUserId" + + val createPollArgs = CreatePollArgs(A_FAKE_ROOM_ID, null, PollMode.CREATE) + val editPollArgs = CreatePollArgs(A_FAKE_ROOM_ID, A_FAKE_EVENT_ID, PollMode.EDIT) + + const val A_FAKE_QUESTION = "What is your favourite coffee?" + val A_FAKE_OPTIONS = List(CreatePollViewModel.MAX_OPTIONS_COUNT + 1) { "Coffee No${Random.nextInt()}" } + + private val A_POLL_CONTENT = MessagePollContent( + unstablePollCreationInfo = PollCreationInfo( + question = PollQuestion( + unstableQuestion = A_FAKE_QUESTION + ), + maxSelections = 1, + answers = listOf( + PollAnswer( + id = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", + unstableAnswer = A_FAKE_OPTIONS[0] + ), + PollAnswer( + id = "ec1a4db0-46d8-4d7a-9bb6-d80724715938", + unstableAnswer = A_FAKE_OPTIONS[1] + ) + ) + ) + ) + + private val A_POLL_START_EVENT = Event( + type = EventType.POLL_START.first(), + eventId = A_FAKE_EVENT_ID, + originServerTs = 1652435922563, + senderId = A_FAKE_USER_ID, + roomId = A_FAKE_ROOM_ID, + content = A_POLL_CONTENT.toContent() + ) + + val A_POLL_START_TIMELINE_EVENT = TimelineEvent( + root = A_POLL_START_EVENT, + localId = 12345, + eventId = A_FAKE_EVENT_ID, + displayIndex = 1, + senderInfo = SenderInfo(A_FAKE_USER_ID, isUniqueDisplayName = true, avatarUrl = "", displayName = "") + ) + + val initialCreatePollViewState = CreatePollViewState(createPollArgs).copy( + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithOnlyQuestion = initialCreatePollViewState.copy( + question = A_FAKE_QUESTION, + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithQuestionAndNotEnoughOptions = initialCreatePollViewState.copy( + question = A_FAKE_QUESTION, + options = A_FAKE_OPTIONS.take(CreatePollViewModel.MIN_OPTIONS_COUNT - 1).toMutableList().apply { add("") }, + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithoutQuestionAndEnoughOptions = initialCreatePollViewState.copy( + question = "", + options = A_FAKE_OPTIONS.take(CreatePollViewModel.MIN_OPTIONS_COUNT), + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithQuestionAndEnoughOptions = initialCreatePollViewState.copy( + question = A_FAKE_QUESTION, + options = A_FAKE_OPTIONS.take(CreatePollViewModel.MIN_OPTIONS_COUNT), + canCreatePoll = true, + canAddMoreOptions = true + ) + + val pollViewStateWithQuestionAndEnoughOptionsButDeletedLastOption = pollViewStateWithQuestionAndEnoughOptions.copy( + options = A_FAKE_OPTIONS.take(CreatePollViewModel.MIN_OPTIONS_COUNT).toMutableList().apply { removeLast() }, + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithQuestionAndMaxOptions = initialCreatePollViewState.copy( + question = A_FAKE_QUESTION, + options = A_FAKE_OPTIONS.take(CreatePollViewModel.MAX_OPTIONS_COUNT), + canCreatePoll = true, + canAddMoreOptions = false + ) + + val editedPollViewState = pollViewStateWithQuestionAndEnoughOptions.copy( + editedEventId = A_FAKE_EVENT_ID, + mode = PollMode.EDIT + ) +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRelationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRelationService.kt new file mode 100644 index 0000000000..828e3a25b6 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRelationService.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 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 im.vector.app.test.fakes + +import io.mockk.mockk +import org.matrix.android.sdk.api.session.room.model.message.PollType +import org.matrix.android.sdk.api.session.room.model.relation.RelationService +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.Cancelable + +class FakeRelationService : RelationService by mockk() { + + private val cancelable = mockk() + + override fun editPoll(targetEvent: TimelineEvent, pollType: PollType, question: String, options: List): Cancelable = cancelable +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt index ff87ab0fde..865b01551a 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt @@ -21,7 +21,16 @@ import org.matrix.android.sdk.api.session.room.Room class FakeRoom( private val fakeLocationSharingService: FakeLocationSharingService = FakeLocationSharingService(), + private val fakeSendService: FakeSendService = FakeSendService(), + private val fakeTimelineService: FakeTimelineService = FakeTimelineService(), + private val fakeRelationService: FakeRelationService = FakeRelationService(), ) : Room by mockk() { override fun locationSharingService() = fakeLocationSharingService + + override fun sendService() = fakeSendService + + override fun timelineService() = fakeTimelineService + + override fun relationService() = fakeRelationService } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSendService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSendService.kt new file mode 100644 index 0000000000..04b9b95ce1 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSendService.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 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 im.vector.app.test.fakes + +import io.mockk.mockk +import org.matrix.android.sdk.api.session.room.model.message.PollType +import org.matrix.android.sdk.api.session.room.send.SendService +import org.matrix.android.sdk.api.util.Cancelable + +class FakeSendService : SendService by mockk() { + + private val cancelable = mockk() + + override fun sendPoll(pollType: PollType, question: String, options: List): Cancelable = cancelable +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt new file mode 100644 index 0000000000..56f38724b1 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 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 im.vector.app.test.fakes + +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineService + +class FakeTimelineService : TimelineService by mockk() { + + fun givenTimelineEvent(event: TimelineEvent) { + every { getTimelineEvent(any()) } returns event + } +}