498f407585
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
224 lines
7.0 KiB
Swift
224 lines
7.0 KiB
Swift
// 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
|
|
@testable import OSGKeyboardShared
|
|
import XCTest
|
|
|
|
@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
|
|
}
|
|
}
|