merge: W53X —— 分享进来先弹确认面板(预览图片/文件 + 可附文字 + 发送/取消),不再一到就发
This commit is contained in:
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), ""))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user