Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -417,13 +417,27 @@ extension RunnerTests {
}

private func snapshotHasFocus(_ snapshot: XCUIElementSnapshot) -> Bool {
var focused = false
return focusBool(snapshot as! NSObject)
}

/// Either focus is focus. `hasFocus` is the focus engine's answer (tvOS, keyboard navigation);
/// the field a software keyboard is typing into holds `hasKeyboardFocus` instead, which the
/// text-entry readiness check already consults. Both an element and its snapshot answer the two
/// keys through KVC, so one reader serves the XCTest producers (recursive tree, query sweep,
/// collapsed tabs); a key the object lacks reads as false. The private-AX bridge is not among
/// them: the AX server declares no keyboard-focus attribute, so it reads `hasFocus` alone.
func focusBool(_ object: NSObject) -> Bool {
return kvcBool(object, forKey: "hasKeyboardFocus") || kvcBool(object, forKey: "hasFocus")
}

private func kvcBool(_ object: NSObject, forKey key: String) -> Bool {
var result = false
_ = RunnerObjCExceptionCatcher.catchException({
if let value = (snapshot as! NSObject).value(forKey: "hasFocus") as? Bool {
focused = value
if let value = object.value(forKey: key) as? Bool {
result = value
}
})
return focused
return result
}

private func snapshotIsSelected(_ snapshot: XCUIElementSnapshot) -> Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,7 @@ extension RunnerTests {
}

func elementHasFocus(_ element: XCUIElement) -> Bool {
var focused = false
_ = RunnerObjCExceptionCatcher.catchException({
if let value = (element as NSObject).value(forKey: "hasFocus") as? Bool {
focused = value
}
})
return focused
return focusBool(element as NSObject)
}

func activateElement(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,48 @@ extension RunnerTests {
XCTAssertNil(tvRemoteButton(from: raw))
}
}

/// `focusBool` is the one reader behind `snapshotHasFocus` and `elementHasFocus`. A live element
/// or snapshot answers both keys through KVC; the fixtures answer the same way, so the read that
/// production performs is the read under test.
func testFocusBoolReadsKeyboardFocusBesideTheFocusEnginesFocus() {
XCTAssertTrue(
focusBool(FocusFixture(hasFocus: false, hasKeyboardFocus: true)),
"the field a software keyboard is typing into holds keyboard focus alone"
)
XCTAssertTrue(
focusBool(FocusFixture(hasFocus: true, hasKeyboardFocus: false)),
"the focus engine's focus (tvOS, keyboard navigation) still counts"
)
XCTAssertFalse(focusBool(FocusFixture(hasFocus: false, hasKeyboardFocus: false)))
XCTAssertTrue(
focusBool(KeyboardFocusOnlyFixture()),
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
"an object that exposes no hasFocus key at all still reports its keyboard focus"
)
XCTAssertTrue(
focusBool(NativeFocusOnlyFixture()),
"an object that exposes no hasKeyboardFocus key at all still reports the focus engine's focus"
)
XCTAssertFalse(focusBool(NSObject()), "an object with neither key reads as unfocused, not as an exception")
}
}

/// KVC-readable the way XCTest exposes an element's or a snapshot's focus attributes.
private final class FocusFixture: NSObject {
@objc let hasFocus: NSNumber
@objc let hasKeyboardFocus: NSNumber

init(hasFocus: Bool, hasKeyboardFocus: Bool) {
self.hasFocus = NSNumber(value: hasFocus)
self.hasKeyboardFocus = NSNumber(value: hasKeyboardFocus)
}
}

private final class KeyboardFocusOnlyFixture: NSObject {
@objc let hasKeyboardFocus: NSNumber = true
}

private final class NativeFocusOnlyFixture: NSObject {
@objc let hasFocus: NSNumber = true
}
#endif
Loading