import MarkdownUI import SwiftUI import Testing import UIKit @testable import Sybil @Suite @MainActor struct MessagePresentationTests { @Test func agentMarkdownKeepsAltAndFormattingWithoutImageOrHTMLNodes() { let source = """ **Caption** ![Inline *landscape*](https://images.invalid/inline.png) ![Reference landscape][photo] [![Linked landscape](//images.invalid/linked.png)](https://example.invalid) > ![Nested ![inner](file:///private/image.png)](data:image/png;base64,AAAA) HTML landscape [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("Caption")) #expect(html.contains(#"href="https://example.invalid""#)) // Normal chat still uses unmodified MarkdownUI content. #expect(MarkdownContent(source).renderHTML().contains("\n").renderHTML() == "

HTML landscape

\n") } @Test func agentMarkdownRetainsCodeTablesAndEscapesImageLikeAltText() { let source = """ `![literal](https://images.invalid/code.png)` ```html ``` | Image | Value | | --- | --- | | ![Table alt](https://images.invalid/table.png) | **Kept** | ![literal alt](https://images.invalid/alt.png) """ let html = AgentMessageMarkdown.content(source).renderHTML() #expect(html.contains("")); #expect(html.contains("Table alt")) #expect(html.contains("Kept")) #expect(html.contains("<img src=")); #expect(html.contains("![literal]")) #expect(html.contains("![literal alt]")); #expect(!html.contains("")) #expect(!content.renderHTML().contains("")) #expect(!content.renderHTML().contains("
    ")) #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() == "

    First\nSecond

    \n

    Third

    \n") // Empty paragraphs retain layout space instead of disappearing. #expect(SybilMessageText.literalContent("\n\nFirst\n\n\n\nLast\n\n").renderHTML().components(separatedBy: "

    ").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() == "

    First

    \n

    Second

    \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) } }