feat: overhaul controller menu accessibility
This commit is contained in:
parent
a7837c3711
commit
4f9c85989b
46 changed files with 4091 additions and 396 deletions
|
|
@ -69,6 +69,34 @@ func _run() -> void:
|
|||
"inversion clears when a focused control is defocused",
|
||||
)
|
||||
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.position = Vector2(0.0, 80.0)
|
||||
scroll.size = Vector2(180.0, 90.0)
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
stage.add_child(scroll)
|
||||
var list := VBoxContainer.new()
|
||||
list.custom_minimum_size.x = 160.0
|
||||
scroll.add_child(list)
|
||||
var last_scroll_button: Button
|
||||
for index: int in 8:
|
||||
var scroll_button := Button.new()
|
||||
scroll_button.text = "scroll option %d" % index
|
||||
scroll_button.custom_minimum_size = Vector2(160.0, 40.0)
|
||||
list.add_child(scroll_button)
|
||||
last_scroll_button = scroll_button
|
||||
await process_frame
|
||||
last_scroll_button.grab_focus()
|
||||
await process_frame
|
||||
await process_frame
|
||||
_expect(
|
||||
scroll.scroll_vertical > 0,
|
||||
"controller focus scrolls an off-screen selection into view",
|
||||
)
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == last_scroll_button,
|
||||
"scroll following preserves the selected controller control",
|
||||
)
|
||||
|
||||
stage.queue_free()
|
||||
if _failures.is_empty():
|
||||
print("Controller focus presentation validation: PASS")
|
||||
|
|
|
|||
|
|
@ -94,6 +94,16 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String:
|
|||
ControllerMappingManagerType.ROLE_ORDER.size(),
|
||||
str(defaults.keys()),
|
||||
]
|
||||
if ControllerMappingManagerType.ROLE_LABELS.get(
|
||||
ControllerMappingManagerType.ROLE_RIGHT_STICK_CLICK, ""
|
||||
) != "sneak":
|
||||
return "right-stick click is not presented as sneak"
|
||||
if ControllerMappingManagerType.BUTTON_ACTION_ROLES.get(
|
||||
&"sneak", &""
|
||||
) != ControllerMappingManagerType.ROLE_RIGHT_STICK_CLICK:
|
||||
return "right-stick click is not assigned to the sneak action"
|
||||
if not _has_joy_button(&"sneak", JOY_BUTTON_RIGHT_STICK):
|
||||
return "sneak does not default to right-stick click"
|
||||
var trigger_button := InputEventJoypadButton.new()
|
||||
trigger_button.device = manager.get_active_device_id()
|
||||
trigger_button.button_index = JOY_BUTTON_MISC1
|
||||
|
|
@ -226,10 +236,41 @@ func _validate_portmaster_launcher() -> String:
|
|||
|
||||
|
||||
func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
|
||||
if manager.get_role_prompt(ControllerMappingManagerType.ROLE_LT) != (
|
||||
"press or squeeze LT"
|
||||
):
|
||||
return "auto-map does not identify the LT control"
|
||||
if manager.get_role_prompt(ControllerMappingManagerType.ROLE_RT) != (
|
||||
"press or squeeze RT"
|
||||
):
|
||||
return "auto-map does not identify the RT control"
|
||||
var panel := ControllerMappingPanelType.new()
|
||||
root.add_child(panel)
|
||||
panel.setup(manager)
|
||||
panel.open_panel()
|
||||
var mapping_scrolls: Array[Node] = panel.find_children(
|
||||
"", "ScrollContainer", true, false
|
||||
)
|
||||
if mapping_scrolls.is_empty():
|
||||
return "controller mapper does not contain its mapping list"
|
||||
if not (mapping_scrolls[0] as ScrollContainer).follow_focus:
|
||||
return "controller mapper does not scroll to follow controller focus"
|
||||
var first_binding := panel._binding_buttons.get(
|
||||
ControllerMappingManagerType.ROLE_ORDER.front()
|
||||
) as Button
|
||||
var last_binding := panel._binding_buttons.get(
|
||||
ControllerMappingManagerType.ROLE_ORDER.back()
|
||||
) as Button
|
||||
if first_binding.get_node(first_binding.focus_neighbor_top) != first_binding:
|
||||
return "controller mapper loses focus above its first row"
|
||||
if last_binding.get_node(last_binding.focus_neighbor_bottom) != (
|
||||
panel._close_button
|
||||
):
|
||||
return "controller mapper cannot reach Done from its last row"
|
||||
if panel._close_button.get_node(
|
||||
panel._close_button.focus_neighbor_top
|
||||
) != last_binding:
|
||||
return "controller mapper cannot return from Done to its last row"
|
||||
panel._begin_auto_map()
|
||||
for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
|
||||
panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
|
||||
|
|
@ -391,3 +432,11 @@ func _keyboard_event_count(action: StringName) -> int:
|
|||
if event is InputEventKey or event is InputEventMouseButton:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _has_joy_button(action: StringName, button_index: JoyButton) -> bool:
|
||||
for event: InputEvent in InputMap.action_get_events(action):
|
||||
var button := event as InputEventJoypadButton
|
||||
if button != null and button.button_index == button_index:
|
||||
return true
|
||||
return false
|
||||
|
|
|
|||
457
tests/controller_menu_accessibility_validation.gd
Normal file
457
tests/controller_menu_accessibility_validation.gd
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
extends SceneTree
|
||||
|
||||
const JoinGamePageScene = preload(
|
||||
"res://ui/network/join_game_page.tscn"
|
||||
)
|
||||
const SettingsPanelScene = preload("res://ui/settings_panel.tscn")
|
||||
const BubbleConfirmationScene = preload(
|
||||
"res://ui/components/bubble_menu/bubble_confirmation_page.tscn"
|
||||
)
|
||||
const TitleConfirmationScene = preload(
|
||||
"res://ui/title_confirmation_bubble_page.tscn"
|
||||
)
|
||||
const MailPageType = preload("res://ui/mail_page.gd")
|
||||
const ProfilePageType = preload("res://ui/profile_page.gd")
|
||||
const DialogControllerNavigationType = preload(
|
||||
"res://ui/file_dialog_controller_navigation.gd"
|
||||
)
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
await _validate_join_game_navigation()
|
||||
await _validate_data_settings_navigation()
|
||||
await _validate_mail_navigation()
|
||||
await _validate_profile_confirmation_focus()
|
||||
await _validate_confirmation_dialog_navigation()
|
||||
await _validate_bubble_confirmation_navigation()
|
||||
_validate_mapping_capture_contract()
|
||||
if _failures.is_empty():
|
||||
print("Controller menu accessibility validation: PASS")
|
||||
quit(0)
|
||||
return
|
||||
for failure: String in _failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _validate_join_game_navigation() -> void:
|
||||
var page := JoinGamePageScene.instantiate() as Control
|
||||
root.add_child(page)
|
||||
await process_frame
|
||||
page.show()
|
||||
var discover := page.get_node("%DiscoverButton") as Button
|
||||
var direct := page.get_node("%DirectButton") as Button
|
||||
var saved := page.get_node("%SavedButton") as Button
|
||||
var recent := page.get_node("%RecentButton") as Button
|
||||
var address := page.get_node("%Address") as LineEdit
|
||||
var name_edit := page.get_node("%NameEdit") as LineEdit
|
||||
var server_list := page.get_node("%ServerList") as ItemList
|
||||
var refresh := page.get_node("%RefreshButton") as Button
|
||||
var join := page.get_node("%JoinButton") as Button
|
||||
var save := page.get_node("%SaveButton") as Button
|
||||
var edit := page.get_node("%EditButton") as Button
|
||||
var favorite := page.get_node("%FavoriteButton") as Button
|
||||
var delete := page.get_node("%DeleteButton") as Button
|
||||
var cancel := page.get_node("%CancelButton") as Button
|
||||
var back := page.get_node("%BackButton") as Button
|
||||
var modes: Array[Control] = [discover, direct, saved, recent]
|
||||
|
||||
address.hide()
|
||||
name_edit.hide()
|
||||
server_list.show()
|
||||
_set_button_state(refresh, true)
|
||||
_set_button_state(join, true)
|
||||
_set_button_state(save, false)
|
||||
_set_button_state(edit, false)
|
||||
_set_button_state(favorite, false)
|
||||
_set_button_state(delete, false)
|
||||
_set_button_state(cancel, false)
|
||||
_set_button_state(back, true)
|
||||
page.set("_mode", 0)
|
||||
page.call("_configure_controller_navigation")
|
||||
await process_frame
|
||||
_assert_neighbor(discover, &"focus_neighbor_bottom", server_list)
|
||||
_assert_neighbor(server_list, &"focus_neighbor_top", discover)
|
||||
_assert_neighbor(server_list, &"focus_neighbor_bottom", refresh)
|
||||
_assert_neighbor(refresh, &"focus_neighbor_right", join)
|
||||
_assert_neighbor(back, &"focus_neighbor_left", join)
|
||||
var discover_controls: Array[Control] = modes.duplicate()
|
||||
discover_controls.append_array([server_list, refresh, join, back])
|
||||
_assert_directionally_reachable(discover, discover_controls)
|
||||
|
||||
address.show()
|
||||
address.editable = true
|
||||
server_list.hide()
|
||||
_set_button_state(refresh, false)
|
||||
_set_button_state(join, true)
|
||||
_set_button_state(save, true)
|
||||
page.set("_mode", 1)
|
||||
page.call("_configure_controller_navigation")
|
||||
await process_frame
|
||||
_assert_neighbor(direct, &"focus_neighbor_bottom", address)
|
||||
_assert_neighbor(address, &"focus_neighbor_top", direct)
|
||||
_assert_neighbor(address, &"focus_neighbor_bottom", join)
|
||||
var direct_controls: Array[Control] = modes.duplicate()
|
||||
direct_controls.append_array([address, join, save, back])
|
||||
_assert_directionally_reachable(direct, direct_controls)
|
||||
|
||||
name_edit.show()
|
||||
name_edit.editable = true
|
||||
page.set("_name_entry_active", true)
|
||||
page.call("_configure_controller_navigation")
|
||||
await process_frame
|
||||
_assert_neighbor(address, &"focus_neighbor_bottom", name_edit)
|
||||
_assert_neighbor(name_edit, &"focus_neighbor_top", address)
|
||||
page.call("request_back")
|
||||
_expect(
|
||||
not bool(page.get("_name_entry_active")),
|
||||
"Join-game Back should leave the server-name edit substate.",
|
||||
)
|
||||
_expect(
|
||||
page.visible,
|
||||
"Join-game Back should not close the browser from an edit substate.",
|
||||
)
|
||||
await process_frame
|
||||
|
||||
page.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_data_settings_navigation() -> void:
|
||||
var panel := SettingsPanelScene.instantiate() as Control
|
||||
root.add_child(panel)
|
||||
await process_frame
|
||||
panel.show()
|
||||
var data_page := panel.get_node("%DataPage") as SettingsBubblePage
|
||||
data_page.show_page(false)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
var controls: Array[Control] = [
|
||||
panel.get_node("%OpenDataFolder") as Control,
|
||||
panel.get_node("%ChangeDataFolder") as Control,
|
||||
panel.get_node("%CopyPlayerFingerprint") as Control,
|
||||
panel.get_node("%ExportPlayerIdentity") as Control,
|
||||
panel.get_node("%ImportPlayerIdentity") as Control,
|
||||
panel.get_node("%ExportHostIdentity") as Control,
|
||||
panel.get_node("%ImportHostIdentity") as Control,
|
||||
panel.get_node("%DataBackButton") as Control,
|
||||
]
|
||||
for control: Control in controls:
|
||||
_expect(
|
||||
control.focus_mode == Control.FOCUS_ALL,
|
||||
"Data & Identity control %s is not controller-focusable."
|
||||
% control.name,
|
||||
)
|
||||
_assert_directionally_reachable(controls.front(), controls)
|
||||
panel.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_mail_navigation() -> void:
|
||||
var page := MailPageType.new() as Control
|
||||
root.add_child(page)
|
||||
await process_frame
|
||||
page.call("activate")
|
||||
page.call("set_interactive", true)
|
||||
var inbox_list := page.get("_inbox_list") as VBoxContainer
|
||||
var entry := Button.new()
|
||||
entry.text = "test letter"
|
||||
entry.custom_minimum_size = Vector2(1008.0, 54.0)
|
||||
inbox_list.add_child(entry)
|
||||
await process_frame
|
||||
page.call("_refresh_controller_navigation")
|
||||
var archive_view := page.get("_archive_view_button") as Button
|
||||
var send_mail := page.get("_send_mail_button") as Button
|
||||
_assert_neighbor(
|
||||
archive_view, &"focus_neighbor_right", send_mail
|
||||
)
|
||||
_assert_neighbor(archive_view, &"focus_neighbor_bottom", entry)
|
||||
_assert_neighbor(send_mail, &"focus_neighbor_bottom", entry)
|
||||
_assert_neighbor(entry, &"focus_neighbor_top", archive_view)
|
||||
_assert_directionally_reachable(
|
||||
archive_view, [archive_view, send_mail, entry]
|
||||
)
|
||||
|
||||
var inbox := page.get("_inbox") as Control
|
||||
var compose := page.get("_compose") as Control
|
||||
var letter := page.get("_letter") as Control
|
||||
inbox.hide()
|
||||
compose.hide()
|
||||
letter.show()
|
||||
var accept := page.get("_accept") as Button
|
||||
var decline := page.get("_decline") as Button
|
||||
var close := page.get("_letter_close") as Button
|
||||
var archive := page.get("_archive") as Button
|
||||
var delete := page.get("_delete") as Button
|
||||
accept.show()
|
||||
decline.show()
|
||||
delete.disabled = false
|
||||
page.call("_refresh_controller_navigation")
|
||||
_assert_neighbor(accept, &"focus_neighbor_bottom", decline)
|
||||
_assert_neighbor(decline, &"focus_neighbor_bottom", delete)
|
||||
_assert_neighbor(delete, &"focus_neighbor_left", archive)
|
||||
_assert_neighbor(archive, &"focus_neighbor_left", close)
|
||||
_assert_neighbor(close, &"focus_neighbor_top", close)
|
||||
_assert_directionally_reachable(
|
||||
accept, [accept, decline, close, archive, delete]
|
||||
)
|
||||
page.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_profile_confirmation_focus() -> void:
|
||||
var page := ProfilePageType.new() as Control
|
||||
root.add_child(page)
|
||||
await process_frame
|
||||
page.call("activate")
|
||||
page.call("set_interactive", true)
|
||||
page.call("reset_controller_zone")
|
||||
var suggestions := page.get("_suggestions") as HBoxContainer
|
||||
var suggestion := Button.new()
|
||||
suggestion.text = "alternate name"
|
||||
suggestions.add_child(suggestion)
|
||||
page.call("_apply_controller_zone_focus")
|
||||
var account_controls: Array[Control] = []
|
||||
for item: Variant in page.call("_account_controller_controls"):
|
||||
account_controls.append(item as Control)
|
||||
var preview := page.get("_preview") as Control
|
||||
var reset_view := page.get("_reset_view_button") as Button
|
||||
_expect(
|
||||
suggestion in account_controls,
|
||||
"Profile name-conflict choices are outside the account zone.",
|
||||
)
|
||||
_expect(
|
||||
suggestion.focus_mode == Control.FOCUS_ALL,
|
||||
"Profile name-conflict choices are not controller-focusable.",
|
||||
)
|
||||
_expect(
|
||||
preview.focus_mode == Control.FOCUS_NONE,
|
||||
"The profile preview must not take controller focus.",
|
||||
)
|
||||
_expect(
|
||||
reset_view.focus_mode == Control.FOCUS_NONE,
|
||||
"The profile reset-view button must not take controller focus.",
|
||||
)
|
||||
page.call("_show_confirmation", "defaults")
|
||||
var confirmation := page.get("_discard_confirmation") as Control
|
||||
var confirm := page.get("_confirmation_confirm") as Button
|
||||
var keep_editing := page.get("_keep_editing_button") as Button
|
||||
_expect(confirmation.visible, "The profile confirmation did not open.")
|
||||
_expect(
|
||||
confirm.focus_mode == Control.FOCUS_ALL,
|
||||
"The profile confirmation action is not controller-focusable.",
|
||||
)
|
||||
_expect(
|
||||
keep_editing.focus_mode == Control.FOCUS_ALL,
|
||||
"The profile confirmation cancel action is not controller-focusable.",
|
||||
)
|
||||
for control: Control in account_controls:
|
||||
_expect(
|
||||
control.focus_mode == Control.FOCUS_NONE,
|
||||
"Profile confirmation leaked focus to %s." % control.name,
|
||||
)
|
||||
var cancel_event := InputEventAction.new()
|
||||
cancel_event.action = &"ui_cancel"
|
||||
cancel_event.pressed = true
|
||||
_expect(
|
||||
bool(page.call("handle_controller_input", cancel_event)),
|
||||
"Profile confirmation did not consume controller Back.",
|
||||
)
|
||||
_expect(
|
||||
not confirmation.visible,
|
||||
"Controller Back did not close the profile confirmation.",
|
||||
)
|
||||
for control: Control in account_controls:
|
||||
_expect(
|
||||
control.focus_mode == Control.FOCUS_ALL,
|
||||
"Profile account focus was not restored after confirmation.",
|
||||
)
|
||||
page.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_mapping_capture_contract() -> void:
|
||||
var title_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/title_screen.gd"
|
||||
)
|
||||
var game_ui_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/game_ui.gd"
|
||||
)
|
||||
var chat_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/chat_ui.gd"
|
||||
)
|
||||
var main_source: String = FileAccess.get_file_as_string(
|
||||
"res://main/main.gd"
|
||||
)
|
||||
_expect(
|
||||
title_source.contains(
|
||||
"_settings_panel.is_input_mapping_capturing()"
|
||||
),
|
||||
"Title input does not respect every active binding capture.",
|
||||
)
|
||||
_expect(
|
||||
game_ui_source.contains("or is_input_mapping_capturing()"),
|
||||
"Controller menu scrolling does not respect every binding capture.",
|
||||
)
|
||||
_expect(
|
||||
chat_source.contains("func _configure_controller_focus()"),
|
||||
"Chat does not author controller routes for its exterior controls.",
|
||||
)
|
||||
_expect(
|
||||
chat_source.contains(
|
||||
"Control.FOCUS_ALL if _opened else Control.FOCUS_NONE"
|
||||
),
|
||||
"Passive chat controls can steal world controller focus.",
|
||||
)
|
||||
_expect(
|
||||
main_source.contains("func _configure_popup_dialog("),
|
||||
"Runtime popup dialogs do not receive authored controller routes.",
|
||||
)
|
||||
|
||||
|
||||
func _validate_confirmation_dialog_navigation() -> void:
|
||||
var dialog := ConfirmationDialog.new()
|
||||
dialog.ok_button_text = "continue"
|
||||
var fields := VBoxContainer.new()
|
||||
var first := LineEdit.new()
|
||||
first.placeholder_text = "passphrase"
|
||||
first.custom_minimum_size = Vector2(420.0, 42.0)
|
||||
fields.add_child(first)
|
||||
var second := LineEdit.new()
|
||||
second.placeholder_text = "confirm passphrase"
|
||||
second.custom_minimum_size = Vector2(420.0, 42.0)
|
||||
fields.add_child(second)
|
||||
dialog.add_child(fields)
|
||||
root.add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(560, 300))
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
DialogControllerNavigationType.configure_scope(dialog, first)
|
||||
await process_frame
|
||||
var controls: Array[Control] = (
|
||||
DialogControllerNavigationType.interactive_controls(dialog)
|
||||
)
|
||||
_expect(
|
||||
controls.has(first) and controls.has(second),
|
||||
"Confirmation-dialog text fields are not controller reachable.",
|
||||
)
|
||||
_expect(
|
||||
controls.has(dialog.get_ok_button())
|
||||
and controls.has(dialog.get_cancel_button()),
|
||||
"Confirmation-dialog actions are not controller reachable.",
|
||||
)
|
||||
_assert_directionally_reachable(first, controls)
|
||||
_expect(
|
||||
dialog.gui_get_focus_owner() == first,
|
||||
"Confirmation dialog did not focus its requested entry control.",
|
||||
)
|
||||
dialog.hide()
|
||||
await process_frame
|
||||
root.gui_release_focus()
|
||||
dialog.popup_centered(Vector2i(560, 300))
|
||||
await process_frame
|
||||
DialogControllerNavigationType.configure_scope(dialog, first)
|
||||
await process_frame
|
||||
_expect(
|
||||
dialog.gui_get_focus_owner() == first,
|
||||
"A reopened dialog did not restore controller focus.",
|
||||
)
|
||||
dialog.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_bubble_confirmation_navigation() -> void:
|
||||
for scene: PackedScene in [
|
||||
BubbleConfirmationScene,
|
||||
TitleConfirmationScene,
|
||||
]:
|
||||
var page := scene.instantiate() as Control
|
||||
root.add_child(page)
|
||||
await process_frame
|
||||
page.show()
|
||||
page.call("_set_interactive", true)
|
||||
var confirm := page.get_node("BubbleCluster/ConfirmButton") as Button
|
||||
var cancel := page.get_node("BubbleCluster/CancelButton") as Button
|
||||
_assert_neighbor(confirm, &"focus_neighbor_left", cancel)
|
||||
_assert_neighbor(confirm, &"focus_neighbor_right", cancel)
|
||||
_assert_neighbor(confirm, &"focus_neighbor_top", confirm)
|
||||
_assert_neighbor(confirm, &"focus_neighbor_bottom", confirm)
|
||||
_assert_neighbor(cancel, &"focus_neighbor_left", confirm)
|
||||
_assert_neighbor(cancel, &"focus_neighbor_right", confirm)
|
||||
_assert_neighbor(cancel, &"focus_neighbor_top", cancel)
|
||||
_assert_neighbor(cancel, &"focus_neighbor_bottom", cancel)
|
||||
page.queue_free()
|
||||
await process_frame
|
||||
func _set_button_state(button: Button, shown: bool) -> void:
|
||||
button.visible = shown
|
||||
button.disabled = false
|
||||
|
||||
|
||||
func _assert_neighbor(
|
||||
origin: Control,
|
||||
property: StringName,
|
||||
expected: Control,
|
||||
) -> void:
|
||||
var path: NodePath = origin.get(property)
|
||||
_expect(
|
||||
not path.is_empty(),
|
||||
"%s has no %s neighbor." % [origin.name, property],
|
||||
)
|
||||
if path.is_empty():
|
||||
return
|
||||
var actual := origin.get_node_or_null(path) as Control
|
||||
_expect(
|
||||
actual == expected,
|
||||
"%s points %s to %s instead of %s."
|
||||
% [
|
||||
origin.name,
|
||||
property,
|
||||
actual.name if actual != null else "nothing",
|
||||
expected.name,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
func _assert_directionally_reachable(
|
||||
start: Control,
|
||||
controls: Array[Control],
|
||||
) -> void:
|
||||
var expected: Dictionary[int, bool] = {}
|
||||
for control: Control in controls:
|
||||
expected[control.get_instance_id()] = true
|
||||
var visited: Dictionary[int, bool] = {start.get_instance_id(): true}
|
||||
var pending: Array[Control] = [start]
|
||||
while not pending.is_empty():
|
||||
var current: Control = pending.pop_front()
|
||||
for path: NodePath in [
|
||||
current.focus_neighbor_left,
|
||||
current.focus_neighbor_right,
|
||||
current.focus_neighbor_top,
|
||||
current.focus_neighbor_bottom,
|
||||
]:
|
||||
var neighbor := current.get_node_or_null(path) as Control
|
||||
if (
|
||||
neighbor == null
|
||||
or not expected.has(neighbor.get_instance_id())
|
||||
or visited.has(neighbor.get_instance_id())
|
||||
):
|
||||
continue
|
||||
visited[neighbor.get_instance_id()] = true
|
||||
pending.append(neighbor)
|
||||
_expect(
|
||||
visited.size() == expected.size(),
|
||||
"Only %d of %d controls are directionally reachable from %s."
|
||||
% [visited.size(), expected.size(), start.name],
|
||||
)
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
_failures.append(message)
|
||||
1
tests/controller_menu_accessibility_validation.gd.uid
Normal file
1
tests/controller_menu_accessibility_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cdvy3ln6d3b1v
|
||||
|
|
@ -14,7 +14,12 @@ func _initialize() -> void:
|
|||
|
||||
func _run() -> void:
|
||||
_validate_spatial_navigation()
|
||||
_validate_strict_directional_navigation()
|
||||
_validate_disabled_control_exclusion()
|
||||
_validate_traversal_cycle()
|
||||
_validate_controller_hierarchy_contract()
|
||||
_validate_player_page_zone_contracts()
|
||||
_validate_shop_navigation_contract()
|
||||
_validate_four_by_three_centering()
|
||||
_validate_low_end_profile_contract()
|
||||
print("Controller UI navigation validation: PASS")
|
||||
|
|
@ -38,16 +43,118 @@ func _validate_spatial_navigation() -> void:
|
|||
host.queue_free()
|
||||
|
||||
|
||||
func _validate_strict_directional_navigation() -> void:
|
||||
var host := Control.new()
|
||||
root.add_child(host)
|
||||
var origin := _make_button(host, "origin", Vector2(200.0, 200.0))
|
||||
var mostly_below := _make_button(
|
||||
host, "mostly_below", Vector2(250.0, 400.0)
|
||||
)
|
||||
var controls: Array[Control] = [origin, mostly_below]
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
|
||||
assert(origin.get_node(origin.focus_neighbor_right) == origin)
|
||||
assert(origin.get_node(origin.focus_neighbor_top) == origin)
|
||||
assert(origin.get_node(origin.focus_neighbor_bottom) == mostly_below)
|
||||
host.queue_free()
|
||||
|
||||
|
||||
func _validate_disabled_control_exclusion() -> void:
|
||||
var host := Control.new()
|
||||
root.add_child(host)
|
||||
var left := _make_button(host, "left", Vector2(50.0, 100.0))
|
||||
var disabled := _make_button(host, "disabled", Vector2(200.0, 100.0))
|
||||
var right := _make_button(host, "right", Vector2(350.0, 100.0))
|
||||
disabled.disabled = true
|
||||
disabled.focus_next = disabled.get_path_to(right)
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(
|
||||
[left, disabled, right]
|
||||
)
|
||||
assert(left.get_node(left.focus_neighbor_right) == right)
|
||||
assert(right.get_node(right.focus_neighbor_left) == left)
|
||||
assert(disabled.focus_next.is_empty())
|
||||
host.queue_free()
|
||||
|
||||
|
||||
func _validate_traversal_cycle() -> void:
|
||||
var host := Control.new()
|
||||
root.add_child(host)
|
||||
var first := _make_button(host, "first", Vector2(50.0, 50.0))
|
||||
var second := _make_button(host, "second", Vector2(200.0, 50.0))
|
||||
var third := _make_button(host, "third", Vector2(50.0, 150.0))
|
||||
var controls: Array[Control] = [third, first, second]
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
|
||||
assert(first.get_node(first.focus_next) == second)
|
||||
assert(second.get_node(second.focus_next) == third)
|
||||
assert(third.get_node(third.focus_next) == first)
|
||||
assert(first.get_node(first.focus_previous) == third)
|
||||
host.queue_free()
|
||||
|
||||
|
||||
func _validate_controller_hierarchy_contract() -> void:
|
||||
var source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/player_menu.gd"
|
||||
)
|
||||
assert(source.contains("ROLE_POINTER_MODIFIER"))
|
||||
assert(source.contains("ROLE_CAMERA_ZOOM"))
|
||||
assert(source.contains("_handle_controller_secondary_switch"))
|
||||
assert(source.contains("ROLE_LB"))
|
||||
assert(source.contains("ROLE_RB"))
|
||||
assert(not source.contains("_handle_controller_secondary_switch"))
|
||||
assert(source.contains("ControllerOwnership.INVENTORY_TABS"))
|
||||
assert(source.contains("ControllerOwnership.SORT_FILTER"))
|
||||
assert(source.contains("_reset_controller_zone_for_section"))
|
||||
assert(source.contains("preserve_tab_zone"))
|
||||
assert(source.contains("active_tab.call_deferred(\"grab_focus\")"))
|
||||
assert(source.contains("CONTROLLER_PICKUP_HOLD_SECONDS"))
|
||||
assert(source.contains("_reserve_main_navigation_for_page_switching"))
|
||||
assert(source.contains("configure_spatial_neighbors(candidates)"))
|
||||
|
||||
|
||||
func _validate_player_page_zone_contracts() -> void:
|
||||
for path: String in [
|
||||
"res://ui/logbook_page.gd",
|
||||
"res://ui/the_net_page.gd",
|
||||
"res://ui/mail_page.gd",
|
||||
"res://ui/profile_page.gd",
|
||||
"res://ui/players_page.gd",
|
||||
]:
|
||||
var source: String = FileAccess.get_file_as_string(path)
|
||||
assert(source.contains("func reset_controller_zone()"))
|
||||
assert(source.contains("func handle_controller_input(event: InputEvent)"))
|
||||
var profile_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/profile_page.gd"
|
||||
)
|
||||
assert(profile_source.contains("ControllerZone.COLOR_PICKER"))
|
||||
assert(profile_source.contains("_adjust_controller_color_gamut"))
|
||||
assert(profile_source.contains("_customize_button.text = \"customize\""))
|
||||
assert(profile_source.contains("_enter_controller_customization"))
|
||||
assert(not profile_source.contains(
|
||||
"if event.is_action_pressed(\"ui_down\"):\n"
|
||||
+ "\t\t\t_controller_zone = ControllerZone.CATEGORIES"
|
||||
))
|
||||
assert(profile_source.contains(
|
||||
"_reset_view_button.focus_mode = Control.FOCUS_NONE"
|
||||
))
|
||||
var preview_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/profile_preview.gd"
|
||||
)
|
||||
assert(preview_source.contains("focus_mode = Control.FOCUS_NONE"))
|
||||
assert(not preview_source.contains("grab_focus()"))
|
||||
var mail_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/mail_page.gd"
|
||||
)
|
||||
assert(mail_source.contains("_configure_compose_controller_navigation"))
|
||||
assert(mail_source.contains("_set_compose_neighbors"))
|
||||
var net_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/the_net_page.gd"
|
||||
)
|
||||
assert(net_source.contains("ROLE_RIGHT_STICK_Y"))
|
||||
|
||||
|
||||
func _validate_shop_navigation_contract() -> void:
|
||||
var source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/fishing_shop.gd"
|
||||
)
|
||||
assert(source.contains("ROLE_LB"))
|
||||
assert(source.contains("ROLE_RB"))
|
||||
assert(source.contains("_configure_controller_focus"))
|
||||
|
||||
|
||||
func _validate_four_by_three_centering() -> void:
|
||||
|
|
|
|||
183
tests/file_dialog_controller_navigation_validation.gd
Normal file
183
tests/file_dialog_controller_navigation_validation.gd
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
extends SceneTree
|
||||
|
||||
const FileDialogControllerNavigationType = preload(
|
||||
"res://ui/file_dialog_controller_navigation.gd"
|
||||
)
|
||||
const InterfaceFontControllerType = preload(
|
||||
"res://ui/interface_font_controller.gd"
|
||||
)
|
||||
const OnScreenKeyboardType = preload("res://ui/on_screen_keyboard.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var dialog := FileDialog.new()
|
||||
dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
|
||||
dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
dialog.use_native_dialog = false
|
||||
root.add_child(dialog)
|
||||
dialog.current_dir = "/tmp"
|
||||
dialog.popup_centered(Vector2i(900, 540))
|
||||
for _frame: int in 3:
|
||||
await process_frame
|
||||
FileDialogControllerNavigationType.configure(dialog)
|
||||
await process_frame
|
||||
|
||||
var root_scope: Window = (
|
||||
FileDialogControllerNavigationType.active_scope(dialog)
|
||||
)
|
||||
assert(root_scope == dialog)
|
||||
var root_controls: Array[Control] = (
|
||||
FileDialogControllerNavigationType.interactive_controls(root_scope)
|
||||
)
|
||||
var directory_list: ItemList
|
||||
var create_folder_button: Button
|
||||
var path_edit: LineEdit
|
||||
var has_menu_button: bool = false
|
||||
var has_cancel: bool = false
|
||||
var has_select: bool = false
|
||||
for control: Control in root_controls:
|
||||
if (
|
||||
control is ItemList
|
||||
and control.accessibility_name == "Directories & Files:"
|
||||
):
|
||||
directory_list = control as ItemList
|
||||
if control is LineEdit and control.accessibility_name == "Path:":
|
||||
path_edit = control as LineEdit
|
||||
var button := control as Button
|
||||
if button != null:
|
||||
create_folder_button = (
|
||||
button
|
||||
if button.tooltip_text == "Create a new folder."
|
||||
else create_folder_button
|
||||
)
|
||||
has_cancel = has_cancel or button.text == "Cancel"
|
||||
has_select = has_select or button.text == "Select Current Folder"
|
||||
has_menu_button = has_menu_button or control is MenuButton
|
||||
assert(directory_list != null)
|
||||
assert(path_edit != null)
|
||||
assert(create_folder_button != null)
|
||||
assert(has_menu_button)
|
||||
assert(has_cancel)
|
||||
assert(has_select)
|
||||
assert(dialog.gui_get_focus_owner() == directory_list)
|
||||
_assert_directionally_reachable(directory_list, root_controls)
|
||||
|
||||
create_folder_button.pressed.emit()
|
||||
for _frame: int in 3:
|
||||
await process_frame
|
||||
FileDialogControllerNavigationType.configure(dialog)
|
||||
await process_frame
|
||||
var folder_scope: Window = (
|
||||
FileDialogControllerNavigationType.active_scope(dialog)
|
||||
)
|
||||
assert(folder_scope != null and folder_scope != dialog)
|
||||
var folder_controls: Array[Control] = (
|
||||
FileDialogControllerNavigationType.interactive_controls(folder_scope)
|
||||
)
|
||||
var folder_name_edit: LineEdit
|
||||
var folder_cancel: Button
|
||||
var folder_ok: Button
|
||||
for control: Control in folder_controls:
|
||||
if control is LineEdit:
|
||||
folder_name_edit = control as LineEdit
|
||||
var button := control as Button
|
||||
if button != null and button.text == "Cancel":
|
||||
folder_cancel = button
|
||||
if button != null and button.text == "OK":
|
||||
folder_ok = button
|
||||
assert(folder_name_edit != null)
|
||||
assert(folder_cancel != null)
|
||||
assert(folder_ok != null)
|
||||
assert(folder_scope.gui_get_focus_owner() == folder_name_edit)
|
||||
_assert_directionally_reachable(folder_name_edit, folder_controls)
|
||||
|
||||
var keyboard := OnScreenKeyboardType.new()
|
||||
root.add_child(keyboard)
|
||||
keyboard.set_enabled(true)
|
||||
folder_name_edit.grab_focus()
|
||||
assert(keyboard.request_for_control(folder_name_edit))
|
||||
assert(keyboard.is_open())
|
||||
assert(keyboard.get_parent() == folder_scope)
|
||||
keyboard.call("_close_keyboard", true)
|
||||
|
||||
var main_source: String = FileAccess.get_file_as_string(
|
||||
"res://main/main.gd"
|
||||
)
|
||||
assert(main_source.contains("picker_scope != _data_folder_dialog"))
|
||||
assert(main_source.contains("picker_scope.hide()"))
|
||||
assert(main_source.contains("is_controller_text_entry_open()"))
|
||||
|
||||
dialog.queue_free()
|
||||
keyboard.queue_free()
|
||||
await process_frame
|
||||
await _validate_compact_dialog()
|
||||
print("File dialog controller navigation validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _validate_compact_dialog() -> void:
|
||||
root.size = Vector2i(640, 480)
|
||||
var font_controller := InterfaceFontControllerType.new()
|
||||
root.add_child(font_controller)
|
||||
await process_frame
|
||||
var dialog := FileDialog.new()
|
||||
dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
|
||||
dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
dialog.use_native_dialog = false
|
||||
root.add_child(dialog)
|
||||
dialog.current_dir = "/tmp"
|
||||
font_controller.popup_file_dialog(dialog)
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
assert(dialog.size.x <= 616 and dialog.size.y <= 456)
|
||||
var scope: Window = FileDialogControllerNavigationType.active_scope(dialog)
|
||||
var controls: Array[Control] = (
|
||||
FileDialogControllerNavigationType.interactive_controls(scope)
|
||||
)
|
||||
var directory_list: ItemList
|
||||
for control: Control in controls:
|
||||
if (
|
||||
control is ItemList
|
||||
and control.accessibility_name == "Directories & Files:"
|
||||
):
|
||||
directory_list = control as ItemList
|
||||
break
|
||||
assert(directory_list != null)
|
||||
_assert_directionally_reachable(directory_list, controls)
|
||||
dialog.queue_free()
|
||||
font_controller.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _assert_directionally_reachable(
|
||||
start: Control,
|
||||
controls: Array[Control],
|
||||
) -> void:
|
||||
var expected: Dictionary[int, bool] = {}
|
||||
for control: Control in controls:
|
||||
expected[control.get_instance_id()] = true
|
||||
var visited: Dictionary[int, bool] = {start.get_instance_id(): true}
|
||||
var pending: Array[Control] = [start]
|
||||
while not pending.is_empty():
|
||||
var current: Control = pending.pop_front()
|
||||
for path: NodePath in [
|
||||
current.focus_neighbor_left,
|
||||
current.focus_neighbor_right,
|
||||
current.focus_neighbor_top,
|
||||
current.focus_neighbor_bottom,
|
||||
]:
|
||||
var neighbor := current.get_node_or_null(path) as Control
|
||||
if (
|
||||
neighbor == null
|
||||
or not expected.has(neighbor.get_instance_id())
|
||||
or visited.has(neighbor.get_instance_id())
|
||||
):
|
||||
continue
|
||||
visited[neighbor.get_instance_id()] = true
|
||||
pending.append(neighbor)
|
||||
assert(visited.size() == expected.size())
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://bgnku8bs7oxx7
|
||||
160
tests/keyboard_mouse_mapping_validation.gd
Normal file
160
tests/keyboard_mouse_mapping_validation.gd
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
extends SceneTree
|
||||
|
||||
const KeyboardMouseMappingManagerType = preload(
|
||||
"res://settings/keyboard_mouse_mapping_manager.gd"
|
||||
)
|
||||
const KeyboardMouseMappingPanelType = preload(
|
||||
"res://ui/keyboard_mouse_mapping_panel.gd"
|
||||
)
|
||||
const SettingsPanelScene = preload("res://ui/settings_panel.tscn")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var manager := KeyboardMouseMappingManagerType.new()
|
||||
root.add_child(manager)
|
||||
await process_frame
|
||||
var defaults: Dictionary = manager.get_active_bindings()
|
||||
assert(
|
||||
defaults.size()
|
||||
== KeyboardMouseMappingManagerType.ROLE_ORDER.size()
|
||||
)
|
||||
assert(
|
||||
str(defaults[
|
||||
str(KeyboardMouseMappingManagerType.ROLE_PRIMARY_ACTION)
|
||||
].get("kind", "")) == "mouse_button"
|
||||
)
|
||||
assert(
|
||||
str(defaults[
|
||||
str(KeyboardMouseMappingManagerType.ROLE_CHAT)
|
||||
].get("kind", "")) == "key"
|
||||
)
|
||||
|
||||
var joypad_events_before: int = _event_count(&"jump", true)
|
||||
var rebind_key := InputEventKey.new()
|
||||
rebind_key.physical_keycode = KEY_R
|
||||
rebind_key.pressed = true
|
||||
var jump_binding: Dictionary = manager.binding_from_event(rebind_key)
|
||||
assert(manager.validate_binding(jump_binding))
|
||||
assert(manager.set_binding(
|
||||
KeyboardMouseMappingManagerType.ROLE_JUMP,
|
||||
jump_binding,
|
||||
))
|
||||
assert(manager.has_custom_mapping())
|
||||
assert(FileAccess.file_exists(
|
||||
KeyboardMouseMappingManagerType.MAPPING_PATH
|
||||
))
|
||||
assert(_has_physical_key(&"jump", KEY_R))
|
||||
assert(_event_count(&"jump", true) == joypad_events_before)
|
||||
|
||||
var mouse_event := InputEventMouseButton.new()
|
||||
mouse_event.button_index = MOUSE_BUTTON_XBUTTON1
|
||||
mouse_event.pressed = true
|
||||
var mouse_binding: Dictionary = manager.binding_from_event(mouse_event)
|
||||
assert(manager.set_binding(
|
||||
KeyboardMouseMappingManagerType.ROLE_PRIMARY_ACTION,
|
||||
mouse_binding,
|
||||
))
|
||||
assert(_has_mouse_button(&"fish_primary", MOUSE_BUTTON_XBUTTON1))
|
||||
assert(not _has_mouse_button(&"fish_primary", MOUSE_BUTTON_LEFT))
|
||||
|
||||
var joypad_event := InputEventJoypadButton.new()
|
||||
joypad_event.button_index = JOY_BUTTON_A
|
||||
joypad_event.pressed = true
|
||||
assert(manager.binding_from_event(joypad_event).is_empty())
|
||||
|
||||
var panel := KeyboardMouseMappingPanelType.new()
|
||||
root.add_child(panel)
|
||||
panel.setup(manager)
|
||||
panel.open_panel()
|
||||
await process_frame
|
||||
assert(_has_label_text(panel, "keyboard binds"))
|
||||
var scrolls: Array[Node] = panel.find_children(
|
||||
"", "ScrollContainer", true, false
|
||||
)
|
||||
assert(not scrolls.is_empty())
|
||||
assert((scrolls.front() as ScrollContainer).follow_focus)
|
||||
var first_binding := panel._binding_buttons.get(
|
||||
KeyboardMouseMappingManagerType.ROLE_ORDER.front()
|
||||
) as Button
|
||||
var last_binding := panel._binding_buttons.get(
|
||||
KeyboardMouseMappingManagerType.ROLE_ORDER.back()
|
||||
) as Button
|
||||
assert(first_binding.get_node(
|
||||
first_binding.focus_neighbor_top
|
||||
) == first_binding)
|
||||
assert(last_binding.get_node(
|
||||
last_binding.focus_neighbor_bottom
|
||||
) == panel._close_button)
|
||||
assert(panel._close_button.get_node(
|
||||
panel._close_button.focus_neighbor_top
|
||||
) == last_binding)
|
||||
panel._begin_capture(KeyboardMouseMappingManagerType.ROLE_INTERACT)
|
||||
var interact_key := InputEventKey.new()
|
||||
interact_key.physical_keycode = KEY_F
|
||||
interact_key.pressed = true
|
||||
panel._input(interact_key)
|
||||
assert(_has_physical_key(&"interact", KEY_F))
|
||||
assert(not panel.is_capturing())
|
||||
|
||||
var settings_panel := SettingsPanelScene.instantiate() as SettingsPanel
|
||||
root.add_child(settings_panel)
|
||||
await process_frame
|
||||
var controller_bubble := settings_panel.get_node(
|
||||
"%ControllerMapping"
|
||||
) as Button
|
||||
var keyboard_bubble := settings_panel.get_node("%KeyboardMapping") as Button
|
||||
assert(controller_bubble.text == "controller\nbinds")
|
||||
assert(keyboard_bubble.text == "keyboard\nbinds")
|
||||
|
||||
assert(manager.reset_mapping())
|
||||
assert(not manager.has_custom_mapping())
|
||||
assert(not FileAccess.file_exists(
|
||||
KeyboardMouseMappingManagerType.MAPPING_PATH
|
||||
))
|
||||
assert(_has_physical_key(&"jump", KEY_SPACE))
|
||||
assert(_has_mouse_button(&"fish_primary", MOUSE_BUTTON_LEFT))
|
||||
assert(_has_physical_key(&"open_chat", KEY_T))
|
||||
assert(_event_count(&"jump", true) == joypad_events_before)
|
||||
|
||||
settings_panel.queue_free()
|
||||
panel.queue_free()
|
||||
manager.queue_free()
|
||||
print("Keyboard and mouse mapping validation: PASS")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _event_count(action: StringName, joypad: bool) -> int:
|
||||
var result: int = 0
|
||||
for event: InputEvent in InputMap.action_get_events(action):
|
||||
if joypad == (
|
||||
event is InputEventJoypadButton or event is InputEventJoypadMotion
|
||||
):
|
||||
result += 1
|
||||
return result
|
||||
|
||||
|
||||
func _has_physical_key(action: StringName, key: Key) -> bool:
|
||||
for event: InputEvent in InputMap.action_get_events(action):
|
||||
var key_event := event as InputEventKey
|
||||
if key_event != null and key_event.physical_keycode == key:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _has_mouse_button(action: StringName, button: MouseButton) -> bool:
|
||||
for event: InputEvent in InputMap.action_get_events(action):
|
||||
var mouse_event := event as InputEventMouseButton
|
||||
if mouse_event != null and mouse_event.button_index == button:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _has_label_text(parent: Node, expected: String) -> bool:
|
||||
for node: Node in parent.find_children("", "Label", true, false):
|
||||
if (node as Label).text == expected:
|
||||
return true
|
||||
return false
|
||||
1
tests/keyboard_mouse_mapping_validation.gd.uid
Normal file
1
tests/keyboard_mouse_mapping_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://br6aeelrpa64k
|
||||
|
|
@ -249,6 +249,36 @@ func _validate_page() -> void:
|
|||
assert(_detail_text(page).contains(LogbookCatalog.facts_for(bluegill)))
|
||||
_validate_detail_field_fonts(page)
|
||||
_validate_handwritten_numeric_scale(page)
|
||||
var detail_buttons: Array = page.get("_detail_buttons") as Array
|
||||
assert(detail_buttons.size() == 4)
|
||||
var portrait_detail := detail_buttons[0] as Button
|
||||
var facts_detail := detail_buttons[1] as Button
|
||||
var quality_detail := detail_buttons[2] as Button
|
||||
var stats_detail := detail_buttons[3] as Button
|
||||
assert(
|
||||
portrait_detail.get_node(portrait_detail.focus_neighbor_right)
|
||||
== facts_detail
|
||||
)
|
||||
assert(
|
||||
portrait_detail.get_node(portrait_detail.focus_neighbor_bottom)
|
||||
== quality_detail
|
||||
)
|
||||
assert(
|
||||
facts_detail.get_node(facts_detail.focus_neighbor_left)
|
||||
== portrait_detail
|
||||
)
|
||||
assert(
|
||||
facts_detail.get_node(facts_detail.focus_neighbor_bottom)
|
||||
== quality_detail
|
||||
)
|
||||
assert(
|
||||
quality_detail.get_node(quality_detail.focus_neighbor_bottom)
|
||||
== stats_detail
|
||||
)
|
||||
assert(
|
||||
stats_detail.get_node(stats_detail.focus_neighbor_top)
|
||||
== quality_detail
|
||||
)
|
||||
var portrait_button := page.get("_detail_portrait_button") as Button
|
||||
assert(portrait_button != null)
|
||||
portrait_button.pressed.emit()
|
||||
|
|
@ -266,6 +296,29 @@ func _validate_page() -> void:
|
|||
(page.get("_portrait_overlay_backdrop") as Button).pressed.emit()
|
||||
await process_frame
|
||||
assert(not portrait_overlay.visible)
|
||||
facts_detail.pressed.emit()
|
||||
await process_frame
|
||||
var overlay_text := page.get("_portrait_overlay_text") as Label
|
||||
assert(overlay_text.visible)
|
||||
assert(
|
||||
overlay_text.get_theme_font_size("font_size")
|
||||
== LogbookPage.DETAIL_OVERLAY_TEXT_FONT_SIZE
|
||||
)
|
||||
assert(
|
||||
overlay_text.horizontal_alignment
|
||||
== HORIZONTAL_ALIGNMENT_LEFT
|
||||
)
|
||||
assert(
|
||||
portrait_overlay.size.x
|
||||
- overlay_text.size.x
|
||||
<= float(
|
||||
(LogbookPage.DETAIL_OVERLAY_EDGE_MARGIN
|
||||
+ LogbookPage.DETAIL_OVERLAY_CONTENT_MARGIN) * 2
|
||||
+ 8
|
||||
)
|
||||
)
|
||||
(page.get("_portrait_overlay_backdrop") as Button).pressed.emit()
|
||||
await process_frame
|
||||
|
||||
inventory.remove_catch_by_id(fish_catch.catch_id)
|
||||
await process_frame
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue