fix(chat): searchable tool/reasoning rows and live markdown (#1361)
This commit is contained in:
19 files changed
+454
-146
No files matched your search
@@ -0,0 +1,49 @@
|
||||
package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
import com.m57.hermescontrol.ui.chat.components.ReasoningCard
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@MediumTest
|
||||
class ChatSearchRenderingTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun reasoningMatchExpandsCollapsedCardAndShowsTheHit() {
|
||||
composeTestRule.setContent {
|
||||
MaterialTheme {
|
||||
ReasoningCard(
|
||||
reasoningText = "a hidden needle in the trace",
|
||||
searchQuery = "needle",
|
||||
isCurrentMatch = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("a hidden needle in the trace").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toolMatchRemainsVisibleInCollapsedHeader() {
|
||||
composeTestRule.setContent {
|
||||
MaterialTheme {
|
||||
ToolBubble(
|
||||
message = ChatMessage(role = MessageRole.TOOL, content = "opaque", toolName = "terminal"),
|
||||
searchQuery = "terminal",
|
||||
isCurrentMatch = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("terminal").assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
+17
@@ -61,4 +61,21 @@ class MarkdownHeadingRenderingTest {
|
||||
composeTestRule.onNodeWithText("val quoted = 1", substring = true).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("val listed = 2", substring = true).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun streamingMarkdownRendersBlocksBeforeCompletion() {
|
||||
composeTestRule.setContent {
|
||||
MaterialTheme {
|
||||
MarkdownText(
|
||||
text = "## Live heading\n\n**ready**\n\n```kotlin\nval x = 1\n```",
|
||||
textColor = MaterialTheme.colorScheme.onSurface,
|
||||
isStreaming = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Live heading").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("ready").assertIsDisplayed()
|
||||
composeTestRule.onAllNodesWithTag("code_block").assertCountEquals(1)
|
||||
}
|
||||
}
|
||||
+31
-10
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
@@ -25,11 +26,11 @@ import com.m57.hermescontrol.ui.chat.ChatSearchState
|
||||
import com.m57.hermescontrol.ui.chat.ChatTimelineState
|
||||
import com.m57.hermescontrol.ui.chat.ChatUiState
|
||||
import com.m57.hermescontrol.ui.chat.MessageRole
|
||||
import com.m57.hermescontrol.ui.chat.SearchTarget
|
||||
import com.m57.hermescontrol.ui.chat.StreamingState
|
||||
import com.m57.hermescontrol.ui.chat.ToolStatus
|
||||
import com.m57.hermescontrol.ui.chat.components.ChatScrollController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import com.m57.hermescontrol.ui.chat.components.rememberChatScrollController
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -78,17 +79,13 @@ class FullBleedChatListTest {
|
||||
showUserMessageTokens: Boolean = true,
|
||||
showAssistantMessageTokens: Boolean = true,
|
||||
showTokensPerSecond: Boolean = true,
|
||||
searchState: ChatSearchState = ChatSearchState(),
|
||||
onController: (ChatScrollController) -> Unit = {},
|
||||
) {
|
||||
composeTestRule.setContent {
|
||||
val listState = remember { LazyListState() }
|
||||
val scrollController =
|
||||
remember(listState) {
|
||||
ChatScrollController(
|
||||
listState = listState,
|
||||
scope = CoroutineScope(Dispatchers.Main.immediate),
|
||||
)
|
||||
}
|
||||
// Frame-clocked scope, matching ChatScreen: animated scrolls need a MonotonicFrameClock.
|
||||
val scrollController = rememberChatScrollController(listState, rememberCoroutineScope())
|
||||
SideEffect { onController(scrollController) }
|
||||
FullBleedChatList(
|
||||
transcript =
|
||||
@@ -119,7 +116,7 @@ class FullBleedChatListTest {
|
||||
speakingMessageId = null,
|
||||
),
|
||||
actions = testTranscriptActions(),
|
||||
searchState = ChatSearchState(),
|
||||
searchState = searchState,
|
||||
listState = listState,
|
||||
scrollController = scrollController,
|
||||
)
|
||||
@@ -139,6 +136,30 @@ class FullBleedChatListTest {
|
||||
composeTestRule.runOnIdle { assertFalse(scrollController.isFollowingBottom) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun searchNavigatesToToolRowBeyondInitialViewport() {
|
||||
val search =
|
||||
ChatSearchState().apply {
|
||||
isActive = true
|
||||
query = "terminal"
|
||||
matchIndices = listOf(30)
|
||||
matchOffsets = listOf(0)
|
||||
matchTargets = listOf(SearchTarget.TOOL)
|
||||
currentIndex = 0
|
||||
matchedIds = setOf("tool")
|
||||
currentMatchId = "tool"
|
||||
}
|
||||
render(
|
||||
messages =
|
||||
List(30) { index -> msg("user-$index", MessageRole.USER) } +
|
||||
ChatMessage("tool", MessageRole.TOOL, "opaque", toolName = "terminal"),
|
||||
searchState = search,
|
||||
)
|
||||
|
||||
composeTestRule.onNodeWithTag("fullbleed_tool_row").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("terminal").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun userMessages_keepBubble_agentProse_isFullBleed() {
|
||||
render(
|
||||
|
||||
Reference in new issue
Block a user