feat(keyboard): ship AI hint carousel, home library cards, and clipboard polish
Rotate AI idle suggestions with optional remote packs, move history/dictionary onto self-sizing Home preview cards, harden clipboard capture/prompting, and simplify keyboard chrome by dropping most liquid-glass shadows.
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
// ClipboardSuggestionLifecycleTests.swift
|
||||
// OSGKeyboard · Keyboard Extension Tests
|
||||
//
|
||||
// Verifies that the transient suggestion belongs to one pasteboard generation
|
||||
// and one keyboard presentation; clipboard history remains independently usable.
|
||||
|
||||
import Combine
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
@MainActor
|
||||
final class ClipboardSuggestionLifecycleTests: XCTestCase {
|
||||
private var suiteName = ""
|
||||
private var defaults: UserDefaults!
|
||||
private var state: KeyboardState!
|
||||
private var history: ClipboardHistoryStore!
|
||||
private var pasteboard: FakeClipboardPasteboard!
|
||||
private var coordinator: ClipboardCaptureCoordinator!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
suiteName = "ClipboardSuggestionLifecycleTests.\(UUID().uuidString)"
|
||||
defaults = UserDefaults(suiteName: suiteName)
|
||||
state = KeyboardState()
|
||||
state.clipboardHistoryEnabled = true
|
||||
state.clipboardCandidateBarEnabled = true
|
||||
history = ClipboardHistoryStore(defaults: defaults)
|
||||
history.lastObservedChangeCount = 10
|
||||
pasteboard = FakeClipboardPasteboard(changeCount: 10)
|
||||
coordinator = ClipboardCaptureCoordinator(
|
||||
state: state,
|
||||
history: history,
|
||||
pasteboard: pasteboard
|
||||
)
|
||||
coordinator.configure(isSecure: { false }, hasFullAccess: { true })
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
coordinator.keyboardWillDisappear()
|
||||
coordinator = nil
|
||||
pasteboard = nil
|
||||
history = nil
|
||||
state = nil
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
defaults = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testNewAcceptedCopyShowsSuggestion() {
|
||||
copy("a fresh sentence")
|
||||
|
||||
coordinator.captureIfNeeded()
|
||||
|
||||
XCTAssertEqual(state.clipboardSuggestionText, "a fresh sentence")
|
||||
XCTAssertEqual(state.clipboardSuggestionChangeCount, 11)
|
||||
}
|
||||
|
||||
func testTypingEndsSuggestionForSameGeneration() {
|
||||
copy("meeting notes")
|
||||
coordinator.captureIfNeeded()
|
||||
|
||||
coordinator.noteUserDidInputText()
|
||||
coordinator.captureIfNeeded()
|
||||
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
XCTAssertEqual(history.suggestionDismissedChangeCount, 11)
|
||||
}
|
||||
|
||||
func testKeyboardDismissalEndsSuggestionForSameGeneration() {
|
||||
copy("shipping address")
|
||||
coordinator.captureIfNeeded()
|
||||
|
||||
coordinator.keyboardWillDisappear()
|
||||
coordinator.keyboardDidAppear()
|
||||
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
XCTAssertEqual(history.suggestionDismissedChangeCount, 11)
|
||||
}
|
||||
|
||||
func testKeyboardAppearancePrimesPasteAccessWithoutRepublishingCurrentGeneration() {
|
||||
pasteboard.setText("already observed", changeCount: 10)
|
||||
|
||||
coordinator.keyboardDidAppear()
|
||||
|
||||
XCTAssertEqual(pasteboard.stringReadCount, 1)
|
||||
XCTAssertTrue(history.entries.isEmpty)
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
}
|
||||
|
||||
func testRejectedNewCopyClearsInsteadOfReplayingHistory() {
|
||||
copy("draft reply")
|
||||
coordinator.captureIfNeeded()
|
||||
|
||||
copy("482913")
|
||||
coordinator.captureIfNeeded()
|
||||
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
XCTAssertEqual(history.entries.map(\.text), ["draft reply"])
|
||||
XCTAssertEqual(history.lastObservedChangeCount, 12)
|
||||
}
|
||||
|
||||
func testNonTextGenerationClearsInsteadOfReplayingHistory() {
|
||||
copy("draft reply")
|
||||
coordinator.captureIfNeeded()
|
||||
|
||||
pasteboard.setNonText(changeCount: 12)
|
||||
coordinator.captureIfNeeded()
|
||||
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
XCTAssertEqual(history.entries.map(\.text), ["draft reply"])
|
||||
XCTAssertEqual(history.lastObservedChangeCount, 12)
|
||||
}
|
||||
|
||||
func testEnablingCandidateBarDoesNotReplayHistory() {
|
||||
state.clipboardCandidateBarEnabled = false
|
||||
history.ingest(rawText: "old history item", changeCount: 9)
|
||||
|
||||
state.clipboardCandidateBarEnabled = true
|
||||
coordinator.refreshFlagsFromStore()
|
||||
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
}
|
||||
|
||||
func testDeletingCurrentEntryDoesNotPromoteOlderHistory() throws {
|
||||
history.ingest(rawText: "older item", changeCount: 9)
|
||||
copy("current item")
|
||||
coordinator.captureIfNeeded()
|
||||
let currentID = try XCTUnwrap(history.newestEntry?.id)
|
||||
|
||||
coordinator.deleteEntry(id: currentID)
|
||||
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
XCTAssertEqual(history.entries.map(\.text), ["older item"])
|
||||
}
|
||||
|
||||
func testUnchangedPollingDoesNotRepublishSuggestion() {
|
||||
copy("stable clipboard")
|
||||
coordinator.captureIfNeeded()
|
||||
var publications = 0
|
||||
let cancellable = state.$clipboardSuggestionText
|
||||
.dropFirst()
|
||||
.sink { _ in publications += 1 }
|
||||
defer { cancellable.cancel() }
|
||||
|
||||
coordinator.captureIfNeeded()
|
||||
coordinator.captureIfNeeded()
|
||||
coordinator.captureIfNeeded()
|
||||
|
||||
XCTAssertEqual(publications, 0)
|
||||
XCTAssertEqual(pasteboard.stringReadCount, 1)
|
||||
}
|
||||
|
||||
func testClearHistoryEndsSuggestionBeforeRemovingEntries() {
|
||||
history.ingest(rawText: "older item", changeCount: 9)
|
||||
copy("current item")
|
||||
coordinator.captureIfNeeded()
|
||||
|
||||
coordinator.clearHistory()
|
||||
|
||||
XCTAssertTrue(history.entries.isEmpty)
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
XCTAssertNil(state.clipboardSuggestionChangeCount)
|
||||
XCTAssertEqual(history.suggestionDismissedChangeCount, 11)
|
||||
}
|
||||
|
||||
func testExplicitDismissAndInsertRemainTerminal() {
|
||||
copy("dismiss once")
|
||||
coordinator.captureIfNeeded()
|
||||
coordinator.dismissSuggestion()
|
||||
coordinator.captureIfNeeded()
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
|
||||
copy("insert once")
|
||||
coordinator.captureIfNeeded()
|
||||
coordinator.insertText("insert once", via: { _ in })
|
||||
coordinator.captureIfNeeded()
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
}
|
||||
|
||||
func testSecureFieldTransitionEndsSuggestion() {
|
||||
copy("private note")
|
||||
coordinator.captureIfNeeded()
|
||||
|
||||
coordinator.secureEntryDidChange(isSecure: true)
|
||||
coordinator.secureEntryDidChange(isSecure: false)
|
||||
coordinator.captureIfNeeded()
|
||||
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
}
|
||||
|
||||
private func copy(_ text: String) {
|
||||
pasteboard.setText(text, changeCount: pasteboard.changeCount + 1)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class FakeClipboardPasteboard: ClipboardPasteboardProviding {
|
||||
private(set) var changeCount: Int
|
||||
private(set) var hasStrings = false
|
||||
private var storedString: String?
|
||||
private(set) var stringReadCount = 0
|
||||
|
||||
var string: String? {
|
||||
stringReadCount += 1
|
||||
return storedString
|
||||
}
|
||||
|
||||
init(changeCount: Int) {
|
||||
self.changeCount = changeCount
|
||||
}
|
||||
|
||||
func setText(_ text: String, changeCount: Int) {
|
||||
self.changeCount = changeCount
|
||||
hasStrings = true
|
||||
storedString = text
|
||||
}
|
||||
|
||||
func setNonText(changeCount: Int) {
|
||||
self.changeCount = changeCount
|
||||
hasStrings = false
|
||||
storedString = nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// EditHintSchedulerTests.swift
|
||||
// OSGKeyboard · Keyboard Extension Tests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
@MainActor
|
||||
final class EditHintSchedulerTests: XCTestCase {
|
||||
func testShowPublishesImmediately() async {
|
||||
let state = KeyboardState()
|
||||
let sleeper = ControlledSleeper()
|
||||
let scheduler = makeScheduler(state: state, sleeper: sleeper)
|
||||
|
||||
scheduler.show(message: "Unavailable", isPositive: false, duration: .seconds(2.5))
|
||||
|
||||
XCTAssertEqual(state.editHint, "Unavailable")
|
||||
XCTAssertFalse(state.editHintIsPositive)
|
||||
|
||||
await sleeper.waitForRequestCount(1)
|
||||
scheduler.invalidate()
|
||||
sleeper.resumeAll()
|
||||
await drainTasks()
|
||||
}
|
||||
|
||||
func testForwardsErrorAndPositiveDurations() async {
|
||||
let state = KeyboardState()
|
||||
let sleeper = ControlledSleeper()
|
||||
let scheduler = makeScheduler(state: state, sleeper: sleeper)
|
||||
|
||||
scheduler.show(message: "Failure", isPositive: false, duration: .milliseconds(2_500))
|
||||
await sleeper.waitForRequestCount(1)
|
||||
scheduler.show(message: "Available", isPositive: true, duration: .seconds(10))
|
||||
await sleeper.waitForRequestCount(2)
|
||||
|
||||
XCTAssertEqual(sleeper.recordedDurations, [.milliseconds(2_500), .seconds(10)])
|
||||
|
||||
scheduler.invalidate()
|
||||
sleeper.resumeAll()
|
||||
await drainTasks()
|
||||
}
|
||||
|
||||
func testExpirationClearsHint() async {
|
||||
let state = KeyboardState()
|
||||
let sleeper = ControlledSleeper()
|
||||
let scheduler = makeScheduler(state: state, sleeper: sleeper)
|
||||
|
||||
scheduler.show(message: "Available", isPositive: true, duration: .seconds(10))
|
||||
await sleeper.waitForRequestCount(1)
|
||||
sleeper.resumeFirst()
|
||||
await drainTasks()
|
||||
|
||||
XCTAssertNil(state.editHint)
|
||||
XCTAssertFalse(state.editHintIsPositive)
|
||||
}
|
||||
|
||||
func testSameMessageABADoesNotLetOldExpirationClearNewHint() async {
|
||||
let state = KeyboardState()
|
||||
let sleeper = ControlledSleeper()
|
||||
let scheduler = makeScheduler(state: state, sleeper: sleeper)
|
||||
|
||||
scheduler.show(message: "Same", isPositive: true, duration: .seconds(10))
|
||||
await sleeper.waitForRequestCount(1)
|
||||
scheduler.show(message: "Same", isPositive: true, duration: .seconds(10))
|
||||
await sleeper.waitForRequestCount(2)
|
||||
|
||||
sleeper.resumeFirst()
|
||||
await drainTasks()
|
||||
XCTAssertEqual(state.editHint, "Same")
|
||||
XCTAssertTrue(state.editHintIsPositive)
|
||||
|
||||
sleeper.resumeFirst()
|
||||
await drainTasks()
|
||||
XCTAssertNil(state.editHint)
|
||||
XCTAssertFalse(state.editHintIsPositive)
|
||||
}
|
||||
|
||||
func testOldTaskThatIgnoresCancellationCannotClearNewHint() async {
|
||||
let state = KeyboardState()
|
||||
let sleeper = ControlledSleeper()
|
||||
let scheduler = makeScheduler(state: state, sleeper: sleeper)
|
||||
|
||||
scheduler.show(message: "Old", isPositive: false, duration: .seconds(2.5))
|
||||
await sleeper.waitForRequestCount(1)
|
||||
scheduler.show(message: "New", isPositive: true, duration: .seconds(10))
|
||||
await sleeper.waitForRequestCount(2)
|
||||
|
||||
// ControlledSleeper intentionally ignores cancellation and still wakes
|
||||
// the old task, so generation is the only correctness boundary.
|
||||
sleeper.resumeFirst()
|
||||
await drainTasks()
|
||||
|
||||
XCTAssertEqual(state.editHint, "New")
|
||||
XCTAssertTrue(state.editHintIsPositive)
|
||||
|
||||
scheduler.invalidate()
|
||||
sleeper.resumeAll()
|
||||
await drainTasks()
|
||||
}
|
||||
|
||||
func testClearPositiveDoesNotClearFailure() async {
|
||||
let state = KeyboardState()
|
||||
let sleeper = ControlledSleeper()
|
||||
let scheduler = makeScheduler(state: state, sleeper: sleeper)
|
||||
|
||||
scheduler.show(message: "Failure", isPositive: false, duration: .seconds(2.5))
|
||||
await sleeper.waitForRequestCount(1)
|
||||
scheduler.clearPositive()
|
||||
|
||||
XCTAssertEqual(state.editHint, "Failure")
|
||||
XCTAssertFalse(state.editHintIsPositive)
|
||||
|
||||
sleeper.resumeFirst()
|
||||
await drainTasks()
|
||||
XCTAssertNil(state.editHint)
|
||||
XCTAssertFalse(state.editHintIsPositive)
|
||||
}
|
||||
|
||||
func testInvalidateClearsAndPreventsDelayedWriteBack() async {
|
||||
let state = KeyboardState()
|
||||
let sleeper = ControlledSleeper()
|
||||
let scheduler = makeScheduler(state: state, sleeper: sleeper)
|
||||
|
||||
scheduler.show(message: "Available", isPositive: true, duration: .seconds(10))
|
||||
await sleeper.waitForRequestCount(1)
|
||||
scheduler.invalidate()
|
||||
|
||||
XCTAssertNil(state.editHint)
|
||||
XCTAssertFalse(state.editHintIsPositive)
|
||||
|
||||
sleeper.resumeFirst()
|
||||
await drainTasks()
|
||||
XCTAssertNil(state.editHint)
|
||||
XCTAssertFalse(state.editHintIsPositive)
|
||||
}
|
||||
|
||||
func testReleasedSchedulerDoesNotWriteBack() async {
|
||||
let state = KeyboardState()
|
||||
let sleeper = ControlledSleeper()
|
||||
var scheduler: EditHintScheduler? = makeScheduler(state: state, sleeper: sleeper)
|
||||
let schedulerReference = WeakSchedulerReference(scheduler)
|
||||
|
||||
scheduler?.show(message: "Available", isPositive: true, duration: .seconds(10))
|
||||
await sleeper.waitForRequestCount(1)
|
||||
scheduler = nil
|
||||
|
||||
XCTAssertNil(schedulerReference.value)
|
||||
|
||||
sleeper.resumeFirst()
|
||||
await drainTasks()
|
||||
XCTAssertEqual(state.editHint, "Available")
|
||||
XCTAssertTrue(state.editHintIsPositive)
|
||||
}
|
||||
|
||||
private func makeScheduler(
|
||||
state: KeyboardState,
|
||||
sleeper: ControlledSleeper
|
||||
) -> EditHintScheduler {
|
||||
EditHintScheduler(
|
||||
state: state,
|
||||
sleeper: { duration in
|
||||
await sleeper.sleep(for: duration)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func drainTasks() async {
|
||||
for _ in 0..<3 {
|
||||
await Task.yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class WeakSchedulerReference {
|
||||
weak var value: EditHintScheduler?
|
||||
|
||||
init(_ value: EditHintScheduler?) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class ControlledSleeper {
|
||||
private(set) var recordedDurations: [Duration] = []
|
||||
private var continuations: [CheckedContinuation<Void, Never>] = []
|
||||
|
||||
func sleep(for duration: Duration) async {
|
||||
recordedDurations.append(duration)
|
||||
await withCheckedContinuation { continuation in
|
||||
continuations.append(continuation)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForRequestCount(_ count: Int) async {
|
||||
while recordedDurations.count < count {
|
||||
await Task.yield()
|
||||
}
|
||||
}
|
||||
|
||||
func resumeFirst() {
|
||||
guard !continuations.isEmpty else {
|
||||
XCTFail("Expected a pending sleep request")
|
||||
return
|
||||
}
|
||||
continuations.removeFirst().resume()
|
||||
}
|
||||
|
||||
func resumeAll() {
|
||||
let pending = continuations
|
||||
continuations.removeAll()
|
||||
pending.forEach { $0.resume() }
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,111 @@ final class EnglishTypingTests: XCTestCase {
|
||||
XCTAssertEqual(typing.composition.preedit, "hel")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testOldWordBackspaceRehydratesSuggestionsFromDocumentContext() {
|
||||
var preceding = "board"
|
||||
let following = "\n"
|
||||
let typing = TypingSessionController()
|
||||
typing.suggestionsEnabled = true
|
||||
typing.precedingTextProvider = { preceding }
|
||||
typing.followingTextProvider = { following }
|
||||
_ = typing.setLanguage(.english)
|
||||
typing.enterTypingMode()
|
||||
typing.synchronizeEnglishDocumentContext(caretMoved: true)
|
||||
|
||||
func apply(_ output: TypingOutput) {
|
||||
for _ in 0..<output.deleteCount {
|
||||
if !preceding.isEmpty { preceding.removeLast() }
|
||||
}
|
||||
preceding += output.text
|
||||
typing.syncAutocapitalization(
|
||||
accountingForInsert: output.text,
|
||||
deleteCount: output.deleteCount
|
||||
)
|
||||
}
|
||||
|
||||
apply(typing.handleKey("⌫"))
|
||||
XCTAssertEqual(preceding, "boar")
|
||||
XCTAssertEqual(typing.composition.preedit, "boar")
|
||||
|
||||
apply(typing.handleKey("⌫"))
|
||||
XCTAssertEqual(preceding, "boa")
|
||||
XCTAssertEqual(typing.composition.preedit, "boa")
|
||||
XCTAssertTrue(
|
||||
typing.composition.candidates.contains {
|
||||
$0.text.compare("boat", options: .caseInsensitive) == .orderedSame
|
||||
}
|
||||
)
|
||||
|
||||
apply(typing.handleKey("t"))
|
||||
XCTAssertEqual(preceding, "boat")
|
||||
XCTAssertEqual(typing.composition.preedit.lowercased(), "boat")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testCandidateReplacementRejectsStaleDocumentAnchor() {
|
||||
var preceding = ""
|
||||
let typing = TypingSessionController()
|
||||
typing.suggestionsEnabled = true
|
||||
typing.precedingTextProvider = { preceding }
|
||||
typing.followingTextProvider = { "" }
|
||||
_ = typing.setLanguage(.english)
|
||||
typing.enterTypingMode()
|
||||
|
||||
for key in ["b", "o", "a"] {
|
||||
let output = typing.handleKey(key)
|
||||
preceding += output.text
|
||||
typing.syncAutocapitalization(accountingForInsert: output.text)
|
||||
}
|
||||
guard let boatIndex = typing.composition.candidates.firstIndex(where: {
|
||||
$0.text.compare("boat", options: .caseInsensitive) == .orderedSame
|
||||
}) else {
|
||||
return XCTFail("expected boat completion")
|
||||
}
|
||||
|
||||
preceding = "board"
|
||||
let output = typing.selectCandidate(at: boatIndex)
|
||||
|
||||
XCTAssertEqual(output, .none)
|
||||
XCTAssertEqual(typing.composition.preedit.lowercased(), "board")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testMidWordCaretSuppressesUnsafeBackwardOnlyReplacement() {
|
||||
let typing = TypingSessionController()
|
||||
typing.suggestionsEnabled = true
|
||||
typing.precedingTextProvider = { "boa" }
|
||||
typing.followingTextProvider = { "rd" }
|
||||
_ = typing.setLanguage(.english)
|
||||
typing.enterTypingMode()
|
||||
|
||||
typing.synchronizeEnglishDocumentContext(caretMoved: true)
|
||||
|
||||
XCTAssertTrue(typing.composition.candidates.isEmpty)
|
||||
XCTAssertTrue(typing.composition.preedit.isEmpty)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testStaleHostCallbackDoesNotDiscardLocalEnglishWord() {
|
||||
var preceding = ""
|
||||
let typing = TypingSessionController()
|
||||
typing.suggestionsEnabled = true
|
||||
typing.precedingTextProvider = { preceding }
|
||||
typing.followingTextProvider = { "" }
|
||||
_ = typing.setLanguage(.english)
|
||||
typing.enterTypingMode()
|
||||
|
||||
let output = typing.handleKey("h")
|
||||
typing.syncAutocapitalization(accountingForInsert: output.text)
|
||||
typing.synchronizeEnglishDocumentContext(caretMoved: true)
|
||||
|
||||
XCTAssertEqual(typing.composition.preedit.lowercased(), "h")
|
||||
|
||||
preceding = "h"
|
||||
typing.synchronizeEnglishDocumentContext()
|
||||
XCTAssertEqual(typing.composition.preedit.lowercased(), "h")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testAutocorrectUndoRestoresOriginal() {
|
||||
let typing = TypingSessionController()
|
||||
|
||||
@@ -118,4 +118,24 @@ final class KeyboardStateTests: XCTestCase {
|
||||
XCTAssertEqual(KeyboardState.InputMode.allCases, [.polish])
|
||||
XCTAssertEqual(KeyboardState.InputMode(rawValue: "polish"), .polish)
|
||||
}
|
||||
|
||||
func testSecureFieldImmediatelyHidesClipboardUIWithoutRestoringBody() {
|
||||
let state = KeyboardState()
|
||||
state.clipboardSuggestionText = "private clipboard body"
|
||||
state.clipboardSuggestionChangeCount = 7
|
||||
state.clipboardOverlay = .historyPanel
|
||||
|
||||
state.setSecureTextEntry(true)
|
||||
|
||||
XCTAssertFalse(state.canShowClipboardEntry)
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
XCTAssertNil(state.clipboardSuggestionChangeCount)
|
||||
XCTAssertEqual(state.clipboardOverlay, .none)
|
||||
|
||||
state.setSecureTextEntry(false)
|
||||
|
||||
XCTAssertTrue(state.canShowClipboardEntry)
|
||||
XCTAssertNil(state.clipboardSuggestionText)
|
||||
XCTAssertEqual(state.clipboardOverlay, .none)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,12 +50,12 @@ final class RimeSchemaGeneratorTests: XCTestCase {
|
||||
let configuration = TypingInputConfiguration(defaults: defaults)
|
||||
XCTAssertEqual(configuration.schema, .fullPinyin)
|
||||
XCTAssertTrue(configuration.fuzzyPairs.isEmpty)
|
||||
XCTAssertFalse(configuration.defaultToTyping)
|
||||
XCTAssertEqual(configuration.defaultInputMode, .voice)
|
||||
XCTAssertFalse(configuration.rememberLastSurface)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testDefaultTypingPreferencePersistsAndDefaultsToOff() {
|
||||
func testDefaultInputModePersistsAndDefaultsToVoice() {
|
||||
let suiteName = "TypingInputConfigurationTests.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
@@ -65,12 +65,38 @@ final class RimeSchemaGeneratorTests: XCTestCase {
|
||||
TypingInputConfiguration.preferredSurfaceOnOpen(defaults: defaults),
|
||||
.voice
|
||||
)
|
||||
XCTAssertNil(TypingInputConfiguration.preferredTypingLanguageOnOpen(defaults: defaults))
|
||||
|
||||
let configuration = TypingInputConfiguration(defaults: defaults)
|
||||
configuration.defaultToTyping = true
|
||||
configuration.defaultInputMode = .pinyin
|
||||
|
||||
XCTAssertTrue(TypingInputConfiguration.prefersTypingOnOpen(defaults: defaults))
|
||||
XCTAssertTrue(TypingInputConfiguration(defaults: defaults).defaultToTyping)
|
||||
XCTAssertEqual(TypingInputConfiguration(defaults: defaults).defaultInputMode, .pinyin)
|
||||
XCTAssertEqual(
|
||||
TypingInputConfiguration.preferredOpenPreference(defaults: defaults).surface,
|
||||
.typing
|
||||
)
|
||||
XCTAssertEqual(
|
||||
TypingInputConfiguration.preferredOpenPreference(defaults: defaults).typingLanguage,
|
||||
.chinese
|
||||
)
|
||||
|
||||
configuration.defaultInputMode = .english
|
||||
XCTAssertEqual(
|
||||
TypingInputConfiguration.preferredOpenPreference(defaults: defaults).typingLanguage,
|
||||
.english
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testLegacyDefaultToTypingMigratesToPinyin() {
|
||||
let suiteName = "TypingInputConfigurationTests.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
|
||||
defaults.set(true, forKey: "typing.input.defaultToTyping")
|
||||
let configuration = TypingInputConfiguration(defaults: defaults)
|
||||
XCTAssertEqual(configuration.defaultInputMode, .pinyin)
|
||||
XCTAssertEqual(
|
||||
TypingInputConfiguration.preferredSurfaceOnOpen(defaults: defaults),
|
||||
.typing
|
||||
@@ -78,13 +104,13 @@ final class RimeSchemaGeneratorTests: XCTestCase {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testRememberLastSurfaceOverridesDefaultToTyping() {
|
||||
func testRememberLastSurfaceOverridesDefaultInputMode() {
|
||||
let suiteName = "TypingInputConfigurationTests.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
|
||||
let configuration = TypingInputConfiguration(defaults: defaults)
|
||||
configuration.defaultToTyping = true
|
||||
configuration.defaultInputMode = .pinyin
|
||||
configuration.rememberLastSurface = true
|
||||
TypingInputConfiguration.persistLastSurface(.voice, defaults: defaults)
|
||||
|
||||
@@ -94,10 +120,10 @@ final class RimeSchemaGeneratorTests: XCTestCase {
|
||||
)
|
||||
|
||||
TypingInputConfiguration.persistLastSurface(.typing, defaults: defaults)
|
||||
XCTAssertEqual(
|
||||
TypingInputConfiguration.preferredSurfaceOnOpen(defaults: defaults),
|
||||
.typing
|
||||
)
|
||||
TypingInputConfiguration.persistLastTypingLanguage(.english, defaults: defaults)
|
||||
let preference = TypingInputConfiguration.preferredOpenPreference(defaults: defaults)
|
||||
XCTAssertEqual(preference.surface, .typing)
|
||||
XCTAssertEqual(preference.typingLanguage, .english)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -107,13 +133,17 @@ final class RimeSchemaGeneratorTests: XCTestCase {
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
|
||||
let configuration = TypingInputConfiguration(defaults: defaults)
|
||||
configuration.defaultToTyping = true
|
||||
configuration.defaultInputMode = .pinyin
|
||||
configuration.rememberLastSurface = true
|
||||
|
||||
XCTAssertEqual(
|
||||
TypingInputConfiguration.preferredSurfaceOnOpen(defaults: defaults),
|
||||
.typing
|
||||
)
|
||||
XCTAssertEqual(
|
||||
TypingInputConfiguration.preferredTypingLanguageOnOpen(defaults: defaults),
|
||||
.chinese
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
Reference in New Issue
Block a user