checkpoint before checking out main
This commit is contained in:
@@ -84,6 +84,69 @@ final class CandidatePanelExpandTests: XCTestCase {
|
||||
XCTAssertFalse(typing.isCandidatePanelExpanded)
|
||||
XCTAssertTrue(typing.composition.candidates.isEmpty)
|
||||
}
|
||||
|
||||
func testEndingDocumentPresentationClearsChineseStateWithoutTeardown() {
|
||||
let engine = StubRimeEngine()
|
||||
let typing = TypingSessionController(engine: { engine })
|
||||
_ = typing.beginDocumentPresentation()
|
||||
_ = typing.handleKey("n")
|
||||
XCTAssertFalse(typing.composition.candidates.isEmpty)
|
||||
|
||||
typing.endDocumentPresentation()
|
||||
|
||||
XCTAssertTrue(typing.composition.candidates.isEmpty)
|
||||
XCTAssertEqual(engine.teardownCallCount, 0)
|
||||
XCTAssertEqual(engine.clearCompositionCallCount, 1)
|
||||
}
|
||||
|
||||
func testCandidateSelectionRejectsReplacedSnapshot() {
|
||||
let engine = StubRimeEngine()
|
||||
let typing = TypingSessionController(engine: { engine })
|
||||
_ = typing.handleKey("n")
|
||||
guard let staleCandidateID = typing.composition.candidates.first?.id else {
|
||||
return XCTFail("expected candidate")
|
||||
}
|
||||
let staleRevision = typing.candidateRevision
|
||||
|
||||
_ = typing.handleKey("i")
|
||||
let output = typing.selectCandidate(
|
||||
id: staleCandidateID,
|
||||
candidateRevision: staleRevision
|
||||
)
|
||||
|
||||
XCTAssertEqual(output, .none)
|
||||
XCTAssertFalse(typing.composition.candidates.isEmpty)
|
||||
}
|
||||
|
||||
func testPrepareContinuesWhenHostBecomesHeavyBeforeTaskStarts() async {
|
||||
let hostState = HostHeavyState()
|
||||
let engine = StubRimeEngine()
|
||||
let prepared = expectation(description: "engine prepared")
|
||||
engine.onPrepare = { prepared.fulfill() }
|
||||
let typing = TypingSessionController(
|
||||
engine: { engine },
|
||||
hostHeavyProvider: { hostState.isHeavy }
|
||||
)
|
||||
|
||||
typing.enterTypingMode()
|
||||
hostState.isHeavy = true
|
||||
await Task.yield()
|
||||
XCTAssertTrue(typing.isPreparingEngine)
|
||||
XCTAssertEqual(engine.prepareCallCount, 0)
|
||||
|
||||
hostState.isHeavy = false
|
||||
await fulfillment(of: [prepared], timeout: 1)
|
||||
await Task.yield()
|
||||
|
||||
XCTAssertEqual(engine.prepareCallCount, 1)
|
||||
XCTAssertTrue(typing.engineReady)
|
||||
XCTAssertFalse(typing.isPreparingEngine)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class HostHeavyState {
|
||||
var isHeavy = false
|
||||
}
|
||||
|
||||
// Minimal engine that returns a two-candidate snapshot after any letter.
|
||||
@@ -94,9 +157,17 @@ private final class StubRimeEngine: RimeEngineBridging {
|
||||
var schema: TypingInputSchema = .fullPinyin
|
||||
/// Optional override applied on the next processBackspace.
|
||||
var nextComposition: TypingComposition?
|
||||
var teardownCallCount = 0
|
||||
var clearCompositionCallCount = 0
|
||||
var prepareCallCount = 0
|
||||
var onPrepare: (() -> Void)?
|
||||
|
||||
func prepare() async throws {}
|
||||
func prepare() async throws {
|
||||
prepareCallCount += 1
|
||||
onPrepare?()
|
||||
}
|
||||
func teardown() {
|
||||
teardownCallCount += 1
|
||||
composition = .empty
|
||||
}
|
||||
|
||||
@@ -116,8 +187,8 @@ private final class StubRimeEngine: RimeEngineBridging {
|
||||
composition = TypingComposition(
|
||||
preedit: String(character),
|
||||
candidates: [
|
||||
TypingCandidate(text: "你"),
|
||||
TypingCandidate(text: "泥")
|
||||
TypingCandidate(id: "first", text: "你"),
|
||||
TypingCandidate(id: "second", text: "泥")
|
||||
]
|
||||
)
|
||||
return nil
|
||||
@@ -158,6 +229,7 @@ private final class StubRimeEngine: RimeEngineBridging {
|
||||
}
|
||||
|
||||
func clearComposition() {
|
||||
clearCompositionCallCount += 1
|
||||
composition = .empty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,79 @@ final class EnglishTypingTests: XCTestCase {
|
||||
XCTAssertEqual(typing.composition.preedit, "h")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testNewDocumentPresentationRehydratesEnglishContext() {
|
||||
var preceding = "hel"
|
||||
let typing = makeTypingSession()
|
||||
typing.precedingTextProvider = { preceding }
|
||||
typing.followingTextProvider = { "" }
|
||||
typing.enterTypingMode()
|
||||
|
||||
let firstPresentation = typing.beginDocumentPresentation()
|
||||
typing.synchronizeEnglishDocumentContext(
|
||||
caretMoved: true,
|
||||
presentationID: firstPresentation
|
||||
)
|
||||
XCTAssertEqual(typing.composition.preedit.lowercased(), "hel")
|
||||
XCTAssertFalse(typing.composition.candidates.isEmpty)
|
||||
|
||||
typing.endDocumentPresentation()
|
||||
XCTAssertTrue(typing.composition.preedit.isEmpty)
|
||||
XCTAssertTrue(typing.composition.candidates.isEmpty)
|
||||
|
||||
preceding = "wor"
|
||||
let secondPresentation = typing.beginDocumentPresentation()
|
||||
typing.synchronizeEnglishDocumentContext(
|
||||
caretMoved: true,
|
||||
presentationID: firstPresentation
|
||||
)
|
||||
XCTAssertTrue(typing.composition.preedit.isEmpty)
|
||||
XCTAssertTrue(typing.composition.candidates.isEmpty)
|
||||
|
||||
typing.synchronizeEnglishDocumentContext(
|
||||
caretMoved: true,
|
||||
presentationID: secondPresentation
|
||||
)
|
||||
XCTAssertEqual(typing.composition.preedit.lowercased(), "wor")
|
||||
XCTAssertFalse(typing.composition.candidates.isEmpty)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testEndingDocumentPresentationClearsLocalEnglishShadow() {
|
||||
var preceding = ""
|
||||
let typing = makeTypingSession()
|
||||
typing.precedingTextProvider = { preceding }
|
||||
typing.followingTextProvider = { "" }
|
||||
typing.enterTypingMode()
|
||||
_ = typing.beginDocumentPresentation()
|
||||
|
||||
_ = typing.handleKey("h")
|
||||
XCTAssertEqual(typing.composition.preedit.lowercased(), "h")
|
||||
|
||||
typing.endDocumentPresentation()
|
||||
preceding = "new"
|
||||
let nextPresentation = typing.beginDocumentPresentation()
|
||||
typing.synchronizeEnglishDocumentContext(
|
||||
caretMoved: true,
|
||||
presentationID: nextPresentation
|
||||
)
|
||||
|
||||
XCTAssertEqual(typing.composition.preedit.lowercased(), "new")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testDisablingSuggestionsImmediatelyClearsEnglishCandidates() {
|
||||
let typing = makeTypingSession()
|
||||
typing.enterTypingMode()
|
||||
_ = typing.handleKey("h")
|
||||
XCTAssertFalse(typing.composition.candidates.isEmpty)
|
||||
|
||||
typing.suggestionsEnabled = false
|
||||
|
||||
XCTAssertTrue(typing.composition.preedit.isEmpty)
|
||||
XCTAssertTrue(typing.composition.candidates.isEmpty)
|
||||
}
|
||||
|
||||
func testEnglishTypingHotwordsFilterOutChineseTerms() {
|
||||
var dictionary = PersonalDictionary()
|
||||
_ = dictionary.upsertManual(term: "张三")
|
||||
@@ -541,6 +614,70 @@ final class EnglishTypingTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testRepeatedRareEnglishCommitBecomesDictionarySuggestion() {
|
||||
let suite = "english.frequent.term.test.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defaults.removePersistentDomain(forName: suite)
|
||||
let frequentStore = FrequentTermStore(defaults: defaults)
|
||||
let typing = makeTypingSession(frequentTermStore: frequentStore)
|
||||
typing.enterTypingMode()
|
||||
|
||||
for _ in 0..<2 {
|
||||
for character in "OpenAI" {
|
||||
_ = typing.handleKey(String(character))
|
||||
}
|
||||
let output = typing.handleSpace()
|
||||
XCTAssertEqual(output.deleteCount, 0)
|
||||
}
|
||||
|
||||
let storedData = defaults.data(forKey: FrequentTermStore.defaultsKey)
|
||||
let storedTerms = storedData.flatMap { try? JSONDecoder().decode([FrequentTerm].self, from: $0) }
|
||||
XCTAssertEqual(storedTerms?.first?.commitCount, 2)
|
||||
let suggestion = frequentStore.suggestions(excludingPersonalTerms: []).first
|
||||
XCTAssertEqual(suggestion?.term, "OpenAI")
|
||||
XCTAssertEqual(suggestion?.commitCount, 2)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testCommonEnglishCommitDoesNotBecomeDictionarySuggestion() {
|
||||
let suite = "english.frequent.common.test.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defaults.removePersistentDomain(forName: suite)
|
||||
let frequentStore = FrequentTermStore(defaults: defaults)
|
||||
let typing = makeTypingSession(frequentTermStore: frequentStore)
|
||||
typing.enterTypingMode()
|
||||
|
||||
for _ in 0..<2 {
|
||||
for character in "the" {
|
||||
_ = typing.handleKey(String(character))
|
||||
}
|
||||
_ = typing.handleSpace()
|
||||
}
|
||||
|
||||
XCTAssertTrue(frequentStore.suggestions(excludingPersonalTerms: []).isEmpty)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testSecureEnglishCommitDoesNotBecomeDictionarySuggestion() {
|
||||
let suite = "english.frequent.secure.test.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defaults.removePersistentDomain(forName: suite)
|
||||
let frequentStore = FrequentTermStore(defaults: defaults)
|
||||
let typing = makeTypingSession(frequentTermStore: frequentStore)
|
||||
typing.enterTypingMode()
|
||||
typing.suggestionsEnabled = false
|
||||
|
||||
for _ in 0..<2 {
|
||||
for character in "OpenAI" {
|
||||
_ = typing.handleKey(String(character))
|
||||
}
|
||||
_ = typing.handleSpace()
|
||||
}
|
||||
|
||||
XCTAssertTrue(frequentStore.suggestions(excludingPersonalTerms: []).isEmpty)
|
||||
}
|
||||
|
||||
func testQWERTYNeighborsIncludeDiagonals() {
|
||||
let aroundG = EnglishQWERTYProximity.neighbors(of: "g", includingSelf: true)
|
||||
XCTAssertTrue(aroundG.contains("t"))
|
||||
@@ -570,6 +707,7 @@ final class EnglishTypingTests: XCTestCase {
|
||||
@MainActor
|
||||
private func makeTypingSession(
|
||||
learningStore: EnglishLearningStore? = nil,
|
||||
frequentTermStore: FrequentTermStore? = nil,
|
||||
language: TypingInputLanguage = .english
|
||||
) -> TypingSessionController {
|
||||
let store = learningStore ?? EnglishLearningStore(
|
||||
@@ -577,7 +715,10 @@ final class EnglishTypingTests: XCTestCase {
|
||||
)
|
||||
let typing = TypingSessionController(
|
||||
engine: { NoopRimeEngine() },
|
||||
learningStore: store
|
||||
learningStore: store,
|
||||
frequentTermStore: frequentTermStore ?? FrequentTermStore(
|
||||
defaults: UserDefaults(suiteName: "frequent.tests.\(UUID().uuidString)")!
|
||||
)
|
||||
)
|
||||
typing.suggestionsEnabled = true
|
||||
_ = typing.setLanguage(language)
|
||||
|
||||
@@ -36,6 +36,7 @@ final class KeyboardStateTests: XCTestCase {
|
||||
XCTAssertEqual(s.level, 0)
|
||||
XCTAssertFalse(s.onDeviceSupported)
|
||||
XCTAssertFalse(s.undoAvailable)
|
||||
XCTAssertFalse(s.hasFullAccess)
|
||||
}
|
||||
|
||||
func testPhaseTransitionsIdleToRequestingPermissionsAndBack() {
|
||||
@@ -145,6 +146,20 @@ final class KeyboardStateTests: XCTestCase {
|
||||
XCTAssertEqual(state.clipboardOverlay, .none)
|
||||
}
|
||||
|
||||
func testClipboardFullAccessHintTracksCaptureAvailability() {
|
||||
let state = KeyboardState()
|
||||
state.clipboardHistoryEnabled = true
|
||||
|
||||
XCTAssertTrue(state.needsClipboardFullAccessHint)
|
||||
|
||||
state.hasFullAccess = true
|
||||
XCTAssertFalse(state.needsClipboardFullAccessHint)
|
||||
|
||||
state.hasFullAccess = false
|
||||
state.clipboardHistoryEnabled = false
|
||||
XCTAssertFalse(state.needsClipboardFullAccessHint)
|
||||
}
|
||||
|
||||
func testResolvedSkillCopyPublishesWhenIDsStayStable() {
|
||||
let state = KeyboardState()
|
||||
state.enabledClipboardSkillIDs = ["official.rewrite"]
|
||||
|
||||
Reference in New Issue
Block a user