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

119 lines
6.5 KiB
Swift

#if canImport(UIKit)
import XCTest
import UIKit
@testable import RichTextEditorUIKit
import RichTextEditorCore
final class NativeTextCheckingClientTests: XCTestCase {
private func makeCanvas(_ text: String, id: String = "p") -> DocumentCanvasView {
let v = DocumentCanvasView()
v.setBlocks([.paragraph(ParagraphBlock(id: BlockID(id), runs: [TextRun(text: text)]))], width: 320)
v.frame = CGRect(x: 0, y: 0, width: 320, height: 600); v.layoutIfNeeded()
return v
}
private func firstStart(_ v: DocumentCanvasView) -> Int { v.boxes[0].textStart }
func test_applyNativeAnnotations_populatesRegionLocalWithStyle() {
let v = makeCanvas("hello wrold today")
let base = firstStart(v) // 1-based axis
v.applyNativeAnnotations(global: NSRange(location: base + 6, length: 5), style: .spelling) // "wrold"
let entry = v.spellResults[BlockID("p")]
XCTAssertEqual(entry?.ranges.map { $0.range }, [NSRange(location: 6, length: 5)])
XCTAssertEqual(entry?.ranges.map { $0.style }, [.spelling])
}
func test_clearNativeAnnotations_removesRange() {
let v = makeCanvas("hello wrold")
let base = firstStart(v)
v.applyNativeAnnotations(global: NSRange(location: base + 6, length: 5), style: .spelling)
v.clearNativeAnnotations(global: NSRange(location: base + 6, length: 5))
XCTAssertEqual(v.spellResults[BlockID("p")]?.ranges.count ?? 0, 0)
}
func test_applyNativeAnnotations_dropsLinkExcludedRange() {
let v = DocumentCanvasView()
v.setBlocks([.paragraph(ParagraphBlock(id: BlockID("p"), runs: [
TextRun(text: "see "), TextRun(text: "wrold", attributes: CharacterAttributes(link: "https://x.com"))]))], width: 320)
v.frame = CGRect(x: 0, y: 0, width: 320, height: 600); v.layoutIfNeeded()
let base = v.boxes[0].textStart
v.applyNativeAnnotations(global: NSRange(location: base + 4, length: 5), style: .spelling) // "wrold" (link)
XCTAssertEqual(v.spellResults[BlockID("p")]?.ranges.count ?? 0, 0) // excluded
}
// MARK: N3 — style classification + style-aware color
func test_style_spellingFromDisplayStyle2() {
// Observed live: a spelling flag carries NSTextAlternativesDisplayStyle == 2 and no alternatives.
let attrs: [NSAttributedString.Key: Any] = [DocumentCanvasView.displayStyleKey: 2]
XCTAssertEqual(DocumentCanvasView.style(from: attrs), .spelling)
}
func test_style_grammarFromOtherDisplayStyle() {
let attrs: [NSAttributedString.Key: Any] = [DocumentCanvasView.displayStyleKey: 1]
XCTAssertEqual(DocumentCanvasView.style(from: attrs), .grammar)
}
func test_style_correctionWhenAlternativesPresent() {
// style(from:) only checks PRESENCE of the alternatives key (the correction data), so any non-nil value.
let attrs: [NSAttributedString.Key: Any] = [
DocumentCanvasView.displayStyleKey: 0,
DocumentCanvasView.alternativesKey: "correction-alternatives",
]
XCTAssertEqual(DocumentCanvasView.style(from: attrs), .correction)
}
// MARK: N4 — `.correction` alternatives stash (via `nativeReplace`, the real controller-facing path)
/// Stands in for the private, non-publicly-constructible `NSTextAlternatives`: exposes the same two
/// KVC-readable properties the production code reads (`alternativeStrings`/`primaryString`).
private final class FakeTextAlternatives: NSObject {
@objc let alternativeStrings: [String]
@objc let primaryString: String
init(alternativeStrings: [String], primaryString: String) {
self.alternativeStrings = alternativeStrings
self.primaryString = primaryString
}
}
func test_nativeReplace_correctionStashesAlternativesViaKVC() {
let v = makeCanvas("hello wrold today")
let base = firstStart(v)
let range = v.nativeTextRange(forGlobalLocation: base + 6, length: 5)!
let alt = FakeTextAlternatives(alternativeStrings: ["world", "word"], primaryString: "world")
let s = NSAttributedString(string: "wrold", attributes: [
DocumentCanvasView.displayStyleKey: 0,
DocumentCanvasView.alternativesKey: alt,
])
v.nativeReplace(range, withAnnotatedString: s, relativeReplacementRange: NSRange(location: 0, length: 5))
let stashed = v.spellingAlternatives[BlockID("p")]?.first { $0.range == NSRange(location: 6, length: 5) }
XCTAssertEqual(stashed?.candidates, ["world", "word"])
XCTAssertEqual(stashed?.primary, "world")
XCTAssertEqual(v.spellResults[BlockID("p")]?.ranges.map { $0.style }, [.correction])
}
func test_nativeReplace_correctionWithUnexpectedAlternativesShape_doesNotCrashOrStash() {
let v = makeCanvas("hello wrold today")
let base = firstStart(v)
let range = v.nativeTextRange(forGlobalLocation: base + 6, length: 5)!
// Not an NSTextAlternatives-shaped object — the KVC reads must guard and no-op, not crash.
let s = NSAttributedString(string: "wrold", attributes: [
DocumentCanvasView.displayStyleKey: 0,
DocumentCanvasView.alternativesKey: "not-an-NSTextAlternatives-instance",
])
v.nativeReplace(range, withAnnotatedString: s, relativeReplacementRange: NSRange(location: 0, length: 5))
XCTAssertNil(v.spellingAlternatives[BlockID("p")]?.first { $0.range == NSRange(location: 6, length: 5) })
// The style classification + `spellResults` flag are unaffected by the missing candidates.
XCTAssertEqual(v.spellResults[BlockID("p")]?.ranges.map { $0.style }, [.correction])
}
func test_underlineColor_perStyle() {
let v = DocumentCanvasView()
v.setBlocks([.paragraph(ParagraphBlock(id: BlockID("p"), runs: [TextRun(text: "x")]))], width: 320)
XCTAssertEqual(v.spellingUnderlineColor(.spelling), v.mapper.theme.misspellingUnderline)
XCTAssertEqual(v.spellingUnderlineColor(.grammar), v.mapper.theme.grammarUnderline)
XCTAssertEqual(v.spellingUnderlineColor(.correction), v.mapper.theme.correctionUnderline)
}
func test_themeDefaults_grammarGreen_correctionPeriwinkle() {
XCTAssertEqual(RichTextEditorTheme.default.grammarUnderline, .systemGreen)
// Measured from a real UITextView's native iOS 26 autocorrect underline (#99ACEB) — see Task 2 brief.
XCTAssertEqual(RichTextEditorTheme.default.correctionUnderline,
UIColor(red: 153.0/255.0, green: 172.0/255.0, blue: 235.0/255.0, alpha: 1.0))
}
}
#endif