feat(sessions): show project and last activity on history cards (#1133)
This commit is contained in:
1 parent
529b424eb4
commit
aed8ba4a6d
25 files changed
+1617
-80
No files matched your search
@@ -0,0 +1,173 @@
|
||||
package com.m57.hermescontrol.ui.sessions
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
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.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
import com.m57.hermescontrol.data.model.SessionInfo
|
||||
import com.m57.hermescontrol.data.model.SessionLiveStatus
|
||||
import com.m57.hermescontrol.theme.HermesControlTheme
|
||||
import com.m57.hermescontrol.ui.sessions.components.SessionCard
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* History card content: project, activity age (with a spoken description), preview and footer.
|
||||
* The card is clickable, so it merges its children; finders read the unmerged tree.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@MediumTest
|
||||
class SessionCardTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val nowMillis = 1_800_000_000_000L
|
||||
private var clicks = 0
|
||||
|
||||
private fun setCard(
|
||||
session: SessionInfo,
|
||||
project: SessionProject?,
|
||||
liveStatus: SessionLiveStatus? = null,
|
||||
fontScale: Float = 1f,
|
||||
) {
|
||||
composeTestRule.setContent {
|
||||
val density = LocalDensity.current
|
||||
CompositionLocalProvider(LocalDensity provides Density(density.density, fontScale)) {
|
||||
HermesControlTheme {
|
||||
Box(modifier = Modifier.width(360.dp)) {
|
||||
SessionCard(
|
||||
session = session,
|
||||
displayTitle = session.title ?: "Untitled",
|
||||
branchStem = null,
|
||||
query = "",
|
||||
isSelecting = false,
|
||||
isSelected = false,
|
||||
isDeleting = false,
|
||||
isPinned = false,
|
||||
liveStatus = liveStatus,
|
||||
project = project,
|
||||
nowMillis = nowMillis,
|
||||
highlightBackground = Color.Unspecified,
|
||||
highlightForeground = Color.Unspecified,
|
||||
onCardClick = { clicks++ },
|
||||
onToggleSelection = {},
|
||||
onSelect = {},
|
||||
onRename = {},
|
||||
onTogglePin = {},
|
||||
onToggleHide = {},
|
||||
onDelete = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed 360dp width so the fit doesn't depend on the device screen. The one-line case keeps
|
||||
// plenty of slack (one badge) because font metrics vary between devices and emulators.
|
||||
private fun longModelSession(hidden: Boolean) =
|
||||
SessionInfo(
|
||||
id = "long",
|
||||
title = "Draft the release notes",
|
||||
model = "openrouter/anthropic/a-very-long-model-identifier-name",
|
||||
message_count = 128,
|
||||
hidden = hidden,
|
||||
)
|
||||
|
||||
private fun bounds(tag: String) =
|
||||
composeTestRule.onNodeWithTag(tag, useUnmergedTree = true).getUnclippedBoundsInRoot()
|
||||
|
||||
@Test
|
||||
fun longModel_shrinksInsteadOfWrappingTheBadges() {
|
||||
setCard(session = longModelSession(hidden = false), project = null, liveStatus = SessionLiveStatus.WORKING)
|
||||
|
||||
val count = bounds("session_footer_count_long")
|
||||
val badge = bounds("session_live_status_long")
|
||||
val countCenter = (count.top + count.bottom) / 2
|
||||
val badgeCenter = (badge.top + badge.bottom) / 2
|
||||
assertEquals("badges share the model's line", countCenter.value, badgeCenter.value, 1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun largeFont_wrapsTheBadgesToASecondLine() {
|
||||
setCard(
|
||||
session = longModelSession(hidden = true),
|
||||
project = null,
|
||||
liveStatus = SessionLiveStatus.WORKING,
|
||||
fontScale = 2f,
|
||||
)
|
||||
|
||||
assertTrue(bounds("session_live_status_long").top >= bounds("session_footer_count_long").bottom)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rendersProjectAgePreviewAndFooter() {
|
||||
setCard(
|
||||
session =
|
||||
SessionInfo(
|
||||
id = "s1",
|
||||
title = "Restyle the palette",
|
||||
preview = "Can you check\nthe contrast?",
|
||||
model = "gpt-5.5",
|
||||
message_count = 24,
|
||||
last_active = nowMillis / 1000.0 - 2 * 3600,
|
||||
),
|
||||
project = SessionProject(label = "Hermes", color = "hsl(120 68% 58%)"),
|
||||
)
|
||||
|
||||
composeTestRule.onNodeWithTag("session_project_s1", useUnmergedTree = true).assertTextEquals("Hermes")
|
||||
composeTestRule
|
||||
.onNodeWithTag("session_age_s1", useUnmergedTree = true)
|
||||
.assertTextEquals("2h")
|
||||
.assert(SemanticsMatcher.expectValue(SemanticsProperties.ContentDescription, listOf("Active 2 hours ago")))
|
||||
composeTestRule.onNodeWithText("Restyle the palette", useUnmergedTree = true).assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithTag(
|
||||
"session_preview_s1",
|
||||
useUnmergedTree = true,
|
||||
).assertTextEquals("Can you check the contrast?")
|
||||
composeTestRule.onNodeWithText("gpt-5.5", useUnmergedTree = true).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("24 messages", useUnmergedTree = true).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sessionWithoutWorkspace_showsHome_andNoPreviewLine() {
|
||||
setCard(
|
||||
session = SessionInfo(id = "s2", title = "Quick question", message_count = 1),
|
||||
project = null,
|
||||
)
|
||||
|
||||
composeTestRule.onNodeWithTag("session_project_s2", useUnmergedTree = true).assertTextEquals("Home")
|
||||
composeTestRule.onNodeWithTag("session_preview_s2", useUnmergedTree = true).assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("session_age_s2", useUnmergedTree = true).assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("1 message", useUnmergedTree = true).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tappingTheCard_opensIt() {
|
||||
setCard(session = SessionInfo(id = "s3", title = "Open me"), project = null)
|
||||
|
||||
composeTestRule.onNodeWithTag("session_card_s3").performClick()
|
||||
|
||||
composeTestRule.runOnIdle { assertEquals(1, clicks) }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user