merge: W53X —— 分享进来先弹确认面板(预览图片/文件 + 可附文字 + 发送/取消),不再一到就发

This commit is contained in:
AskaEth committed 2026-10-06 18:55:36 +08:00
commit 62f552f87f
3 files changed
+502 -4

No files matched your search

@@ -0,0 +1,148 @@
package top.yeij.cyrene.ui.chat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import top.yeij.cyrene.domain.share.SharedContent
import top.yeij.cyrene.domain.share.SharedDocument
/**
* 分享确认面板的三条判据([shareConfirmPreview] / [shareConfirmDraft] / [shareConfirmCanSend])。
*
* 背景:主人 2026-10-06 拍板——分享进来的内容不再「一到就发」,要像其他 IM 一样先给一个确认面板。
* 面板本身是 Compose 的东西、只能在真机上看(观感由主人验收),但它**该显示什么**、**输入框从哪
* 个值开始**、**「发送」是否可点**这三件事不该只活在 Composable 里:它们决定了「用户看到的是不是
* 他要发的那一份」。本测试钉住四件事:
*
* 1. **内容一样都不少**:图片与文件按分享进来的顺序原样进预览(多张图的顺序就是发送顺序);
* 2. **空的也算没有**:空地址画出来是一个坏掉的缩略图,全空白的正文占着一行什么也不说;
* 3. **输入框的初值就是分享进来的正文本身,一个字不加**——这是红线:往用户正文里拼来源标记
* (「[用户分享]」之类)等于让用户正文冒充系统指令(`02/F4` / `design/11 §10`);
* 4. **「发送」可点 ≙ 真的会有东西发出去**:与 `sendShared` 开头那条早退同一条判据——那边在
* 「正文空白 + 没有图片 + 没有文件」时原地返回,一次发送都不会发生。
*/
class ShareConfirmTest {
@Test
fun `the preview shows what came in, in the order it came in`() {
val preview = shareConfirmPreview(
SharedContent(
text = "看这个",
images = listOf("file:///cache/share-a.gif", "file:///cache/share-b.png"),
documents = listOf(SharedDocument(uri = "file:///cache/share-0.txt", displayName = "笔记.txt")),
),
)
// 顺序 = 发送顺序(第一条消息带第一张,见 imageSendPlan):预览里换一个顺序就是在替用户
// 改他要发的东西。
assertEquals(listOf("file:///cache/share-a.gif", "file:///cache/share-b.png"), preview.images)
assertEquals(listOf("笔记.txt"), preview.documents.map { it.displayName })
assertEquals("看这个", preview.text)
assertTrue(preview.hasText)
assertFalse(preview.isEmpty)
}
@Test
fun `blank pieces count as nothing to show`() {
val preview = shareConfirmPreview(
SharedContent(
text = " ",
images = listOf("", " "),
documents = listOf(SharedDocument(uri = " ")),
),
)
// 一条空地址画出来就是一个空白缩略图(看着像图坏了);一段全空白的正文占着一行什么也不说。
assertTrue(preview.images.isEmpty())
assertTrue(preview.documents.isEmpty())
assertFalse(preview.hasText)
assertTrue(preview.isEmpty)
}
@Test
fun `a share nothing could be read from has nothing to preview`() {
// `contentUnreadable` 那条分享(对方给了东西、本端连地址都没认下来)照样会走到面板:
// 它的预览是空的。这一档仍由既有的 shareNoticeFor 成句,面板不替它编话。
val preview = shareConfirmPreview(SharedContent(contentUnreadable = true))
assertTrue(preview.isEmpty)
}
@Test
fun `the draft starts as the shared text itself, with nothing added`() {
// 从浏览器分享一个链接进来:那段地址就是用户要发的东西,初值一字不差就是它。
val link = "https://example.com/某个页面?id=1"
assertEquals(link, shareConfirmDraft(SharedContent(text = link)))
// 分享里本来没有正文 ⇒ 空手开始(不是一句模板、也不是一个空行)。
assertEquals("", shareConfirmDraft(SharedContent(images = listOf("file:///cache/share-a.png"))))
// 两侧空白归一(与 ShareRules.extract 取正文那一处同一个口径)。
assertEquals("看这个", shareConfirmDraft(SharedContent(text = " 看这个 ")))
}
@Test
fun `the draft is exactly what the preview shows`() {
// 两边必须同值:预览里那一块说的是「分享进来的是什么」,而输入框的初值就是它的可编辑副本。
// 分成两个判据之后若各写各的,窗口里就会出现「预览显示 A、输入框里是 B」这种一眼看去
// 分不清要发哪一份的状态。
listOf(
SharedContent(text = "看这个"),
SharedContent(text = " 带空白的正文 "),
SharedContent(images = listOf("file:///cache/share-a.png")),
SharedContent(),
).forEach { share ->
assertEquals(
"预览与输入框的初值必须同源",
shareConfirmPreview(share).text,
shareConfirmDraft(share),
)
}
}
@Test
fun `the draft never carries a source marker`() {
val draft = shareConfirmDraft(SharedContent(text = "看这个"))
// 来源标识走后端的 RoleSystem 来源消息 + 结构化 SenderName,人类可见的标记只能放在本端
// 渲染层——把它拼进正文 = 让用户正文冒充系统指令,项目红线。
assertEquals("看这个", draft)
assertFalse(draft.contains("分享"))
assertFalse(draft.contains("[用户"))
}
@Test
fun `send is offered whenever something would really go out`() {
val nothing = SharedContent()
val text = SharedContent(text = "看这个")
val image = SharedContent(images = listOf("file:///cache/share-a.png"))
val document = SharedContent(documents = listOf(SharedDocument(uri = "file:///cache/share-0.txt")))
// 用户改过的正文也算(他把分享的链接改短、或者换上自己想说的话)。
assertTrue(shareConfirmCanSend(text, "改过的正文"))
assertTrue(shareConfirmCanSend(nothing, "只写一句话也行"))
// 纯图片 / 纯文件照样可发:服务端支持只带 image / documents 的回合,本端不因为「没写文字」拦它。
assertTrue(shareConfirmCanSend(image, ""))
assertTrue(shareConfirmCanSend(document, ""))
assertTrue(shareConfirmCanSend(image, " "))
// 把正文整段删光,图片与文件照样发得出去:用户反悔的只是那段文字,不是这次分享的其余部分。
val textAndImage = SharedContent(text = "看这个", images = listOf("file:///cache/share-a.png"))
assertTrue(shareConfirmCanSend(textAndImage, ""))
}
@Test
fun `send is not offered when nothing at all would go out`() {
// 这条与 `sendShared` 开头那条早退**同一条**:正文(去空白后)为空、图片为空、文件也为空时
// 那边原地 `return`——一次发送都不会发生。键若画成可点的,用户按下去只看到面板关掉、
// 什么都没发生,而他会以为自己少发了一条。
assertFalse(shareConfirmCanSend(SharedContent(), ""))
assertFalse(shareConfirmCanSend(SharedContent(), " "))
assertFalse(shareConfirmCanSend(SharedContent(text = "看这个"), " "))
// 正文被删光的**纯文本分享**(一个链接分享就是这一档)就什么都不剩了——此时没有可发的
// 东西,而这正是「发送」该变灰的那种情形。
assertFalse(shareConfirmCanSend(SharedContent(text = "看这个"), ""))
// 只剩「丢件计数」的分享(有 2 张图读不到)同样没有可发的东西。
assertFalse(shareConfirmCanSend(SharedContent(failedImages = 2), ""))
}
}