From a18523f032eac4a24e7d87bd949b4c5fa9b2a97b 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:41:17 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20Task=2012=20=E7=94=A8=E4=BE=8B=E4=B8=89?= =?UTF-8?q?=E8=A1=A5=E9=89=B4=E5=88=AB=E5=8A=9B=E3=80=81previewPoints=20?= =?UTF-8?q?=E8=A1=A5=20step=20=E5=8F=82=E6=95=B0=EF=BC=88ledger=20R34?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-09-10-ring-health-app-m0-m1.md | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/superpowers/plans/2026-09-10-ring-health-app-m0-m1.md b/superpowers/plans/2026-09-10-ring-health-app-m0-m1.md index cb91ae8..f4ada80 100644 --- a/superpowers/plans/2026-09-10-ring-health-app-m0-m1.md +++ b/superpowers/plans/2026-09-10-ring-health-app-m0-m1.md @@ -3897,6 +3897,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 @@ -3952,16 +3953,31 @@ class MetricDetailViewModelTest { } @Test - fun `构造时传入的 metric 被原样带进状态`() = runTest(dispatcher) { + fun `构造时传入的 metric 既进状态、也决定了查询哪一组数据`() = runTest(dispatcher) { // 名字就叫它断言的事,不要写成「标题映射不同」——`metricTitleRes` 是 @Composable, // JVM 测试里根本调不了,那样命名会让人以为这里验了标题映射(其实没有, - // 映射本身由 `when` 的穷尽性在编译期保证)。本用例只锁「metric 被原样带进状态」。 + // 映射本身由 `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 + // ⚠️ 这一条**单独放在这里是没有鉴别力的**:`metric` 本来就在 stateIn 的 initialValue 里 + // (`MetricDetailUiState(loading = true, metric = metric, …)`),数据管线从不发射时它照样绿。 + // 留着它是因为它锁住了「构造参数被原样带进状态」,但**真正干活的是下面两条**(ledger R34)。 + assertEquals(MetricType.SLEEP, state.metric) + + // 鉴别力来源一:排除「管线从不发射」。此时 points 停在 initialValue 的 emptyList()。 + assertEquals(7, state.points.size) + // 鉴别力来源二:把构造参数与**实际查询到的数据**绑起来。 + // `MetricDetailViewModel` 里调的是 `observeMetricHistory(metric, currentRange)`; + // 若这一行被错传成 `MetricType.STEP`(3000~12000),或参数索引错位,下面的区间断言就会挂。 + // 区间取自 MockDataProvider.valueRange,务必与那边保持一致: + // STEP 3000~12000 / HEART_RATE 58~96 / SLEEP 330~510 / … + // ⚠️ 不要只写 `all {}` —— **空列表上 `all` 恒真**,那样会留出一个洞; + // 上面的 size 断言就是用来堵这个洞的,两条必须同时在。 + assertTrue(state.points.all { it.value in 330.0..510.0 }) } } ``` @@ -4390,13 +4406,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` 递增。 + * + * `step` 必须与 `base` 同量级、且让整段值落在该指标的合理区间内,否则柱状图会**看起来是平的** + * (`MetricBarChart` 按 `value / maxValue` 定高,7 根柱子之间只差 1% 就等于没有高低差, + * 那正是预览最该暴露的渲染问题)。睡眠用 `base` 低、`step` 小;步数用 `base` 大、`step` 也大。 + * 统计摘要的三行数字也依赖它不相等 —— 都相等就看不出渲染问题。 */ -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) } } /** @@ -4412,7 +4432,8 @@ private fun MetricDetailContentSleepPreview() { loading = false, metric = MetricType.SLEEP, range = DateRange.WEEK, - points = previewPoints(base = 420.0), // 睡眠时长(分钟),与 Mock 的 330~510 同量级 + // 睡眠时长(分钟):360 → 504,落在 Mock 的 330~510 内,柱子有可见高低差 + points = previewPoints(base = 360.0, step = 24.0), ), onBack = {}, onSelectRange = {}, @@ -4433,7 +4454,10 @@ private fun MetricDetailContentStepsPreview() { loading = false, metric = MetricType.STEP, range = DateRange.WEEK, - points = previewPoints(base = 7500.0), // 步数,与 Mock 的 3000~12000 同量级 + // 步数:5200 → 11720,落在 Mock 的 3000~12000 内; + // step 必须够大 —— 若像旧稿那样 base 7500 / step 12,7 根柱子全在 99%~100% 高度, + // 预览看起来就是一排等高的方块,等于白预览(ledger R34) + points = previewPoints(base = 5200.0, step = 1000.0), ), onBack = {}, onSelectRange = {},