// 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];
}
}
}
}