M8e: recurring editor options as fixed rows (D-107)
S-19's account, payer and split chips (the wrapping EditorChipRow whose chips moved when a value changed) become three fixed full-width rows in a stable order: Konto, Betalt av, Delning, using M8d's EditorOptionRows with accountOption, payerOption and splitOption (no date row; the schedule stays in its own "Schema" section). TalkBack sentences unchanged. A Robolectric test lays out S-19 with short and long values at 100 % and 200 % and asserts identical row bounds. The chip composables stay in :core:design: S-9 uses EditorChipRow and DateChip, and the debug catalogue still shows AccountChip, PayerChip and SplitChip. Goldens re-recorded: every s19_recurring_editor_* variant; 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:
+12
-9
@@ -44,7 +44,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import se.hagstedt.kvitt.design.KvittElevation
|
||||
import se.hagstedt.kvitt.design.KvittTheme
|
||||
import se.hagstedt.kvitt.design.component.AccountChip
|
||||
import se.hagstedt.kvitt.design.component.AccountPicker
|
||||
import se.hagstedt.kvitt.design.component.AmountDisplay
|
||||
import se.hagstedt.kvitt.design.component.AmountKeypad
|
||||
@@ -53,7 +52,7 @@ import se.hagstedt.kvitt.design.component.CategoryGrid
|
||||
import se.hagstedt.kvitt.design.component.CategoryQuickRow
|
||||
import se.hagstedt.kvitt.design.component.ConfirmDialog
|
||||
import se.hagstedt.kvitt.design.component.DescriptionField
|
||||
import se.hagstedt.kvitt.design.component.EditorChipRow
|
||||
import se.hagstedt.kvitt.design.component.EditorOptionRows
|
||||
import se.hagstedt.kvitt.design.component.ExactSplitEditor
|
||||
import se.hagstedt.kvitt.design.component.FullSplitPicker
|
||||
import se.hagstedt.kvitt.design.component.KvittButton
|
||||
@@ -62,18 +61,19 @@ import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.KvittTextField
|
||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||
import se.hagstedt.kvitt.design.component.NotesField
|
||||
import se.hagstedt.kvitt.design.component.PayerChip
|
||||
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
||||
import se.hagstedt.kvitt.design.component.RatioEditor
|
||||
import se.hagstedt.kvitt.design.component.RatioField
|
||||
import se.hagstedt.kvitt.design.component.SplitBar
|
||||
import se.hagstedt.kvitt.design.component.SplitChip
|
||||
import se.hagstedt.kvitt.design.component.SplitPreview
|
||||
import se.hagstedt.kvitt.design.component.SplitSection
|
||||
import se.hagstedt.kvitt.design.component.TopBarNavigation
|
||||
import se.hagstedt.kvitt.design.component.accountOption
|
||||
import se.hagstedt.kvitt.design.component.defaultModeLabel
|
||||
import se.hagstedt.kvitt.design.component.kvittLanguage
|
||||
import se.hagstedt.kvitt.design.component.payerOption
|
||||
import se.hagstedt.kvitt.design.component.splitModeLabel
|
||||
import se.hagstedt.kvitt.design.component.splitOption
|
||||
import se.hagstedt.kvitt.design.format.AmountInput
|
||||
import se.hagstedt.kvitt.design.format.AmountKey
|
||||
import se.hagstedt.kvitt.design.format.DateFormatter
|
||||
@@ -230,15 +230,18 @@ private fun EditorBody(state: RecurringEditorState, actions: RecurringEditorActi
|
||||
val payer = if ((form.payer ?: me.id) == me.id) me else partner
|
||||
val viewerPercent = state.viewerDefaultPercent
|
||||
val defaultLabel = defaultModeLabel(viewerPercent)
|
||||
EditorChipRow {
|
||||
AccountChip(state.account?.name.orEmpty(), state.accountColorIndex, { actions.onOpenSheet(EditorSheet.Account) })
|
||||
PayerChip(payer, if (payer == me) partner else me, actions.onTogglePayer)
|
||||
SplitChip(
|
||||
// Three fixed rows in a stable order (Konto, Betalt av, Delning), so nothing moves when a value changes (D-107).
|
||||
EditorOptionRows(
|
||||
listOf(
|
||||
accountOption(state.account?.name.orEmpty(), state.accountColorIndex) { actions.onOpenSheet(EditorSheet.Account) },
|
||||
payerOption(payer, if (payer == me) partner else me, actions.onTogglePayer),
|
||||
splitOption(
|
||||
if (form.mode == SplitMode.RATIO) stringResource(DesignR.string.editor_split_mode_ratio) + " " + stringResource(DesignR.string.common_ratio, (form.viewerBp / 100).toString(), ((10_000 - form.viewerBp) / 100).toString()) else splitModeLabel(form.mode, defaultLabel),
|
||||
form.splitOpen,
|
||||
actions.onToggleSplit,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (form.splitOpen) {
|
||||
SplitSection(form.mode, defaultLabel, actions.onMode, actions.onToggleSplit) {
|
||||
when (form.mode) {
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package se.hagstedt.kvitt.feature.settings.recurring
|
||||
|
||||
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.fakes.settings.ScriptedSettingsRepositories
|
||||
import se.hagstedt.kvitt.fakes.settings.SettingsScenarios
|
||||
import se.hagstedt.kvitt.feature.settings.screenshot.ScreenSnapshots
|
||||
import se.hagstedt.kvitt.model.SplitMode
|
||||
|
||||
/**
|
||||
* The three option rows of S-19 (Konto, Betalt av, Delning; D-107, extending D-102) keep their places and sizes when
|
||||
* their values change: the editor is laid out with the short values of "Hyra", then with long ones (a long account
|
||||
* name, 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-h914dp-mdpi")
|
||||
class RecurringOptionRowsLayoutTest {
|
||||
@get:Rule
|
||||
val compose = createComposeRule()
|
||||
|
||||
private val short: RecurringEditorState by lazy {
|
||||
val r = ScriptedSettingsRepositories(SettingsScenarios.loaded)
|
||||
val f = r.fakes
|
||||
val handle = SavedStateHandle(mapOf(RecurringEditorViewModel.ARG_TEMPLATE_ID to SettingsScenarios.hyra.id.value))
|
||||
val vm = RecurringEditorViewModel(handle, f.expenses, f.home, r.recurring, f.connectivity)
|
||||
ScreenSnapshots.idle()
|
||||
requireNotNull(vm.state.value)
|
||||
}
|
||||
|
||||
private fun long(state: RecurringEditorState): RecurringEditorState {
|
||||
val partner = state.options.partner.copy(displayName = "Anna-Karin Lindqvist Bergström")
|
||||
return state.copy(
|
||||
options = state.options.copy(partner = partner),
|
||||
account = requireNotNull(state.account).copy(name = "Sommarstugan i Österåker med bryggan, bastun och båthuset"),
|
||||
form = state.form.copy(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 { RecurringEditorContent(state, RecurringEditorActions()) }
|
||||
}
|
||||
}
|
||||
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)
|
||||
// Three 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) }
|
||||
compose.onNodeWithTag(editorOptionTag(ROWS)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rowsStayPutWhenValuesChange() = assertStable(1f)
|
||||
|
||||
@Test
|
||||
fun rowsStayPutWhenValuesChangeAt200Percent() = assertStable(2f)
|
||||
|
||||
private companion object {
|
||||
const val ROWS = 3
|
||||
const val MIN_ROW_DP = 48f
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user