feat: cursor navigation, key sounds, dictionary tooling, key security
Batch of in-progress app work from the working tree. - feat(keyboard): CursorNavigation + CursorDragPad for caret movement; KeyboardSoundFeedback for system key click sounds - feat(dictionary): DictionaryAliasGenerator + PersonalDictionaryEntrySheet; TranscriptPostProcessor quality gate; retire DictionaryLearner - feat(ui): TabBarVisibility handling; drop PageHeaderRow / PageHeaderConfirmButton; refresh views and localizable strings - fix(security): move the hardcoded DeepSeek key out of PreconfiguredKeys.swift into a gitignored PreconfiguredKeys.local.swift (seeded from .example by generate-xcodeproj.sh) - docs(agents): add Conventional Commits versioning + bilingual changelog rules - chore(gitignore): ignore PreconfiguredKeys.local.swift, .cache/, pycache Custom language model / lexicon work stays on feature/custom-language-model-asr. Changelog bullets added under [Unreleased]; no version bump.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// CursorNavigationTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class CursorNavigationTests: XCTestCase {
|
||||
|
||||
/// Uses the built-in 1/2-unit width table. `lineWidth` is therefore in
|
||||
/// "units" (≈ Latin characters) for these tests.
|
||||
private func config(lineWidth: CGFloat) -> CursorNavigation.VisualLineLayoutConfig {
|
||||
CursorNavigation.VisualLineLayoutConfig(lineWidth: lineWidth)
|
||||
}
|
||||
|
||||
func testColumnOnFirstLine() {
|
||||
XCTAssertEqual(CursorNavigation.column(before: "hello"), 5)
|
||||
XCTAssertEqual(CursorNavigation.column(before: nil), 0)
|
||||
}
|
||||
|
||||
func testColumnAfterNewline() {
|
||||
XCTAssertEqual(CursorNavigation.column(before: "hello\nwor"), 3)
|
||||
}
|
||||
|
||||
func testDefaultDisplayWidthLatinAndCJK() {
|
||||
XCTAssertEqual(CursorNavigation.defaultDisplayWidth("a"), 1)
|
||||
XCTAssertEqual(CursorNavigation.defaultDisplayWidth("中"), 2)
|
||||
XCTAssertEqual(CursorNavigation.defaultDisplayWidth("\n"), 0)
|
||||
}
|
||||
|
||||
func testVisualLineDownAcrossSoftWrap() {
|
||||
// 20 Latin chars, wrap at 16 → line0 [0,16), line1 [16,20).
|
||||
let text = String(repeating: "a", count: 20)
|
||||
let before = String(text.prefix(8))
|
||||
let after = String(text.suffix(12))
|
||||
|
||||
let result = CursorNavigation.visualLineDownOffset(
|
||||
before: before,
|
||||
after: after,
|
||||
preferredDisplayColumn: nil,
|
||||
config: config(lineWidth: 16)
|
||||
)
|
||||
XCTAssertNotNil(result)
|
||||
// Sticky column 8; line1 only has 4 units → clamp to its end.
|
||||
XCTAssertEqual(result?.offset, 12)
|
||||
}
|
||||
|
||||
func testVisualLineDownToShorterWrappedLineClampsToEnd() {
|
||||
// Caret at col 15 (clearly on line0); line1 has only 4 chars.
|
||||
let text = String(repeating: "a", count: 20)
|
||||
let before = String(text.prefix(15))
|
||||
let after = String(text.suffix(5))
|
||||
|
||||
let result = CursorNavigation.visualLineDownOffset(
|
||||
before: before,
|
||||
after: after,
|
||||
preferredDisplayColumn: 15,
|
||||
config: config(lineWidth: 16)
|
||||
)
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?.offset, 5)
|
||||
XCTAssertEqual(result?.stickyColumn, 15)
|
||||
}
|
||||
|
||||
func testVisualLineUpAcrossSoftWrap() {
|
||||
let text = String(repeating: "a", count: 20)
|
||||
let before = String(text.prefix(18))
|
||||
let after = String(text.suffix(2))
|
||||
|
||||
let result = CursorNavigation.visualLineUpOffset(
|
||||
before: before,
|
||||
after: after,
|
||||
preferredDisplayColumn: 8,
|
||||
config: config(lineWidth: 16)
|
||||
)
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?.offset, -10)
|
||||
}
|
||||
|
||||
func testVisualLineDownAcrossHardNewline() {
|
||||
let before = "hello\nwor"
|
||||
let after = "ld\nfoo"
|
||||
|
||||
let result = CursorNavigation.visualLineDownOffset(
|
||||
before: before,
|
||||
after: after,
|
||||
preferredDisplayColumn: nil,
|
||||
config: config(lineWidth: 100)
|
||||
)
|
||||
XCTAssertNotNil(result)
|
||||
// "hello\nworld\nfoo" — caret before "ld"; column 3 lands after "foo".
|
||||
XCTAssertEqual(result?.offset, 6)
|
||||
}
|
||||
|
||||
func testVisualLineDownPreservesStickyColumnOnLongerNextLine() {
|
||||
let before = "hello\nwor"
|
||||
let after = "ld\nfoobarbaz"
|
||||
|
||||
let result = CursorNavigation.visualLineDownOffset(
|
||||
before: before,
|
||||
after: after,
|
||||
preferredDisplayColumn: 5,
|
||||
config: config(lineWidth: 100)
|
||||
)
|
||||
XCTAssertNotNil(result)
|
||||
// "ld\n" (3) + column 5 on "foobarbaz" = 8 total from cursor.
|
||||
XCTAssertEqual(result?.offset, 8)
|
||||
XCTAssertEqual(result?.stickyColumn, 5)
|
||||
}
|
||||
|
||||
func testVisualLineUpOnFirstLineReturnsNil() {
|
||||
XCTAssertNil(
|
||||
CursorNavigation.visualLineUpOffset(
|
||||
before: "hello",
|
||||
after: " world",
|
||||
preferredDisplayColumn: nil,
|
||||
config: config(lineWidth: 100)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testVisualLineDownWithNoFollowingTextReturnsNil() {
|
||||
XCTAssertNil(
|
||||
CursorNavigation.visualLineDownOffset(
|
||||
before: "hello",
|
||||
after: nil,
|
||||
preferredDisplayColumn: nil,
|
||||
config: config(lineWidth: 100)
|
||||
)
|
||||
)
|
||||
XCTAssertNil(
|
||||
CursorNavigation.visualLineDownOffset(
|
||||
before: "hello",
|
||||
after: "",
|
||||
preferredDisplayColumn: nil,
|
||||
config: config(lineWidth: 100)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user