184 lines
5.1 KiB
GDScript
184 lines
5.1 KiB
GDScript
extends SceneTree
|
|
|
|
const ControllerFocusRecoveryType = preload(
|
|
"res://ui/controller_focus_recovery.gd"
|
|
)
|
|
|
|
var _failures: Array[String] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
_run.call_deferred()
|
|
|
|
|
|
func _run() -> void:
|
|
var stage := Control.new()
|
|
stage.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
root.add_child(stage)
|
|
var recovery := ControllerFocusRecoveryType.new()
|
|
stage.add_child(recovery)
|
|
var option_list := VBoxContainer.new()
|
|
option_list.position = Vector2(100.0, 100.0)
|
|
stage.add_child(option_list)
|
|
var original := _make_button("cute", "cute (show variants)")
|
|
option_list.add_child(original)
|
|
await process_frame
|
|
original.grab_focus()
|
|
await process_frame
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == null,
|
|
"programmatic focus stays pending outside navigation mode",
|
|
)
|
|
var keyboard_navigation := InputEventKey.new()
|
|
keyboard_navigation.keycode = KEY_DOWN
|
|
keyboard_navigation.pressed = true
|
|
recovery._input(keyboard_navigation)
|
|
await process_frame
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == original,
|
|
"keyboard navigation restores pending initial focus",
|
|
)
|
|
var menu_shortcut := InputEventKey.new()
|
|
menu_shortcut.physical_keycode = KEY_TAB
|
|
menu_shortcut.pressed = true
|
|
recovery._input(menu_shortcut)
|
|
await process_frame
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == null,
|
|
"the menu shortcut does not leave a pseudo-focused option",
|
|
)
|
|
|
|
var controller_event := InputEventJoypadButton.new()
|
|
controller_event.button_index = JOY_BUTTON_A
|
|
controller_event.pressed = true
|
|
recovery._input(controller_event)
|
|
await process_frame
|
|
await process_frame
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == original,
|
|
"controller navigation restores pending initial focus",
|
|
)
|
|
|
|
option_list.remove_child(original)
|
|
original.queue_free()
|
|
var replacement := _make_button("cute", "cute (hide variants)")
|
|
option_list.add_child(replacement)
|
|
await process_frame
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == replacement,
|
|
"focus follows a rebuilt semantic option",
|
|
)
|
|
|
|
var explicit_target := _make_button("explicit", "explicit")
|
|
option_list.add_child(explicit_target)
|
|
option_list.remove_child(replacement)
|
|
replacement.queue_free()
|
|
explicit_target.grab_focus()
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == explicit_target,
|
|
"explicit focus changes take precedence over recovery",
|
|
)
|
|
var pointer_motion := InputEventMouseMotion.new()
|
|
pointer_motion.position = Vector2(4.0, 4.0)
|
|
recovery._input(pointer_motion)
|
|
await process_frame
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == null,
|
|
"mouse motion releases controller focus",
|
|
)
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == null,
|
|
"mouse motion prevents automatic focus recovery",
|
|
)
|
|
recovery._input(controller_event)
|
|
await process_frame
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == explicit_target,
|
|
"controller navigation can resume from pointer mode",
|
|
)
|
|
var pointer_press := InputEventMouseButton.new()
|
|
pointer_press.button_index = MOUSE_BUTTON_LEFT
|
|
pointer_press.pressed = true
|
|
recovery._input(pointer_press)
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == explicit_target,
|
|
"mouse-down preserves focus through button activation",
|
|
)
|
|
var pointer_release := InputEventMouseButton.new()
|
|
pointer_release.button_index = MOUSE_BUTTON_LEFT
|
|
pointer_release.pressed = false
|
|
recovery._input(pointer_release)
|
|
await process_frame
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == null,
|
|
"mouse-up clears the completed button focus",
|
|
)
|
|
recovery._input(controller_event)
|
|
await process_frame
|
|
await process_frame
|
|
|
|
var outside_button := _make_button("outside", "outside")
|
|
stage.add_child(outside_button)
|
|
option_list.hide()
|
|
root.gui_release_focus()
|
|
await process_frame
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == null,
|
|
"focus recovery never escapes a hidden menu scope",
|
|
)
|
|
option_list.show()
|
|
explicit_target.grab_focus()
|
|
await process_frame
|
|
var leave_world_ui_event := InputEventJoypadButton.new()
|
|
leave_world_ui_event.button_index = JOY_BUTTON_LEFT_SHOULDER
|
|
leave_world_ui_event.pressed = true
|
|
recovery._input(leave_world_ui_event)
|
|
root.gui_release_focus()
|
|
await process_frame
|
|
await process_frame
|
|
_expect(
|
|
root.gui_get_focus_owner() == null,
|
|
"LB intentionally leaving world UI is never recovered",
|
|
)
|
|
var accessibility_only := Control.new()
|
|
accessibility_only.focus_mode = Control.FOCUS_ACCESSIBILITY
|
|
stage.add_child(accessibility_only)
|
|
await process_frame
|
|
_expect(
|
|
not bool(recovery.call("_is_focusable", accessibility_only)),
|
|
"accessibility-only controls are not recovery targets",
|
|
)
|
|
|
|
stage.queue_free()
|
|
if _failures.is_empty():
|
|
print("Controller focus recovery validation: PASS")
|
|
quit(0)
|
|
return
|
|
for failure: String in _failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func _make_button(text: String, tooltip: String) -> Button:
|
|
var button := Button.new()
|
|
button.text = text
|
|
button.tooltip_text = tooltip
|
|
button.custom_minimum_size = Vector2(120.0, 48.0)
|
|
return button
|
|
|
|
|
|
func _expect(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
_failures.append(message)
|