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.
186 lines
6.0 KiB
Swift
186 lines
6.0 KiB
Swift
// KeyHitTestingTests.swift
|
|
// OSGKeyboard · Ext unit tests
|
|
//
|
|
// Phase 1 / 3: gap fill, nearest-center, intent offset, edge expansion.
|
|
|
|
@testable import OSGKeyboardShared
|
|
import XCTest
|
|
|
|
final class KeyHitTestingTests: XCTestCase {
|
|
private func makePairTargets(
|
|
left: CGRect = CGRect(x: 0, y: 0, width: 40, height: 50),
|
|
gap: CGFloat = 6
|
|
) -> (targets: [TypingKeyHitTarget], plane: CGRect) {
|
|
let right = CGRect(
|
|
x: left.maxX + gap,
|
|
y: left.minY,
|
|
width: left.width,
|
|
height: left.height
|
|
)
|
|
let targets = [
|
|
TypingKeyHitTarget(
|
|
id: "L",
|
|
label: "A",
|
|
visualFrame: left,
|
|
behavior: .commitOnRelease
|
|
),
|
|
TypingKeyHitTarget(
|
|
id: "R",
|
|
label: "S",
|
|
visualFrame: right,
|
|
behavior: .commitOnRelease
|
|
)
|
|
]
|
|
let plane = left.union(right)
|
|
return (targets, plane)
|
|
}
|
|
|
|
func testGapMidpointHitsNearestKey() {
|
|
let pair = makePairTargets()
|
|
let mid = CGPoint(x: 43, y: 25) // center of 6pt gap between 40 and 46
|
|
let hit = KeyHitTesting.hitTarget(
|
|
at: mid,
|
|
targets: pair.targets,
|
|
keyPlaneBounds: pair.plane,
|
|
horizontalGap: 6,
|
|
verticalGap: 7,
|
|
edgeExpansion: 0
|
|
)
|
|
// Midpoint is equidistant; either is acceptable, but must not miss.
|
|
XCTAssertNotNil(hit)
|
|
}
|
|
|
|
func testGapCloserToLeftSelectsLeft() {
|
|
let pair = makePairTargets()
|
|
// Just right of left key visual edge, still in gap, closer to left center.
|
|
let point = CGPoint(x: 41, y: 25)
|
|
let hit = KeyHitTesting.hitTarget(
|
|
at: point,
|
|
targets: pair.targets,
|
|
keyPlaneBounds: pair.plane,
|
|
horizontalGap: 6,
|
|
verticalGap: 7,
|
|
edgeExpansion: 0
|
|
)
|
|
XCTAssertEqual(hit?.id, "L")
|
|
}
|
|
|
|
func testGapCloserToRightSelectsRight() {
|
|
let pair = makePairTargets()
|
|
let point = CGPoint(x: 45, y: 25)
|
|
let hit = KeyHitTesting.hitTarget(
|
|
at: point,
|
|
targets: pair.targets,
|
|
keyPlaneBounds: pair.plane,
|
|
horizontalGap: 6,
|
|
verticalGap: 7,
|
|
edgeExpansion: 0
|
|
)
|
|
XCTAssertEqual(hit?.id, "R")
|
|
}
|
|
|
|
func testOutsidePlaneReturnsNil() {
|
|
let pair = makePairTargets()
|
|
let hit = KeyHitTesting.hitTarget(
|
|
at: CGPoint(x: 200, y: 200),
|
|
targets: pair.targets,
|
|
keyPlaneBounds: pair.plane,
|
|
horizontalGap: 6,
|
|
verticalGap: 7,
|
|
edgeExpansion: 0
|
|
)
|
|
XCTAssertNil(hit)
|
|
}
|
|
|
|
func testIntentOffsetShiftsHitUpward() {
|
|
// Key occupies y 10…60. Raw touch at y=62 is below the key; with a
|
|
// 4pt upward intent offset it maps to y=58 and should still hit.
|
|
let target = TypingKeyHitTarget(
|
|
id: "K",
|
|
label: "M",
|
|
visualFrame: CGRect(x: 0, y: 10, width: 40, height: 50),
|
|
behavior: .commitOnRelease
|
|
)
|
|
let plane = target.visualFrame
|
|
let raw = CGPoint(x: 20, y: 62)
|
|
let withoutOffset = KeyHitTesting.hitTarget(
|
|
at: raw,
|
|
targets: [target],
|
|
keyPlaneBounds: plane,
|
|
horizontalGap: 0,
|
|
verticalGap: 0,
|
|
edgeExpansion: 0
|
|
)
|
|
let withOffset = KeyHitTesting.hitTarget(
|
|
rawTouch: raw,
|
|
targets: [target],
|
|
keyPlaneBounds: plane,
|
|
horizontalGap: 0,
|
|
verticalGap: 0,
|
|
intentOffsetY: 4,
|
|
edgeExpansion: 0
|
|
)
|
|
XCTAssertNil(withoutOffset)
|
|
XCTAssertEqual(withOffset?.id, "K")
|
|
}
|
|
|
|
func testEdgeExpansionExtendsOuterHit() {
|
|
let target = TypingKeyHitTarget(
|
|
id: "Q",
|
|
label: "Q",
|
|
visualFrame: CGRect(x: 10, y: 0, width: 40, height: 50),
|
|
behavior: .commitOnRelease
|
|
)
|
|
let plane = target.visualFrame
|
|
// Point just left of the visual key, inside edge expansion.
|
|
let point = CGPoint(x: 7, y: 25)
|
|
let hit = KeyHitTesting.hitTarget(
|
|
at: point,
|
|
targets: [target],
|
|
keyPlaneBounds: plane,
|
|
horizontalGap: 0,
|
|
verticalGap: 0,
|
|
edgeExpansion: 5
|
|
)
|
|
XCTAssertEqual(hit?.id, "Q")
|
|
}
|
|
|
|
func testLayoutBuilderCoversGapsBetweenKeys() {
|
|
let layout = TypingKeyLayoutBuilder.build(
|
|
size: CGSize(width: 300, height: 200),
|
|
letterRows: [
|
|
["Q", "W", "E"],
|
|
["A", "S", "D"],
|
|
["Z", "X", "C"]
|
|
],
|
|
pageSwitchLabel: "123",
|
|
spaceLabel: "space",
|
|
returnLabel: "return",
|
|
keyWeight: { _, _, _ in 1 }
|
|
)
|
|
// 9 letters + 4 bottom (globe · pageSwitch · space · return)
|
|
XCTAssertEqual(layout.keys.count, 13)
|
|
|
|
// Mid-gap between Q and W on first row should hit something.
|
|
let q = layout.keys.first { $0.label == "Q" }!
|
|
let w = layout.keys.first { $0.label == "W" }!
|
|
let mid = CGPoint(x: (q.visualFrame.maxX + w.visualFrame.minX) / 2, y: q.center.y)
|
|
let hit = KeyHitTesting.hitTarget(
|
|
at: mid,
|
|
targets: layout.keys,
|
|
keyPlaneBounds: layout.keyPlaneBounds,
|
|
horizontalGap: layout.horizontalGap,
|
|
verticalGap: layout.verticalGap
|
|
)
|
|
XCTAssertNotNil(hit)
|
|
XCTAssertTrue(hit?.label == "Q" || hit?.label == "W")
|
|
}
|
|
|
|
func testBehaviorResolver() {
|
|
XCTAssertEqual(TypingKeyBehaviorResolver.behavior(for: "⌫"), .deleteRepeat)
|
|
XCTAssertEqual(TypingKeyBehaviorResolver.behavior(for: "⇧"), .shiftHold)
|
|
XCTAssertEqual(TypingKeyBehaviorResolver.behavior(for: "A"), .commitOnRelease)
|
|
XCTAssertEqual(TypingKeyBehaviorResolver.behavior(for: "123"), .commitOnRelease)
|
|
}
|
|
}
|