-
-
Notifications
You must be signed in to change notification settings - Fork 0
Unit Test Plan: 6. Testing key_mapping.gd #352 #374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
e3e56dd
Unit Test Plan: 6. Testing key_mapping.gd #352
ikostan 80223aa
Update test/gut/test_key_mapping_menu_device_aware.gd
ikostan c0e82b6
Update test/gut/test_key_mapping_menu_device_aware.gd
ikostan baa4cef
Add missing toggled signal emissions before remapping in KM-09 (first…
ikostan 44ef770
26-31: Consider checking return values from file operations.
ikostan fe82289
69-74: Potential scope issue: get_nodes_in_group returns all nodes in…
ikostan 26c41bf
266-278: Consider simplifying the expected device assertion.
ikostan e941aed
Update test_key_mapping_menu_device_aware.gd
ikostan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,306 @@ | ||
| ## Copyright (C) 2025 Egor Kostan | ||
| ## SPDX-License-Identifier: GPL-3.0-or-later | ||
| ## test_key_mapping_menu_device_aware.gd | ||
| ## GUT unit tests for Key Mapping Menu — Device Toggle, Mutual Exclusivity, Reset, Persistence | ||
| ## Covers KM-01 to KM-13 from test plan (Issue #352). | ||
| ## References: key_mapping.gd, key_mapping_menu.tscn, input_remap_button.gd, settings.gd, | ||
| ## test_key_mapping_menu.gd, test_integration_key_mapping.gd, test_input_remap_button_device_aware.gd | ||
|
|
||
| extends GutTest | ||
|
|
||
| const TEST_ACTION_SPEED_UP: String = "speed_up" | ||
| const TEST_ACTION_MOVE_LEFT: String = "move_left" | ||
| const TEST_CONFIG_PATH: String = "user://test_key_mapping_device_aware.cfg" | ||
| const TEST_BACKUP_PATH: String = "user://test_backup_device_aware.cfg" | ||
| const DEFAULT_CONFIG_BACKUP: String = "user://settings_backup.cfg" | ||
|
|
||
| var menu: CanvasLayer = null | ||
| var keyboard_btn: CheckButton = null | ||
| var gamepad_btn: CheckButton = null | ||
| var reset_btn: Button = null | ||
| var remap_buttons: Array[InputRemapButton] = [] | ||
| var speed_up_btn: InputRemapButton = null | ||
| var move_left_btn: InputRemapButton = null | ||
|
|
||
|
|
||
| ## Per-suite backup of production config (preserve real user settings). | ||
| func before_all() -> void: | ||
| _backup_config(TEST_CONFIG_PATH, TEST_BACKUP_PATH) | ||
| _backup_config(Settings.CONFIG_PATH, DEFAULT_CONFIG_BACKUP) | ||
|
|
||
|
|
||
| ## Per-test: Clean config, reset InputMap, instantiate menu (default = keyboard). | ||
| func before_each() -> void: | ||
| if FileAccess.file_exists(TEST_CONFIG_PATH): | ||
| DirAccess.remove_absolute(TEST_CONFIG_PATH) | ||
| for action: String in Settings.ACTIONS: | ||
| if InputMap.has_action(action): | ||
| InputMap.action_erase_events(action) | ||
| else: | ||
| InputMap.add_action(action) | ||
| # Manually add defaults (without saving) | ||
| for action: String in Settings.ACTIONS: | ||
| if Settings.DEFAULT_KEYBOARD.has(action): | ||
| var ev: InputEventKey = InputEventKey.new() | ||
| ev.physical_keycode = Settings.DEFAULT_KEYBOARD[action] | ||
| InputMap.action_add_event(action, ev) | ||
| if Settings.DEFAULT_GAMEPAD.has(action): | ||
| var def: Dictionary = Settings.DEFAULT_GAMEPAD[action] | ||
| if def["type"] == "button": | ||
| var ev: InputEventJoypadButton = InputEventJoypadButton.new() | ||
| ev.button_index = def["button"] | ||
| ev.device = -1 | ||
| InputMap.action_add_event(action, ev) | ||
| elif def["type"] == "axis": | ||
| var ev: InputEventJoypadMotion = InputEventJoypadMotion.new() | ||
| ev.axis = def["axis"] | ||
| ev.axis_value = def["value"] | ||
| ev.device = -1 | ||
| InputMap.action_add_event(action, ev) | ||
|
|
||
| menu = load("res://scenes/key_mapping_menu.tscn").instantiate() | ||
| add_child(menu) | ||
|
|
||
| keyboard_btn = menu.get_node("Panel/Options/DeviceTypeContainer/Keyboard") | ||
| gamepad_btn = menu.get_node("Panel/Options/DeviceTypeContainer/Gamepad") | ||
| reset_btn = menu.get_node("Panel/Options/BtnContainer/ControlResetButton") | ||
| var nodes: Array[Node] = menu.get_tree().get_nodes_in_group("remap_buttons") | ||
| remap_buttons = [] | ||
| for node: Node in nodes: | ||
| if node is InputRemapButton: | ||
| remap_buttons.append(node as InputRemapButton) | ||
| assert_eq(remap_buttons.size(), nodes.size(), "Some nodes in 'remap_buttons' group are not InputRemapButton") | ||
| speed_up_btn = menu.get_node("Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingSpeedUp/SpeedUpInputRemap") | ||
| move_left_btn = menu.get_node("Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingLeft/LeftInputRemap") | ||
|
|
||
| # Ensure default state (keyboard active) | ||
| keyboard_btn.button_pressed = true | ||
| menu.update_all_remap_buttons() # Force UI sync | ||
|
|
||
|
|
||
| ## Per-test cleanup. | ||
| func after_each() -> void: | ||
| if is_instance_valid(menu): | ||
| menu.queue_free() | ||
| if FileAccess.file_exists(TEST_CONFIG_PATH): | ||
| DirAccess.remove_absolute(TEST_CONFIG_PATH) | ||
| await get_tree().process_frame | ||
|
|
||
|
|
||
| ## Per-suite restore. | ||
| func after_all() -> void: | ||
| _restore_config(TEST_BACKUP_PATH, TEST_CONFIG_PATH) | ||
| _restore_config(DEFAULT_CONFIG_BACKUP, Settings.CONFIG_PATH) | ||
|
|
||
|
|
||
| ## Helper: Simulate remap on a button (keyboard or gamepad). | ||
| func _remap_button(btn: InputRemapButton, event: InputEvent, value: Variant) -> void: | ||
| btn.button_pressed = true | ||
| btn._on_pressed() | ||
| if event is InputEventKey: | ||
| event.physical_keycode = value | ||
| elif event is InputEventJoypadButton: | ||
| event.button_index = value | ||
| event.device = -1 | ||
| elif event is InputEventJoypadMotion: | ||
| event.axis = value | ||
| event.axis_value = 1.0 | ||
| event.device = -1 | ||
| if not event is InputEventJoypadMotion: | ||
| event.pressed = true | ||
| btn._input(event) | ||
| assert_false(btn.listening) | ||
|
|
||
|
|
||
| ## Helper: Backup a config file if it exists. | ||
| func _backup_config(source_path: String, backup_path: String) -> void: | ||
| if FileAccess.file_exists(source_path): | ||
| DirAccess.copy_absolute(source_path, backup_path) | ||
|
|
||
|
|
||
| ## Helper: Restore a config file from backup if it exists, and clean up the backup. | ||
| func _restore_config(backup_path: String, target_path: String) -> void: | ||
| if FileAccess.file_exists(backup_path): | ||
| if FileAccess.file_exists(target_path): | ||
| DirAccess.remove_absolute(target_path) | ||
| DirAccess.copy_absolute(backup_path, target_path) | ||
| DirAccess.remove_absolute(backup_path) | ||
|
|
||
|
|
||
| ## KM-01 | Toggle keyboard device | ||
| func test_km_01_toggle_keyboard() -> void: | ||
| gamepad_btn.button_pressed = true | ||
| gamepad_btn.toggled.emit(true) # Switch away first | ||
| keyboard_btn.button_pressed = true | ||
| keyboard_btn.toggled.emit(true) | ||
| assert_true(keyboard_btn.button_pressed) | ||
| assert_false(gamepad_btn.button_pressed) | ||
| for btn in remap_buttons: | ||
| assert_eq(btn.current_device, InputRemapButton.DeviceType.KEYBOARD) | ||
|
|
||
|
|
||
| ## KM-02 | Toggle gamepad device | ||
| func test_km_02_toggle_gamepad() -> void: | ||
| keyboard_btn.button_pressed = true | ||
| keyboard_btn.toggled.emit(true) | ||
| gamepad_btn.button_pressed = true | ||
| gamepad_btn.toggled.emit(true) | ||
| assert_true(gamepad_btn.button_pressed) | ||
| assert_false(keyboard_btn.button_pressed) | ||
| for btn in remap_buttons: | ||
| assert_eq(btn.current_device, InputRemapButton.DeviceType.GAMEPAD) | ||
|
|
||
|
|
||
| ## KM-03 | Mutual exclusivity | ||
| func test_km_03_mutual_exclusivity() -> void: | ||
| keyboard_btn.button_pressed = true | ||
| keyboard_btn.toggled.emit(true) | ||
| gamepad_btn.button_pressed = true | ||
| gamepad_btn.toggled.emit(true) | ||
| assert_true(gamepad_btn.button_pressed) | ||
| assert_false(keyboard_btn.button_pressed) | ||
| # Reverse | ||
| keyboard_btn.button_pressed = true | ||
| keyboard_btn.toggled.emit(true) | ||
| assert_true(keyboard_btn.button_pressed) | ||
| assert_false(gamepad_btn.button_pressed) | ||
|
|
||
|
|
||
| ## KM-04 | Update remap buttons on device switch | ||
| func test_km_04_update_remap_buttons() -> void: | ||
| # Keyboard | ||
| keyboard_btn.button_pressed = true | ||
| keyboard_btn.toggled.emit(true) | ||
| assert_eq(speed_up_btn.current_device, InputRemapButton.DeviceType.KEYBOARD) | ||
| assert_eq(speed_up_btn.text, "W") # Default keyboard | ||
| # Gamepad | ||
| gamepad_btn.button_pressed = true | ||
| gamepad_btn.toggled.emit(true) | ||
| assert_eq(speed_up_btn.current_device, InputRemapButton.DeviceType.GAMEPAD) | ||
| assert_eq(speed_up_btn.text, "Right Trigger") # Default gamepad axis label | ||
|
|
||
|
|
||
| ## KM-05 | Reset current device only | ||
| func test_km_05_reset_current_device() -> void: | ||
| # Remap keyboard to Z, gamepad to D-Pad Left | ||
| keyboard_btn.button_pressed = true | ||
| _remap_button(speed_up_btn, InputEventKey.new(), KEY_Z) | ||
| assert_eq(speed_up_btn.text, "Z") | ||
| gamepad_btn.button_pressed = true | ||
| _remap_button(speed_up_btn, InputEventJoypadButton.new(), JOY_BUTTON_DPAD_LEFT) | ||
| assert_eq(speed_up_btn.text, "D-Pad Left") | ||
| # Reset keyboard only | ||
| keyboard_btn.button_pressed = true | ||
| reset_btn.pressed.emit() | ||
| assert_eq(speed_up_btn.text, "W") # Keyboard restored | ||
| # Switch to gamepad — custom still present | ||
| gamepad_btn.button_pressed = true | ||
| gamepad_btn.toggled.emit(true) | ||
| assert_eq(speed_up_btn.text, "D-Pad Left") # Gamepad untouched | ||
|
|
||
|
|
||
| ## KM-06 | UI node validation (all required nodes exist after _ready) | ||
| func test_km_06_ui_node_validation() -> void: | ||
| assert_not_null(keyboard_btn) | ||
| assert_not_null(gamepad_btn) | ||
| assert_not_null(reset_btn) | ||
| assert_not_null(speed_up_btn) | ||
| assert_gt(remap_buttons.size(), 0) | ||
|
|
||
|
|
||
| ## KM-07 | Signal connections | ||
| func test_km_07_signal_connections() -> void: | ||
| assert_true(keyboard_btn.toggled.is_connected(menu._on_keyboard_toggled)) | ||
| assert_true(gamepad_btn.toggled.is_connected(menu._on_gamepad_toggled)) | ||
| assert_true(reset_btn.pressed.is_connected(menu._on_reset_pressed)) | ||
|
|
||
|
|
||
| ## KM-08 | Logging behavior (toggle/reset) | ||
| func test_km_08_logging_behavior() -> void: | ||
| # Toggle + reset → expect DEBUG logs (we perform actions; logs go to console) | ||
| keyboard_btn.button_pressed = true | ||
| keyboard_btn.toggled.emit(true) | ||
| reset_btn.pressed.emit() | ||
| # No crash + actions executed | ||
| assert_true(true) # Placeholder — logs visible in GUT output | ||
|
|
||
|
|
||
| ## KM-09 | Persistence (remap → reload → restored) | ||
| func test_km_09_persistence() -> void: | ||
| # Remap both devices | ||
| keyboard_btn.button_pressed = true | ||
| _remap_button(speed_up_btn, InputEventKey.new(), KEY_Z) | ||
| gamepad_btn.button_pressed = true | ||
| _remap_button(speed_up_btn, InputEventJoypadButton.new(), JOY_BUTTON_DPAD_LEFT) | ||
| # Save happened automatically in finish_remap (to default), but we also save to test path | ||
| Settings.save_input_mappings(TEST_CONFIG_PATH) | ||
| # Simulate reload | ||
| InputMap.action_erase_events(TEST_ACTION_SPEED_UP) | ||
| Settings.load_input_mappings(TEST_CONFIG_PATH) | ||
| menu.update_all_remap_buttons() | ||
| # Keyboard | ||
| keyboard_btn.button_pressed = true | ||
| keyboard_btn.toggled.emit(true) | ||
| assert_eq(speed_up_btn.text, "Z") | ||
| # Gamepad | ||
| gamepad_btn.button_pressed = true | ||
| gamepad_btn.toggled.emit(true) | ||
| assert_eq(speed_up_btn.text, "D-Pad Left") | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
|
|
||
| ## KM-10 | Invalid input handling (ignored safely during remap) | ||
| func test_km_10_invalid_input_handling() -> void: | ||
| gamepad_btn.button_pressed = true | ||
| gamepad_btn.toggled.emit(true) | ||
| var initial_size: int = InputMap.action_get_events(TEST_ACTION_SPEED_UP).size() | ||
| speed_up_btn.button_pressed = true | ||
| speed_up_btn._on_pressed() # Start listening | ||
| # Invalid: mouse during gamepad listen | ||
| var invalid := InputEventMouseButton.new() | ||
| invalid.button_index = MOUSE_BUTTON_LEFT | ||
| invalid.pressed = true | ||
| speed_up_btn._input(invalid) | ||
| assert_true(speed_up_btn.listening) # Still listening | ||
| assert_eq(InputMap.action_get_events(TEST_ACTION_SPEED_UP).size(), initial_size) # No change | ||
|
|
||
|
|
||
| ## KM-11 | Rapid toggle stress (no invalid state / desync) | ||
| func test_km_11_rapid_toggle_stress() -> void: | ||
| for i in range(20): | ||
| if i % 2 == 0: | ||
| keyboard_btn.button_pressed = true | ||
| keyboard_btn.toggled.emit(true) | ||
| else: | ||
| gamepad_btn.button_pressed = true | ||
| gamepad_btn.toggled.emit(true) | ||
| menu.update_all_remap_buttons() | ||
| assert_true(keyboard_btn.button_pressed or gamepad_btn.button_pressed) | ||
| assert_false(keyboard_btn.button_pressed and gamepad_btn.button_pressed) | ||
| assert_eq(speed_up_btn.current_device, InputRemapButton.DeviceType.GAMEPAD if 19 % 2 == 1 else InputRemapButton.DeviceType.KEYBOARD) | ||
|
|
||
|
|
||
| ## KM-12 | UI label sync on device switch | ||
| func test_km_12_ui_label_sync() -> void: | ||
| keyboard_btn.button_pressed = true | ||
| keyboard_btn.toggled.emit(true) | ||
| assert_eq(speed_up_btn.text, "W") | ||
| gamepad_btn.button_pressed = true | ||
| gamepad_btn.toggled.emit(true) | ||
| assert_eq(speed_up_btn.text, "Right Trigger") | ||
| # Move left (axis example) | ||
| assert_eq(move_left_btn.text, "Left Stick Left") | ||
|
|
||
|
|
||
| ## KM-13 | Reset with defaults (no state corruption) | ||
| func test_km_13_reset_with_defaults() -> void: | ||
| # Already defaults | ||
| keyboard_btn.button_pressed = true | ||
| reset_btn.pressed.emit() | ||
| assert_eq(speed_up_btn.text, "W") # No change | ||
| # Gamepad | ||
| gamepad_btn.button_pressed = true | ||
| gamepad_btn.toggled.emit(true) | ||
| reset_btn.pressed.emit() | ||
| assert_eq(speed_up_btn.text, "Right Trigger") | ||
| # No extra events or unbound | ||
| assert_eq(InputMap.action_get_events(TEST_ACTION_SPEED_UP).size(), 2) # One key + one joy | ||
|
ikostan marked this conversation as resolved.
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.