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) } }