Here I have a fragment of a component whose task i...
# compose-android
y
Here I have a fragment of a component whose task is only to respond to 'onClick', and 'doubleClick' should be consumed.
Copy code
@Test
    fun combinedClickableTest() {
        var counter = 0
        val onCLick: () -> Unit = { ++counter }
        val tag = "ButtonTag"
        composeTestRule.setContent {
            BasicText(
                text = "Button",
                modifier = Modifier
                    .testTag(tag)
                    .combinedClickable(
                        onClick = {
                            onCLick()
                        },
                        onDoubleClick = {
                        }
                    ))
        }

        composeTestRule.onNodeWithTag(tag).performClick()  <------ It is not working
        composeTestRule.onNodeWithTag(tag).performSemanticsAction(SemanticsActions.OnClick) <------- It is working

        composeTestRule.onNodeWithTag(tag)
            .assertIsDisplayed()
            .assertHasClickAction()

        composeTestRule.runOnIdle { assertEquals(1, counter) }
    }
Why does the test that executes 'performClick' fail, while the test that executes 'performSemanticsAction' succeeds?
a
Because there is non-null lambda passed to
onDoubleClick
, the
combinedClickable
internally needs to disambiguate between whether the user is performing a click, or if they are performing a double click. When
performClick()
is called the inputs that are dispatched advance through time just enough to perform a single click - but at that point,
combinedClickable
still doesn't know if there is another click coming afterwards (to trigger the double click) or if that's all the input that it is going to see. To figure that out, more time needs to pass. If you call
composeTestRule.mainClock.advanceTimeBy(1000)
after
performClick()
, that will force enough time to pass to trigger the
onClick
call since it wasn't a double click. It is a bit confusing why
runOnIdle
doesn't do that. The exact value to wait for is defined from the platform:
LocalViewConfiguration.current.doubleTapTimeoutMillis
The
performSemanticsAction
bypasses the input disambiguation and immediately executes the
OnClick
action, so that's why that one works directly.
👍 1
y
Thank you very much for explaining this missing part. Now I'm wondering which solution is recommended. After all, in E2E tests, we want to replicate user behavior and use
performClick()
, but on the other hand, such tests are more susceptible to being flaky.
a
Hmm - I don't think there's much you can do other than the
advanceTimeBy
right now. What I feel like should be possible (but doesn't work right now) is defining something like this:
Copy code
fun SemanticsNodeInteraction.performSingleClick(): SemanticsNodeInteraction =
    performTouchInput {
        click()
        advanceEventTime(viewConfiguration.doubleTapTimeoutMillis) // advance time so that we go past the double click timeout
    }
But that doesn't work right now, since the
advanceEventTime
will only call through to
advanceTimeBy
if another event is sent. And you explicitly don't want another event sent 🙁
👍 1
y
Okay, thanks for the clarification. The reason I want to consume double clicks is that I use Redux, and double clicks cause a lot of confusion.
l
That seems a bit suspicious though, instead of consuming double clicks, shouldn't you just handle that inside your click handling logic? If a click is received within some time of the previous one, ignore it
But you shouldn't be consuming / defining a semantic double click action, if nothing happens
y
I thought about it, but I was wondering what is easier to manage - skipping the double click event, which is 100% certain, or trying to add a timeout, which may not cover all cases