From 0e1750233483ce255f46788da7c5f27eb5455e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=94=E9=B2=81=E6=AD=A3?= <13780741225@163.com> Date: Thu, 10 Sep 2026 12:43:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=87=8D=E5=86=99=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E9=A1=B5=20ViewModel=20=E7=94=A8=E4=BE=8B=E4=BD=BF=E5=85=B6?= =?UTF-8?q?=E5=85=B7=E5=A4=87=E9=89=B4=E5=88=AB=E5=8A=9B=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20Preview=20=E5=A4=B9=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查指出第 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 --- .../feature/detail/MetricDetailScreen.kt | 21 ++++++++++---- .../detail/MetricDetailViewModelTest.kt | 28 +++++++++++++++---- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/placeholder/ringhealth/feature/detail/MetricDetailScreen.kt b/app/src/main/java/com/placeholder/ringhealth/feature/detail/MetricDetailScreen.kt index ba011d9..a5bc952 100644 --- a/app/src/main/java/com/placeholder/ringhealth/feature/detail/MetricDetailScreen.kt +++ b/app/src/main/java/com/placeholder/ringhealth/feature/detail/MetricDetailScreen.kt @@ -270,13 +270,17 @@ private fun StatLine(label: String, value: Double, modifier: Modifier = Modifier private fun List.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 { +private fun previewPoints(base: Double, step: Double): List { val dayMillis = 24L * 60 * 60 * 1000 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, metric = MetricType.SLEEP, 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 = {}, onSelectRange = {}, @@ -313,7 +319,10 @@ private fun MetricDetailContentStepsPreview() { loading = false, metric = MetricType.STEP, 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 = {}, onSelectRange = {}, diff --git a/app/src/test/java/com/placeholder/ringhealth/feature/detail/MetricDetailViewModelTest.kt b/app/src/test/java/com/placeholder/ringhealth/feature/detail/MetricDetailViewModelTest.kt index 8b32593..9c866ef 100644 --- a/app/src/test/java/com/placeholder/ringhealth/feature/detail/MetricDetailViewModelTest.kt +++ b/app/src/test/java/com/placeholder/ringhealth/feature/detail/MetricDetailViewModelTest.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -69,15 +70,32 @@ class MetricDetailViewModelTest { } @Test - fun `构造时传入的 metric 被原样带进状态`() = runTest(dispatcher) { - // 名字就叫它断言的事,不要写成「标题映射不同」——`metricTitleRes` 是 @Composable, - // JVM 测试里根本调不了,那样命名会让人以为这里验了标题映射(其实没有, - // 映射本身由 `when` 的穷尽性在编译期保证)。本用例只锁「metric 被原样带进状态」。 + fun `SLEEP 指标查到的是睡眠区间的数据`() = runTest(dispatcher) { + // 名字不叫「构造时传入的 metric 被原样带进状态」——本用例真正锁的是 + // 「构造函数的 metric 被传给了仓库查询」,而不是状态字段回显(ledger R34)。 + // 也不要写成「标题映射不同」:`metricTitleRes` 是 @Composable,JVM 测试里根本调不了, + // 那样命名会让人以为这里验了标题映射(其实没有,映射由 `when` 的穷尽性在编译期保证)。 val viewModel = MetricDetailViewModel(MetricType.SLEEP, MockHealthDataRepository()) backgroundScope.launch { viewModel.uiState.collect {} } 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) } }