Files
agentik-desktop/src/jvmTest/kotlin/pw/binom/agentik/desktop/ui/ThemeRenderTest.kt
T
subochev 2c71542b13 Десктоп-клиент: Compose-UI, сессии, SQLite-кэш, голос, живые тесты
- settings/: settings.json (агенты, тема, clientId), атомарная запись
- persistence/: ConversationMetaRepository — группы, lastSeen, превью,
  watermark синхронизации (synced_at через PRAGMA table_info)
- session/: AgentRegistry (параллельное подключение, деградация в офлайн),
  AgentConnection (журнал + мета + живой список диалогов conversationsFlow,
  refresh по SSE-потоку), ChatSession (history + backfill + live + preview,
  дедуп по id, 1 мс шаг watermark, syncMutex), LiveTurn
- UI: рейка агентов, список диалогов (живое обновление), чат со стримингом,
  композер (Enter отправляет, Shift+Enter переносит, Ctrl+M — голос),
  окна «Агенты»/«Группы»/«Новый диалог», тёмная и светлая темы
- voice/: mic-api → Silero VAD → Vosk на одном выделенном потоке;
  VoiceNative.warmUp() до skia (иначе SIGSEGV), модель в артефакте
- markdown/: вендоренный рендер из ai/assistent
- ошибки в баннер через AppState.reportError (CancellationException игнорируется)
- тесты 103 (реальная SQLite + фейковый транспорт); против живого агента —
  RealServerIT, RealAppStateIT, RealSpeechIT
- LIVE-RUN-NOTES.md: живой прогон в GUI и найденные/починенные баги
2026-09-28 10:08:10 +03:00

155 lines
6.6 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package pw.binom.agentik.desktop.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.runComposeUiTest
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import pw.binom.agentik.desktop.ui.theme.AgentikColors
import pw.binom.agentik.desktop.ui.theme.AgentikTheme
import pw.binom.agentik.desktop.ui.theme.darkAgentikColors
import pw.binom.agentik.desktop.ui.theme.lightAgentikColors
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* Палитра и реальный рендер Compose без открытия окна.
*
* Зачем: светлая тема до сих пор проверялась только глазами по скриншоту, а
* markdown-код-блок изначально хардкодил тёмный фон (`#1E1E1E`), из-за чего в
* светлой теме выглядел чужеродно. Здесь тема прогоняется через настоящий
* оффскрин-рендер, а цвета сверяются с проектными токенами.
*/
class ThemeRenderTest {
// ── токены (THEMES.md / sketches/style.css) ────────────────────────────
@Test
fun darkPaletteMatchesDesignTokens() {
val c = darkAgentikColors
assertEquals(Color(0xFF121218), c.background)
assertEquals(Color(0xFF17212B), c.panel)
assertEquals(Color(0xFF1C2733), c.panelElevated)
assertEquals(Color(0xFF6AB2F2), c.accent)
assertEquals(Color(0xFFEBEBEB), c.text)
assertEquals(Color(0xFF768C9E), c.textDim)
assertEquals(Color(0xFFEC3942), c.error)
assertEquals(Color(0xFF2B5278), c.userBubble)
}
@Test
fun lightPaletteKeepsSemanticSlots() {
val c = lightAgentikColors
assertEquals(Color(0xFFF5F6F8), c.background)
assertEquals(Color(0xFFFFFFFF), c.panel)
assertEquals(Color(0xFF2A6FB5), c.accent)
assertEquals(Color(0xFF1A1D23), c.text)
// светлый фон обязан быть светлым: контраст к тексту
assertTrue(luminance(c.background) > luminance(c.text), "фон светлее текста")
assertTrue(luminance(c.panel) > luminance(c.panelElevated), "panel светлее panelElevated")
assertTrue(luminance(c.botBubble) > luminance(c.text), "бот-бабл светлее текста")
}
@Test
fun opaqueSlotsStayOpaqueAndScrimTransparent() {
for (c in listOf(darkAgentikColors, lightAgentikColors)) {
assertEquals(1f, c.background.alpha)
assertEquals(1f, c.panel.alpha)
assertEquals(1f, c.text.alpha)
assertEquals(1f, c.accent.alpha)
// scrim/selection — прозрачные по замыслу
assertTrue(c.scrim.alpha < 1f)
assertTrue(c.selection.alpha < 1f)
}
}
// ── рендер ─────────────────────────────────────────────────────────────
@OptIn(ExperimentalTestApi::class)
@Test
fun codeBlockPaintsThemeCodeBackgroundInLightTheme() = runComposeUiTest {
setContent {
AgentikTheme("light") {
Box(Modifier.fillMaxSize().background(AgentikTheme.colors.botBubble).padding(8.dp)) {
Column {
renderMarkdownSegments(
content = "```kotlin\nval x = 1\n```",
textColor = AgentikTheme.colors.text,
fontSize = 14.sp,
)
}
}
}
}
val pixels = onRoot().captureToImage().toPixelMap()
val histogram = HashMap<Color, Int>()
for (y in 0 until pixels.height) {
for (x in 0 until pixels.width) {
val color = pixels[x, y]
histogram[color] = (histogram[color] ?: 0) + 1
}
}
val top = histogram.entries.sortedByDescending { it.value }.take(4)
.joinToString { "#${it.key.toHexArgb()}×${it.value}" }
assertTrue(
histogram.containsKey(lightAgentikColors.codeBackground),
"код-блок должен краситься в codeBackground светлой темы, а покрасился в: $top",
)
assertFalse(
histogram.containsKey(Color(0xFF1E1E1E)),
"тёмный хардкод код-фона протёк в светлую тему: $top",
)
}
@OptIn(ExperimentalTestApi::class)
@Test
fun darkThemeStillRendersCodeBlockDark() = runComposeUiTest {
setContent {
AgentikTheme("dark") {
Box(Modifier.fillMaxSize().background(AgentikTheme.colors.botBubble).padding(8.dp)) {
Column {
renderMarkdownSegments(
content = "```kotlin\nval x = 1\n```",
textColor = AgentikTheme.colors.text,
fontSize = 14.sp,
)
}
}
}
}
val pixels = onRoot().captureToImage().toPixelMap()
var found = false
for (y in 0 until pixels.height) {
for (x in 0 until pixels.width) {
if (pixels[x, y] == darkAgentikColors.codeBackground) {
found = true
break
}
}
}
assertTrue(found, "в тёмной теме код-блок должен краситься в dark codeBackground")
}
private fun luminance(color: Color): Float =
0.2126f * color.red + 0.7152f * color.green + 0.0722f * color.blue
private fun Color.toHexArgb(): String {
val a = (alpha * 255).toInt()
val r = (red * 255).toInt()
val g = (green * 255).toInt()
val b = (blue * 255).toInt()
return "%02X%02X%02X%02X".format(a, r, g, b)
}
}