You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose Instrumented测试中performTextInput偶发替换全部文本问题

Compose Instrumented测试中performTextInput偶发替换全部文本的优化方案

问题场景

通常Compose Instrumented测试里的performTextInput(...)会在现有文本的光标位置插入内容,但在极少数测试场景下,会直接替换掉输入框的全部文本。比如以下测试代码:

val textBeforeCursor = "abc"
val textAfterCursor = "def"
composeRule.onNodeWithTag("item_field1").assertTextEquals("${textBeforeCursor}${textAfterCursor}")

// 在光标位置执行文本输入
val middleText = "ghi"
composeRule.onNodeWithTag("item_field1").performTextInput(middleText)
composeRule.onNodeWithTag("item_field1").assertTextEquals("${textBeforeCursor}${middleText}${textAfterCursor}")
// 极少数情况下断言失败,实际文本变成了middleText,原有文本被完全替换

原因分析

查看Compose的EditCommand.kt源码可知,performTextInput对应的编辑命令逻辑是:

override fun applyTo(buffer: EditingBuffer) {
    // API description says replace ongoing composition text if there. Then, if there is no
    // composition text, insert text into cursor position or replace selection.
    if (buffer.hasComposition()) {
        buffer.replace(buffer.compositionStart, buffer.compositionEnd, text)
    } else {
        // In this editing buffer, insert into cursor or replace selection are equivalent.
        buffer.replace(buffer.selectionStart, buffer.selectionEnd, text)
    }
    ...
}

当编辑缓冲区存在未完成的composition(输入法输入过程中的临时文本状态,比如中文输入未确认候选词时的状态)时,会直接替换整个composition范围的文本。测试中偶发的问题,大概率是因为测试时序或状态异步导致输入框残留了未完成的composition状态。

现有临时方案的不足

目前采用的try-catch断言方案虽然能绕过测试失败,但逻辑过于粗糙:

try {
    composeRule.onNodeWithTag("item_field1").assertTextEquals("${textBeforeCursor}${middleText}${textAfterCursor}")
} catch (e: AssertionError) {
    composeRule.onNodeWithTag("item_field1").assertTextEquals(middleText)
}

这种写法会掩盖其他真实的断言失败场景(比如输入框文本确实不符合预期的错误),无法精准定位问题。

优化解决办法

1. 提前提交未完成的Composition

在执行performTextInput前,先提交所有未完成的composition,确保缓冲区处于正常状态:

import androidx.compose.ui.text.input.CommitComposition

// 先提交未完成的composition,再执行输入
composeRule.onNodeWithTag("item_field1")
    .performEditCommand(CommitComposition)
    .performTextInput(middleText)

// 正常断言即可
composeRule.onNodeWithTag("item_field1").assertTextEquals("${textBeforeCursor}${middleText}${textAfterCursor}")

2. 明确设置文本与光标位置,避免状态依赖

不依赖之前的文本状态,而是通过performTextReplacement直接设置目标文本,并手动移动光标到指定位置,确保输入前状态完全可控:

import androidx.compose.ui.test.performCursorMove
import androidx.compose.ui.test.CursorPosition

composeRule.onNodeWithTag("item_field1")
    // 直接设置初始文本
    .performTextReplacement("${textBeforeCursor}${textAfterCursor}")
    // 将光标移动到textBeforeCursor的末尾,也就是预期的插入位置
    .performCursorMove(CursorPosition.CharOffset(textBeforeCursor.length))

// 执行输入
composeRule.onNodeWithTag("item_field1").performTextInput(middleText)

// 正常断言
composeRule.onNodeWithTag("item_field1").assertTextEquals("${textBeforeCursor}${middleText}${textAfterCursor}")

3. 等待Compose状态稳定

测试中偶发的状态不一致可能是异步操作未完成导致的,操作前等待Compose进入idle状态:

// 等待所有异步操作完成,确保状态稳定
composeRule.waitForIdle()

composeRule.onNodeWithTag("item_field1").performTextInput(middleText)
composeRule.onNodeWithTag("item_field1").assertTextEquals("${textBeforeCursor}${middleText}${textAfterCursor}")

关于实际用户场景的说明

实际应用中,用户手动输入时,未完成的composition会在确认输入(比如按下空格、回车或选择候选词)后自动提交,因此该问题出现的概率极低,基本不会影响用户正常使用。

内容的提问来源于stack exchange,提问作者Courtesy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 20:40:26