fix: 重写详情页 ViewModel 用例使其具备鉴别力并修正 Preview 夹具
审查指出第 3 条用例的唯一断言与 stateIn 的 initialValue 逐字相等(ledger R30 同类缺陷), 且三条用例都没有把构造函数的 metric 与 observeMetricHistory 的查询参数绑起来。 重写为「SLEEP 指标查到的是睡眠区间的数据」:点数断言杀掉「管线从不发射」, 取值区间断言(330~510,与步数 3000~12000 不相交)杀掉 metric 被写成常量的泄漏; metric 回显断言保留但注释写明其无鉴别力。变异测试已证:把第 43 行的 metric 换成 MetricType.STEP 后该用例失败、另两条仍绿,回退后 3/3 通过。 顺带按 Minor 意见修正 previewPoints:KDoc 原文「贴近区间下限」不实,且步数夹具 7500~7572 在柱状图里渲染成齐平一排。改为带 step 形参,睡眠 330→480、步数 3200→11000, 两个 Preview 的 metric 分支覆盖不变。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -270,13 +270,17 @@ private fun StatLine(label: String, value: Double, modifier: Modifier = Modifier
|
|||||||
private fun List<Double>.averageOrZero(): Double = if (isEmpty()) 0.0 else average()
|
private fun List<Double>.averageOrZero(): Double = if (isEmpty()) 0.0 else average()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preview 用的样例数据:7 个点、每天一个、值从 `base` 起小幅递增。
|
* Preview 用的样例数据:7 个点、每天一个,取值从 `base` 起每点递增 `step`。
|
||||||
* 取值贴近区间下限,好让柱状图有高低差、统计摘要的三行数字不相等(都相等就看不出渲染问题)。
|
*
|
||||||
|
* `base` 与 `step` 由调用方按**所在指标的取值区间**给出:`base` 取区间低位,
|
||||||
|
* `step` 取区间跨度的约 1/7,使 7 个点铺满区间的大半。这样柱状图才有可见的高低差
|
||||||
|
* (`MetricBarChart` 按 `value / maxValue` 定高,点值若都挤在一起,7 根柱子会齐平,
|
||||||
|
* 预览就看不出渲染问题),统计摘要的「平均/最高/最低」三行也不会几乎相等。
|
||||||
*/
|
*/
|
||||||
private fun previewPoints(base: Double): List<MetricPoint> {
|
private fun previewPoints(base: Double, step: Double): List<MetricPoint> {
|
||||||
val dayMillis = 24L * 60 * 60 * 1000
|
val dayMillis = 24L * 60 * 60 * 1000
|
||||||
val start = 1_700_000_000_000L
|
val start = 1_700_000_000_000L
|
||||||
return List(7) { index -> MetricPoint(timestamp = start + index * dayMillis, value = base + index * 12) }
|
return List(7) { index -> MetricPoint(timestamp = start + index * dayMillis, value = base + index * step) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -292,7 +296,9 @@ private fun MetricDetailContentSleepPreview() {
|
|||||||
loading = false,
|
loading = false,
|
||||||
metric = MetricType.SLEEP,
|
metric = MetricType.SLEEP,
|
||||||
range = DateRange.WEEK,
|
range = DateRange.WEEK,
|
||||||
points = previewPoints(base = 420.0), // 睡眠时长(分钟),与 Mock 的 330~510 同量级
|
// 睡眠时长(分钟):330→480,落在 Mock 的 330~510 区间内,跨度 150 分钟,
|
||||||
|
// 柱高从约 69% 递增到 100%,肉眼能看出梯度。
|
||||||
|
points = previewPoints(base = 330.0, step = 25.0),
|
||||||
),
|
),
|
||||||
onBack = {},
|
onBack = {},
|
||||||
onSelectRange = {},
|
onSelectRange = {},
|
||||||
@@ -313,7 +319,10 @@ private fun MetricDetailContentStepsPreview() {
|
|||||||
loading = false,
|
loading = false,
|
||||||
metric = MetricType.STEP,
|
metric = MetricType.STEP,
|
||||||
range = DateRange.WEEK,
|
range = DateRange.WEEK,
|
||||||
points = previewPoints(base = 7500.0), // 步数,与 Mock 的 3000~12000 同量级
|
// 步数:3200→11000,落在 Mock 的 3000~12000 区间内,跨度 7800 步,
|
||||||
|
// 柱高从约 27% 递增到 100%。原来的 7500 起点会让 7 根柱子全挤在 99%~100% 高度上,
|
||||||
|
// 看起来是一条平带 —— 恰是预览本该暴露的问题。
|
||||||
|
points = previewPoints(base = 3200.0, step = 1300.0),
|
||||||
),
|
),
|
||||||
onBack = {},
|
onBack = {},
|
||||||
onSelectRange = {},
|
onSelectRange = {},
|
||||||
|
|||||||
+23
-5
@@ -14,6 +14,7 @@ import kotlinx.coroutines.test.setMain
|
|||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -69,15 +70,32 @@ class MetricDetailViewModelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `构造时传入的 metric 被原样带进状态`() = runTest(dispatcher) {
|
fun `SLEEP 指标查到的是睡眠区间的数据`() = runTest(dispatcher) {
|
||||||
// 名字就叫它断言的事,不要写成「标题映射不同」——`metricTitleRes` 是 @Composable,
|
// 名字不叫「构造时传入的 metric 被原样带进状态」——本用例真正锁的是
|
||||||
// JVM 测试里根本调不了,那样命名会让人以为这里验了标题映射(其实没有,
|
// 「构造函数的 metric 被传给了仓库查询」,而不是状态字段回显(ledger R34)。
|
||||||
// 映射本身由 `when` 的穷尽性在编译期保证)。本用例只锁「metric 被原样带进状态」。
|
// 也不要写成「标题映射不同」:`metricTitleRes` 是 @Composable,JVM 测试里根本调不了,
|
||||||
|
// 那样命名会让人以为这里验了标题映射(其实没有,映射由 `when` 的穷尽性在编译期保证)。
|
||||||
val viewModel = MetricDetailViewModel(MetricType.SLEEP, MockHealthDataRepository())
|
val viewModel = MetricDetailViewModel(MetricType.SLEEP, MockHealthDataRepository())
|
||||||
backgroundScope.launch { viewModel.uiState.collect {} }
|
backgroundScope.launch { viewModel.uiState.collect {} }
|
||||||
|
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
assertEquals(MetricType.SLEEP, viewModel.uiState.value.metric)
|
val state = viewModel.uiState.value
|
||||||
|
// 承重断言一:`initialValue.points` 是 emptyList(),所以数据管线
|
||||||
|
// (observeMetricHistory / flatMapLatest / map)只要不发射,这一条必挂。
|
||||||
|
assertEquals(7, state.points.size)
|
||||||
|
// 承重断言二:上面那条只证明「有数据」,不证明「数据问对了指标」。
|
||||||
|
// `observeMetricHistory(metric, currentRange)` 里若把 metric 写成常量(如 MetricType.STEP),
|
||||||
|
// 点数仍是 7(点数只由 range 决定),只有取值区间会变 —— Mock 按指标区间生成并 coerceIn,
|
||||||
|
// 睡眠是 330~510、步数是 3000~12000,两者不相交(见 MockDataProvider.valueRange)。
|
||||||
|
// 注意 `all {}` 对空列表恒真,所以它必须与上面那条 size 断言成对出现,不能单用。
|
||||||
|
assertTrue(
|
||||||
|
"SLEEP 详情页拿到了区间外的值:${state.points.map { it.value }}",
|
||||||
|
state.points.all { it.value in 330.0..510.0 },
|
||||||
|
)
|
||||||
|
// 这一条单看**没有鉴别力**:`state.metric` 就是构造函数传进来的值,`stateIn` 的
|
||||||
|
// initialValue 里已经有它,数据管线从不发射时它照样成立(ledger R30)。保留它只是为了
|
||||||
|
// 钉住「状态里的 metric 与构造函数一致」,真正承重的是上面两条。
|
||||||
|
assertEquals(MetricType.SLEEP, state.metric)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user