// Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 using GFramework.Game.Abstractions.Input; using GFramework.Godot.Input; namespace GFramework.Godot.Tests.Input; /// /// 验证 Godot 输入绑定存储在纯托管后端上的动作快照、导入与冲突交换语义。 /// [TestFixture] public sealed class GodotInputBindingStoreTests { /// /// 验证导出快照会反映后端提供的框架绑定描述。 /// [Test] public void ExportSnapshot_Should_ReturnBackendBindings() { var backend = new FakeInputMapBackend( new InputBindingSnapshot( [ new InputActionBinding( "ui_accept", [ new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:13", "Enter") ]) ])); var store = new GodotInputBindingStore(backend); var snapshot = store.ExportSnapshot(); var acceptBindings = snapshot.Actions.Single( action => string.Equals(action.ActionName, "ui_accept", StringComparison.Ordinal)); Assert.That(acceptBindings.Bindings[0].Code, Is.EqualTo("key:13")); } /// /// 验证导入快照后会把新绑定回写到后端,并能重新导出。 /// [Test] public void ImportSnapshot_Should_UpdateBackendBindings() { var backend = new FakeInputMapBackend( new InputBindingSnapshot( [ new InputActionBinding( "ui_accept", [ new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:13", "Enter") ]) ])); var store = new GodotInputBindingStore(backend); store.ImportSnapshot( new InputBindingSnapshot( [ new InputActionBinding( "ui_accept", [ new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:32", "Space") ]) ])); var snapshot = store.ExportSnapshot(); var acceptBindings = snapshot.Actions.Single( action => string.Equals(action.ActionName, "ui_accept", StringComparison.Ordinal)); Assert.That(acceptBindings.Bindings[0].Code, Is.EqualTo("key:32")); } /// /// 验证导入快照时,会清空快照中未出现动作的后端绑定。 /// [Test] public void ImportSnapshot_WhenActionMissingFromSnapshot_Should_ClearBackendBindings() { var backend = new FakeInputMapBackend( new InputBindingSnapshot( [ new InputActionBinding( "ui_accept", [ new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:13", "Enter") ]), new InputActionBinding( "ui_cancel", [ new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:27", "Escape") ]) ])); var store = new GodotInputBindingStore(backend); store.ImportSnapshot( new InputBindingSnapshot( [ new InputActionBinding( "ui_accept", [ new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:32", "Space") ]) ])); var snapshot = store.ExportSnapshot(); Assert.Multiple(() => { Assert.That( snapshot.Actions.Single(action => string.Equals(action.ActionName, "ui_accept", StringComparison.Ordinal)).Bindings[0].Code, Is.EqualTo("key:32")); Assert.That( snapshot.Actions.Single(action => string.Equals(action.ActionName, "ui_cancel", StringComparison.Ordinal)).Bindings, Is.Empty); }); } /// /// 验证从纯托管绑定设置主绑定时,会保留 `Game` 层冲突交换语义。 /// [Test] public void SetPrimaryBinding_WhenBindingTaken_SwapsBackendBindings() { var backend = new FakeInputMapBackend( new InputBindingSnapshot( [ new InputActionBinding( "move_left", [ new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:65", "A") ]), new InputActionBinding( "move_right", [ new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:68", "D") ]) ])); var store = new GodotInputBindingStore(backend); store.SetPrimaryBinding( "move_left", new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:68", "D")); var snapshot = store.ExportSnapshot(); var moveLeft = snapshot.Actions.Single( action => string.Equals(action.ActionName, "move_left", StringComparison.Ordinal)); var moveRight = snapshot.Actions.Single( action => string.Equals(action.ActionName, "move_right", StringComparison.Ordinal)); Assert.Multiple(() => { Assert.That(moveLeft.Bindings[0].Code, Is.EqualTo("key:68")); Assert.That(moveRight.Bindings[0].Code, Is.EqualTo("key:65")); }); } /// /// 验证重置全部绑定时,会移除运行时新增且默认快照中不存在的动作。 /// [Test] public void ResetAll_WhenRuntimeActionIsNotInDefaults_Should_RemoveAction() { var backend = new FakeInputMapBackend( new InputBindingSnapshot( [ new InputActionBinding( "ui_accept", [ new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:13", "Enter") ]) ])); var store = new GodotInputBindingStore(backend); store.ImportSnapshot( new InputBindingSnapshot( [ new InputActionBinding( "ui_accept", [ new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:13", "Enter") ]), new InputActionBinding( "debug_toggle", [ new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, "key:192", "QuoteLeft") ]) ])); store.ResetAll(); var snapshot = store.ExportSnapshot(); Assert.Multiple(() => { Assert.That( snapshot.Actions.Any(action => string.Equals(action.ActionName, "ui_accept", StringComparison.Ordinal)), Is.True); Assert.That( snapshot.Actions.Any(action => string.Equals(action.ActionName, "debug_toggle", StringComparison.Ordinal)), Is.False); }); } /// /// 测试用的纯托管 InputMap 后端。 /// private sealed class FakeInputMapBackend : IGodotInputMapBackend { private readonly Dictionary> _defaults; private readonly Dictionary> _current; /// /// 初始化测试后端。 /// /// 初始快照。 public FakeInputMapBackend(InputBindingSnapshot snapshot) { _defaults = snapshot.Actions.ToDictionary( static action => action.ActionName, static action => action.Bindings.ToList(), StringComparer.Ordinal); _current = snapshot.Actions.ToDictionary( static action => action.ActionName, static action => action.Bindings.ToList(), StringComparer.Ordinal); } /// public IReadOnlyList GetActionNames() { return [.._current.Keys.OrderBy(static key => key, StringComparer.Ordinal)]; } /// public IReadOnlyList GetBindings(string actionName) { return _current.TryGetValue(actionName, out var bindings) ? [..bindings] : Array.Empty(); } /// public void SetBindings(string actionName, IReadOnlyList bindings) { _current[actionName] = [..bindings]; } /// public void ResetAction(string actionName) { if (_defaults.TryGetValue(actionName, out var bindings)) { _current[actionName] = [..bindings]; return; } _current.Remove(actionName); } /// public void ResetAll() { _current.Clear(); foreach (var pair in _defaults) { _current[pair.Key] = [..pair.Value]; } } } }