fix(chat): scroll long composer model names (#1129)
This commit is contained in:
2 files changed
+149
-33
No files matched your search
+117
-21
@@ -1,16 +1,26 @@
|
||||
package com.m57.hermescontrol.ui.chat.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.getUnclippedBoundsInRoot
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.compose.ui.test.swipeLeft
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
import com.m57.hermescontrol.data.ws.CommandCatalog
|
||||
@@ -36,30 +46,46 @@ class ComposerInteractionTest {
|
||||
private var selectedLevel: String? = null
|
||||
|
||||
/** Renders the real input bar with live text and a mic that toggles like ChatMediaLaunchers. */
|
||||
private fun setComposer(reasoningLevel: String? = "medium") {
|
||||
private fun setComposer(
|
||||
reasoningLevel: String? = "medium",
|
||||
model: String = "openai/gpt-5.5",
|
||||
composerWidth: Dp? = null,
|
||||
modelAfterTap: String? = null,
|
||||
) {
|
||||
composeTestRule.setContent {
|
||||
var input by remember { mutableStateOf(TextFieldValue("")) }
|
||||
var listening by remember { mutableStateOf(false) }
|
||||
ChatInputBar(
|
||||
inputFieldValue = input,
|
||||
onInputChange = { input = it },
|
||||
onSend = {
|
||||
sends++
|
||||
input = TextFieldValue("")
|
||||
},
|
||||
onMicTap = {
|
||||
micTaps++
|
||||
listening = !listening
|
||||
},
|
||||
isListening = listening,
|
||||
isAgentTyping = false,
|
||||
isConnected = true,
|
||||
commandCatalog = CommandCatalog(),
|
||||
currentSessionModel = "openai/gpt-5.5",
|
||||
reasoningLevel = reasoningLevel,
|
||||
onModelTap = { modelTaps++ },
|
||||
onReasoningTap = { selectedLevel = it },
|
||||
)
|
||||
var currentModel by remember { mutableStateOf(model) }
|
||||
val composer: @Composable () -> Unit = {
|
||||
ChatInputBar(
|
||||
inputFieldValue = input,
|
||||
onInputChange = { input = it },
|
||||
onSend = {
|
||||
sends++
|
||||
input = TextFieldValue("")
|
||||
},
|
||||
onMicTap = {
|
||||
micTaps++
|
||||
listening = !listening
|
||||
},
|
||||
isListening = listening,
|
||||
isAgentTyping = false,
|
||||
isConnected = true,
|
||||
commandCatalog = CommandCatalog(),
|
||||
currentSessionModel = currentModel,
|
||||
reasoningLevel = reasoningLevel,
|
||||
onModelTap = {
|
||||
modelTaps++
|
||||
modelAfterTap?.let { currentModel = it }
|
||||
},
|
||||
onReasoningTap = { selectedLevel = it },
|
||||
)
|
||||
}
|
||||
if (composerWidth == null) {
|
||||
composer()
|
||||
} else {
|
||||
Box(Modifier.width(composerWidth).testTag("composer_toolbar")) { composer() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,4 +177,74 @@ class ComposerInteractionTest {
|
||||
assertEquals("reasoning side must not open the model picker", 0, modelTaps)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun longModelName_narrowComposer_scrollsWithoutOpeningPicker_orHidingControls() {
|
||||
val longModel = "openrouter/some-extremely-long-model-name-preview-2026-with-extra-characters"
|
||||
setComposer(model = longModel, composerWidth = 280.dp)
|
||||
|
||||
composeTestRule.onNodeWithTag("reasoning_chip").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("mic_button").assertIsDisplayed()
|
||||
val modelText = composeTestRule.onNodeWithText(longModel)
|
||||
val leftBefore = modelText.getUnclippedBoundsInRoot().left
|
||||
|
||||
composeTestRule.onNodeWithTag("model_chip").performTouchInput { swipeLeft() }
|
||||
val leftAfter = modelText.getUnclippedBoundsInRoot().left
|
||||
|
||||
composeTestRule.runOnIdle {
|
||||
assertEquals("swiping the model must not open the picker", 0, modelTaps)
|
||||
}
|
||||
check(leftAfter < leftBefore) { "model text did not move after horizontal swipe" }
|
||||
composeTestRule.onNodeWithTag("reasoning_chip").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("mic_button").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shortModelName_wideComposerKeepsPillCompact() {
|
||||
setComposer(composerWidth = 420.dp)
|
||||
|
||||
val toolbarBounds =
|
||||
composeTestRule
|
||||
.onNodeWithTag("composer_toolbar", useUnmergedTree = true)
|
||||
.getUnclippedBoundsInRoot()
|
||||
val toolbarWidth = toolbarBounds.right - toolbarBounds.left
|
||||
val modelChip = composeTestRule.onNodeWithTag("model_chip").getUnclippedBoundsInRoot()
|
||||
val reasoningChip = composeTestRule.onNodeWithTag("reasoning_chip").getUnclippedBoundsInRoot()
|
||||
val actionButton = composeTestRule.onNodeWithTag("mic_button").getUnclippedBoundsInRoot()
|
||||
val pillWidth = maxOf(modelChip.right, reasoningChip.right) - minOf(modelChip.left, reasoningChip.left)
|
||||
|
||||
check(pillWidth < toolbarWidth * 0.8f) {
|
||||
"short model pill should remain content-sized, width=$pillWidth toolbar=$toolbarWidth"
|
||||
}
|
||||
check(actionButton.right > toolbarBounds.right - 32.dp) {
|
||||
"trailing action should stay near the toolbar edge, right=${actionButton.right} toolbarRight=${toolbarBounds.right}"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun changingModel_resetsScrolledLabelToTheStart() {
|
||||
val firstModel = "openrouter/some-extremely-long-model-name-preview-2026-with-extra-characters"
|
||||
val secondModel = "anthropic/another-extremely-long-model-name-preview-2026-with-extra-characters"
|
||||
setComposer(model = firstModel, modelAfterTap = secondModel, composerWidth = 280.dp)
|
||||
|
||||
val firstText = composeTestRule.onNodeWithText(firstModel)
|
||||
val leftBefore = firstText.getUnclippedBoundsInRoot().left
|
||||
composeTestRule.onNodeWithTag("model_chip").performTouchInput { swipeLeft() }
|
||||
check(firstText.getUnclippedBoundsInRoot().left < leftBefore) {
|
||||
"first model text did not move after horizontal swipe"
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("model_chip").performClick()
|
||||
val secondText = composeTestRule.onNodeWithText(secondModel)
|
||||
val leftAfterModelChange = secondText.getUnclippedBoundsInRoot().left
|
||||
|
||||
check(leftAfterModelChange >= leftBefore - 1.dp) {
|
||||
"new model label should reset to its initial left position"
|
||||
}
|
||||
composeTestRule.onNodeWithTag("reasoning_chip").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("mic_button").assertIsDisplayed()
|
||||
composeTestRule.runOnIdle {
|
||||
assertEquals("model selection should still invoke the picker", 1, modelTaps)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user