Espresso点击Compose内AndroidView嵌套ButtonView时onClick不生效
问题现象
使用Espresso测试通过AndroidView可组合项嵌套原生Button View的Jetpack Compose页面时,执行Espresso.onView(ViewMatchers.withText("Click me")).perform(ViewActions.click())点击操作后,按钮的onClick回调未触发,最终校验文本更新的断言失败。
- 应用正常运行时,该按钮点击功能完全可用
- 测试中校验按钮可见、按钮可点击的两项断言均能通过,仅点击动作无法触发回调
- 同逻辑的纯Compose实现页面,测试可正常执行通过
环境依赖
implementation "androidx.compose.ui:ui:1.1.1" androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.1.1" debugImplementation "androidx.compose.ui:ui-tooling:1.1.1" debugImplementation "androidx.compose.ui:ui-test-manifest:1.1.1"
复现代码
混合Compose页面实现(运行时功能正常)
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { SampleAppTheme { HybridSampleScreen() } } } } @Composable fun HybridSampleScreen() { var text by rememberSaveable { mutableStateOf("Initial Text") } Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background ) { Column() { ComposableWithAndroidButton { text = "I've been clicked" } Text(text) } } } @Composable fun ComposableWithAndroidButton(onButtonClick: () -> Unit) { Column() { Text(text = "Hello World") AndroidView(factory = { context -> // 别名用于在同一文件中区分原生Button和Compose Button ClassicViewButton(context).apply { setOnClickListener { onButtonClick.invoke() } text = "Click me" } }) } }
失败的测试代码
@RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @get:Rule val composeTestRule = createComposeRule() @Test fun testAndroidViewButtonInComposable() { composeTestRule.setContent { SampleAppTheme { HybridSampleScreen() } } composeTestRule.onNodeWithText("Hello World").assertIsDisplayed() // 以下两个断言可正常通过,说明Espresso能正确识别到可点击的Button Espresso.onView(ViewMatchers.withText("Click me")).check(matches(isDisplayed())) Espresso.onView(ViewMatchers.withText("Click me")).check(matches(isClickable())) Espresso.onView(ViewMatchers.withText("Click me")).perform(ViewActions.click()) composeTestRule.waitForIdle() composeTestRule.onNodeWithText("I've been clicked").assertIsDisplayed() } }
对照用纯Compose页面实现
@Composable fun FullComposeScreen() { var text by rememberSaveable { mutableStateOf("Initial Text") } Column { ComposableWithComposeButton { text = "I've been clicked" } Text(text = text) } } @Composable fun ComposableWithComposeButton(onButtonClick: () -> Unit) { Column() { Text(text = "Hello World") Button(onClick = { onButtonClick.invoke() }) { Text(text = "I'm a composable click me") } } }
可正常通过的纯Compose测试代码
@Test fun testFullComposeButtonInComposable() { composeTestRule.setContent { SampleAppTheme { FullComposeScreen() } } composeTestRule.onNodeWithText("Hello World").assertIsDisplayed() composeTestRule.onNodeWithText("I'm a composable click me").assertIsDisplayed() composeTestRule.onNodeWithText("I'm a composable click me").assertHasClickAction() composeTestRule.onNodeWithText("I'm a composable click me").performClick() composeTestRule.waitForIdle() composeTestRule.onNodeWithText("I've been clicked").assertIsDisplayed() }
问题原因
核心原因是无参createComposeRule()默认没有开启Espresso和Compose的完整互操作同步:
- 该规则不会自动注册Compose空闲状态对应的Espresso IdlingResource,Espresso执行点击动作时,无法感知Compose是否完成布局、重组,可能在
AndroidView还没完成事件分发链路初始化时就派发事件,导致点击丢失 - 无参
createComposeRule()使用的测试宿主环境没有正确配置原生View和Compose层的事件传递优先级,Espresso派发的点击事件没有落到实际的原生Button实例上
解决方案
- 方案一(最稳妥,无需修改测试逻辑):将测试规则替换为
createAndroidComposeRule<ComponentActivity>(),该规则是专门为混合场景设计的,会自动处理Espresso和Compose的空闲同步、事件分发逻辑,替换后原有测试代码可直接运行通过 - 方案二:保留现有规则的前提下,在执行Espresso点击前手动同步两个框架的空闲状态,在
perform(click())前增加两行代码:composeTestRule.waitForIdle() Espresso.onIdle() - 方案三:不使用Espresso API,直接通过Compose测试规则点击对应位置的节点,Compose测试框架会自动把事件派发给对应位置的原生View:
注意该方案在Compose 1.1.1版本中需要给composeTestRule.onNodeWithText("Click me").performClick()AndroidView补充Modifier.testTag辅助定位,部分场景下文本匹配可能无法直接命中嵌入的原生View
内容的提问来源于stack exchange,提问作者skon
相关产品推荐
相关产品推荐

