Files
OSGKeyboard/OSGKeyboardExtTests/KeyboardSurfaceStateTests.swift
T
Rocky a8d58d8f0c feat(polish): allow mood emoji on custom styles and ship Flow/ASR fixes
Custom polish styles can opt in to emotion-matched emoji (default off), with
prompt-level opt-in detection so paste-only styles keep model-added emoji.
Also include Volcengine API-Key ASR auth, voice-processing capture, PiP flash
fix, and related keyboard Shift/haptics reliability work.
2026-08-06 15:11:12 +08:00

274 lines
9.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// KeyboardSurfaceStateTests.swift
// OSGKeyboard · Ext unit tests
import XCTest
@testable import OSGKeyboardShared
@MainActor
final class KeyboardSurfaceStateTests: XCTestCase {
func testRecordingLocksTyping() {
let state = KeyboardState()
state.phase = .idle
XCTAssertTrue(state.canEnterTypingSurface)
state.phase = .recording
XCTAssertTrue(state.locksTypingSurface)
XCTAssertFalse(state.canEnterTypingSurface)
}
func testStandardLayoutHasQwertyTopRow() {
let layout = StandardTypingLayout()
let rows = layout.rows(for: .letters, language: .english, shiftActive: false)
XCTAssertEqual(rows.first, ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"])
let shifted = layout.rows(for: .letters, language: .english, shiftActive: true)
XCTAssertEqual(shifted.first?.first, "Q")
}
func testEnglishNumberAndSymbolPagesMatchIOSUS() {
let layout = StandardTypingLayout()
XCTAssertEqual(
layout.rows(for: .numbers, language: .english, shiftActive: false),
[
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""],
["#+=", ".", ",", "?", "!", "'", "⌫"]
]
)
XCTAssertEqual(
layout.rows(for: .symbols, language: .english, shiftActive: false),
[
["[", "]", "{", "}", "#", "%", "^", "*", "+", "="],
["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"],
["123", ".", ",", "?", "!", "'", "⌫"]
]
)
}
func testChineseNumberAndSymbolPagesMatchIOSSimplified() {
let layout = StandardTypingLayout()
XCTAssertEqual(
layout.rows(for: .numbers, language: .chinese, shiftActive: false),
[
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
["-", "/", "", "", "", "", "¥", "@", "“", "”"],
["#+=", "。", "", "、", "", "", ".", "⌫"]
]
)
XCTAssertEqual(
layout.rows(for: .symbols, language: .chinese, shiftActive: false),
[
["【", "】", "「", "」", "#", "%", "^", "*", "+", "="],
["_", "\\", "|", "~", "《", "》", "€", "£", "¥", "·"],
["123", "。", "", "、", "", "", ".", "⌫"]
]
)
}
func testKeyRowsFollowTypingLanguageOnNumberPage() {
let typing = TypingSessionController()
typing.setPage(.numbers)
XCTAssertEqual(typing.keyRows[1], ["-", "/", "", "", "", "", "¥", "@", "“", "”"])
_ = typing.setLanguage(.english)
typing.setPage(.numbers)
XCTAssertEqual(typing.keyRows[1], ["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""])
}
func testVoiceAndTypingChromeShareStableDimensions() {
XCTAssertEqual(KeyboardChromeLayout.totalHeight, 281)
XCTAssertEqual(KeyboardChromeLayout.actionKeyHeight, 50)
XCTAssertEqual(KeyboardChromeLayout.actionKeyCornerRadius, 10)
XCTAssertEqual(KeyboardChromeLayout.actionKeySpacing, 8)
XCTAssertEqual(KeyboardChromeLayout.sideActionKeyFraction, 0.2)
XCTAssertEqual(KeyboardChromeLayout.centerActionKeyFraction, 0.6)
XCTAssertEqual(KeyboardChromeLayout.horizontalInset, 8)
XCTAssertEqual(KeyboardChromeLayout.contentMaxWidth, 700)
let widths = KeyboardChromeLayout.actionKeyWidths(availableWidth: 374)
XCTAssertEqual(widths.side, 71.6, accuracy: 0.001)
XCTAssertEqual(widths.center, 214.8, accuracy: 0.001)
}
func testSharedCapsuleCanSelectSpecificTypingLanguage() {
let typing = TypingSessionController()
XCTAssertEqual(typing.language, .chinese)
XCTAssertEqual(typing.setLanguage(.english), .none)
XCTAssertEqual(typing.language, .english)
XCTAssertEqual(typing.setLanguage(.chinese), .none)
XCTAssertEqual(typing.language, .chinese)
}
func testShiftStateProducesUppercaseKeyRows() {
let typing = TypingSessionController()
XCTAssertFalse(typing.shiftActive)
_ = typing.handleKey("⇧")
XCTAssertTrue(typing.shiftActive)
XCTAssertTrue(typing.isShiftEnabled)
XCTAssertEqual(typing.keyRows.first?.first, "Q")
}
func testManualShiftSurvivesAutocapitalizationSync() {
let typing = TypingSessionController()
// Providers must be set before language switch (which syncs autocap).
typing.precedingTextProvider = { "hello " } // mid-sentence: auto stays off
typing.autocapitalizationModeProvider = { .sentences }
_ = typing.setLanguage(.english)
XCTAssertFalse(typing.shiftActive)
_ = typing.handleKey("⇧")
XCTAssertTrue(typing.shiftActive)
typing.syncAutocapitalization()
XCTAssertTrue(typing.shiftActive, "manual one-shot must outrank autocap sync")
XCTAssertEqual(typing.keyRows.first?.first, "Q")
}
func testShiftHoldTypesUppercaseWithoutEnteringCapsLock() {
let typing = TypingSessionController()
_ = typing.setLanguage(.english)
typing.precedingTextProvider = { "hello " }
typing.autocapitalizationModeProvider = { .sentences }
typing.beginShiftHold()
XCTAssertTrue(typing.shiftHeld)
XCTAssertEqual(typing.keyRows.first?.first, "Q")
_ = typing.handleKey("S")
_ = typing.handleKey("m")
XCTAssertFalse(typing.capsLock)
typing.endShiftHold()
XCTAssertFalse(typing.shiftHeld)
XCTAssertFalse(typing.capsLock)
XCTAssertFalse(typing.shiftActive)
}
func testShiftHoldWithoutTypingActsAsTap() {
let typing = TypingSessionController()
typing.beginShiftHold()
typing.endShiftHold()
XCTAssertTrue(typing.shiftActive)
XCTAssertFalse(typing.shiftHeld)
XCTAssertFalse(typing.capsLock)
}
func testChineseShiftInsertsUppercaseLatinBypassingRime() {
let engine = TrackingStubRimeEngine()
let typing = TypingSessionController(engine: { engine })
_ = typing.handleKey("⇧")
XCTAssertTrue(typing.shiftActive)
let output = typing.handleKey("N")
XCTAssertEqual(output, .insert("N"))
XCTAssertEqual(engine.processCharacterCallCount, 0)
XCTAssertFalse(typing.shiftActive, "one-shot Shift clears after Latin insert")
XCTAssertTrue(typing.composition.preedit.isEmpty)
}
func testChineseShiftPreservesExistingComposition() {
let engine = TrackingStubRimeEngine()
let typing = TypingSessionController(engine: { engine })
// Seed session composition via a lowercase letter (goes to Rime).
_ = typing.handleKey("n")
XCTAssertEqual(typing.composition.preedit, "n")
_ = typing.handleKey("⇧")
let output = typing.handleKey("A")
XCTAssertEqual(output, .insert("A"))
XCTAssertEqual(engine.processCharacterCallCount, 1, "only the unshifted letter hits Rime")
XCTAssertEqual(typing.composition.preedit, "n", "Shift Latin must not clear preedit")
}
func testChineseCapsLockKeepsInsertingUppercaseLatin() {
let engine = TrackingStubRimeEngine()
let typing = TypingSessionController(engine: { engine })
_ = typing.handleKey("⇧")
_ = typing.handleKey("⇧") // second tap → Caps Lock
XCTAssertTrue(typing.capsLock)
XCTAssertEqual(typing.handleKey("O"), .insert("O"))
XCTAssertEqual(typing.handleKey("S"), .insert("S"))
XCTAssertEqual(engine.processCharacterCallCount, 0)
XCTAssertTrue(typing.capsLock)
}
func testChineseUnshiftedLetterStillComposes() {
let engine = TrackingStubRimeEngine()
let typing = TypingSessionController(engine: { engine })
let output = typing.handleKey("n")
XCTAssertEqual(output, .none)
XCTAssertEqual(engine.processCharacterCallCount, 1)
XCTAssertEqual(engine.lastProcessedCharacter, "n")
XCTAssertEqual(typing.composition.preedit, "n")
}
}
/// Stub that records `processCharacter` calls for Chinese Shift bypass tests.
@MainActor
private final class TrackingStubRimeEngine: RimeEngineBridging {
var composition: TypingComposition = .empty
var isReady: Bool = true
var schema: TypingInputSchema = .fullPinyin
private(set) var processCharacterCallCount = 0
private(set) var lastProcessedCharacter: Character?
func prepare() async throws {}
func teardown() { composition = .empty }
func setLanguage(_ language: TypingInputLanguage) {
if language == .english { composition = .empty }
}
@discardableResult
func setSchema(_ schema: TypingInputSchema) -> Bool {
self.schema = schema
return true
}
func processCharacter(_ character: Character) -> String? {
processCharacterCallCount += 1
lastProcessedCharacter = character
composition = TypingComposition(
preedit: String(character).lowercased(),
candidates: [TypingCandidate(text: "你")]
)
return nil
}
func processBackspace() -> String? {
composition = .empty
return nil
}
func processSpace() -> String? {
composition = .empty
return " "
}
func processReturn() -> String? {
composition = .empty
return "\n"
}
func selectCandidate(at index: Int) -> String {
composition = .empty
return ""
}
func flushPreedit() -> String {
let raw = composition.preedit
composition = .empty
return raw
}
func clearComposition() {
composition = .empty
}
}