Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,14 @@ class FilteredDeckOptionsViewModel(
init {
viewModelScope.launch {
Timber.i("Starting filtered deck options setup, deckId=$did")
// needed on both paths: name validation is done against this list
decksNames = withCol { safeGetDecksNames() }
val previousState = savedStateHandle.get<FilteredDeckOptions>(ARG_DATA)
if (previousState != null) {
initialState = savedStateHandle[ARG_INITIAL_DATA]
state.update { previousState }
Comment thread
david-allison marked this conversation as resolved.
// changes made before the view model was destroyed are still unsaved
hasUnsavedChanges.update { wasStateModified() }
return@launch
}
Timber.i("No previous stored state, querying the collection")
Expand All @@ -82,14 +87,14 @@ class FilteredDeckOptionsViewModel(
state.update { Initializing(throwable = throwable) }
return@launch
}
decksNames = withCol { safeGetDecksNames() }
filteredDeckData
.asInitialState(
cardsOptions = cardsOptions,
defaultSearch1 = search,
defaultSearch2 = search2,
).apply {
savedStateHandle[ARG_DATA] = this
savedStateHandle[ARG_INITIAL_DATA] = this
initialState = this
}
state.update { currentState() }
Expand All @@ -98,13 +103,16 @@ class FilteredDeckOptionsViewModel(

fun onDeckNameChange(name: String) {
Timber.i("Filtered deck name is changing")
val current = currentState()
val error =
when {
name.isBlank() -> FilteredNameInputError.Empty
// when editing a deck, its own name doesn't conflict with itself
current.id != null && name == current.title -> null
decksNames.contains(name) -> FilteredNameInputError.AlreadyExists
else -> null
}
if (currentState().name == name) return
if (current.name == name) return
updateCurrentState { copy(name = name, nameInputError = error) }
hasUnsavedChanges.update { wasStateModified() }
}
Expand Down Expand Up @@ -492,5 +500,11 @@ class FilteredDeckOptionsViewModel(
companion object {
/** Key used to store/retrieve our state in [SavedStateHandle]. */
private const val ARG_DATA = "arg_data"

/**
* Key used to store/retrieve the state as it was first loaded in [SavedStateHandle]. Needed
* to detect unsaved changes after the view model is recreated.
*/
private const val ARG_INITIAL_DATA = "arg_initial_data"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -391,10 +391,85 @@ class FilteredDeckOptionsViewModelTest : RobolectricTest() {
}
}

@Test
fun `name validation still works after the screen is restored`() =
runTest {
addDeck("A")
withRestoredViewModel {
onDeckNameChange("A")
assertThat(current.nameInputError, equalTo(FilteredNameInputError.AlreadyExists))
assertFalse(current.isBuildingAllowed)
}
}

@Test
fun `changed status still works after the screen is restored`() =
runTest {
addDeck("A")
withRestoredViewModel {
assertFalse(hasUnsavedChanges.value)
onSearchChange(FilterIndex.First, "flag:3")
assertTrue(hasUnsavedChanges.value)
}
}

@Test
fun `changes made before the screen is restored are still reported`() =
runTest {
addDeck("A")
withRestoredViewModel(
beforeDestroy = { onSearchChange(FilterIndex.First, "flag:3") },
) {
assertTrue(hasUnsavedChanges.value)
}
}

@Test
fun `reverting to the deck's own name is not a duplicate`() =
runTest {
val testDid = createTestFilteredDeck()
withViewModel(did = testDid) {
onDeckNameChange("Not")
onDeckNameChange("Filtered")
assertNull(current.nameInputError)
assertTrue(current.isBuildingAllowed)
}
}

/** Returns the current state as a [FilteredDeckOptions] or throw otherwise */
private val FilteredDeckOptionsViewModel.current: FilteredDeckOptions
get() = state.value as FilteredDeckOptions

/**
* Builds a view model and invokes [beforeDestroy] on it, then discards it and builds a second
* one over the same [SavedStateHandle], simulating the view model being destroyed while its
* saved state survives.
*/
private fun TestScope.withRestoredViewModel(
did: DeckId = 0,
beforeDestroy: FilteredDeckOptionsViewModel.() -> Unit = {},
action: FilteredDeckOptionsViewModel.() -> Unit,
) {
val handle =
SavedStateHandle().apply {
set(FilteredDeckOptionsFragment.ARG_DECK_ID, did)
set(FilteredDeckOptionsFragment.ARG_SEARCH, null as String?)
set(FilteredDeckOptionsFragment.ARG_SEARCH_2, null as String?)
}
FilteredDeckOptionsViewModel(handle).apply {
advanceUntilIdle()
advanceRobolectricLooper()
beforeDestroy()
}
advanceUntilIdle()
advanceRobolectricLooper()

val restoredViewModel = FilteredDeckOptionsViewModel(handle)
advanceUntilIdle()
advanceRobolectricLooper()
restoredViewModel.action()
}

private fun TestScope.withViewModel(
did: DeckId = 0,
search: String? = null,
Expand Down
Loading