diff --git a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ShareConfirmPanel.kt b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ShareConfirmPanel.kt index 714fdc6..f831465 100644 --- a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ShareConfirmPanel.kt +++ b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ShareConfirmPanel.kt @@ -1,6 +1,7 @@ package top.yeij.cyrene.ui.chat import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -249,7 +250,8 @@ const val SHARE_CONFIRM_CAPTION_PLACEHOLDER = "加一句描述…" * 极端的一次分享(4 张图 + 5 份文件 + 一段长正文,键盘还弹着)会把这一个面板顶得很高, * 而面板在手机上贴着屏幕底(`BottomCenter`):超出的部分会把下面的输入框与两枚按钮挤出屏幕。 * 所以预览区自己带一个高度上限 + 内部滚动:内容少时它按内容高(上限不生效),内容多时它滚动、 - * 输入框与按钮原地不动。 + * 输入框与按钮原地不动。上限按状态取([sharePreviewMaxHeight]):收起态 + * [SHARE_PREVIEW_MAX_HEIGHT]、用户展开后 [SHARE_PREVIEW_EXPANDED_MAX_HEIGHT]。 * * ## 正文给三行,但三行之外是「滚」不是「省略号」 * 预览只是「分享进来的是什么」的一眼,而用户要的是按一下发送:把一段几万字的正文整个铺开,这个面板 @@ -260,13 +262,32 @@ const val SHARE_CONFIRM_CAPTION_PLACEHOLDER = "加一句描述…" * 个地方放着这段正文**——输入框是「用户附加的描述」(初值恒为空),不再是正文的可编辑副本。于是截断 * 成省略号就等于「按发送之前读不到要发的那段正文」,而用户点这颗「发送」要确认的恰恰是它。所以这一窗 * 内部可滚([ShareConfirmBody]):**面板的高度一分不涨,正文滚着读完**。 + * + * ## 三行读不下时,再给一次「展开」 + * 三行窗 + 内部滚动管的是「读得到」,但一次只看得见三行;主人要的是**也能一次多看几行** + * (W56 拍板:滚动与展开**两个都要**)。所以正文排下来超过三行时(判据是 + * [sharePreviewTextOverflowsWindow],正好三行不给)卡片右上一枚「展开」把这一窗放到 + * [SHARE_PREVIEW_EXPANDED_MAX_HEIGHT],再点「收起」回到三行。没超过三行时这一枚**不出现**—— + * 短正文上摆一枚点了没反应的「展开」比没有更糟。 + * + * 「读不到尾」这条红线在两种状态下都守:收起态窗内滚,展开态由外层这一层滚(正文那一层不再设上限, + * 见 [ShareConfirmBody] 的「展开态为什么干脆不留这一层上限」)。 */ @Composable private fun ShareConfirmPreviews(preview: ShareConfirmPreview) { + // 展开是用户对**这一段正文**的选择,键在 `preview.text` 上:换一份分享就回到默认的收起,同一份 + // 分享里重组(写描述、列表刷新…)保持用户选过的样子。 + // + // ⚠️ 转屏(Activity 重建)会回到收起:跨转屏要活下来就得进 `ChatUiState`——W54C 给「已取走的 + // 分享」与描述走的就是那条路,而那是 `ChatScreen` / `ChatViewModel` 的地盘(本批不许碰)。 + // `rememberSaveable` 是本仓一次都没用过的新写法,而且弹层窗口有没有带上 saveable registry 没在 + // 真机上验过,不在这里试——如实留给主人定。 + var textExpanded by remember(preview.text) { mutableStateOf(false) } + Column( modifier = Modifier .fillMaxWidth() - .heightIn(max = SHARE_PREVIEW_MAX_HEIGHT) + .heightIn(max = sharePreviewMaxHeight(textExpanded)) .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp), ) { @@ -295,6 +316,9 @@ private fun ShareConfirmPreviews(preview: ShareConfirmPreview) { DocumentChipRow(chips = preview.documents.map { DocumentChips.of(it.toDraftDocument()) }) } if (preview.hasText) { + // 「超没超过三行」只有排完才知道(折行位置由字号与系统的大字体缩放决定),所以它由正文 + // 本体量出来之后回调上来,标签这一行凭它决定要不要给出展开控件。 + var bodyOverflows by remember(preview.text) { mutableStateOf(false) } Column( modifier = Modifier .fillMaxWidth() @@ -302,19 +326,68 @@ private fun ShareConfirmPreviews(preview: ShareConfirmPreview) { .background(MiuixTheme.colorScheme.surfaceContainer) .padding(10.dp), ) { - Text( - text = "分享的文字", - style = MiuixTheme.textStyles.footnote2, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "分享的文字", + style = MiuixTheme.textStyles.footnote2, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + // 标签是这一行唯一的权重子项,`fill = false` 让这五个字只占自己需要的宽度; + // 真正保证展开控件不被挤掉的是 Row 的测量顺序——**非权重子项(控件与它前面 + // 的间距)先量**,权重只分配剩下的空间(同 `SessionDrawerContent` 里那一行)。 + modifier = Modifier.weight(1f, fill = false), + ) + if (bodyOverflows) { + // 控件挂在**标签这一行**,不挂在正文末尾:展开后正文可能有好几屏,末尾那枚 + // 「收起」就成了要滚到底才找得到的东西。 + Spacer(Modifier.width(8.dp)) + ShareConfirmExpandToggle( + expanded = textExpanded, + onToggle = { textExpanded = !textExpanded }, + ) + } + } + ShareConfirmBody( + text = preview.text, + expanded = textExpanded, + onOverflowChange = { bodyOverflows = it }, ) - ShareConfirmBody(preview.text) } } } } /** - * 正文那一窗:**高度恒为三行,再长就在这一窗里滚**(W54D 主人拍板,见 [ShareConfirmPreviews])。 + * 「展开 / 收起」那一枚(见 [ShareConfirmPreviews])。 + * + * 为什么是两个字而不是箭头图标:基础图标那一套(`MiuixIcons.Basic.*`)里没有展开 / 收起这一类字形 + * (只有 `Close` / `Search` / `SearchCleanup` / `Sidebar` / `Check` / `ArrowRight` / `ArrowUpDown` + * 七枚),而这两句话本身就说清了点下去会怎样。另一处折叠入口——`AdminOverviewScreen` 的路径组头 + * ——用的是扩展图标里的 `ChevronForward` 加旋转,那是列表行里的形态;这里是一张阅读用的卡片, + * 一句话更省地方,也更好认。 + * + * 用 `Text` + `clickable` 而不是 `TextButton`:后者自带按钮高度,会把这一行(以及整块预览)撑高 + * 一截——同 `MessageBubble` 里那枚「更多」的取舍。 + */ +@Composable +private fun ShareConfirmExpandToggle(expanded: Boolean, onToggle: () -> Unit) { + Text( + text = if (expanded) SHARE_PREVIEW_COLLAPSE_LABEL else SHARE_PREVIEW_EXPAND_LABEL, + style = MiuixTheme.textStyles.footnote2, + color = MiuixTheme.colorScheme.primary, + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onToggle) + // 内边距写在 `clickable` **后面**:命中的就是这块带内边距的区域——只包住字身的触点太小。 + .padding(horizontal = 6.dp, vertical = 2.dp), + ) +} + +/** + * 正文那一窗:**高度恒为三行,再长就在这一窗里滚**(W54D 主人拍板,见 [ShareConfirmPreviews]); + * 用户点了「展开」之后这一窗撤掉自己的上限,整段正文交给外层预览区那一层滚(W56)。 * * ## 为什么量行高,而不是写一个 dp 常量 * 这个上限必须**正好三行**:矮了会在字身上切一刀(滚动停在一个被切开的字形上),高了就等于把「三行」 @@ -324,27 +397,52 @@ private fun ShareConfirmPreviews(preview: ShareConfirmPreview) { * 所以这里改为**量**:先按没有上限排一次,从 `TextLayoutResult` 上取第三行的底边当上限。 * * 上限只写一次(`cap` 还是 [Dp.Infinity] 时才改),所以不会来回跳:拿到上限后重新排版,正文的宽度 - * 与样式都没变,行位置与首帧一致,量出来的值也一样。 + * 与样式都没变,行位置与首帧一致,量出来的值也一样。展开 / 收起切换**也不会改写它**——`cap` 记的 + * 恒是收起态那一窗的高度(这一窗的折行只由宽度决定,高度不受限,两种状态下排出来的行位置一样)。 * - * ⚠️ 代价是**首帧先按完整正文排一次**、下一帧才收到三行:正文很长时,这一窗理论上会有一帧高出一截、 - * 然后回落。另一条路——写死一个 dp 上限——没有这一帧,但在非默认字号档上必然切字,两害相权取了 - * 「量」。这一帧在实际渲染里看不看得见(弹层这时正在进场,可能被动画盖住)**没有在真机上验过**, - * 留给主人真机验收。 + * ## 展开态为什么干脆不留这一层上限 + * 展开之后正文可能几屏长。若在这一窗上再设一个上限,就成了「窗外滚 + 窗内滚」两层都要滚的手势 + * (手指先被内层吃掉、内层滚到底才轮到外层),而两层的上界还互相牵制——最容易出的错就是「明明展开 + * 了、还剩半截读不到」。所以展开态**只留外层那一层**:预览区的上限抬到 + * [SHARE_PREVIEW_EXPANDED_MAX_HEIGHT],整段正文摊在里面,一层滚到底,读不读得到尾只取决于那一层。 + * 代价是展开后不能「只滚正文、图片与文件钉着不动」——那要把两层滚动接起来,这个面板不值这个复杂度。 + * + * ## 首帧的那次回流:留着,且确认它看不见(W56 复核) + * 上限是**首帧之后**才量到的,所以长正文会先按没有上限排一次,下一帧才收到三行。上一批留了个悬念: + * 真机上会不会看见一次跳动。复核的结论是**看不见**,两条理由: + * 1. 首帧那一下撑不动任何别的东西——正文块是预览区里的**最后一个**元素(图片、文件都在它上面), + * 而预览区自己还有一层独立的上限在管可视高度:下面没有东西被它顶下去,上面那两样的位置一分 + * 不动,可视的像素与量完之后**一样**(那些像素由 `heightIn` 的裁剪决定,裁剪高度不变)。 + * 2. 加展开态**不会把它变明显**:展开态本来就没有这一层上限(见上),也就没有「量完再回落」这一步; + * 而展开 / 收起是用户主动的一次布局变化,那一帧的尺寸变化是它的**目的**,不是回流。真正多出来的 + * 是展开控件本身——它要到第二帧才出现(行数是排完才知道的),但这一枚也顶不动东西:标签行里那 + * 两句话用的是同一个 `footnote2`,行高不变。 + * + * 另一条路是在组合期用 `rememberTextMeasurer` 把上限量出来,好让控件从首帧就摆上:那要先知道宽度 + * (得外包一层 `BoxWithConstraints`),还要自己复现字号、字体缩放与字体回退,等于把排版做两遍—— + * 为一个看不见的跳动换掉结构不划算,所以不做。 * * ## 两层滚动怎么分工 - * 这一窗自己滚(正文读完),外层 [ShareConfirmPreviews] 也滚(图片 + 文件芯片 + 这一块合起来的高度 - * 上限)。手指停在正文上时先滚正文、滚到底再把余量交给外层——`verticalScroll` 本来就走嵌套滚动, - * 不用额外接线。 + * 收起态:这一窗自己滚(正文读完),外层 [ShareConfirmPreviews] 也滚(图片 + 文件芯片 + 这一块 + * 合起来的高度上限)。手指停在正文上时先滚正文、滚到底再把余量交给外层——`verticalScroll` 本来 + * 就走嵌套滚动,不用额外接线。展开态下这一窗没有可滚的余量(`heightIn` 给了 [Dp.Infinity]), + * 手势整个交给外层。 */ @Composable -private fun ShareConfirmBody(text: String) { +private fun ShareConfirmBody( + text: String, + expanded: Boolean, + onOverflowChange: (Boolean) -> Unit, +) { val density = LocalDensity.current var cap by remember(text) { mutableStateOf(Dp.Infinity) } Column( modifier = Modifier .fillMaxWidth() - .heightIn(max = cap) + // 展开态给 [Dp.Infinity] = 这一层没有上限;这正是**首帧那个状态**(`cap` 一开始就是它), + // 所以展开态不多一条代码路径,也不会比首帧更糟。 + .heightIn(max = if (expanded) Dp.Infinity else cap) .verticalScroll(rememberScrollState()), ) { Text( @@ -356,22 +454,79 @@ private fun ShareConfirmBody(text: String) { val lastVisible = layout.lineCount.coerceAtMost(SHARE_PREVIEW_TEXT_LINES) - 1 cap = with(density) { layout.getLineBottom(lastVisible).toDp() } } + // 行数与上限无关(这一窗没有 `maxLines`、测量高度不受限,排一次行数就定了),所以 + // 这一句同时管着「收不起时要不要给出展开控件」与「展开态下要不要留着它」。 + onOverflowChange(sharePreviewTextOverflowsWindow(layout.lineCount)) }, ) } } -/** 预览区的高度上限(见 [ShareConfirmPreviews]:超出的部分靠内部滚动看)。 */ -private val SHARE_PREVIEW_MAX_HEIGHT = 150.dp +/** 预览区在**收起态**的高度上限(见 [ShareConfirmPreviews]:超出的部分靠内部滚动看)。 */ +internal val SHARE_PREVIEW_MAX_HEIGHT = 150.dp + +/** + * 预览区在**展开态**的高度上限(用户点了「展开」之后,见 [ShareConfirmPreviews])。 + * + * 为什么要抬:展开是**用户自己的一个动作**,要的就是「一次多看几行」。上限不动的话这一窗永远只有 + * 三行高,点开等于没点——正文那一窗顶到 [SHARE_PREVIEW_MAX_HEIGHT] 也只是把三行换成十来行里的一 + * 小截。所以展开态必须真的给回高度。 + * + * 为什么是 240dp 而不是更大:这个上限管的是**整个预览区**,而它下面还压着描述输入框(最多 4 行) + * 与两枚按钮,面板又贴着屏幕底(`BottomCenter`)、键盘一弹可用高度只剩半屏上下。150 → 240 在默认 + * 字号档上把正文窗从三行抬到十行左右(`footnote1` 13sp,行高由系统字体决定、随机型与大字体设置一起 + * 变),加上标题 / 输入框 / 按钮之后整块仍在半屏出头;再往上(例如给到整屏的六成)先被顶掉的会是 + * 两枚按钮。 + * + * ⚠️ 这个数**没有在真机上量过**,是按上面那笔账取的保守值,观感由主人验收。要调只改这一处: + * 预览区那一层的高度上限取的就是 [sharePreviewMaxHeight] 这一个出口。 + */ +internal val SHARE_PREVIEW_EXPANDED_MAX_HEIGHT = 240.dp /** * 预览里正文那一窗给几行(W54D 起**不再截断**,超出的部分在窗内滚动,见 [ShareConfirmBody])。 * + * 它同时是「要不要给出展开控件」的那条线([sharePreviewTextOverflowsWindow]):正好这么多行就没有 + * 东西可展开。 + * * `internal` 是为了让 `ShareConfirmTest` 能把这个数钉成字面量:面板的滚动行为单测够不着(本模块没有 * Compose UI 测试的依赖),能钉住的只有「这一窗是三行」这条判据本身。 */ internal const val SHARE_PREVIEW_TEXT_LINES = 3 +/** + * 展开控件上那两句话:点它就在这两句之间切换。 + * + * 两句都必须是**动作**(「展开」/「收起」),不能是「更多」/「更少」——后者说的是内容有多少,读的人 + * 不知道点下去会发生什么。 + */ +internal const val SHARE_PREVIEW_EXPAND_LABEL = "展开" +internal const val SHARE_PREVIEW_COLLAPSE_LABEL = "收起" + +/** + * 正文**排下来**超过三行窗口了吗(**纯函数**,可单测)——也就是「要不要给出展开控件」。 + * + * 判据是**行数**,不是 `hasVisualOverflow`:正文这一窗的裁剪落在它外面那层 `Column` 的 + * `heightIn(max = cap)` 上,而这一窗自己的排版宽度是满宽、高度不受限(`verticalScroll` 给子项的 + * 就是无上限约束),所以 `Text` 自己**永远**报「没有溢出」——拿它当判据会在最长的那种正文上漏掉 + * 控件。行数则与上限无关:只要排下来比窗口多一行,第四行就一定在窗口之外,展开就有东西可看。 + * + * 边界是**严格大于**:正好三行时整段都在窗里(`getLineBottom(2)` 就是这一窗的底),此时给一枚 + * 「展开」是点了没有反应的假控件。 + */ +internal fun sharePreviewTextOverflowsWindow(lineCount: Int): Boolean = + lineCount > SHARE_PREVIEW_TEXT_LINES + +/** + * 预览区当前该给多高(**纯函数**,可单测):收起态 [SHARE_PREVIEW_MAX_HEIGHT]、展开态 + * [SHARE_PREVIEW_EXPANDED_MAX_HEIGHT]。 + * + * 把「哪个状态用哪个上限」收在这一处,调用点([ShareConfirmPreviews])只读结论:单测钉住的就是这条 + * 判据本身——两个状态与两个上限的对应关系要是被改反了(例如展开反而变矮),会在这里被抓住。 + */ +internal fun sharePreviewMaxHeight(expanded: Boolean): Dp = + if (expanded) SHARE_PREVIEW_EXPANDED_MAX_HEIGHT else SHARE_PREVIEW_MAX_HEIGHT + /** * 分享进来的一份文件 ⇒ 待发附件。 * diff --git a/app/src/test/kotlin/top/yeij/cyrene/ui/chat/ShareConfirmTest.kt b/app/src/test/kotlin/top/yeij/cyrene/ui/chat/ShareConfirmTest.kt index b6210a6..a331c19 100644 --- a/app/src/test/kotlin/top/yeij/cyrene/ui/chat/ShareConfirmTest.kt +++ b/app/src/test/kotlin/top/yeij/cyrene/ui/chat/ShareConfirmTest.kt @@ -1,7 +1,9 @@ package top.yeij.cyrene.ui.chat +import androidx.compose.ui.unit.dp import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue import org.junit.Test import top.yeij.cyrene.domain.share.SharedContent @@ -13,7 +15,7 @@ import top.yeij.cyrene.domain.share.SharedDocument * 背景:主人 2026-10-06 拍板——分享进来的内容不再「一到就发」,要像其他 IM 一样先给一个确认面板。 * 面板本身是 Compose 的东西、只能在真机上看(观感由主人验收),但它**该显示什么**、**发送时正文 * 是哪几段拼起来的**、**「发送」是否可点**这三件事不该只活在 Composable 里:它们决定了「用户看到 - * 的是不是他要发的那一份」。本测试钉住四件事: + * 的是不是他要发的那一份」。本测试钉住四件事(W56 加的两条判据另见文末那一节): * * 1. **内容一样都不少**:图片与文件按分享进来的顺序原样进预览(多张图的顺序就是发送顺序); * 2. **空的也算没有**:空地址画出来是一个坏掉的缩略图,全空白的正文占着一行什么也不说; @@ -39,6 +41,14 @@ import top.yeij.cyrene.domain.share.SharedDocument * 没有 Compose UI 测试的依赖),所以这里钉的是它的两个前提——**正文完整地进到预览**(一个字都不少) * 与**这一窗仍然是三行**(面板不随正文变高),见 `the whole body reaches the preview …` 与 * `the preview window stays three lines tall`。 + * + * ## W56 又加上了什么(主人拍板:「可以滚动 可以展开」,两个都要) + * 三行窗 + 内部滚动管的是「读得到」,但一次只看得见三行;所以正文排下来超过三行时([sharePreviewTextOverflowsWindow]) + * 卡片右上一枚「展开」把这一窗放到 [SHARE_PREVIEW_EXPANDED_MAX_HEIGHT],再点「收起」回到三行。 + * **展开与滚动本身仍然是渲染层的事、单测够不着**,所以这里钉的同样是它们的两个前提:**什么时候该 + * 出现那枚控件**、**展开态给多高**——两条判据都做成了纯函数([sharePreviewTextOverflowsWindow] / + * [sharePreviewMaxHeight]),见 `the expand control shows up only when the body does not fit the + * window` 与 `expanding gives the preview more room, still within a bound`。 */ class ShareConfirmTest { @@ -247,4 +257,63 @@ class ShareConfirmTest { // 当成「用户把正文删光了」而变灰)。它单独钉一条字面量,免得将来有人把它当成回归改回去。 assertTrue(shareConfirmCanSend(SharedContent(text = "看这个"), "")) } + + // ——— W56:「展开 / 收起」(主人拍板:滚动与展开**两个都要**)——— + // 展开与滚动本身在单测里断言不了(本模块没有 Compose UI 测试的依赖),能钉住的是它们的两个前提: + // 那枚控件什么时候出现、展开态给多高。两条判据都在 ShareConfirmPanel.kt 里做成了纯函数。 + + @Test + fun `the expand control shows up only when the body does not fit the window`() { + // 正好三行 = 整段都在窗里(这一窗的底就是第三行的底边):此时给一枚「展开」就是主人说的 + // 「点了没反应的展开」。多一行就是第四行落在窗外,展开确有东西可看。 + assertFalse(sharePreviewTextOverflowsWindow(0)) + assertFalse(sharePreviewTextOverflowsWindow(1)) + assertFalse(sharePreviewTextOverflowsWindow(SHARE_PREVIEW_TEXT_LINES)) + assertTrue(sharePreviewTextOverflowsWindow(SHARE_PREVIEW_TEXT_LINES + 1)) + assertTrue(sharePreviewTextOverflowsWindow(40)) + + // 判据跟着那一窗的行数走(上面用的是常量),所以再用主人拍的那个数字面量钉一遍「这条线是 + // 3」——两条一起才既钉住行为、又钉住那个数(同 `the preview window stays three lines tall` + // 的口径)。 + assertFalse(sharePreviewTextOverflowsWindow(3)) + assertTrue(sharePreviewTextOverflowsWindow(4)) + + // 接一份真分享走一遍:正文里每有一个换行,排下来就至少多一行,所以 40 行的那段一定超过窗口 + // (排版后的行数只会比 `lines().size` 更多——单行太长还会折行)。 + val preview = shareConfirmPreview(SharedContent(text = (1..40).joinToString("\n") { "第 $it 行" })) + assertTrue(sharePreviewTextOverflowsWindow(preview.text.lines().size)) + } + + @Test + fun `expanding gives the preview more room, still within a bound`() { + // 「哪个状态用哪个上限」只有这一处出处(调用点只读结论),所以两个数都钉成字面量:改数就是 + // 改面板的高度预算,必须是有意为之。 + assertEquals(150.dp, sharePreviewMaxHeight(expanded = false)) + assertEquals(240.dp, sharePreviewMaxHeight(expanded = true)) + assertEquals(SHARE_PREVIEW_MAX_HEIGHT, sharePreviewMaxHeight(expanded = false)) + assertEquals(SHARE_PREVIEW_EXPANDED_MAX_HEIGHT, sharePreviewMaxHeight(expanded = true)) + + // 展开**必须**比收起高:不高就等于「点开等于没点」(主人要的正是「一次多看几行」)——展开态 + // 若反而变矮,这条会抓住。 + assertTrue(sharePreviewMaxHeight(expanded = true) > sharePreviewMaxHeight(expanded = false)) + + // 但也不许大到把面板顶穿:这个上限管的是**整个预览区**,而它下面还压着描述输入框(最多 4 行) + // 与两枚按钮,面板又贴着屏幕底。两倍收起态是这条的边界(240 < 300)。 + assertTrue( + "展开态的上限不该到收起态的两倍", + sharePreviewMaxHeight(expanded = true) < sharePreviewMaxHeight(expanded = false) * 2, + ) + } + + @Test + fun `the two labels of the control say which way the tap will go`() { + // 控件就在这两句之间切换:两句必须**不同**(否则用户看不出此刻是哪一种状态、也猜不到点下去 + // 会怎样),也都不能是空的(空标签 = 一个看不出是什么的可点区域)。这两句是主人措辞的一部分, + // 钉成字面量。 + assertEquals("展开", SHARE_PREVIEW_EXPAND_LABEL) + assertEquals("收起", SHARE_PREVIEW_COLLAPSE_LABEL) + assertNotEquals(SHARE_PREVIEW_EXPAND_LABEL, SHARE_PREVIEW_COLLAPSE_LABEL) + assertTrue(SHARE_PREVIEW_EXPAND_LABEL.isNotBlank()) + assertTrue(SHARE_PREVIEW_COLLAPSE_LABEL.isNotBlank()) + } }