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() 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) } }