Добавлен тест на вертикальное центрирование подписи «Временный диалог» относительно квадрата чекбокса и исправлена стилизация текста в компонентах диалога.

This commit is contained in:
2026-09-29 11:02:28 +03:00
parent 7a9f60274b
commit 2ad2d65f06
2 changed files with 101 additions and 15 deletions
@@ -10,8 +10,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@@ -21,8 +20,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -32,12 +31,14 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -372,6 +373,7 @@ private fun TemporalCheckbox(temporal: Boolean) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.testTag("temporal-checkbox")
.clip(RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(10.dp))
.background(colors.panelElevated) .background(colors.panelElevated)
.padding(10.dp), .padding(10.dp),
@@ -390,20 +392,32 @@ private fun TemporalCheckbox(temporal: Boolean) {
Text("✓", color = onAccent, fontSize = 13.sp, fontWeight = FontWeight.Bold) Text("✓", color = onAccent, fontSize = 13.sp, fontWeight = FontWeight.Bold)
} }
} }
Box( // Однострочный текст в Compose может «проседать» вниз, если lineHeight
modifier = Modifier // не задан явно: LineHeightStyle.Alignment.Center применяется только
.weight(1f) // при заданном lineHeight. Рецепт ровного центра: lineHeight == fontSize
.height(20.dp), // + LineHeightStyle(Center, Trim.Both).
contentAlignment = Alignment.CenterStart,
) {
Text( Text(
text = "Временный диалог", text = "Временный диалог",
color = colors.text, color = colors.text,
fontSize = 13.sp, fontSize = 13.sp,
lineHeight = 13.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
modifier = Modifier
.weight(1f)
// Типографская компенсация: строчные глифы (х-высота) визуально
// «проседают» на ~1.5px вниз относительно геометрического центра
// строки из-за метрик шрифта (ascent > x-height). Сдвигаем текст
// чуть вверх, чтобы его визуальная масса совпала с центром
// квадратика чекбокса.
.offset(y = (-1.5).dp),
style = LocalTextStyle.current.copy(
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.Both,
),
),
) )
} }
}
} }
@Composable @Composable
@@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.captureToImage import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
@@ -367,8 +368,79 @@ class DialogInfoDialogTest {
) )
} }
// ── helpers ──────────────────────────────────────────────────────────── /**
* Регрессия на вертикальное центрирование подписи «Временный диалог»
* относительно квадратика чекбокса. Рендерит строку в битмап и сравнивает
* вертикальные центры синего квадратика (accent) и белых глифов текста.
*/
@OptIn(ExperimentalTestApi::class)
@Test
fun `temporal checkbox label is vertically centered with the box`() = runComposeUiTest {
val conversation = sampleConversation(title = "X", temporal = true)
setContent {
withAvatarStorage {
ProvideAgentikTheme("dark".toAgentikTheme()) {
Box(Modifier.fillMaxSize().background(darkAgentikColors.background)) {
DialogInfoDialog(
conversation = conversation,
agentName = null,
agentAddress = null,
agentColor = null,
online = false,
onClose = {},
onRename = { _, _ -> },
onDelete = {},
)
}
}
}
}
val pixels = onNodeWithTag("temporal-checkbox").captureToImage().toPixelMap()
var sqMinY = Int.MAX_VALUE; var sqMaxY = -1
var sqMaxX = -1
var txMinY = Int.MAX_VALUE; var txMaxY = -1
var sawText = false
for (y in 0 until pixels.height) {
for (x in 0 until pixels.width) {
val c = pixels[x, y]
val isAccent = kotlin.math.abs(c.red - 0.416f) < 0.12f &&
kotlin.math.abs(c.green - 0.698f) < 0.12f &&
kotlin.math.abs(c.blue - 0.949f) < 0.12f
if (isAccent) {
if (y < sqMinY) sqMinY = y
if (y > sqMaxY) sqMaxY = y
if (x > sqMaxX) sqMaxX = x
}
}
}
// Текст ищем только правее квадратика, чтобы ✓ внутри квадрата не мешал.
for (y in 0 until pixels.height) {
for (x in (sqMaxX + 3) until pixels.width) {
val c = pixels[x, y]
val isText = c.red > 0.55f && c.green > 0.55f && c.blue > 0.55f &&
(maxOf(c.red, c.green, c.blue) - minOf(c.red, c.green, c.blue)) < 0.2f
if (isText) {
sawText = true
if (y < txMinY) txMinY = y
if (y > txMaxY) txMaxY = y
}
}
}
assertTrue(sawText, "глифы подписи не найдены в битмапе")
val sqCenter = (sqMinY + sqMaxY) / 2f
val txCenter = (txMinY + txMaxY) / 2f
val delta = txCenter - sqCenter
assertTrue(
kotlin.math.abs(delta) <= 1.0f,
"центр подписи (y=$txCenter) должен совпадать с центром квадратика " +
"(y=$sqCenter); смещение=${delta}px (sqY=$sqMinY..$sqMaxY, txY=$txMinY..$txMaxY)",
)
}
// ── helpers ────────────────────────────────────────────────────────────
private fun sampleConversation( private fun sampleConversation(
title: String = "Диалог", title: String = "Диалог",
temporal: Boolean = false, temporal: Boolean = false,