M8d: expense editor options as fixed rows (D-102)
S-7's account, date, payer and split chips (a wrapping FlowRow whose chips moved when a value changed) become four fixed full-width rows in a stable order: Konto, Datum, Betalt av, Delning. `EditorOptionRows` in :core:design (segmented-group shape, the chips' tonal surface-container-high, secondary-container while the split section is open; leading 24 dp element, label column as wide as the widest label, value one line with an ellipsis, trailing chevron or swap). At >= 1.5x font scale the label sits over the value, which takes up to two lines and whose two-line height every row keeps. A Robolectric test lays out S-7 with short and long values at 100 % and 200 % and asserts identical row bounds. The chip composables stay (S-9, S-19). S-11: connected buttons get 8 dp side padding so "Household" fits half the filter bar at 1x. Goldens re-recorded: every s7_* variant, the S-11 en_light goldens, the analytics_filter_and_total en_light sheet, and the new editor_option_rows sheet; oxipng -o 4 --strip safe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+114
@@ -0,0 +1,114 @@
|
||||
package se.hagstedt.kvitt.feature.expense
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.test.getUnclippedBoundsInRoot
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.DpRect
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
import se.hagstedt.kvitt.design.KvittTheme
|
||||
import se.hagstedt.kvitt.design.component.LocalKvittClock
|
||||
import se.hagstedt.kvitt.design.component.editorOptionTag
|
||||
import se.hagstedt.kvitt.design.format.AmountKey
|
||||
import se.hagstedt.kvitt.fakes.FakeRepositories
|
||||
import se.hagstedt.kvitt.fakes.expense.ExpenseScenarios
|
||||
import se.hagstedt.kvitt.feature.expense.screenshot.NoActions
|
||||
import se.hagstedt.kvitt.feature.expense.screenshot.ScreenSnapshots
|
||||
import se.hagstedt.kvitt.model.SplitMode
|
||||
|
||||
/**
|
||||
* The four option rows of S-7 (Konto, Datum, Betalt av, Delning; D-102) keep their places and sizes when their values
|
||||
* change: the editor is laid out with short values, then with long ones (a long account name, a date that is not today
|
||||
* or yesterday, the partner with a long name as payer, "Allt på <long name>"), and every row's bounds must be identical,
|
||||
* at 100 % and at 200 % font scale. Native graphics: only real text layout makes a long value wrap.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@Config(sdk = [36], qualifiers = "w411dp-h891dp-mdpi")
|
||||
class EditorOptionRowsLayoutTest {
|
||||
@get:Rule
|
||||
val compose = createComposeRule()
|
||||
|
||||
private val short: ExpenseEditorState = ScreenSnapshots.build { idle ->
|
||||
val fakes = FakeRepositories(ExpenseScenarios.household)
|
||||
val vm = ExpenseEditorViewModel(SavedStateHandle(), fakes.expenses, fakes.home, fakes.accounts, fakes.connectivity)
|
||||
idle()
|
||||
"412".forEach { vm.onKey(AmountKey.Digit(it.digitToInt())) }
|
||||
vm.onDescription("Systembolaget")
|
||||
idle()
|
||||
vm.focusKeypad(null)
|
||||
vm.state.value
|
||||
}
|
||||
|
||||
private fun long(state: ExpenseEditorState): ExpenseEditorState {
|
||||
val partner = requireNotNull(state.partner).copy(displayName = "Anna-Karin Lindqvist Bergström")
|
||||
val form = requireNotNull(state.form)
|
||||
return state.copy(
|
||||
partner = partner,
|
||||
account = requireNotNull(state.account).copy(name = "Sommarstugan i Österåker med bryggan, bastun och båthuset"),
|
||||
form = form.copy(date = "2026-09-14", payer = partner.id, mode = SplitMode.FULL, fullOn = partner.id),
|
||||
)
|
||||
}
|
||||
|
||||
private fun rowBounds(fontScale: Float): Pair<List<DpRect>, List<DpRect>> {
|
||||
var state by mutableStateOf(short)
|
||||
compose.setContent {
|
||||
val base = LocalContext.current
|
||||
val baseConfiguration = LocalConfiguration.current
|
||||
val context = remember { base.createConfigurationContext(Configuration(baseConfiguration).apply { this.fontScale = fontScale }) }
|
||||
CompositionLocalProvider(
|
||||
LocalContext provides context,
|
||||
LocalConfiguration provides context.resources.configuration,
|
||||
LocalResources provides context.resources,
|
||||
LocalDensity provides Density(context),
|
||||
LocalKvittClock provides ScreenSnapshots.clock,
|
||||
) {
|
||||
KvittTheme { ExpenseEditorContent(state, NoActions) }
|
||||
}
|
||||
}
|
||||
fun bounds() = (0 until ROWS).map { compose.onNodeWithTag(editorOptionTag(it)).getUnclippedBoundsInRoot() }
|
||||
compose.waitForIdle()
|
||||
val before = bounds()
|
||||
state = long(short)
|
||||
compose.waitForIdle()
|
||||
return before to bounds()
|
||||
}
|
||||
|
||||
private fun assertStable(fontScale: Float) {
|
||||
val (before, after) = rowBounds(fontScale)
|
||||
assertEquals(before, after)
|
||||
// Four rows, top to bottom, full width, each at least the 48 dp touch target.
|
||||
before.zipWithNext { a, b -> assertTrue("rows in order: $a above $b", a.bottom <= b.top) }
|
||||
assertEquals(1, before.map { it.left to it.right }.distinct().size)
|
||||
before.forEach { assertTrue("row height of $it", (it.bottom - it.top).value >= MIN_ROW_DP) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rowsStayPutWhenValuesChange() = assertStable(1f)
|
||||
|
||||
@Test
|
||||
fun rowsStayPutWhenValuesChangeAt200Percent() = assertStable(2f)
|
||||
|
||||
private companion object {
|
||||
const val ROWS = 4
|
||||
const val MIN_ROW_DP = 48f
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user