diff --git a/changelog.d/1886.feature b/changelog.d/1886.feature new file mode 100644 index 0000000000..ac88b61b8c --- /dev/null +++ b/changelog.d/1886.feature @@ -0,0 +1 @@ +Confirm back navigation when editing a poll only if the poll was changed \ No newline at end of file diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollConstants.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollConstants.kt new file mode 100644 index 0000000000..3e75bc2427 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollConstants.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.poll.impl + +internal object PollConstants { + const val MIN_ANSWERS = 2 + const val MAX_ANSWERS = 20 + const val MAX_ANSWER_LENGTH = 240 + const val MAX_SELECTIONS = 1 +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt index 67faaa43da..484e92eb15 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt @@ -18,13 +18,11 @@ package io.element.android.features.poll.impl.create import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import dagger.assisted.Assisted @@ -34,21 +32,19 @@ import im.vector.app.features.analytics.plan.Composer import im.vector.app.features.analytics.plan.PollCreation import io.element.android.features.messages.api.MessageComposerContext import io.element.android.features.poll.api.create.CreatePollMode +import io.element.android.features.poll.impl.PollConstants.MAX_SELECTIONS import io.element.android.features.poll.impl.data.PollRepository import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.poll.isDisclosed import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch import timber.log.Timber -private const val MIN_ANSWERS = 2 -private const val MAX_ANSWERS = 20 -private const val MAX_ANSWER_LENGTH = 240 -private const val MAX_SELECTIONS = 1 - class CreatePollPresenter @AssistedInject constructor( private val repository: PollRepository, private val analyticsService: AnalyticsService, @@ -64,18 +60,31 @@ class CreatePollPresenter @AssistedInject constructor( @Composable override fun present(): CreatePollState { - var question: String by rememberSaveable { mutableStateOf("") } - var answers: List by rememberSaveable { mutableStateOf(listOf("", "")) } - var pollKind: PollKind by rememberSaveable(saver = pollKindSaver) { mutableStateOf(PollKind.Disclosed) } + // The initial state of the form. In edit mode this will be populated with the poll being edited. + var initialPoll: PollFormState by rememberSaveable(stateSaver = pollFormStateSaver) { + mutableStateOf(PollFormState.Empty) + } + // The current state of the form. + var poll: PollFormState by rememberSaveable(stateSaver = pollFormStateSaver) { + mutableStateOf(initialPoll) + } + + // Whether the form has been changed from the initial state + val isDirty: Boolean by remember { derivedStateOf { poll != initialPoll } } + var showBackConfirmation: Boolean by rememberSaveable { mutableStateOf(false) } var showDeleteConfirmation: Boolean by rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { if (mode is CreatePollMode.EditPoll) { repository.getPoll(mode.eventId).onSuccess { - question = it.question - answers = it.answers.map(PollAnswer::text) - pollKind = it.kind + val loadedPoll = PollFormState( + question = it.question, + answers = it.answers.map(PollAnswer::text).toPersistentList(), + isDisclosed = it.kind.isDisclosed, + ) + initialPoll = loadedPoll + poll = loadedPoll }.onFailure { analyticsService.trackGetPollFailed(it) navigateUp() @@ -83,9 +92,9 @@ class CreatePollPresenter @AssistedInject constructor( } } - val canSave: Boolean by remember { derivedStateOf { canSave(question, answers) } } - val canAddAnswer: Boolean by remember { derivedStateOf { canAddAnswer(answers) } } - val immutableAnswers: ImmutableList by remember { derivedStateOf { answers.toAnswers() } } + val canSave: Boolean by remember { derivedStateOf { poll.isValid } } + val canAddAnswer: Boolean by remember { derivedStateOf { poll.canAddAnswer } } + val immutableAnswers: ImmutableList by remember { derivedStateOf { poll.toUiAnswers() } } val scope = rememberCoroutineScope() @@ -98,14 +107,14 @@ class CreatePollPresenter @AssistedInject constructor( is CreatePollMode.EditPoll -> mode.eventId is CreatePollMode.NewPoll -> null }, - question = question, - answers = answers, - pollKind = pollKind, + question = poll.question, + answers = poll.answers, + pollKind = poll.pollKind, maxSelections = MAX_SELECTIONS, ).onSuccess { analyticsService.capturePollSaved( - isUndisclosed = pollKind == PollKind.Undisclosed, - numberOfAnswers = answers.size, + isUndisclosed = poll.pollKind == PollKind.Undisclosed, + numberOfAnswers = poll.answers.size, ) }.onFailure { analyticsService.trackSavePollFailed(it, mode) @@ -132,27 +141,25 @@ class CreatePollPresenter @AssistedInject constructor( } } is CreatePollEvents.AddAnswer -> { - answers = answers + "" + poll = poll.withNewAnswer() } is CreatePollEvents.RemoveAnswer -> { - answers = answers.filterIndexed { index, _ -> index != event.index } + poll= poll.withAnswerRemoved(event.index) } is CreatePollEvents.SetAnswer -> { - answers = answers.toMutableList().apply { - this[event.index] = event.text.take(MAX_ANSWER_LENGTH) - } + poll = poll.withAnswerChanged(event.index, event.text) } is CreatePollEvents.SetPollKind -> { - pollKind = event.pollKind + poll = poll.copy(isDisclosed = event.pollKind.isDisclosed) } is CreatePollEvents.SetQuestion -> { - question = event.question + poll = poll.copy(question = event.question) } is CreatePollEvents.NavBack -> { navigateUp() } CreatePollEvents.ConfirmNavBack -> { - val shouldConfirm = question.isNotBlank() || answers.any { it.isNotBlank() } + val shouldConfirm = isDirty if (shouldConfirm) { showBackConfirmation = true } else { @@ -173,9 +180,9 @@ class CreatePollPresenter @AssistedInject constructor( }, canSave = canSave, canAddAnswer = canAddAnswer, - question = question, + question = poll.question, answers = immutableAnswers, - pollKind = pollKind, + pollKind = poll.pollKind, showBackConfirmation = showBackConfirmation, showDeleteConfirmation = showDeleteConfirmation, eventSink = ::handleEvents, @@ -228,35 +235,12 @@ private fun AnalyticsService.trackSavePollFailed(cause: Throwable, mode: CreateP trackError(exception) } -private fun canSave( - question: String, - answers: List -) = question.isNotBlank() && answers.size >= MIN_ANSWERS && answers.all { it.isNotBlank() } - -private fun canAddAnswer(answers: List) = answers.size < MAX_ANSWERS - -fun List.toAnswers(): ImmutableList { - return map { answer -> +fun PollFormState.toUiAnswers(): ImmutableList { + return answers.map { answer -> Answer( text = answer, - canDelete = this.size > MIN_ANSWERS, + canDelete = canDeleteAnswer, ) }.toImmutableList() } -private val pollKindSaver: Saver, Boolean> = Saver( - save = { - when (it.value) { - PollKind.Disclosed -> false - PollKind.Undisclosed -> true - } - }, - restore = { - mutableStateOf( - when (it) { - true -> PollKind.Undisclosed - else -> PollKind.Disclosed - } - ) - } -) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt index a1c50332cb..fbea82608a 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt @@ -76,11 +76,13 @@ fun CreatePollView( val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) } BackHandler(onBack = navBack) - if (state.showBackConfirmation) ConfirmationDialog( - content = stringResource(id = R.string.screen_create_poll_cancel_confirmation_content_android), - onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) }, - onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } - ) + if (state.showBackConfirmation) { + ConfirmationDialog( + content = stringResource(id = R.string.screen_create_poll_cancel_confirmation_content_android), + onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) }, + onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } + ) + } if (state.showDeleteConfirmation) { ConfirmationDialog( title = stringResource(id = R.string.screen_edit_poll_delete_confirmation_title), diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/PollFormState.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/PollFormState.kt new file mode 100644 index 0000000000..461b96b018 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/PollFormState.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.poll.impl.create + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.mapSaver +import io.element.android.features.poll.impl.PollConstants +import io.element.android.features.poll.impl.PollConstants.MIN_ANSWERS +import io.element.android.libraries.matrix.api.poll.PollKind +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +/** + * Represents the state of the poll creation / edit form. + * + * Save this state using [pollFormStateSaver]. + */ +data class PollFormState( + val question: String, + val answers: ImmutableList, + val isDisclosed: Boolean, +) { + companion object { + val Empty = PollFormState( + question = "", + answers = MutableList(MIN_ANSWERS) { "" }.toPersistentList(), + isDisclosed = true, + ) + } + + val pollKind + get() = when (isDisclosed) { + true -> PollKind.Disclosed + false -> PollKind.Undisclosed + } + + /** + * Create a copy of the [PollFormState] with a new blank answer added. + * + * If the maximum number of answers has already been reached an answer is not added. + */ + fun withNewAnswer(): PollFormState { + if (!canAddAnswer) { + return this + } + + return copy(answers = (answers + "").toPersistentList()) + } + + /** + * Create a copy of the [PollFormState] with the answer at [index] removed. + * + * If the answer doesn't exist or can't be removed, the state is unchanged. + * + * @param index the index of the answer to remove. + * + * @return a new [PollFormState] with the answer at [index] removed. + */ + fun withAnswerRemoved(index: Int): PollFormState { + if (!canDeleteAnswer) { + return this + } + + return copy(answers = answers.filterIndexed { i, _ -> i != index }.toPersistentList()) + } + + /** + * Create a copy of the [PollFormState] with the answer at [index] changed. + * + * If the new answer is longer than [PollConstants.MAX_ANSWER_LENGTH], it will be truncated. + * + * @param index the index of the answer to change. + * @param rawAnswer the new answer as the user typed it. + * + * @return a new [PollFormState] with the answer at [index] changed. + */ + fun withAnswerChanged(index: Int, rawAnswer: String): PollFormState = + copy(answers = answers.toMutableList().apply { + this[index] = rawAnswer.take(PollConstants.MAX_ANSWER_LENGTH) + }.toPersistentList()) + + /** + * Whether a new answer can be added. + */ + val canAddAnswer get() = answers.size < PollConstants.MAX_ANSWERS + + /** + * Whether any answer can be deleted. + */ + val canDeleteAnswer get() = answers.size > MIN_ANSWERS + + /** + * Whether the form is currently valid. + */ + val isValid get() = question.isNotBlank() && answers.size >= MIN_ANSWERS && answers.all { it.isNotBlank() } +} + +/** + * A [Saver] for [PollFormState]. + */ +internal val pollFormStateSaver = mapSaver( + save = { + mutableMapOf( + "question" to it.question, + "answers" to it.answers.toTypedArray(), + "isDisclosed" to it.isDisclosed, + ) + }, + restore = { saved -> + PollFormState( + question = saved["question"] as String, + answers = (saved["answers"] as Array<*>).map { it as String }.toPersistentList(), + isDisclosed = saved["isDisclosed"] as Boolean, + ) + } +) diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt index dae6b604f6..ae54c6a1e5 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt @@ -364,7 +364,7 @@ class CreatePollPresenterTest { } @Test - fun `confirm nav back with blank fields calls nav back lambda`() = runTest { + fun `confirm nav back from new poll with blank fields calls nav back lambda`() = runTest { val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -378,20 +378,52 @@ class CreatePollPresenterTest { } @Test - fun `confirm nav back with non blank fields shows confirmation dialog and sending hides it`() = runTest { + fun `confirm nav back from new poll with non blank fields shows confirmation dialog and cancelling hides it`() = runTest { val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initial = awaitItem() initial.eventSink(CreatePollEvents.SetQuestion("Non blank")) - Truth.assertThat(navUpInvocationsCount).isEqualTo(0) Truth.assertThat(awaitItem().showBackConfirmation).isFalse() initial.eventSink(CreatePollEvents.ConfirmNavBack) - Truth.assertThat(navUpInvocationsCount).isEqualTo(0) Truth.assertThat(awaitItem().showBackConfirmation).isTrue() initial.eventSink(CreatePollEvents.HideConfirmation) Truth.assertThat(awaitItem().showBackConfirmation).isFalse() + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) + } + } + + @Test + fun `confirm nav back from existing poll with unchanged fields calls nav back lambda`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitDefaultItem() + val loaded = awaitPollLoaded() + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) + Truth.assertThat(loaded.showBackConfirmation).isFalse() + loaded.eventSink(CreatePollEvents.ConfirmNavBack) + Truth.assertThat(navUpInvocationsCount).isEqualTo(1) + } + } + + @Test + fun `confirm nav back from existing poll with changed fields shows confirmation dialog and cancelling hides it`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitDefaultItem() + val loaded = awaitPollLoaded() + loaded.eventSink(CreatePollEvents.SetQuestion("CHANGED")) + Truth.assertThat(awaitItem().showBackConfirmation).isFalse() + loaded.eventSink(CreatePollEvents.ConfirmNavBack) + Truth.assertThat(awaitItem().showBackConfirmation).isTrue() + loaded.eventSink(CreatePollEvents.HideConfirmation) + Truth.assertThat(awaitItem().showBackConfirmation).isFalse() + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) } } @@ -442,6 +474,7 @@ class CreatePollPresenterTest { } } + private suspend fun TurbineTestContext.awaitDefaultItem() = awaitItem().apply { Truth.assertThat(canSave).isFalse() diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateSaverTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateSaverTest.kt new file mode 100644 index 0000000000..62b3918372 --- /dev/null +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateSaverTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.poll.impl.create + +import androidx.compose.runtime.saveable.SaverScope +import com.google.common.truth.Truth.assertThat +import kotlinx.collections.immutable.toPersistentList +import org.junit.Test + +class PollFormStateSaverTest { + companion object { + val CanSaveScope = SaverScope { true } + } + @Test + fun `test save and restore`() { + val state = PollFormState( + question = "question", + answers = listOf("answer1", "answer2").toPersistentList(), + isDisclosed = true, + ) + + val saved = with(CanSaveScope) { + with(pollFormStateSaver) { + save(state) + } + } + + val restored = saved?.let { + pollFormStateSaver.restore(it) + } + + assertThat(restored).isEqualTo(state) + } +} diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateTest.kt new file mode 100644 index 0000000000..cf97967f23 --- /dev/null +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.poll.impl.create + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.poll.impl.PollConstants +import io.element.android.libraries.matrix.api.poll.PollKind +import kotlinx.collections.immutable.toPersistentList +import org.junit.Test + +class PollFormStateTest { + + @Test + fun `with new answer`() { + val state = PollFormState.Empty + val newState = state.withNewAnswer() + assertThat(newState.answers).isEqualTo(listOf("", "", "")) + } + + @Test + fun `with new answer, given cannot add, doesn't add`() { + val state = PollFormState.Empty.withBlankAnswers(PollConstants.MAX_ANSWERS) + val newState = state.withNewAnswer() + assertThat(newState).isEqualTo(state) + } + + @Test + fun `with answer deleted, given cannot delete, doesn't delete`() { + val state = PollFormState.Empty + val newState = state.withAnswerRemoved(0) + assertThat(newState).isEqualTo(state) + } + + @Test + fun `with answer deleted, given can delete`() { + val state = PollFormState.Empty.withNewAnswer() + val newState = state.withAnswerRemoved(0) + assertThat(newState).isEqualTo(PollFormState.Empty) + } + + @Test + fun `with answer changed`() { + val state = PollFormState.Empty + val newState = state.withAnswerChanged(1, "New answer") + assertThat(newState).isEqualTo(PollFormState.Empty.copy( + answers = listOf("", "New answer").toPersistentList() + )) + } + + @Test + fun `with answer changed, given it is too long, truncates`() { + val tooLongAnswer = "a".repeat(PollConstants.MAX_ANSWER_LENGTH * 2) + val truncatedAnswer = "a".repeat(PollConstants.MAX_ANSWER_LENGTH) + val state = PollFormState.Empty + val newState = state.withAnswerChanged(1, tooLongAnswer) + assertThat(newState).isEqualTo(PollFormState.Empty.copy( + answers = listOf("", truncatedAnswer).toPersistentList() + )) + } + + @Test + fun `can add answer is true when it does not have max answers`() { + val state = PollFormState.Empty.withBlankAnswers(PollConstants.MAX_ANSWERS - 1) + assertThat(state.canAddAnswer).isTrue() + } + + @Test + fun `can add answer is false when it has max answers`() { + val state = PollFormState.Empty.withBlankAnswers(PollConstants.MAX_ANSWERS) + assertThat(state.canAddAnswer).isFalse() + } + + @Test + fun `can delete answer is false when it has min answers`() { + val state = PollFormState.Empty.withBlankAnswers(PollConstants.MIN_ANSWERS) + assertThat(state.canDeleteAnswer).isFalse() + } + + @Test + fun `can delete answer is true when it has more than min answers`() { + val numAnswers = PollConstants.MIN_ANSWERS + 1 + val state = PollFormState.Empty.withBlankAnswers(numAnswers) + assertThat(state.canDeleteAnswer).isTrue() + } + + @Test + fun `is valid is true when it is valid`() { + val state = aValidPollFormState() + assertThat(state.isValid).isTrue() + } + + @Test + fun `is valid is false when question is blank`() { + val state = aValidPollFormState().copy(question = "") + assertThat(state.isValid).isFalse() + } + + @Test + fun `is valid is false when not enough answers`() { + val state = aValidPollFormState().copy(answers = listOf("").toPersistentList()) + assertThat(state.isValid).isFalse() + } + + @Test + fun `is valid is false when one answer is blank`() { + val state = aValidPollFormState().withNewAnswer() + assertThat(state.isValid).isFalse() + } + + @Test + fun `poll kind when is disclosed`() { + val state = PollFormState.Empty.copy(isDisclosed = true) + assertThat(state.pollKind).isEqualTo(PollKind.Disclosed) + } + + @Test + fun `poll kind when is not disclosed`() { + val state = PollFormState.Empty.copy(isDisclosed = false) + assertThat(state.pollKind).isEqualTo(PollKind.Undisclosed) + } +} + + +private fun aValidPollFormState(): PollFormState { + return PollFormState.Empty.copy( + question = "question", + answers = listOf("answer1", "answer2").toPersistentList(), + isDisclosed = true, + ) +} + +private fun PollFormState.withBlankAnswers(numAnswers: Int): PollFormState = + copy(answers = List(numAnswers) { "" }.toPersistentList())