merge: W24 —— 编辑弹层只显示纯文本(原文含标记时如实告知会被替换,绝不静默丢旁白)

This commit is contained in:
AskaEth committed 2026-10-04 22:08:08 +08:00
commit 1794e53d1c
4 files changed
+333 -11

No files matched your search

@@ -0,0 +1,185 @@
package top.yeij.cyrene.domain.chat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* 编辑框初值:什么时候逐字节恒等,什么时候剥标记,剥完还剩下什么。
*
* 红线是「不许静默丢东西」,所以两个方向都要钉住:
* - **纯文本一个字符都不许动**——哪怕带连续空行、CRLF、全角空格,哪怕里面有 `<`(`1 < 2`、`<div>`
* 都是普通文本,见到尖括号就剥是把用户打的字当标签吃掉);
* - **真有标记时也不许把旁白/动作丢掉**——旁白是她说的话的一部分,剥掉标签不等于可以丢内容。
*
* 判据本身在 [editableMessageOf],剥标记完全交给 `domain/parse`(项目里唯一的解析器),
* 这里不重复实现它的规则。
*/
class MessageEditTextTest {
// ------------------------------------------------------------------
// 一、纯文本:逐字节恒等
// ------------------------------------------------------------------
/** 纯文本路径的唯一断言:`hasMarkup` 为 false,且文本与原文**逐字符**相等。 */
private fun assertUntouched(content: String) {
val editable = editableMessageOf(content)
assertFalse("「$content」不该被判成带标记", editable.hasMarkup)
assertEquals("「$content」必须逐字符原样", content, editable.text)
}
@Test
fun `普通中英文纯文本逐字节恒等`() {
assertUntouched("你好呀,今天过得怎么样?Hello, world — 我在这儿。")
}
@Test
fun `空串就是空串`() {
assertEquals(EditableMessage(text = "", hasMarkup = false), editableMessageOf(""))
}
@Test
fun `首尾空白与各种空白字符原样保留`() {
// 用户打的就是这些字:不 trim,也不把全角空格 / 不换行空格 / 零宽空格 / 制表符换成别的。
assertUntouched("\t 第一行\u3000\u00A0\u200B\n 第二行 \n")
}
@Test
fun `连续三个换行不折叠`() {
// 解析器的空行切分是**渲染**的规则,编辑框不欠它什么:多打的空行照旧是用户的东西。
assertUntouched("第一段\n\n\n\n第二段")
}
@Test
fun `换行符不统一 —— CRLF 与单独的 CR 都原样保留`() {
assertUntouched("第一行\r\n第二行\r第三行\n")
}
@Test
fun `小于号不是标记 —— 数学符号与普通标签都算普通文本`() {
// 判据只认解析器真正认识的那三个标签。见到 `<` 就当标记,就会把用户打的数学符号吃掉。
listOf(
"1 < 2 且 3 > 2",
"if (a < b) { c }",
"a<b>c",
"<3",
"<div>这是一段 HTML</div>",
"</span>",
"<p2>这不关 p 标签的事</p2>",
"<P>大小写不认(解析器只认小写 p)</P>",
).forEach(::assertUntouched)
}
@Test
fun `裸的星号旁白不触发剥标记`() {
// 星号式旁白(*…* / *动作:…*)不是本模块认的标记:编辑框原样显示,保存也原样发。
assertUntouched("*弯起眼睛笑了*我今天想你了")
assertUntouched("*动作:她歪了歪头*")
}
// ------------------------------------------------------------------
// 二、真有标记:剥掉标签,内容一件不少
// ------------------------------------------------------------------
@Test
fun `线上真实形状 —— 旁白与正文都在,且不再有尖括号`() {
val content = "<p><action>把整卷纸条从桌头铺到桌尾,最后终于摸到了尾巴</action>" +
"读到头也读到尾啦,亲爱的♪</p>\n\n<p>完整长度 624046 字符。</p>"
val editable = editableMessageOf(content)
assertTrue(editable.hasMarkup)
assertEquals(
"把整卷纸条从桌头铺到桌尾,最后终于摸到了尾巴读到头也读到尾啦,亲爱的♪\n\n完整长度 624046 字符。",
editable.text,
)
assertFalse("剥完不该再剩标签", editable.text.contains('<') || editable.text.contains('>'))
// 旁白(动作)与正文都得在:旁白是她说的话的一部分,不是可以顺手丢掉的东西。
assertTrue(editable.text.contains("把整卷纸条从桌头铺到桌尾,最后终于摸到了尾巴"))
assertTrue(editable.text.contains("读到头也读到尾啦,亲爱的♪"))
assertTrue(editable.text.contains("完整长度 624046 字符。"))
}
@Test
fun `纯旁白消息 —— 旁白那句话本身留下`() {
val editable = editableMessageOf("<action>弯起眼睛笑了</action>")
assertTrue(editable.hasMarkup)
assertEquals("弯起眼睛笑了", editable.text)
}
@Test
fun `narration 标签同样算标记`() {
val editable = editableMessageOf("<narration>内心:她也在想你</narration>")
assertTrue(editable.hasMarkup)
assertEquals("内心:她也在想你", editable.text)
}
@Test
fun `两个气泡之间只空一行`() {
assertEquals("第一段\n\n第二段", editableMessageOf("<p>第一段</p>\n\n<p>第二段</p>").text)
}
@Test
fun `带属性的 p 标签也认 —— 容差与解析器一致`() {
val editable = editableMessageOf("<p class=\"line\">你好呀</p>")
assertTrue(editable.hasMarkup)
assertEquals("你好呀", editable.text)
}
@Test
fun `气泡内的换行保留`() {
assertEquals("第一行\n第二行", editableMessageOf("<p>第一行\n第二行</p>").text)
}
@Test
fun `气泡内的 emoji 不被拆坏`() {
assertEquals("你好🎀再见", editableMessageOf("<p>你好🎀再见</p>").text)
}
@Test
fun `head 旁白排在正文之前 —— 按原文顺序,不按渲染分块重排`() {
val editable = editableMessageOf("<p><action>她歪了歪头</action>你好呀</p>")
assertTrue(editable.hasMarkup)
assertEquals("她歪了歪头你好呀", editable.text)
}
@Test
fun `tail 旁白也留下`() {
assertEquals("你好呀她挥了挥手", editableMessageOf("<p>你好呀<action>她挥了挥手</action></p>").text)
}
@Test
fun `标签外散落的正文不丢`() {
assertEquals(
"开头这几句没包在标签里\n\n第二段",
editableMessageOf("开头这几句没包在标签里\n\n<p>第二段</p>").text,
)
}
@Test
fun `气泡内的星号旁白随标签一起剥掉 —— 口径来自解析器`() {
// 有标记时怎么剥由 parse 说了算:它认 *笑* 是旁白,所以编辑框里留下旁白那句话、没有星号。
assertEquals("hi 笑", editableMessageOf("<p>hi *笑*</p>").text)
}
// ------------------------------------------------------------------
// 三、畸形:宁可不剥,也不许丢字
// ------------------------------------------------------------------
@Test
fun `没闭合的 p 不抛异常 —— 解析器按普通文本处理,这里就恒等`() {
assertUntouched("<p>没闭合就结束")
}
@Test
fun `没闭合的 action 同样原样留着`() {
assertUntouched("<action>她笑了,可是没写结尾")
}
@Test
fun `只有一对空标签的消息恒等 —— 解析不出气泡就不剥`() {
// parse("<p></p>") 是空列表:宁可把 `content` 原样留着,也不赌一把把整条消息删空。
assertUntouched("<p></p>")
}
}