Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

172 lines
9.7 KiB
Swift

#if canImport(UIKit)
import XCTest
import UIKit
@testable import RichTextEditorUIKit
import RichTextEditorCore
final class MapperTests: XCTestCase {
private let mapper = AttributedStringMapper()
private func formulaRenderResult(size: CGSize = CGSize(width: 28.0, height: 12.0)) -> RichTextFormulaRenderResult {
let image = UIGraphicsImageRenderer(size: size).image { _ in }
return RichTextFormulaRenderResult(image: image, size: size, ascent: 9.0, descent: 3.0)
}
func test_characterAttributes_roundTripThroughDict() {
// No link here: a link deliberately suppresses foreground/underline on read-back (covered by
// AttributedStringMapperLinkTests), so those fields are exercised here on an unlinked run.
// Use a NON-default foreground (red) so it round-trips as explicit — an explicit color equal to the
// theme's default would be stripped to nil (that behavior is covered by its own test below).
let ca = CharacterAttributes(bold: true, italic: true, underline: true, strikethrough: true,
fontSize: 18, foreground: RGBAColor(red: 1, green: 0, blue: 0),
highlight: RGBAColor(red: 1, green: 1, blue: 0),
baselineOffset: 4)
let dict = mapper.attributes(for: ca, style: .body)
let back = mapper.characterAttributes(from: dict)
XCTAssertTrue(back.bold); XCTAssertTrue(back.italic)
XCTAssertTrue(back.underline); XCTAssertTrue(back.strikethrough)
XCTAssertEqual(back.foreground, RGBAColor(red: 1, green: 0, blue: 0))
XCTAssertEqual(back.baselineOffset ?? 0, 4, accuracy: 0.01)
XCTAssertNotNil(back.highlight)
}
func test_uncoloredRun_staysNilThroughRoundTrip() {
// A run with no explicit color renders in the theme default but must read back as nil (unset),
// so re-theming recolors it and serialization stays clean.
let dict = mapper.attributes(for: CharacterAttributes(), style: .body)
XCTAssertEqual(dict[.foregroundColor] as? UIColor, RichTextEditorTheme.default.primaryText,
"un-colored body text renders in the primary text color")
let back = mapper.characterAttributes(from: dict, style: .body)
XCTAssertNil(back.foreground, "the injected default must be stripped back to nil")
}
func test_explicitColorEqualToDefault_isStripped() {
// An explicit foreground that happens to equal the theme default is indistinguishable from the
// injected default and is stripped to nil (visually identical, and re-themable). Documented behavior.
let ca = CharacterAttributes(foreground: RichTextEditorTheme.default.primaryText.rgba)
let dict = mapper.attributes(for: ca, style: .body)
let back = mapper.characterAttributes(from: dict, style: .body)
XCTAssertNil(back.foreground)
}
func test_captionUsesSecondaryDefault_andStripsIt() {
let theme = RichTextEditorTheme(primaryText: .red, secondaryText: .blue, placeholder: .placeholderText,
accent: .green, tableBorder: .gray, tableHeaderBackground: .gray, codeBackground: .placeholderText)
let m = AttributedStringMapper(theme: theme)
// Caption renders in secondary; body renders in primary.
XCTAssertEqual(m.attributes(for: CharacterAttributes(), style: .caption)[.foregroundColor] as? UIColor, UIColor.blue)
XCTAssertEqual(m.attributes(for: CharacterAttributes(), style: .body)[.foregroundColor] as? UIColor, UIColor.red)
// The caption default is stripped only when read back WITH the caption style.
let captionDict = m.attributes(for: CharacterAttributes(), style: .caption)
XCTAssertNil(m.characterAttributes(from: captionDict, style: .caption).foreground)
// Reading the caption dict back with the WRONG style (body) must NOT strip the secondary color —
// guards against a caller forgetting to pass .caption (e.g. MediaBlockBox captions).
XCTAssertNotNil(m.characterAttributes(from: captionDict, style: .body).foreground,
"caption secondary must not be stripped when read back as body style")
}
func test_linkUsesAccentColor() {
let theme = RichTextEditorTheme(primaryText: .black, secondaryText: .black, placeholder: .placeholderText,
accent: .green, tableBorder: .gray, tableHeaderBackground: .gray, codeBackground: .placeholderText)
let m = AttributedStringMapper(theme: theme)
let dict = m.attributes(for: CharacterAttributes(link: "https://example.com"), style: .body)
XCTAssertEqual(dict[.foregroundColor] as? UIColor, UIColor.green)
// Link foreground is render-only and never captured into the model.
XCTAssertNil(m.characterAttributes(from: dict, style: .body).foreground)
}
func test_paragraphBlock_roundTripsRuns() {
let block = ParagraphBlock(id: BlockID("p1"), style: .body, runs: [
TextRun(text: "Plain "),
TextRun(text: "bold", attributes: CharacterAttributes(bold: true)),
])
let attr = mapper.attributedString(for: block)
XCTAssertEqual(attr.string, "Plain bold")
let runs = mapper.runs(from: attr)
XCTAssertEqual(runs.count, 2)
XCTAssertEqual(runs[0].text, "Plain ")
XCTAssertFalse(runs[0].attributes.bold)
XCTAssertEqual(runs[1].text, "bold")
XCTAssertTrue(runs[1].attributes.bold)
}
func test_serifHeadingFont_doesNotReadBackAsFontFamily() {
let m = AttributedStringMapper()
// .heading1 resolves to the system serif (New York); a default CharacterAttributes has no fontFamily.
let attrs = m.attributes(for: CharacterAttributes(), style: .heading1)
let ca = m.characterAttributes(from: attrs)
XCTAssertNil(ca.fontFamily, "serif heading font is style-derived and must not leak into the model as a user fontFamily")
}
func test_substitutedFontFamily_doesNotLeakIntoModel() throws {
// TextKit's automatic font substitution (e.g. for Arabic/Hebrew/CJK glyphs the style font can't
// render) rewrites `.font` in the storage to a script-specific family. That is a display artifact,
// not user intent, and must NOT round-trip into the model — otherwise a rebuild (e.g. the paragraph
// merge on backspace) re-applies the substituted family and the font visibly changes.
let substituted = try XCTUnwrap(UIFont(name: "Helvetica Neue", size: 17),
"test needs a font with a non-default family")
XCTAssertNotEqual(substituted.familyName, UIFont.systemFont(ofSize: 17).familyName,
"precondition: the test font has a non-default family")
let ca = AttributedStringMapper().characterAttributes(from: [.font: substituted], style: .body)
XCTAssertNil(ca.fontFamily, "a display-substituted font family must not round-trip into the model")
}
func test_inlineCode_rendersMonospace_andRoundTripsClean() {
let ca = CharacterAttributes(inlineCode: true)
let dict = mapper.attributes(for: ca, style: .body)
let font = dict[.font] as? UIFont
XCTAssertNotNil(font)
XCTAssertNotEqual(font?.fontName, UIFont.systemFont(ofSize: 16).fontName,
"inline code must render with a non-system (monospace) font")
let back = mapper.characterAttributes(from: dict)
XCTAssertTrue(back.inlineCode)
XCTAssertNil(back.fontFamily, "the mono font name must not leak into fontFamily")
XCTAssertNil(back.highlight, "the code background must not be read back as a highlight")
XCTAssertNil(back.fontSize, "a plain inline-code run must not pin a font size")
XCTAssertFalse(back.bold, "monospaced .regular carries no bold trait")
XCTAssertFalse(back.italic, "monospaced .regular carries no italic trait")
XCTAssertEqual((dict[.font] as? UIFont)?.pointSize, 17, "body inline code renders at the body base size")
}
func test_formulaDisplaysRawLatex_andRoundTripsMetadata() {
let latex = "x_{1,2}=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}"
let block = ParagraphBlock(id: BlockID("formula"), style: .body, runs: [
TextRun(text: latex, attributes: CharacterAttributes(formula: latex))
])
let attr = mapper.attributedString(for: block)
XCTAssertEqual(attr.string, latex)
let runs = mapper.runs(from: attr)
XCTAssertEqual(runs.count, 1)
XCTAssertEqual(runs[0].text, latex)
XCTAssertEqual(runs[0].attributes.formula, latex)
}
func test_formulaRendererDisplaysAttachmentAtom_andRoundTripsMetadata() throws {
let latex = "e^{I\\pi}=-1"
var capturedContext: RichTextFormulaRenderContext?
let mapper = AttributedStringMapper(formulaRenderer: { context in
capturedContext = context
return self.formulaRenderResult()
})
let block = ParagraphBlock(id: BlockID("formula"), style: .body, runs: [
TextRun(text: latex, attributes: CharacterAttributes(formula: latex))
])
let attr = mapper.attributedString(for: block)
XCTAssertEqual(attr.string, "\u{FFFC}")
XCTAssertEqual(capturedContext?.latex, latex)
XCTAssertEqual(capturedContext?.fontSize ?? 0.0, 17.0, accuracy: 0.01)
let attachment = try XCTUnwrap(attr.attribute(.attachment, at: 0, effectiveRange: nil) as? FormulaTextAttachment)
XCTAssertEqual(attachment.latex, latex)
let runs = mapper.runs(from: attr)
XCTAssertEqual(runs.count, 1)
XCTAssertEqual(runs[0].text, "\u{FFFC}")
XCTAssertEqual(runs[0].attributes.formula, latex)
}
}
#endif