My use case is the following
I am trying to show a list of emails in a lazy column which in turn supports swipe to dismiss behaviour. I am trying to trigger the on swipe action in compose in my tests but it is not getting triggered. I would appreciate any assistance in this regard
My source is as follows
- LazyColumn to show a list of data.
const val TAG_CONTENT = "content"
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EmailContentList(
modifier: Modifier = Modifier,
emails: List<Email>,
onInboxEvent: (InboxEvent) -> Unit
) {
LazyColumn(modifier = modifier) {
items(emails, key = {item -> item.id }) { email ->
var isEmailDeleted by remember {
mutableStateOf(false)
}
val dismissBoxState = rememberSwipeToDismissBoxState(
positionalThreshold = { distance ->
distance * .25f
}
)
// See https://slack-chats.kotlinlang.org/t/16382816/hi-since-the-compose-bom-composebom-2024-02-00-version-the-i#865df5e5-39c6-4dd3-9bc2-8f087300dd8d
LaunchedEffect(key1 = dismissBoxState) {
snapshotFlow {
dismissBoxState.currentValue
}.collect {
if (it == SwipeToDismissBoxValue.StartToEnd) {
isEmailDeleted = true
onInboxEvent(InboxEvent.DeleteEvent(email.id))
} else {
isEmailDeleted = false
}
}
}
val emailHeight by animateDpAsState(targetValue =
if (isEmailDeleted) {
0.dp
} else {
120.dp
},
label = "email_height",
animationSpec = tween(delayMillis = 300)
)
EmailContent(
modifier = Modifier
.padding(dimensionResource(id = R.dimen.email_padding_half))
.defaultMinSize(minHeight = emailHeight)
.fillMaxWidth(),
email = email,
height = emailHeight,
onAccessibilityDelete = {
onInboxEvent(InboxEvent.DeleteEvent(email.id))
},
dismissState = dismissBoxState
)
}
}
}
InboxEvent is a sealed interface encapsulating various states
sealed interface InboxEvent {
data object RefreshEvent: InboxEvent
data class DeleteEvent(val id: String): InboxEvent
}
data class
@Immutable
data class Email(
val id: String,
val title: String,
val description: String
)
- Composable to display a single email
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EmailContent(
modifier: Modifier = Modifier,
height: Dp,
email: Email,
onAccessibilityDelete: () -> Unit,
dismissState: SwipeToDismissBoxState
) {
SwipeToDismissBox(
modifier = modifier,
state = dismissState,
enableDismissFromStartToEnd = true,
enableDismissFromEndToStart = false,
backgroundContent = {
val deleteAsString = stringResource(id = R.string.inbox_delete)
SwipeDismissBox(
modifier = Modifier
.defaultMinSize(minHeight = height)
.fillMaxWidth()
.semantics {
customActions = listOf(
CustomAccessibilityAction(label = deleteAsString) {
onAccessibilityDelete.invoke()
true
}
)
},
targetValue = dismissState.targetValue
)
}
) {
val cardElevation by animateDpAsState(targetValue =
if (dismissState.targetValue == SwipeToDismissBoxValue.StartToEnd) {
dimensionResource(id = R.dimen.email_padding_half)
} else {
0.dp
},
label = "card_elevation"
)
Card(modifier = modifier.then(Modifier.fillMaxWidth()), elevation = CardDefaults.cardElevation(cardElevation)) {
ListItem(
modifier = Modifier.fillMaxWidth(),
headlineContent = {
Text(text = email.title, style = MaterialTheme.typography.headlineSmall)
},
supportingContent = {
Text(text = email.description, style = MaterialTheme.typography.bodyMedium,
maxLines = 2, overflow = TextOverflow.Ellipsis
)
}
)
}
}
}
The test to trigger a swipe to dismiss action
class EmailContentListTest {
private lateinit var emails: List<Email>
@get:Rule
val composeRule = createComposeRule()
@Before
fun setUp() {
emails = listOf(
Email(id = "1", title = "title", description = "description")
)
}
@Test
fun validateThatEmailListIsSeen() {
composeRule.setContent {
JetpackComposeAuthenticationTheme {
EmailContentList(modifier = Modifier.fillMaxSize(), emails = emails) {
}
}
}
composeRule.run {
emails.forEach { email ->
onNodeWithText(email.title).assertIsDisplayed()
onNodeWithText(email.description).assertIsDisplayed()
}
}
}
@Test
fun validateThatSwipeActionIsLessThanThresholdThenNoActionIsTaken() {
composeRule.setContent {
JetpackComposeAuthenticationTheme {
EmailContentList(modifier = Modifier.fillMaxSize(), emails = emails) {
}
}
}
composeRule.run {
emails.first().also { email ->
onNodeWithText(email.title).assertIsDisplayed()
onNodeWithText(email.description).assertIsDisplayed()
onNodeWithText(email.description).performTouchInput {
// Swipe partially under the threshold to 20% of the horizontal distance
// See https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/BasicSwipeToDismissBoxTest.kt;l=136-146?q=SwipeToDismissBoxTest
swipeWithVelocity(
start = Offset(0f, centerY),
end = Offset(centerX / 5f, centerY),
endVelocity = 1.0f
)
}
onNodeWithText(email.title).assertIsDisplayed()
onNodeWithText(email.description).assertIsDisplayed()
}
}
}
// This is a failing test
@Test
fun validateThatSwipeActionIsGreaterThanThresholdThenDeleteActionIsTaken() {
var isDeleted = false
composeRule.setContent {
JetpackComposeAuthenticationTheme {
EmailContentList(modifier = Modifier.fillMaxSize(), emails = emails) {
isDeleted = true
}
}
}
composeRule.run {
emails.forEachIndexed { index, email ->
onNodeWithTag(TAG_CONTENT).onChildAt(index).performScrollTo().performTouchInput {
swipeRight()
}
// This line fails as the the callback lambda function is not called.
MatcherAssert.assertThat(isDeleted, Matchers.equalTo(true))
}
}
}
}
I have tried to follow the pattern as shown in See https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/BasicSwipeToDismissBoxTest.kt;l=136-146?q=SwipeToDismissBoxTest but it is not triggering the swipe callback. How can I trigger a callback on swipe action in tests?