106 lines
5.3 KiB
Swift
106 lines
5.3 KiB
Swift
import MarkdownUI
|
|
import SwiftUI
|
|
import Testing
|
|
import UIKit
|
|
@testable import Sybil
|
|
|
|
@Suite @MainActor
|
|
struct MessagePresentationTests {
|
|
@Test func agentMarkdownKeepsAltAndFormattingWithoutImageOrHTMLNodes() {
|
|
let source = """
|
|
**Caption** 
|
|
|
|
![Reference landscape][photo]
|
|
|
|
[](https://example.invalid)
|
|
|
|
> ](data:image/png;base64,AAAA)
|
|
|
|
<picture><source srcset="https://images.invalid/source.png"><img src="https://images.invalid/html.png" alt="HTML landscape" title="a > b"></picture>
|
|
|
|
[Useful link](https://example.invalid)
|
|
|
|
[photo]: https://images.invalid/reference.png
|
|
"""
|
|
let content = AgentMessageMarkdown.content(source)
|
|
let html = content.renderHTML(), plain = content.renderPlainText()
|
|
for label in ["Inline landscape", "Reference landscape", "Linked landscape", "Nested inner", "HTML landscape"] {
|
|
#expect(plain.contains(label))
|
|
}
|
|
#expect(!html.contains("<img")); #expect(!html.contains("<picture")); #expect(!html.contains("<source"))
|
|
#expect(!html.contains("images.invalid")); #expect(!html.contains("/private/"))
|
|
#expect(html.contains("<strong>Caption</strong>"))
|
|
#expect(html.contains(#"href="https://example.invalid""#))
|
|
// Normal chat still uses unmodified MarkdownUI content.
|
|
#expect(MarkdownContent(source).renderHTML().contains("<img"))
|
|
#expect(AgentMessageMarkdown.content("<img src='https://images.invalid/a.png' alt='HTML landscape'>\n").renderHTML() == "<p>HTML landscape</p>\n")
|
|
}
|
|
|
|
@Test func agentMarkdownRetainsCodeTablesAndEscapesImageLikeAltText() {
|
|
let source = """
|
|
``
|
|
|
|
```html
|
|
<img src="literal-code">
|
|
```
|
|
|
|
| Image | Value |
|
|
| --- | --- |
|
|
|  | **Kept** |
|
|
|
|
<IMG ALT='' SRC='ignored'>
|
|
"""
|
|
let html = AgentMessageMarkdown.content(source).renderHTML()
|
|
#expect(html.contains("<table>")); #expect(html.contains("Table alt"))
|
|
#expect(html.contains("<strong>Kept</strong>"))
|
|
#expect(html.contains("<img src=")); #expect(html.contains("![literal]"))
|
|
#expect(html.contains("![literal alt]")); #expect(!html.contains("<img"))
|
|
}
|
|
|
|
@Test func userMarkupRemainsLiteral() {
|
|
let source = "**bold** [link](https://example.invalid) `code`\n- item\n<script>literal</script>"
|
|
let content = SybilMessageText.literalContent(source)
|
|
#expect(content.renderPlainText() == source)
|
|
#expect(!content.renderHTML().contains("<strong>"))
|
|
#expect(!content.renderHTML().contains("<a "))
|
|
#expect(!content.renderHTML().contains("<code>"))
|
|
#expect(!content.renderHTML().contains("<ul>"))
|
|
#expect(content.renderHTML().contains("<script>"))
|
|
}
|
|
|
|
@Test func literalLineBreaksAndParagraphsStayDistinct() {
|
|
let content = SybilMessageText.literalContent("First\nSecond\n\nThird")
|
|
#expect(content.renderPlainText() == "First\nSecond\n\nThird")
|
|
#expect(content.renderHTML() == "<p>First\nSecond</p>\n<p>Third</p>\n")
|
|
// Empty paragraphs retain layout space instead of disappearing.
|
|
#expect(SybilMessageText.literalContent("\n\nFirst\n\n\n\nLast\n\n").renderHTML().components(separatedBy: "<p>").count == 6)
|
|
}
|
|
|
|
@Test func pastedParagraphBoundariesMatchMarkdown() {
|
|
for source in ["First\r\n\r\nSecond", "First\n \nSecond", "First\n\t\nSecond"] {
|
|
#expect(SybilMessageText.literalContent(source).renderHTML() == "<p>First</p>\n<p>Second</p>\n")
|
|
}
|
|
#expect(SybilMessageText.literalContent("First\r\nSecond").renderPlainText() == "First\nSecond")
|
|
#expect(SybilMessageText.literalContent(" **literal** \n\nLast").renderPlainText() == " **literal** \n\nLast")
|
|
#expect(SybilMessageText.literalContent("First\r\n \r\n\t\r\n\r\nLast") == SybilMessageText.literalContent("First\n\n\n\nLast"))
|
|
}
|
|
|
|
@Test func bothFormatsShareLineParagraphMetricsAndDynamicType() {
|
|
func height(_ content: String, _ format: SybilMessageText.Format, _ size: DynamicTypeSize) -> CGFloat {
|
|
let host = UIHostingController(rootView: SybilMessageText(content: content, format: format)
|
|
.environment(\.dynamicTypeSize, size))
|
|
return host.sizeThatFits(in: CGSize(width: 320, height: 10_000)).height
|
|
}
|
|
let literal = "First line\nSecond line\n\nAnother paragraph"
|
|
let markdown = "First line \nSecond line\n\nAnother paragraph"
|
|
for size in [DynamicTypeSize.large, .accessibility3] {
|
|
#expect(abs(height(literal, .literal, size) - height(markdown, .markdown, size)) < 1)
|
|
}
|
|
for source in ["First\r\n\r\nSecond", "First\n \nSecond", "First\n\t\nSecond"] {
|
|
#expect(abs(height(source, .literal, .large) - height(source, .markdown, .large)) < 1)
|
|
}
|
|
#expect(height(literal, .literal, .accessibility3) > height(literal, .literal, .large) * 1.5)
|
|
#expect(height(markdown, .markdown, .accessibility3) > height(markdown, .markdown, .large) * 1.5)
|
|
}
|
|
}
|