fix(overview): Reset the review card latch when it leaves the list

The card is a keyed item in the overview's lazy list, which restores the
saveable state of removed items when they come back. A review tap left
the dismiss action permanently disabled once a higher priority card took
the slot and gave it back, and a dismissed card returned fully dead.

Fixes review finding F1.
This commit is contained in:
darken
2026-08-06 09:55:09 +02:00
committed by Matthias Urhahn
parent 19976c89f6
commit 11232e9169
2 changed files with 45 additions and 3 deletions
@@ -19,7 +19,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -43,8 +43,13 @@ fun ReviewCard(
// the review bookkeeping with a snooze) and a review after a dismiss. A repeated review tap is
// harmless, the tool's single-flight lock absorbs it, and blocking it here would leave a dead
// card whenever a Play request fails and nothing gets persisted.
var dismissLocked by rememberSaveable { mutableStateOf(false) }
var fullyLatched by rememberSaveable { mutableStateOf(false) }
//
// Plain remember, not rememberSaveable: the card is a keyed item in a lazy list, which hands
// saveable state back when the item returns, so a card that was removed by a higher priority
// card would come back still latched. Disposal is the intended reset, the latch only has to
// survive the sub-second window until the next state emission takes the card away.
var dismissLocked by remember { mutableStateOf(false) }
var fullyLatched by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
@@ -1,6 +1,8 @@
package eu.darken.capod.main.ui.overview.cards
import android.content.Context
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
@@ -126,6 +128,41 @@ class ReviewCardTest : BaseComposeRobolectricTest() {
}
}
@Test
fun `a card that left the list comes back unlatched`() {
val visible = mutableStateOf(true)
composeRule.setContent {
PreviewWrapper {
// Mirrors OverviewScreen: the card is a keyed item in a lazy list, which keeps the
// saveable state of removed items around and hands it back when they return.
LazyColumn {
if (visible.value) {
item(key = "review") {
ReviewCard(
onReview = { reviews++ },
onDismiss = { dismisses++ },
)
}
}
}
}
}
composeRule.onNode(reviewButton).performSemanticsAction(SemanticsActions.OnClick)
composeRule.runOnIdle { reviews shouldBe 1 }
composeRule.onNodeWithText(dismissLabel).assertIsNotEnabled()
// A higher priority card takes the slot and gives it back, e.g. Bluetooth flipping off
composeRule.runOnIdle { visible.value = false }
composeRule.waitForIdle()
composeRule.runOnIdle { visible.value = true }
composeRule.waitForIdle()
// The latch only guards the taps on one showing of the card, it must not outlive it
composeRule.onNode(reviewButton).assertIsEnabled()
composeRule.onNodeWithText(dismissLabel).assertIsEnabled()
}
@Test
fun `a review tap without an activity consumes neither latch`() {
setContent(withActivity = false)