Browse Source

Confirm back navigation when editing a poll only if the poll was changed (#1886)

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
pull/1916/head
jonnyandrew 10 months ago committed by GitHub
parent
commit
4e43a93dde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      changelog.d/1886.feature
  2. 24
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollConstants.kt
  3. 98
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt
  4. 12
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt
  5. 130
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/PollFormState.kt
  6. 41
      features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt
  7. 48
      features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateSaverTest.kt
  8. 147
      features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateTest.kt

1
changelog.d/1886.feature

@ -0,0 +1 @@
Confirm back navigation when editing a poll only if the poll was changed

24
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
}

98
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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dagger.assisted.Assisted 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 im.vector.app.features.analytics.plan.PollCreation
import io.element.android.features.messages.api.MessageComposerContext import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.features.poll.api.create.CreatePollMode 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.features.poll.impl.data.PollRepository
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.poll.PollAnswer 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.PollKind
import io.element.android.libraries.matrix.api.poll.isDisclosed
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber 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( class CreatePollPresenter @AssistedInject constructor(
private val repository: PollRepository, private val repository: PollRepository,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
@ -64,18 +60,31 @@ class CreatePollPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): CreatePollState { override fun present(): CreatePollState {
var question: String by rememberSaveable { mutableStateOf("") } // The initial state of the form. In edit mode this will be populated with the poll being edited.
var answers: List<String> by rememberSaveable { mutableStateOf(listOf("", "")) } var initialPoll: PollFormState by rememberSaveable(stateSaver = pollFormStateSaver) {
var pollKind: PollKind by rememberSaveable(saver = pollKindSaver) { mutableStateOf(PollKind.Disclosed) } 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 showBackConfirmation: Boolean by rememberSaveable { mutableStateOf(false) }
var showDeleteConfirmation: Boolean by rememberSaveable { mutableStateOf(false) } var showDeleteConfirmation: Boolean by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (mode is CreatePollMode.EditPoll) { if (mode is CreatePollMode.EditPoll) {
repository.getPoll(mode.eventId).onSuccess { repository.getPoll(mode.eventId).onSuccess {
question = it.question val loadedPoll = PollFormState(
answers = it.answers.map(PollAnswer::text) question = it.question,
pollKind = it.kind answers = it.answers.map(PollAnswer::text).toPersistentList(),
isDisclosed = it.kind.isDisclosed,
)
initialPoll = loadedPoll
poll = loadedPoll
}.onFailure { }.onFailure {
analyticsService.trackGetPollFailed(it) analyticsService.trackGetPollFailed(it)
navigateUp() navigateUp()
@ -83,9 +92,9 @@ class CreatePollPresenter @AssistedInject constructor(
} }
} }
val canSave: Boolean by remember { derivedStateOf { canSave(question, answers) } } val canSave: Boolean by remember { derivedStateOf { poll.isValid } }
val canAddAnswer: Boolean by remember { derivedStateOf { canAddAnswer(answers) } } val canAddAnswer: Boolean by remember { derivedStateOf { poll.canAddAnswer } }
val immutableAnswers: ImmutableList<Answer> by remember { derivedStateOf { answers.toAnswers() } } val immutableAnswers: ImmutableList<Answer> by remember { derivedStateOf { poll.toUiAnswers() } }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -98,14 +107,14 @@ class CreatePollPresenter @AssistedInject constructor(
is CreatePollMode.EditPoll -> mode.eventId is CreatePollMode.EditPoll -> mode.eventId
is CreatePollMode.NewPoll -> null is CreatePollMode.NewPoll -> null
}, },
question = question, question = poll.question,
answers = answers, answers = poll.answers,
pollKind = pollKind, pollKind = poll.pollKind,
maxSelections = MAX_SELECTIONS, maxSelections = MAX_SELECTIONS,
).onSuccess { ).onSuccess {
analyticsService.capturePollSaved( analyticsService.capturePollSaved(
isUndisclosed = pollKind == PollKind.Undisclosed, isUndisclosed = poll.pollKind == PollKind.Undisclosed,
numberOfAnswers = answers.size, numberOfAnswers = poll.answers.size,
) )
}.onFailure { }.onFailure {
analyticsService.trackSavePollFailed(it, mode) analyticsService.trackSavePollFailed(it, mode)
@ -132,27 +141,25 @@ class CreatePollPresenter @AssistedInject constructor(
} }
} }
is CreatePollEvents.AddAnswer -> { is CreatePollEvents.AddAnswer -> {
answers = answers + "" poll = poll.withNewAnswer()
} }
is CreatePollEvents.RemoveAnswer -> { is CreatePollEvents.RemoveAnswer -> {
answers = answers.filterIndexed { index, _ -> index != event.index } poll= poll.withAnswerRemoved(event.index)
} }
is CreatePollEvents.SetAnswer -> { is CreatePollEvents.SetAnswer -> {
answers = answers.toMutableList().apply { poll = poll.withAnswerChanged(event.index, event.text)
this[event.index] = event.text.take(MAX_ANSWER_LENGTH)
}
} }
is CreatePollEvents.SetPollKind -> { is CreatePollEvents.SetPollKind -> {
pollKind = event.pollKind poll = poll.copy(isDisclosed = event.pollKind.isDisclosed)
} }
is CreatePollEvents.SetQuestion -> { is CreatePollEvents.SetQuestion -> {
question = event.question poll = poll.copy(question = event.question)
} }
is CreatePollEvents.NavBack -> { is CreatePollEvents.NavBack -> {
navigateUp() navigateUp()
} }
CreatePollEvents.ConfirmNavBack -> { CreatePollEvents.ConfirmNavBack -> {
val shouldConfirm = question.isNotBlank() || answers.any { it.isNotBlank() } val shouldConfirm = isDirty
if (shouldConfirm) { if (shouldConfirm) {
showBackConfirmation = true showBackConfirmation = true
} else { } else {
@ -173,9 +180,9 @@ class CreatePollPresenter @AssistedInject constructor(
}, },
canSave = canSave, canSave = canSave,
canAddAnswer = canAddAnswer, canAddAnswer = canAddAnswer,
question = question, question = poll.question,
answers = immutableAnswers, answers = immutableAnswers,
pollKind = pollKind, pollKind = poll.pollKind,
showBackConfirmation = showBackConfirmation, showBackConfirmation = showBackConfirmation,
showDeleteConfirmation = showDeleteConfirmation, showDeleteConfirmation = showDeleteConfirmation,
eventSink = ::handleEvents, eventSink = ::handleEvents,
@ -228,35 +235,12 @@ private fun AnalyticsService.trackSavePollFailed(cause: Throwable, mode: CreateP
trackError(exception) trackError(exception)
} }
private fun canSave( fun PollFormState.toUiAnswers(): ImmutableList<Answer> {
question: String, return answers.map { answer ->
answers: List<String>
) = question.isNotBlank() && answers.size >= MIN_ANSWERS && answers.all { it.isNotBlank() }
private fun canAddAnswer(answers: List<String>) = answers.size < MAX_ANSWERS
fun List<String>.toAnswers(): ImmutableList<Answer> {
return map { answer ->
Answer( Answer(
text = answer, text = answer,
canDelete = this.size > MIN_ANSWERS, canDelete = canDeleteAnswer,
) )
}.toImmutableList() }.toImmutableList()
} }
private val pollKindSaver: Saver<MutableState<PollKind>, Boolean> = Saver(
save = {
when (it.value) {
PollKind.Disclosed -> false
PollKind.Undisclosed -> true
}
},
restore = {
mutableStateOf(
when (it) {
true -> PollKind.Undisclosed
else -> PollKind.Disclosed
}
)
}
)

12
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) } val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) }
BackHandler(onBack = navBack) BackHandler(onBack = navBack)
if (state.showBackConfirmation) ConfirmationDialog( if (state.showBackConfirmation) {
content = stringResource(id = R.string.screen_create_poll_cancel_confirmation_content_android), ConfirmationDialog(
onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) }, content = stringResource(id = R.string.screen_create_poll_cancel_confirmation_content_android),
onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) },
) onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) }
)
}
if (state.showDeleteConfirmation) { if (state.showDeleteConfirmation) {
ConfirmationDialog( ConfirmationDialog(
title = stringResource(id = R.string.screen_edit_poll_delete_confirmation_title), title = stringResource(id = R.string.screen_edit_poll_delete_confirmation_title),

130
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<String>,
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,
)
}
)

41
features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt

@ -364,7 +364,7 @@ class CreatePollPresenterTest {
} }
@Test @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) val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -378,20 +378,52 @@ class CreatePollPresenterTest {
} }
@Test @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) val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initial = awaitItem() val initial = awaitItem()
initial.eventSink(CreatePollEvents.SetQuestion("Non blank")) initial.eventSink(CreatePollEvents.SetQuestion("Non blank"))
Truth.assertThat(navUpInvocationsCount).isEqualTo(0)
Truth.assertThat(awaitItem().showBackConfirmation).isFalse() Truth.assertThat(awaitItem().showBackConfirmation).isFalse()
initial.eventSink(CreatePollEvents.ConfirmNavBack) initial.eventSink(CreatePollEvents.ConfirmNavBack)
Truth.assertThat(navUpInvocationsCount).isEqualTo(0)
Truth.assertThat(awaitItem().showBackConfirmation).isTrue() Truth.assertThat(awaitItem().showBackConfirmation).isTrue()
initial.eventSink(CreatePollEvents.HideConfirmation) initial.eventSink(CreatePollEvents.HideConfirmation)
Truth.assertThat(awaitItem().showBackConfirmation).isFalse() 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<CreatePollState>.awaitDefaultItem() = private suspend fun TurbineTestContext<CreatePollState>.awaitDefaultItem() =
awaitItem().apply { awaitItem().apply {
Truth.assertThat(canSave).isFalse() Truth.assertThat(canSave).isFalse()

48
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)
}
}

147
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())
Loading…
Cancel
Save