|
| 1 | +using System.Collections; |
| 2 | +using System.Linq; |
| 3 | +using NUnit.Framework; |
| 4 | +using UnityEditor; |
| 5 | +using UnityEngine; |
| 6 | +using UnityEngine.InputSystem; |
| 7 | +using UnityEngine.InputSystem.Editor; |
| 8 | +using UnityEngine.TestTools; |
| 9 | +using UnityEngine.UIElements; |
| 10 | +using Object = UnityEngine.Object; |
| 11 | + |
| 12 | +/// <summary> |
| 13 | +/// Tests for the custom inspector of the PlayerInput component. |
| 14 | +/// </summary> |
| 15 | +internal sealed class PlayerInputEditorTests |
| 16 | +{ |
| 17 | + private GameObject go; |
| 18 | + private PlayerInput playerInput; |
| 19 | + private InputActionAsset actions; |
| 20 | + private PlayerInputEditor editor; |
| 21 | + private EditorWindow window; |
| 22 | + private bool onGuiCalled; |
| 23 | + |
| 24 | + private class TestHostWindow : EditorWindow {} |
| 25 | + |
| 26 | + [SetUp] |
| 27 | + public void SetUp() |
| 28 | + { |
| 29 | + go = new GameObject("Host"); |
| 30 | + playerInput = go.AddComponent<PlayerInput>(); |
| 31 | + actions = AssetDatabase.LoadAssetAtPath<InputActionAsset>(PlayerInputEditor.kDefaultInputActionsAssetPath); |
| 32 | + window = EditorWindow.GetWindow<TestHostWindow>(); |
| 33 | + } |
| 34 | + |
| 35 | + [TearDown] |
| 36 | + public void TearDown() |
| 37 | + { |
| 38 | + if (editor != null) |
| 39 | + Object.DestroyImmediate(editor); |
| 40 | + window.Close(); |
| 41 | + Undo.ClearUndo(playerInput); |
| 42 | + Object.DestroyImmediate(go); |
| 43 | + } |
| 44 | + |
| 45 | + private IEnumerator RenderInspector() |
| 46 | + { |
| 47 | + onGuiCalled = false; |
| 48 | + |
| 49 | + var container = new IMGUIContainer(() => |
| 50 | + { |
| 51 | + // Like the Inspector window, pick up changes made to the target outside the editor (e.g. undo). |
| 52 | + editor.serializedObject.UpdateIfRequiredOrScript(); |
| 53 | + editor.OnInspectorGUI(); |
| 54 | + onGuiCalled = true; |
| 55 | + }); |
| 56 | + |
| 57 | + window.rootVisualElement.Add(container); |
| 58 | + window.Repaint(); |
| 59 | + |
| 60 | + var timeoutTime = Time.realtimeSinceStartupAsDouble + 3.0; |
| 61 | + do |
| 62 | + { |
| 63 | + yield return null; // let Unity process a frame |
| 64 | + } |
| 65 | + while (!onGuiCalled && Time.realtimeSinceStartupAsDouble < timeoutTime); |
| 66 | + |
| 67 | + container.RemoveFromHierarchy(); |
| 68 | + Assert.That(onGuiCalled, Is.True, "Inspector GUI was not drawn"); |
| 69 | + } |
| 70 | + |
| 71 | + // Replaces the actions of the asset with the given ones in a single map, the way a reimport does. The IDs are |
| 72 | + // derived from the position, so actions keep their ID across calls. |
| 73 | + private static void LoadActions(InputActionAsset asset, params string[] actionNames) |
| 74 | + { |
| 75 | + var actionsJson = actionNames.Select((name, i) => |
| 76 | + $"{{\"name\":\"{name}\",\"type\":\"Button\",\"id\":\"00000000-0000-0000-0000-{i + 1:D12}\"}}"); |
| 77 | + asset.LoadFromJson( |
| 78 | + $"{{\"maps\":[{{\"name\":\"map\",\"actions\":[{string.Join(",", actionsJson)}],\"bindings\":[]}}]}}"); |
| 79 | + } |
| 80 | + |
| 81 | + // UUM-150956: switching to InvokeUnityEvents on a freshly added component did not show the per-action events |
| 82 | + // until the inspector was recreated. |
| 83 | + [UnityTest] |
| 84 | + public IEnumerator ActionEventsAreListed_WhenBehaviorIsSwitchedToInvokeUnityEvents( |
| 85 | + [Values(PlayerNotifications.SendMessages, PlayerNotifications.BroadcastMessages, PlayerNotifications.InvokeCSharpEvents)] |
| 86 | + PlayerNotifications initialBehavior) |
| 87 | + { |
| 88 | + Assert.That(actions, Is.Not.Null); |
| 89 | + |
| 90 | + playerInput.m_Actions = actions; |
| 91 | + playerInput.m_NotificationBehavior = initialBehavior; |
| 92 | + editor = (PlayerInputEditor)UnityEditor.Editor.CreateEditor(playerInput); |
| 93 | + |
| 94 | + yield return RenderInspector(); |
| 95 | + Assert.That(playerInput.m_ActionEvents, Is.Null.Or.Empty); |
| 96 | + |
| 97 | + editor.serializedObject.FindProperty(nameof(PlayerInput.m_NotificationBehavior)).intValue = |
| 98 | + (int)PlayerNotifications.InvokeUnityEvents; |
| 99 | + editor.serializedObject.ApplyModifiedPropertiesWithoutUndo(); |
| 100 | + |
| 101 | + yield return RenderInspector(); |
| 102 | + |
| 103 | + Assert.That(playerInput.m_ActionEvents.Select(e => e.actionId), |
| 104 | + Is.EquivalentTo(actions.Select(a => a.id.ToString()))); |
| 105 | + } |
| 106 | + |
| 107 | + [UnityTest] |
| 108 | + public IEnumerator ActionEventsFollowActionsAsset_WhenSwitchingBackToInvokeUnityEvents() |
| 109 | + { |
| 110 | + Assert.That(actions, Is.Not.Null); |
| 111 | + |
| 112 | + playerInput.m_Actions = actions; |
| 113 | + playerInput.m_NotificationBehavior = PlayerNotifications.InvokeUnityEvents; |
| 114 | + editor = (PlayerInputEditor)UnityEditor.Editor.CreateEditor(playerInput); |
| 115 | + |
| 116 | + yield return RenderInspector(); |
| 117 | + Assert.That(playerInput.m_ActionEvents, Is.Not.Empty); |
| 118 | + |
| 119 | + // Switch away, swap in an asset with different actions, then switch back. |
| 120 | + var otherActions = ScriptableObject.CreateInstance<InputActionAsset>(); |
| 121 | + try |
| 122 | + { |
| 123 | + otherActions.AddActionMap("map").AddAction("other"); |
| 124 | + |
| 125 | + var behaviorProperty = editor.serializedObject.FindProperty(nameof(PlayerInput.m_NotificationBehavior)); |
| 126 | + behaviorProperty.intValue = (int)PlayerNotifications.SendMessages; |
| 127 | + editor.serializedObject.ApplyModifiedPropertiesWithoutUndo(); |
| 128 | + yield return RenderInspector(); |
| 129 | + |
| 130 | + editor.serializedObject.FindProperty(nameof(PlayerInput.m_Actions)).objectReferenceValue = otherActions; |
| 131 | + editor.serializedObject.ApplyModifiedPropertiesWithoutUndo(); |
| 132 | + yield return RenderInspector(); |
| 133 | + |
| 134 | + behaviorProperty.intValue = (int)PlayerNotifications.InvokeUnityEvents; |
| 135 | + editor.serializedObject.ApplyModifiedPropertiesWithoutUndo(); |
| 136 | + yield return RenderInspector(); |
| 137 | + |
| 138 | + Assert.That(playerInput.m_ActionEvents.Select(e => e.actionId), |
| 139 | + Is.EquivalentTo(otherActions.Select(a => a.id.ToString()))); |
| 140 | + } |
| 141 | + finally |
| 142 | + { |
| 143 | + Object.DestroyImmediate(otherActions); |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + // Leaving InvokeUnityEvents and coming back with the same asset must pick up actions added in the meantime. |
| 148 | + [UnityTest] |
| 149 | + public IEnumerator ActionEventsFollowActionsAsset_WhenReenteringInvokeUnityEventsWithSameAsset() |
| 150 | + { |
| 151 | + var asset = ScriptableObject.CreateInstance<InputActionAsset>(); |
| 152 | + try |
| 153 | + { |
| 154 | + LoadActions(asset, "first"); |
| 155 | + |
| 156 | + playerInput.m_Actions = asset; |
| 157 | + playerInput.m_NotificationBehavior = PlayerNotifications.InvokeUnityEvents; |
| 158 | + editor = (PlayerInputEditor)UnityEditor.Editor.CreateEditor(playerInput); |
| 159 | + yield return RenderInspector(); |
| 160 | + |
| 161 | + var behaviorProperty = editor.serializedObject.FindProperty(nameof(PlayerInput.m_NotificationBehavior)); |
| 162 | + behaviorProperty.intValue = (int)PlayerNotifications.SendMessages; |
| 163 | + editor.serializedObject.ApplyModifiedPropertiesWithoutUndo(); |
| 164 | + yield return RenderInspector(); |
| 165 | + |
| 166 | + LoadActions(asset, "first", "second"); |
| 167 | + |
| 168 | + behaviorProperty.intValue = (int)PlayerNotifications.InvokeUnityEvents; |
| 169 | + editor.serializedObject.ApplyModifiedPropertiesWithoutUndo(); |
| 170 | + yield return RenderInspector(); |
| 171 | + |
| 172 | + Assert.That(playerInput.m_ActionEvents.Select(e => e.actionId), |
| 173 | + Is.EquivalentTo(asset.Select(a => a.id.ToString()))); |
| 174 | + } |
| 175 | + finally |
| 176 | + { |
| 177 | + Object.DestroyImmediate(asset); |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + // Behavior changes made through undo/redo don't go through the inspector's dropdown. |
| 182 | + [UnityTest] |
| 183 | + public IEnumerator ActionEventsAreListed_WhenInvokeUnityEventsIsRestoredByUndoRedo() |
| 184 | + { |
| 185 | + var asset = ScriptableObject.CreateInstance<InputActionAsset>(); |
| 186 | + try |
| 187 | + { |
| 188 | + LoadActions(asset, "first"); |
| 189 | + |
| 190 | + playerInput.m_Actions = asset; |
| 191 | + playerInput.m_NotificationBehavior = PlayerNotifications.SendMessages; |
| 192 | + editor = (PlayerInputEditor)UnityEditor.Editor.CreateEditor(playerInput); |
| 193 | + yield return RenderInspector(); |
| 194 | + |
| 195 | + Undo.IncrementCurrentGroup(); |
| 196 | + editor.serializedObject.FindProperty(nameof(PlayerInput.m_NotificationBehavior)).intValue = |
| 197 | + (int)PlayerNotifications.InvokeUnityEvents; |
| 198 | + editor.serializedObject.ApplyModifiedProperties(); |
| 199 | + Undo.IncrementCurrentGroup(); |
| 200 | + yield return RenderInspector(); |
| 201 | + |
| 202 | + Assert.That(playerInput.m_ActionEvents.Select(e => e.actionId), |
| 203 | + Is.EquivalentTo(asset.Select(a => a.id.ToString()))); |
| 204 | + |
| 205 | + Undo.PerformUndo(); |
| 206 | + yield return RenderInspector(); |
| 207 | + Assert.That(playerInput.m_NotificationBehavior, Is.EqualTo(PlayerNotifications.SendMessages)); |
| 208 | + |
| 209 | + LoadActions(asset, "first", "second"); |
| 210 | + |
| 211 | + Undo.PerformRedo(); |
| 212 | + yield return RenderInspector(); |
| 213 | + Assert.That(playerInput.m_NotificationBehavior, Is.EqualTo(PlayerNotifications.InvokeUnityEvents)); |
| 214 | + |
| 215 | + Assert.That(playerInput.m_ActionEvents.Select(e => e.actionId), |
| 216 | + Is.EquivalentTo(asset.Select(a => a.id.ToString()))); |
| 217 | + } |
| 218 | + finally |
| 219 | + { |
| 220 | + Object.DestroyImmediate(asset); |
| 221 | + } |
| 222 | + } |
| 223 | +} |
0 commit comments