Files
OSGKeyboard/OSGKeyboardExtTests/KeyboardStateTests.swift
T
Rocky 320dc777c9 feat(keyboard): add Notes skill and system-style typing
Add save-to-Notes export and direct map handoff while aligning multi-touch typing, period shortcuts, return actions, and navigation visuals with system behavior.
2026-08-14 18:22:41 +08:00

159 lines
6.1 KiB
Swift

// KeyboardStateTests.swift
// OSGKeyboard · Keyboard Extension Tests
//
// Target existence tests for the new `OSGKeyboardExtTests` target
// (TEST-4). We focus on `KeyboardState` (formerly `KeyboardViewController.State`,
// now extracted to `OSGKeyboardShared`) because it's the highest-leverage
// thing to test: it owns the published view model that every keyboard UI
// view reads from.
//
// The `KeyboardViewController` itself is hard to instantiate in a test
// host because it derives from `UIInputViewController` and needs a real
// input view, microphone permission prompts, etc. We deliberately
// *don't* attempt that here — the State class is what we care about
// for correctness.
import XCTest
@testable import OSGKeyboardShared
@MainActor
final class KeyboardStateTests: XCTestCase {
func testTargetCompiles() {
// Pure existence check — the build itself proves the target links.
// This test exists so `xcodebuild test` for `OSGKeyboardExtTests`
// has at least one passing assertion.
XCTAssertTrue(true)
}
func testInitialState() {
let s = KeyboardState()
XCTAssertEqual(s.phase, .idle)
XCTAssertEqual(s.mode, .polish)
XCTAssertEqual(s.localeId, "auto")
XCTAssertEqual(s.lastTranscript, "")
XCTAssertEqual(s.level, 0)
XCTAssertFalse(s.onDeviceSupported)
XCTAssertFalse(s.undoAvailable)
}
func testPhaseTransitionsIdleToRequestingPermissionsAndBack() {
let s = KeyboardState()
s.phase = .requestingPermissions
XCTAssertNotEqual(s.phase, .idle)
s.phase = .recording
XCTAssertEqual(s.phase, .recording)
s.phase = .processing
XCTAssertEqual(s.phase, .processing)
s.phase = .idle
XCTAssertEqual(s.phase, .idle)
}
func testStructuredErrorCarriesLLMError() {
let s = KeyboardState()
let underlying = LLMError.http(status: 401)
s.phase = .error(.llm(underlying), message: "API Key 无效")
if case .error(let kind, let msg) = s.phase {
XCTAssertEqual(kind, .llm(underlying))
XCTAssertEqual(msg, "API Key 无效")
} else {
XCTFail("expected structured .error phase")
}
}
func testFlowStructuredErrorKinds() {
let s = KeyboardState()
s.phase = .error(.manualOpenRequired, message: "open app")
if case .error(.manualOpenRequired, let msg) = s.phase {
XCTAssertEqual(msg, "open app")
} else {
XCTFail("expected manualOpenRequired")
}
s.phase = .error(.polishDegraded("warn"), message: "warn")
if case .error(.polishDegraded("warn"), _) = s.phase {} else {
XCTFail("expected polishDegraded")
}
s.phase = .error(.flowResultTimeout, message: "timeout")
if case .error(.flowResultTimeout, _) = s.phase {} else {
XCTFail("expected flowResultTimeout")
}
s.phase = .error(.flowSessionExpired, message: "expired")
if case .error(.flowSessionExpired, _) = s.phase {} else {
XCTFail("expected flowSessionExpired")
}
s.phase = .error(.fullAccessRequired, message: "full access")
if case .error(.fullAccessRequired, _) = s.phase {} else {
XCTFail("expected fullAccessRequired")
}
s.phase = .error(.noSpeechDetected, message: "no speech")
if case .error(.noSpeechDetected, _) = s.phase {} else {
XCTFail("expected noSpeechDetected")
}
s.phase = .error(.recognitionInterrupted, message: "interrupted")
if case .error(.recognitionInterrupted, _) = s.phase {} else {
XCTFail("expected recognitionInterrupted")
}
s.phase = .error(.hostAudioUnavailable, message: "audio")
if case .error(.hostAudioUnavailable, _) = s.phase {} else {
XCTFail("expected hostAudioUnavailable")
}
let flowError = FlowTranscriptionError(message: "asr failed", kind: .asrFailed)
XCTAssertEqual(
KeyboardState.Phase.ErrorKind.fromFlowTranscription(flowError),
.hostTranscriptionFailed("asr failed")
)
let discarded = FlowTranscriptionError(message: "", kind: .discardedEmpty)
XCTAssertEqual(
KeyboardState.Phase.ErrorKind.fromFlowTranscription(discarded),
.noSpeechDetected
)
}
func testInputModeIsPolishOnly() {
let s = KeyboardState()
XCTAssertEqual(s.mode, .polish)
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)
}
func testReturnKeyRoleActionFillAndTitles() {
XCTAssertFalse(KeyboardState.ReturnKeyRole.newline.usesActionFill)
XCTAssertTrue(KeyboardState.ReturnKeyRole.send.usesActionFill)
XCTAssertTrue(KeyboardState.ReturnKeyRole.search.usesActionFill)
XCTAssertTrue(KeyboardState.ReturnKeyRole.done.usesActionFill)
XCTAssertTrue(KeyboardState.ReturnKeyRole.next.usesActionFill)
XCTAssertEqual(KeyboardState.ReturnKeyRole.search.titleKey, "keyboard.return.search")
XCTAssertEqual(KeyboardState.ReturnKeyRole.next.titleKey, "keyboard.return.next")
XCTAssertEqual(KeyboardState.ReturnKeyRole.done.titleKey, "common.done")
XCTAssertEqual(KeyboardState.ReturnKeyRole.go.titleKey, "keyboard.return.go")
}
}