457 lines
15 KiB
GDScript
457 lines
15 KiB
GDScript
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)
|