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

125 lines
7.0 KiB
Swift

#if canImport(UIKit)
import XCTest
import UIKit
@testable import RichTextEditorUIKit
import RichTextEditorCore
final class SelectionDrivenSpellCheckTests: XCTestCase {
// "hello wrold today": hello=0..5, wrold=6..11, today=12..17 (region-local UTF-16).
private func makeCanvas() -> DocumentCanvasView {
let v = DocumentCanvasView()
v.setBlocks([.paragraph(ParagraphBlock(id: BlockID("p"), runs: [TextRun(text: "hello wrold today")]))],
width: 320)
v.frame = CGRect(x: 0, y: 0, width: 320, height: 600); v.layoutIfNeeded()
return v
}
private func g(_ v: DocumentCanvasView, _ local: Int) -> Int { v.boxes[0].textStart + local } // local→global
private func wrold(_ v: DocumentCanvasView) -> NSRange { NSRange(location: g(v, 6), length: 5) }
private func today(_ v: DocumentCanvasView) -> NSRange { NSRange(location: g(v, 12), length: 5) }
func test_targets_sameWord_returnsNil() {
let v = makeCanvas()
let t = v.spellCheckTargets(fromCaret: g(v, 7), toCaret: g(v, 9)) // both inside "wrold"
XCTAssertNil(t.check); XCTAssertNil(t.clear)
}
func test_targets_leavingWord_checksLeftClearsEntered() {
let v = makeCanvas()
let t = v.spellCheckTargets(fromCaret: g(v, 8), toCaret: g(v, 14)) // "wrold" → "today"
XCTAssertEqual(t.check, wrold(v)) // re-check the word just left
XCTAssertEqual(t.clear, today(v)) // clear the word now under the caret
}
private func spin(_ s: TimeInterval) {
let e = Date().addingTimeInterval(s)
while Date() < e { RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.02)) }
}
override func setUp() { // warm UITextChecker to avoid the cold-start race (see SpellCheckTapTests)
super.setUp()
_ = UITextChecker().guesses(forWordRange: NSRange(location: 0, length: 4), in: "helo", language: "en_US")
}
func test_movingCaretOutOfMisspelledWord_flagsIt() {
let v = makeCanvas()
_ = v.becomeFirstResponder(); v.installNativeCheckingIfNeeded()
XCTAssertNotNil(v.nativeChecker, "native controller unavailable")
spin(2.5) // async textChecker load
v.setCaret(global: g(v, 8)) // caret INTO "wrold"
v.setCaret(global: g(v, 14)) // caret OUT to "today" → checks "wrold"
spin(1.0)
let ranges = Set((v.spellResults[BlockID("p")]?.ranges ?? []).map { $0.range })
XCTAssertTrue(ranges.contains(NSRange(location: 6, length: 5)), "leaving 'wrold' must flag it")
}
func test_noCaretTraversal_leavesDocumentUnflagged() {
let v = makeCanvas()
_ = v.becomeFirstResponder(); v.installNativeCheckingIfNeeded()
spin(2.5) // never move the caret through a word
XCTAssertNil(v.spellResults[BlockID("p")], "untraversed text must not be flagged (draft/paste parity)")
}
func test_caretIntoFlaggedWord_clearsItsFlag() {
let v = makeCanvas()
// `becomeFirstResponder()` needs a window to actually succeed (see SelectionInteractionTests);
// this canvas is never hosted in one, so install explicitly — same pattern as the two tests above.
_ = v.becomeFirstResponder(); v.installNativeCheckingIfNeeded()
v.applyNativeAnnotations(global: NSRange(location: g(v, 6), length: 5), style: .spelling) // pre-flag "wrold"
v.lastCheckedCaret = g(v, 14) // pretend caret was in "today"
v.setCaret(global: g(v, 8)) // move INTO "wrold" → clears its flag
let ranges = Set((v.spellResults[BlockID("p")]?.ranges ?? []).map { $0.range })
XCTAssertFalse(ranges.contains(NSRange(location: 6, length: 5)), "caret entering a word clears its flag")
}
func test_sentenceGlobalRange_enclosesPosition() {
let v = DocumentCanvasView()
v.setBlocks([.paragraph(ParagraphBlock(id: BlockID("p"), runs: [TextRun(text: "One thing. Two things.")]))],
width: 320)
v.frame = CGRect(x: 0, y: 0, width: 320, height: 600); v.layoutIfNeeded()
let start = v.boxes[0].textStart
// "One thing. " is sentence 0 (local 0..11); a position inside it resolves to that sentence.
let r = v.sentenceGlobalRange(atGlobal: start + 4)
XCTAssertEqual(r?.location, start + 0)
XCTAssertEqual(r?.length, 11)
}
func test_clearOnlyStyle_leavesOtherStyles() {
let v = makeCanvas()
let word = NSRange(location: g(v, 6), length: 5)
v.applyNativeAnnotations(global: word, style: .spelling)
v.applyNativeAnnotations(global: NSRange(location: g(v, 0), length: 5), style: .grammar)
v.clearNativeAnnotations(global: NSRange(location: g(v, 0), length: 11), onlyStyle: .grammar)
let styles = (v.spellResults[BlockID("p")]?.ranges ?? []).map { $0.style }
XCTAssertTrue(styles.contains(.spelling), "spelling flag must survive a grammar-only clear")
XCTAssertFalse(styles.contains(.grammar), "grammar flag in range must be cleared")
}
func test_wordLevelClear_preservesGrammarFlag() {
let v = makeCanvas() // "hello wrold today" — one sentence
// `becomeFirstResponder()` needs a window to actually succeed (see SelectionInteractionTests);
// this canvas is never hosted in one, so install explicitly — same pattern as the tests above.
_ = v.becomeFirstResponder(); v.installNativeCheckingIfNeeded() // installs the native checker so the driver actually runs
v.applyNativeAnnotations(global: NSRange(location: g(v, 0), length: 17), style: .grammar) // grammar over the whole sentence
v.lastCheckedCaret = g(v, 2) // caret was in "hello"
v.setCaret(global: g(v, 13)) // move to "today" — a word move WITHIN the sentence
let styles = (v.spellResults[BlockID("p")]?.ranges ?? []).map { $0.style }
XCTAssertTrue(styles.contains(.grammar),
"a word-level spelling clear must not wipe a grammar flag in the same sentence")
}
func test_crossingSentenceBoundary_clearsGrammarOnEnteredSentence() {
let v = DocumentCanvasView()
v.setBlocks([.paragraph(ParagraphBlock(id: BlockID("p"), runs: [TextRun(text: "One thing. Two things.")]))],
width: 320)
v.frame = CGRect(x: 0, y: 0, width: 320, height: 600); v.layoutIfNeeded()
_ = v.becomeFirstResponder(); v.installNativeCheckingIfNeeded()
let start = v.boxes[0].textStart
v.applyNativeAnnotations(global: NSRange(location: start + 11, length: 11), style: .grammar) // grammar on sentence 2 ("Two things.")
v.lastCheckedCaret = start + 4 // caret in sentence 1 ("One thing.")
v.setCaret(global: start + 15) // cross into sentence 2
let grammar = (v.spellResults[BlockID("p")]?.ranges ?? []).filter { $0.style == .grammar }
XCTAssertTrue(grammar.isEmpty,
"crossing into a new sentence clears its grammar markers before the re-check")
}
}
#endif