forked from woofmeow/straywild
fix: harden controller navigation and text entry
This commit is contained in:
parent
c4e49cdc52
commit
8ea877ecf4
28 changed files with 1320 additions and 96 deletions
|
|
@ -20,7 +20,7 @@ func _init() -> void:
|
|||
assert(_has_joypad_button(&"jump", JOY_BUTTON_A))
|
||||
assert(_has_joypad_button(&"ui_accept", JOY_BUTTON_A))
|
||||
assert(_has_joypad_button(&"ui_cancel", JOY_BUTTON_B))
|
||||
assert(not _has_joypad_button(&"interact", JOY_BUTTON_Y))
|
||||
assert(_has_joypad_button(&"interact", JOY_BUTTON_Y))
|
||||
assert(_has_joypad_button(&"character_call", JOY_BUTTON_Y))
|
||||
assert(_has_joypad_button(&"fish_primary", JOY_BUTTON_RIGHT_SHOULDER))
|
||||
assert(not _has_joypad_motion(
|
||||
|
|
|
|||
|
|
@ -221,6 +221,16 @@ func _run() -> void:
|
|||
assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
|
||||
assert(chat_ui.is_open())
|
||||
chat_ui.refocus_gameplay()
|
||||
chat_ui.open_chat()
|
||||
assert(chat_ui.is_open())
|
||||
typed_chat_entry.text = "/not-a-real-command"
|
||||
chat_ui.call("_send")
|
||||
assert(not chat_ui.is_open())
|
||||
assert(not typed_chat_entry.has_focus())
|
||||
assert(not bool(chat_ui.get("_input_lock_applied")))
|
||||
assert(chat_status.visible)
|
||||
assert(chat_status.text == "Unknown command: /not-a-real-command")
|
||||
chat_ui.call("_set_status", "")
|
||||
var quick_menu := game_ui.get_node(
|
||||
"%QuickRadialMenu"
|
||||
) as QuickRadialMenu
|
||||
|
|
@ -695,8 +705,13 @@ func _validate_pause_browser_transition(
|
|||
var drag_motion := InputEventMouseMotion.new()
|
||||
drag_motion.button_mask = MOUSE_BUTTON_MASK_RIGHT
|
||||
drag_motion.relative = Vector2(24.0, -8.0)
|
||||
drag_motion.screen_relative = drag_motion.relative
|
||||
player.call("_input", drag_motion)
|
||||
assert(not is_equal_approx(camera_yaw.rotation.y, yaw_before_drag))
|
||||
await process_frame
|
||||
assert(bool(player.get("_camera_dragging")))
|
||||
if DisplayServer.get_name() != "headless":
|
||||
assert(Input.mouse_mode == Input.MOUSE_MODE_CAPTURED)
|
||||
var drag_release := InputEventMouseButton.new()
|
||||
drag_release.button_index = MOUSE_BUTTON_RIGHT
|
||||
drag_release.button_mask = 0
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ extends SceneTree
|
|||
const FocusPresentationType = preload(
|
||||
"res://ui/controller_focus_presentation.gd"
|
||||
)
|
||||
const FishBatchSelectionType = preload(
|
||||
"res://ui/fish_batch_selection.gd"
|
||||
)
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
|
|
@ -12,6 +15,7 @@ func _initialize() -> void:
|
|||
|
||||
|
||||
func _run() -> void:
|
||||
_validate_controller_cooler_focus()
|
||||
root.size = Vector2i(1280, 720)
|
||||
var stage := Control.new()
|
||||
stage.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
|
|
@ -23,8 +27,10 @@ func _run() -> void:
|
|||
standard_button.position = Vector2(80.0, 60.0)
|
||||
standard_button.custom_minimum_size = Vector2(120.0, 60.0)
|
||||
var normal_style := _button_style(Color("123f4e"))
|
||||
var old_hover_style := _button_style(Color("58c6d4"))
|
||||
var old_focus_style := _button_style(Color("238697"))
|
||||
standard_button.add_theme_stylebox_override("normal", normal_style)
|
||||
standard_button.add_theme_stylebox_override("hover", old_hover_style)
|
||||
standard_button.add_theme_stylebox_override("focus", old_focus_style)
|
||||
stage.add_child(standard_button)
|
||||
var second_button := Button.new()
|
||||
|
|
@ -82,6 +88,22 @@ func _run() -> void:
|
|||
)
|
||||
stage.scale = Vector2.ONE
|
||||
await process_frame
|
||||
var hover_motion := InputEventMouseMotion.new()
|
||||
hover_motion.position = standard_button.get_global_rect().get_center()
|
||||
hover_motion.global_position = hover_motion.position
|
||||
root.push_input(hover_motion, true)
|
||||
await process_frame
|
||||
_expect(
|
||||
root.gui_get_hovered_control() == standard_button,
|
||||
"the pointer did not reach the hover-suppression test button",
|
||||
)
|
||||
presentation._input(controller_event)
|
||||
standard_button.grab_focus()
|
||||
await process_frame
|
||||
_expect(
|
||||
standard_button.get_theme_stylebox("hover") == normal_style,
|
||||
"controller use leaves a stale mouse-hover highlight visible",
|
||||
)
|
||||
|
||||
second_button.grab_focus()
|
||||
await process_frame
|
||||
|
|
@ -106,6 +128,10 @@ func _run() -> void:
|
|||
standard_button.get_theme_stylebox("focus") == old_focus_style,
|
||||
"native theme overrides were not restored after controller use",
|
||||
)
|
||||
_expect(
|
||||
standard_button.get_theme_stylebox("hover") == old_hover_style,
|
||||
"mouse hover styling was not restored after controller use",
|
||||
)
|
||||
presentation._input(controller_event)
|
||||
|
||||
var item_list := ItemList.new()
|
||||
|
|
@ -284,6 +310,21 @@ func _run() -> void:
|
|||
quit(1)
|
||||
|
||||
|
||||
func _validate_controller_cooler_focus() -> void:
|
||||
var selection := FishBatchSelectionType.new()
|
||||
selection.set_visible_order([&"first", &"second"])
|
||||
selection.select_only(&"first")
|
||||
selection.focus_only(&"second")
|
||||
_expect(
|
||||
selection.get_focused_id() == &"second",
|
||||
"cooler focus does not follow controller navigation",
|
||||
)
|
||||
_expect(
|
||||
selection.get_selected_ids() == [&"first"],
|
||||
"moving cooler focus unexpectedly changes the selected catch set",
|
||||
)
|
||||
|
||||
|
||||
func _button_style(color: Color) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
|
|
|
|||
|
|
@ -104,18 +104,20 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String:
|
|||
return "right-stick click is not assigned to the sneak action"
|
||||
if ControllerMappingManagerType.ROLE_LABELS.get(
|
||||
ControllerMappingManagerType.ROLE_Y, ""
|
||||
) != "character call":
|
||||
return "Y is not presented as the character-call button"
|
||||
) != "interact / character call":
|
||||
return "Y does not present its contextual gameplay actions"
|
||||
if ControllerMappingManagerType.BUTTON_ACTION_ROLES.get(
|
||||
&"interact", &""
|
||||
) != ControllerMappingManagerType.ROLE_Y:
|
||||
return "Y is not assigned to the interact action"
|
||||
if ControllerMappingManagerType.BUTTON_ACTION_ROLES.get(
|
||||
&"character_call", &""
|
||||
) != ControllerMappingManagerType.ROLE_Y:
|
||||
return "Y is not assigned to the character-call action"
|
||||
if ControllerMappingManagerType.BUTTON_ACTION_ROLES.has(&"interact"):
|
||||
return "Y still exposes the obsolete controller interact binding"
|
||||
if not _has_joy_button(&"character_call", JOY_BUTTON_Y):
|
||||
return "character call does not default to Y"
|
||||
if _has_joy_button(&"interact", JOY_BUTTON_Y):
|
||||
return "Y still defaults to interact"
|
||||
if not _has_joy_button(&"interact", JOY_BUTTON_Y):
|
||||
return "interact does not default to Y"
|
||||
if not _has_joy_button(&"sneak", JOY_BUTTON_RIGHT_STICK):
|
||||
return "sneak does not default to right-stick click"
|
||||
var trigger_button := InputEventJoypadButton.new()
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ func _run() -> void:
|
|||
root.size = Vector2i(1280, 720)
|
||||
await _validate_join_game_navigation()
|
||||
await _validate_data_settings_navigation()
|
||||
await _validate_settings_adjustment_navigation()
|
||||
await _validate_mail_navigation()
|
||||
await _validate_profile_confirmation_focus()
|
||||
await _validate_profile_voice_navigation()
|
||||
|
|
@ -196,6 +197,65 @@ func _validate_data_settings_navigation() -> void:
|
|||
await process_frame
|
||||
|
||||
|
||||
func _validate_settings_adjustment_navigation() -> void:
|
||||
var panel := SettingsPanelScene.instantiate() as SettingsPanel
|
||||
root.add_child(panel)
|
||||
await process_frame
|
||||
panel.show()
|
||||
var sound_page := panel.get_node("%SoundPage") as SettingsBubblePage
|
||||
sound_page.show_page(false)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
var environment := panel.get_node("%EnvironmentVolumeSlider") as HSlider
|
||||
var sound_back := panel.get_node("%SoundBackButton") as Button
|
||||
_assert_neighbor(
|
||||
environment,
|
||||
&"focus_neighbor_bottom",
|
||||
sound_back,
|
||||
)
|
||||
_assert_neighbor(sound_back, &"focus_neighbor_top", environment)
|
||||
sound_page.hide_page()
|
||||
|
||||
var controls_page := panel.get_node("%ControlsPage") as SettingsBubblePage
|
||||
controls_page.show_page(false)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
var parent := panel.get_node("%MouseValue") as Button
|
||||
var decrease := panel.get_node("%MouseDecrease") as Button
|
||||
var increase := panel.get_node("%MouseIncrease") as Button
|
||||
_expect(
|
||||
decrease.focus_mode == Control.FOCUS_NONE
|
||||
and increase.focus_mode == Control.FOCUS_NONE,
|
||||
"Sensitivity adjustment bubbles are reachable before their parent.",
|
||||
)
|
||||
parent.grab_focus()
|
||||
var accept := InputEventJoypadButton.new()
|
||||
accept.button_index = JOY_BUTTON_A
|
||||
accept.pressed = true
|
||||
panel.call("_input", accept)
|
||||
_expect(
|
||||
decrease.focus_mode == Control.FOCUS_ALL
|
||||
and increase.focus_mode == Control.FOCUS_ALL,
|
||||
"Selecting a sensitivity parent did not enter its adjustment zone.",
|
||||
)
|
||||
_assert_neighbor(parent, &"focus_neighbor_left", decrease)
|
||||
_assert_neighbor(parent, &"focus_neighbor_right", increase)
|
||||
panel.handle_back()
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
_expect(
|
||||
decrease.focus_mode == Control.FOCUS_NONE
|
||||
and increase.focus_mode == Control.FOCUS_NONE,
|
||||
"Leaving an adjustment zone did not hide its child controls.",
|
||||
)
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == parent,
|
||||
"Leaving an adjustment zone did not restore its parent focus.",
|
||||
)
|
||||
panel.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_mail_navigation() -> void:
|
||||
var page := MailPageType.new() as Control
|
||||
root.add_child(page)
|
||||
|
|
@ -364,6 +424,31 @@ func _validate_profile_confirmation_focus() -> void:
|
|||
root.gui_get_focus_owner() in option_controls,
|
||||
"Selecting an appearance feature did not focus its options.",
|
||||
)
|
||||
var rebuilt_option := root.gui_get_focus_owner() as BaseButton
|
||||
if rebuilt_option != null:
|
||||
rebuilt_option.pressed.emit()
|
||||
for _frame: int in 3:
|
||||
await process_frame
|
||||
option_groups = page.call("_controller_option_groups")
|
||||
option_controls.clear()
|
||||
if not option_groups.is_empty():
|
||||
for item: Variant in option_groups.front():
|
||||
var option_control := item as Control
|
||||
if option_control != null:
|
||||
option_controls.append(option_control)
|
||||
_expect(
|
||||
root.gui_get_focus_owner() in option_controls,
|
||||
"Rebuilding appearance options dropped controller focus.",
|
||||
)
|
||||
root.gui_release_focus()
|
||||
var controller_down := InputEventJoypadButton.new()
|
||||
controller_down.button_index = JOY_BUTTON_DPAD_DOWN
|
||||
controller_down.pressed = true
|
||||
page.call("handle_controller_input", controller_down)
|
||||
_expect(
|
||||
root.gui_get_focus_owner() in option_controls,
|
||||
"Appearance options did not recover a lost controller focus owner.",
|
||||
)
|
||||
var cancel_options := InputEventAction.new()
|
||||
cancel_options.action = &"ui_cancel"
|
||||
cancel_options.pressed = true
|
||||
|
|
@ -774,7 +859,7 @@ func _validate_inventory_tab_zone_transitions() -> void:
|
|||
)
|
||||
|
||||
# Add three representative equipment entries so entering the content zone
|
||||
# exercises the real three-column directional layout.
|
||||
# exercises the real five-column directional layout.
|
||||
var item_field := menu.get_node("%BagItemField") as Control
|
||||
var bag_nodes: Dictionary = menu.get("_bag_item_nodes")
|
||||
var owned_items: Array[OwnedItemType] = []
|
||||
|
|
@ -944,6 +1029,32 @@ func _validate_inventory_tab_zone_transitions() -> void:
|
|||
management_requests[0] == 1,
|
||||
"Equipment-to-hotbar navigation did not open hotbar management.",
|
||||
)
|
||||
var up := InputEventAction.new()
|
||||
up.action = &"ui_up"
|
||||
up.pressed = true
|
||||
_expect(
|
||||
bool(menu.call("_handle_controller_ownership_input", up)),
|
||||
"Up from hotbar management was not consumed.",
|
||||
)
|
||||
_expect(
|
||||
menu.get("_controller_ownership")
|
||||
== PlayerMenuType.ControllerOwnership.ITEM_LIST,
|
||||
"Up from hotbar management did not return to Inventory contents.",
|
||||
)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == hotbar_source,
|
||||
"Up from hotbar management did not restore the source item focus.",
|
||||
)
|
||||
_expect(
|
||||
bool(menu.call("_handle_controller_ownership_input", down)),
|
||||
"Inventory contents could not re-enter hotbar management.",
|
||||
)
|
||||
_expect(
|
||||
management_requests[0] == 2,
|
||||
"Re-entering the hotbar did not reopen hotbar management.",
|
||||
)
|
||||
_expect(
|
||||
bool(menu.call("_handle_controller_ownership_input", accept)),
|
||||
"Hotbar management did not consume controller A.",
|
||||
|
|
|
|||
134
tests/controller_world_interaction_validation.gd
Normal file
134
tests/controller_world_interaction_validation.gd
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
extends SceneTree
|
||||
|
||||
const MainScene = preload("res://main/main.tscn")
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var main := MainScene.instantiate()
|
||||
root.add_child(main)
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
if not bool(main.get("_application_initialized")):
|
||||
main.call("_activate_selected_data_path", "", true)
|
||||
for _frame: int in 8:
|
||||
await process_frame
|
||||
_expect(
|
||||
bool(main.get("_application_initialized")),
|
||||
"application initialization failed",
|
||||
)
|
||||
_expect(
|
||||
bool(main.call("_prepare_private_host")),
|
||||
"private host preparation failed",
|
||||
)
|
||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
_expect(
|
||||
save_manager != null and save_manager.initialize_new_game(),
|
||||
"new-game initialization failed",
|
||||
)
|
||||
main.call("_enter_gameplay")
|
||||
for _frame: int in 8:
|
||||
await process_frame
|
||||
|
||||
var player := main.get("_player") as Player
|
||||
var interaction := main.get("_shop_interaction") as FishingShopInteraction
|
||||
var game_ui := main.get("_game_ui") as GameUI
|
||||
var chat_service := main.get_node(
|
||||
"%NetworkChatService"
|
||||
) as NetworkChatService
|
||||
_expect(player != null, "local player is unavailable")
|
||||
_expect(interaction != null, "shop interaction is unavailable")
|
||||
_expect(game_ui != null, "game UI is unavailable")
|
||||
_expect(chat_service != null, "network chat service is unavailable")
|
||||
if (
|
||||
player != null
|
||||
and interaction != null
|
||||
and game_ui != null
|
||||
and chat_service != null
|
||||
):
|
||||
player.global_position = interaction.global_position
|
||||
for _frame: int in 6:
|
||||
await physics_frame
|
||||
_expect(
|
||||
interaction.is_local_player_in_range(),
|
||||
"local player did not enter the shop interaction range",
|
||||
)
|
||||
var character_calls: Array[String] = []
|
||||
var capture_call := func(
|
||||
_peer_id: int,
|
||||
call_id: String,
|
||||
_pitch_scale: float,
|
||||
) -> void:
|
||||
character_calls.append(call_id)
|
||||
chat_service.character_call_received.connect(capture_call)
|
||||
var press := InputEventJoypadButton.new()
|
||||
press.device = 0
|
||||
press.button_index = JOY_BUTTON_Y
|
||||
press.pressed = true
|
||||
_expect(
|
||||
press.is_action_pressed("interact"),
|
||||
"Y does not resolve to interact",
|
||||
)
|
||||
_expect(
|
||||
press.is_action_pressed("character_call"),
|
||||
"Y does not resolve to character call",
|
||||
)
|
||||
game_ui.call("_input", press)
|
||||
main.call("_unhandled_input", press)
|
||||
await process_frame
|
||||
_expect(
|
||||
game_ui.get_fishing_shop().visible,
|
||||
"Y did not open the shop while the player was in range",
|
||||
)
|
||||
_expect(
|
||||
character_calls.is_empty(),
|
||||
"Y played a character call instead of opening the shop",
|
||||
)
|
||||
player.global_position = interaction.global_position + Vector3(20.0, 0.0, 0.0)
|
||||
for _frame: int in 6:
|
||||
await physics_frame
|
||||
for _frame: int in 60:
|
||||
if not game_ui.get_fishing_shop().visible:
|
||||
break
|
||||
await process_frame
|
||||
_expect(
|
||||
not interaction.is_local_player_in_range(),
|
||||
"local player did not leave the shop interaction range",
|
||||
)
|
||||
_expect(
|
||||
not game_ui.get_fishing_shop().visible,
|
||||
"shop did not close after leaving its interaction range",
|
||||
)
|
||||
game_ui.call("_input", press)
|
||||
await process_frame
|
||||
_expect(
|
||||
character_calls.size() == 1,
|
||||
"Y did not play a character call away from an interaction",
|
||||
)
|
||||
chat_service.character_call_received.disconnect(capture_call)
|
||||
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
if session != null:
|
||||
session.disconnect_session("Controller interaction validation complete.")
|
||||
main.queue_free()
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
await create_timer(0.1).timeout
|
||||
if _failures.is_empty():
|
||||
print("Controller world interaction validation: PASS")
|
||||
quit(0)
|
||||
return
|
||||
for failure: String in _failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
_failures.append(message)
|
||||
1
tests/controller_world_interaction_validation.gd.uid
Normal file
1
tests/controller_world_interaction_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b6fg46sad1ma6
|
||||
|
|
@ -422,7 +422,6 @@ func _test_player_menu_sale(
|
|||
) as SubViewport
|
||||
assert(sell_action.visible and not sell_action.disabled)
|
||||
assert(sell_action.mouse_filter == Control.MOUSE_FILTER_STOP)
|
||||
assert(sell_action.focus_mode == Control.FOCUS_ALL)
|
||||
assert(ui_viewport != null)
|
||||
await _activate_pointer_control(sell_action, ui_viewport)
|
||||
await process_frame
|
||||
|
|
@ -632,7 +631,8 @@ func _test_fishing_shop_sale_ui(
|
|||
}))
|
||||
var balance_before: int = player.wallet.get_balance()
|
||||
assert(shop.open_shop())
|
||||
await process_frame
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
assert(shop_backdrop.visible)
|
||||
assert(shop_backdrop.material is ShaderMaterial)
|
||||
assert(shop.has_node("InputBlocker") and not shop.has_node("Dimmer"))
|
||||
|
|
|
|||
99
tests/fishing_shop_controller_validation.gd
Normal file
99
tests/fishing_shop_controller_validation.gd
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
extends SceneTree
|
||||
|
||||
const FishingShopScene: PackedScene = preload(
|
||||
"res://ui/fishing_shop.tscn"
|
||||
)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var shop := FishingShopScene.instantiate() as FishingShop
|
||||
root.add_child(shop)
|
||||
shop.show()
|
||||
await process_frame
|
||||
|
||||
var tabs: Array = shop.get("_shop_tabs") as Array
|
||||
assert(tabs.size() == FishingShop.ShopSection.size())
|
||||
var supplies_list := shop.get_node("%SuppliesList") as VBoxContainer
|
||||
var dummy_stock := Button.new()
|
||||
dummy_stock.name = "ControllerTestStock"
|
||||
dummy_stock.text = "test stock"
|
||||
supplies_list.add_child(dummy_stock)
|
||||
var activations: Array[int] = [0]
|
||||
dummy_stock.pressed.connect(
|
||||
func() -> void:
|
||||
activations[0] += 1
|
||||
)
|
||||
|
||||
var accept := InputEventJoypadButton.new()
|
||||
accept.button_index = JOY_BUTTON_A
|
||||
accept.pressed = true
|
||||
var cancel := InputEventJoypadButton.new()
|
||||
cancel.button_index = JOY_BUTTON_B
|
||||
cancel.pressed = true
|
||||
|
||||
for section_index: int in range(FishingShop.ShopSection.ART_SUPPLIES + 1):
|
||||
shop.set("_shop_section", section_index)
|
||||
shop.call("_select_shop_section", section_index, false)
|
||||
shop.call("_enter_shop_tabs_zone")
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
var tab := tabs[section_index] as Button
|
||||
assert(
|
||||
root.gui_get_focus_owner() == tab,
|
||||
"section %d tab focus mismatch: focused=%s tab=%s visible=%s mode=%d"
|
||||
% [
|
||||
section_index,
|
||||
root.gui_get_focus_owner(),
|
||||
tab,
|
||||
tab.is_visible_in_tree(),
|
||||
tab.focus_mode,
|
||||
],
|
||||
)
|
||||
assert(
|
||||
shop.get("_controller_zone")
|
||||
== FishingShop.ControllerZone.TABS
|
||||
)
|
||||
shop.call("_input", accept)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
assert(int(shop.get("_shop_section")) == section_index)
|
||||
assert(
|
||||
shop.get("_controller_zone")
|
||||
== FishingShop.ControllerZone.CONTENT
|
||||
)
|
||||
assert(not _array_contains_control(
|
||||
tabs,
|
||||
root.gui_get_focus_owner(),
|
||||
))
|
||||
if section_index == FishingShop.ShopSection.BAIT:
|
||||
assert(root.gui_get_focus_owner() == dummy_stock)
|
||||
shop.call("_input", accept)
|
||||
assert(activations[0] == 1)
|
||||
shop.call("_input", cancel)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
assert(shop.visible)
|
||||
assert(
|
||||
shop.get("_controller_zone")
|
||||
== FishingShop.ControllerZone.TABS
|
||||
)
|
||||
assert(root.gui_get_focus_owner() == tab)
|
||||
|
||||
shop.hide()
|
||||
shop.queue_free()
|
||||
await process_frame
|
||||
print("Fishing shop controller validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _array_contains_control(values: Array, target: Control) -> bool:
|
||||
for value: Variant in values:
|
||||
var control := value as Control
|
||||
if control == target:
|
||||
return true
|
||||
return false
|
||||
1
tests/fishing_shop_controller_validation.gd.uid
Normal file
1
tests/fishing_shop_controller_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://duwikmb08papi
|
||||
|
|
@ -82,6 +82,26 @@ func _validate_page() -> void:
|
|||
page.get("_category") == LogbookCatalog.Category.FRESH_WATER
|
||||
)
|
||||
assert((page.get("_catalog_grid") as GridContainer).columns == 4)
|
||||
var catalog_scroll := page.get("_catalog_scroll") as ScrollContainer
|
||||
assert(catalog_scroll != null and catalog_scroll.follow_focus)
|
||||
page.call(
|
||||
"_set_controller_zone",
|
||||
LogbookPage.ControllerZone.ENTRIES,
|
||||
)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
var initial_entries: Array = (
|
||||
page.get("_entry_buttons") as Dictionary
|
||||
).values()
|
||||
assert(initial_entries.size() > 4)
|
||||
var final_entry := initial_entries.back() as Button
|
||||
final_entry.grab_focus()
|
||||
for _frame: int in 3:
|
||||
await process_frame
|
||||
assert(catalog_scroll.scroll_vertical > 0)
|
||||
page.reset_controller_zone()
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
var category_tabs: Array = page.get("_category_tabs") as Array
|
||||
var category_tab_categories: Array = (
|
||||
page.get("_category_tab_categories") as Array
|
||||
|
|
|
|||
|
|
@ -1,11 +1,26 @@
|
|||
extends SceneTree
|
||||
|
||||
const KeyboardType = preload("res://ui/on_screen_keyboard.gd")
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const SettingsManagerType = preload(
|
||||
"res://settings/player_settings_manager.gd"
|
||||
)
|
||||
|
||||
|
||||
class ControllerInputProbe:
|
||||
extends Node
|
||||
|
||||
var pressed_buttons: Array[int] = []
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
var button := event as InputEventJoypadButton
|
||||
if button != null and button.pressed:
|
||||
pressed_buttons.append(button.button_index)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
|
@ -13,6 +28,7 @@ func _initialize() -> void:
|
|||
func _run() -> void:
|
||||
_validate_default_and_persistence()
|
||||
await _validate_keyboard_entry()
|
||||
await _validate_automapped_face_buttons()
|
||||
print("On-screen keyboard validation: PASS")
|
||||
quit()
|
||||
|
||||
|
|
@ -50,6 +66,8 @@ func _validate_keyboard_entry() -> void:
|
|||
var host := Control.new()
|
||||
root.add_child(host)
|
||||
var edit := LineEdit.new()
|
||||
edit.text = "seed"
|
||||
edit.caret_column = 0
|
||||
edit.focus_mode = Control.FOCUS_ALL
|
||||
host.add_child(edit)
|
||||
var keyboard := KeyboardType.new()
|
||||
|
|
@ -70,25 +88,75 @@ func _validate_keyboard_entry() -> void:
|
|||
edit.grab_focus()
|
||||
keyboard.call("_input", activate_event)
|
||||
assert(keyboard.is_open())
|
||||
assert(edit.caret_column == edit.text.length())
|
||||
assert(keyboard.get("_buffer_caret") == edit.text.length())
|
||||
assert(
|
||||
str((keyboard.get("_preview") as Label).text).begins_with("seed")
|
||||
)
|
||||
var left_page_event := InputEventJoypadButton.new()
|
||||
left_page_event.button_index = JOY_BUTTON_LEFT_SHOULDER
|
||||
left_page_event.pressed = true
|
||||
keyboard.call("_input", left_page_event)
|
||||
assert(keyboard.is_open())
|
||||
assert(keyboard.get("_page") == KeyboardType.Page.SYMBOLS)
|
||||
var right_page_event := InputEventJoypadButton.new()
|
||||
right_page_event.button_index = JOY_BUTTON_RIGHT_SHOULDER
|
||||
right_page_event.pressed = true
|
||||
keyboard.call("_input", right_page_event)
|
||||
assert(keyboard.is_open())
|
||||
assert(keyboard.get("_page") == KeyboardType.Page.LOWER)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
var focused_key := root.gui_get_focus_owner() as Button
|
||||
assert(focused_key != null and focused_key.text == "q")
|
||||
var conflicting_cancel_binding := InputEventJoypadButton.new()
|
||||
conflicting_cancel_binding.button_index = JOY_BUTTON_A
|
||||
InputMap.action_add_event(&"ui_cancel", conflicting_cancel_binding)
|
||||
keyboard.call("_input", activate_event)
|
||||
InputMap.action_erase_event(&"ui_cancel", conflicting_cancel_binding)
|
||||
assert(keyboard.is_open())
|
||||
assert(edit.text == "seedq")
|
||||
var left_trigger := InputEventJoypadMotion.new()
|
||||
left_trigger.axis = JOY_AXIS_TRIGGER_LEFT
|
||||
left_trigger.axis_value = 1.0
|
||||
keyboard.call("_input", left_trigger)
|
||||
assert(keyboard.get("_buffer_caret") == edit.text.length() - 1)
|
||||
var right_trigger := InputEventJoypadMotion.new()
|
||||
right_trigger.axis = JOY_AXIS_TRIGGER_RIGHT
|
||||
right_trigger.axis_value = 1.0
|
||||
keyboard.call("_input", right_trigger)
|
||||
assert(keyboard.get("_buffer_caret") == edit.text.length())
|
||||
keyboard.call("_process", KeyboardType.CARET_BLINK_INTERVAL)
|
||||
assert(
|
||||
not str((keyboard.get("_preview") as Label).text).contains(
|
||||
KeyboardType.CARET_GLYPH
|
||||
)
|
||||
)
|
||||
keyboard.call("_type_character", "a")
|
||||
keyboard.call("_type_space")
|
||||
keyboard.call("_set_page", KeyboardType.Page.UPPER)
|
||||
keyboard.call("_type_character", "B")
|
||||
assert(edit.text == "a B")
|
||||
assert(edit.text == "seedqa B")
|
||||
keyboard.call("_move_caret", -1)
|
||||
keyboard.call("_type_character", "C")
|
||||
assert(edit.text == "a CB")
|
||||
assert(edit.text == "seedqa CB")
|
||||
var input_probe := ControllerInputProbe.new()
|
||||
root.add_child(input_probe)
|
||||
root.move_child(input_probe, 0)
|
||||
var backspace_event := InputEventJoypadButton.new()
|
||||
backspace_event.button_index = JOY_BUTTON_X
|
||||
backspace_event.pressed = true
|
||||
keyboard.call("_input", backspace_event)
|
||||
assert(edit.text == "a B")
|
||||
var defocus_event := InputEventJoypadButton.new()
|
||||
defocus_event.button_index = JOY_BUTTON_LEFT_SHOULDER
|
||||
defocus_event.pressed = true
|
||||
keyboard.call("_input", defocus_event)
|
||||
Input.parse_input_event(backspace_event)
|
||||
await process_frame
|
||||
assert(edit.text == "seedqa B")
|
||||
assert(input_probe.pressed_buttons.is_empty())
|
||||
var close_event := InputEventJoypadButton.new()
|
||||
close_event.button_index = JOY_BUTTON_B
|
||||
close_event.pressed = true
|
||||
keyboard.call("_input", close_event)
|
||||
assert(not keyboard.is_open())
|
||||
assert(root.gui_get_focus_owner() == null)
|
||||
assert(root.gui_get_focus_owner() == edit)
|
||||
input_probe.queue_free()
|
||||
edit.grab_focus()
|
||||
keyboard.call("_input", activate_event)
|
||||
assert(keyboard.is_open())
|
||||
|
|
@ -98,5 +166,57 @@ func _validate_keyboard_entry() -> void:
|
|||
)
|
||||
keyboard.call("_submit")
|
||||
assert(not keyboard.is_open())
|
||||
assert(submitted == ["a B"])
|
||||
assert(submitted == ["seedqa B"])
|
||||
host.queue_free()
|
||||
|
||||
|
||||
func _validate_automapped_face_buttons() -> void:
|
||||
var manager := ControllerMappingManagerType.new()
|
||||
root.add_child(manager)
|
||||
await process_frame
|
||||
var swapped_bindings: Dictionary = (
|
||||
ControllerMappingManagerType.default_bindings()
|
||||
)
|
||||
swapped_bindings[str(ControllerMappingManagerType.ROLE_A)] = {
|
||||
"kind": "button",
|
||||
"button": int(JOY_BUTTON_B),
|
||||
}
|
||||
swapped_bindings[str(ControllerMappingManagerType.ROLE_B)] = {
|
||||
"kind": "button",
|
||||
"button": int(JOY_BUTTON_A),
|
||||
}
|
||||
manager._active_profile_key = "keyboard-mapping-test"
|
||||
manager._profiles[manager._active_profile_key] = {
|
||||
"controller_name": "keyboard mapping test",
|
||||
"bindings": swapped_bindings,
|
||||
}
|
||||
var host := Control.new()
|
||||
root.add_child(host)
|
||||
var edit := LineEdit.new()
|
||||
edit.focus_mode = Control.FOCUS_ALL
|
||||
host.add_child(edit)
|
||||
var keyboard := KeyboardType.new()
|
||||
host.add_child(keyboard)
|
||||
keyboard.setup_controller_mapping(manager)
|
||||
keyboard.set_enabled(true)
|
||||
await process_frame
|
||||
var mapped_accept := InputEventJoypadButton.new()
|
||||
mapped_accept.device = manager.get_active_device_id()
|
||||
mapped_accept.button_index = JOY_BUTTON_B
|
||||
mapped_accept.pressed = true
|
||||
edit.grab_focus()
|
||||
keyboard.call("_input", mapped_accept)
|
||||
assert(keyboard.is_open())
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
keyboard.call("_input", mapped_accept)
|
||||
assert(keyboard.is_open())
|
||||
assert(edit.text == "q")
|
||||
var mapped_cancel := InputEventJoypadButton.new()
|
||||
mapped_cancel.device = manager.get_active_device_id()
|
||||
mapped_cancel.button_index = JOY_BUTTON_A
|
||||
mapped_cancel.pressed = true
|
||||
keyboard.call("_input", mapped_cancel)
|
||||
assert(not keyboard.is_open())
|
||||
host.queue_free()
|
||||
manager.queue_free()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue