fix(chat): 分享面板展开态也会崩 —— 只去掉上限是不够的,必须连 verticalScroll 一起去掉

补上一个修复的漏:上一笔只把展开态的 heightIn 上限去掉,没去掉 verticalScroll。而外层
ShareConfirmPreviews 自己就是一个 verticalScroll,它给内容的是**无界高度** ⇒ 这一层的 scroll
在无界高度下被测量 ⇒ 抛同一个 IllegalStateException。真机上点「展开」的那一下崩了第二次
(pid 2077,01:48:53,reason=4 APP CRASH)。

改:展开态把这一层**整个**去掉(上限 + 滚动一起),只留外层预览区那一层滚 —— 这也正是 W56
原本的设计意图('展开态只留外层那一层')。shareConfirmWindowCap 去掉了 expanded 维度(展开态
不再走它),测试同步改写并补了一条「两种状态在链上都有有限约束」。

真机验收(emulator-5554 + 真后端):收起 153px(3 行)+『展开』→ 点开展开 255px +『收起』→
点回收起 153px,**三次操作同一个 pid 4063,没有任何新崩溃记录**。
This commit is contained in:
AskaEth committed 2026-10-07 09:51:44 +08:00
1 parent 6d4f2cdbae
commit df7b504fa2
2 files changed
+57 -48

No files matched your search

@@ -321,49 +321,47 @@ class ShareConfirmTest {
/**
* W58:正文那一窗的高度上限**永远不许是 [androidx.compose.ui.unit.Dp.Infinity]**。
*
* 背景是真机上的一次崩溃:`heightIn(max = Dp.Infinity)` 等于**不施加约束**,而 `verticalScroll`
* 一旦在无界高度下被测量,Compose 的 `checkScrollableContainerConstraints` 会直接抛
* `IllegalStateException` 把 App 打崩(分享面板一打开就闪退,栈是 `ScrollNode.measure` ←
* `SizeNode.measure` ← `DialogLayout.internalOnMeasure`)。
* 背景是真机上**两次**崩溃、同一个异常:`heightIn(max = Dp.Infinity)` 等于**不施加约束**,而
* `verticalScroll` 一旦在无界高度下被测量,Compose 的 `checkScrollableContainerConstraints` 会
* 直接抛 `IllegalStateException` 把 App 打崩。
*
* 这条不变量只能靠纯函数钉住:本模块的 `testImplementation` 只有 junit,没有 Compose UI 测试依赖,
* 渲染层的崩溃单测抓不到。
* 两次踩在不同的地方,都值得钉住:
* 1. **初值**用了 `Dp.Infinity` 当「还没量到」的哨兵值(分享面板一打开就闪退);
* 2. **展开态**只去掉了上限、把 `verticalScroll` 留着 —— 外层预览区自己就是一个 `verticalScroll`,
* 它给内容的是**无界高度**,于是这一层的 scroll 在无界高度下被测量(点「展开」的那一下闪退)。
*
* 结论:这一层的高度上限只经 [shareConfirmWindowCap] 一个出口(永远有限),且展开态**连滚动一起去掉**。
*
* 这两条都只能靠纯函数钉住:本模块的 `testImplementation` 只有 junit,没有 Compose UI 测试依赖,
* 渲染层的崩溃单测抓不到 —— 这也是它两次都只在真机上暴露的原因。
*/
class ShareConfirmWindowCapTest {
@Test
fun `the fallback cap is finite and never infinity`() {
val fallback = shareConfirmWindowCap(measured = null, expanded = false)
org.junit.Assert.assertNotNull("首帧必须给出一个上限,不能是 null", fallback)
assertTrue("首帧的兜底上限必须是有限值", fallback!!.value.isFinite())
val fallback = shareConfirmWindowCap(measured = null)
assertTrue("首帧的兜底上限必须是有限值", fallback.value.isFinite())
assertEquals(SHARE_PREVIEW_MAX_HEIGHT, fallback)
}
@Test
fun `the expanded state leaves this layer unbounded instead of passing infinity`() {
// 展开态是「这一层不设上限」(null),**不是**「给一个无穷大的上限」——后者正是崩溃的成因。
org.junit.Assert.assertNull(shareConfirmWindowCap(measured = null, expanded = true))
org.junit.Assert.assertNull(shareConfirmWindowCap(measured = 42.dp, expanded = true))
// 而且外层仍然是一层**有限**约束在管事。
assertTrue(sharePreviewMaxHeight(expanded = true).value.isFinite())
}
@Test
fun `a measured cap is passed through`() {
assertEquals(42.dp, shareConfirmWindowCap(measured = 42.dp, expanded = false))
assertEquals(42.dp, shareConfirmWindowCap(measured = 42.dp))
}
@Test
fun `no input combination can produce a non finite cap`() {
val measured = listOf(null, 0.dp, 1.dp, 150.dp, 10_000.dp)
for (m in measured) {
for (expanded in listOf(false, true)) {
val cap = shareConfirmWindowCap(m, expanded)
assertTrue(
"m=$m expanded=$expanded 给出了非有限的上限:$cap",
cap == null || cap.value.isFinite(),
)
}
fun `no input can produce a non finite cap`() {
for (m in listOf(null, 0.dp, 1.dp, 150.dp, 10_000.dp)) {
val cap = shareConfirmWindowCap(m)
assertTrue("measured=$m 给出了非有限的上限:$cap", cap.value.isFinite())
}
}
@Test
fun `both preview states keep a finite height bound somewhere in the chain`() {
// 展开态这一层「连滚动一起去掉」了,但**外层预览区**仍是一层有限约束在管事 ——
// 这正是展开后不会掉进同一个「无界高度」崩溃的原因。
assertTrue(sharePreviewMaxHeight(expanded = true).value.isFinite())
assertTrue(sharePreviewMaxHeight(expanded = false).value.isFinite())
}
}