From 3fb8395f53e11485a82dc80e57ca565c07e7f594 Mon Sep 17 00:00:00 2001 From: Kurtis Date: Wed, 15 Oct 2025 17:17:07 -0700 Subject: [PATCH 1/2] Add a new test to check various select modes and select/deselect ordering with events --- .../Tests/Runtime/BasicInputTests.cs | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) diff --git a/org.mixedrealitytoolkit.input/Tests/Runtime/BasicInputTests.cs b/org.mixedrealitytoolkit.input/Tests/Runtime/BasicInputTests.cs index a6ae95b3b..3004d7ac2 100644 --- a/org.mixedrealitytoolkit.input/Tests/Runtime/BasicInputTests.cs +++ b/org.mixedrealitytoolkit.input/Tests/Runtime/BasicInputTests.cs @@ -310,6 +310,277 @@ public IEnumerator GazePinchSmokeTest() Assert.IsTrue(interactable.IsGazePinchHovered); } + [UnityTest] + public IEnumerator TestStatefulInteractableSelectMode( + [Values(InteractableSelectMode.Single, InteractableSelectMode.Multiple)] InteractableSelectMode selectMode, + [Values(true, false)] bool releaseInSelectOrder) + { + GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); + StatefulInteractable interactable = cube.AddComponent(); + cube.transform.position = InputTestUtilities.InFrontOfUser(new Vector3(0.2f, 0.2f, 0.5f)); + cube.transform.localScale = Vector3.one * 0.1f; + + bool isSelected = false; + bool selectEntered = false; + bool selectExited = false; + + // For this test, we won't use poke or grab selection + interactable.DisableInteractorType(typeof(PokeInteractor)); + interactable.DisableInteractorType(typeof(GrabInteractor)); + interactable.selectMode = selectMode; + + interactable.firstSelectEntered.AddListener((SelectEnterEventArgs) => { isSelected = true; }); + interactable.lastSelectExited.AddListener((SelectEnterEventArgs) => { isSelected = false; }); + + interactable.selectEntered.AddListener((SelectEnterEventArgs) => { selectEntered = true; }); + interactable.selectExited.AddListener((SelectEnterEventArgs) => { selectExited = true; }); + + // Introduce the first hand + var rightHand = new TestHand(Handedness.Right); + yield return rightHand.Show(InputTestUtilities.InFrontOfUser(0.4f)); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsFalse(interactable.IsRayHovered, + "StatefulInteractable was already RayHovered."); + Assert.IsFalse(interactable.isHovered, + "StatefulInteractable was already hovered."); + + // Aim the first hand to hover the cube + yield return rightHand.AimAt(cube.transform.position); + yield return RuntimeTestUtilities.WaitForUpdates(); + Assert.IsTrue(interactable.IsRayHovered, + "StatefulInteractable did not get RayHovered."); + Assert.IsTrue(interactable.isHovered, + "StatefulInteractable did not get hovered."); + + Assert.IsTrue(interactable.HoveringRayInteractors.Count == 1, + "StatefulInteractable should only have 1 hovering RayInteractor."); + Assert.IsTrue(interactable.interactorsHovering.Count == 1, + "StatefulInteractable should only have 1 hovering interactor."); + + Assert.IsFalse(isSelected, + "StatefulInteractable should not be selected."); + Assert.IsFalse(selectEntered, + "StatefulInteractable should not have had a select enter."); + Assert.IsFalse(selectExited, + "StatefulInteractable should not have had a select exit."); + + // Pinch the first hand to select the cube + yield return rightHand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + Assert.IsTrue(interactable.IsRaySelected, + "StatefulInteractable did not get RaySelected."); + Assert.IsTrue(interactable.isSelected, + "StatefulInteractable did not get selected."); + Assert.IsTrue(interactable.interactorsSelecting.Count == 1, + "StatefulInteractable should only have 1 selecting interactor."); + Assert.IsTrue(isSelected, + "StatefulInteractable did not get selected."); + Assert.IsTrue(selectEntered, + "StatefulInteractable should have had a select enter."); + Assert.IsFalse(selectExited, + "StatefulInteractable should not have had a select exit."); + + // Reset to continue testing + selectEntered = false; + + // Release the first hand to deselect the cube + yield return rightHand.SetHandshape(HandshapeId.Open); + yield return RuntimeTestUtilities.WaitForUpdates(); + Assert.IsFalse(interactable.IsRaySelected, + "StatefulInteractable did not get de-RaySelected."); + Assert.IsFalse(interactable.isSelected, + "StatefulInteractable did not get deselected."); + Assert.IsTrue(interactable.interactorsSelecting.Count == 0, + "StatefulInteractable should not have any selecting interactors."); + Assert.IsFalse(isSelected, + "StatefulInteractable should not be selected."); + Assert.IsFalse(selectEntered, + "StatefulInteractable should not have had a select enter."); + Assert.IsTrue(selectExited, + "StatefulInteractable should have had a select exit."); + + // Reset to continue testing + selectExited = false; + + // Introduce the second hand + var leftHand = new TestHand(Handedness.Left); + yield return leftHand.Show(InputTestUtilities.InFrontOfUser(0.4f)); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Aim the second hand to hover the cube + yield return leftHand.AimAt(cube.transform.position); + yield return RuntimeTestUtilities.WaitForUpdates(); + Assert.IsTrue(interactable.IsRayHovered, + "StatefulInteractable did not stay RayHovered."); + Assert.IsTrue(interactable.isHovered, + "StatefulInteractable did not stay hovered."); + + Assert.IsTrue(interactable.HoveringRayInteractors.Count == 2, + "StatefulInteractable should have 2 hovering RayInteractors."); + Assert.IsTrue(interactable.interactorsHovering.Count == 2, + "StatefulInteractable should have 2 hovering interactors."); + + Assert.IsFalse(isSelected, + "StatefulInteractable should not be selected."); + Assert.IsFalse(selectEntered, + "StatefulInteractable should not have had a select enter."); + Assert.IsFalse(selectExited, + "StatefulInteractable should not have had a select exit."); + + // Pinch the first hand to select the cube + yield return rightHand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + Assert.IsTrue(interactable.IsRaySelected, + "StatefulInteractable did not get RaySelected."); + Assert.IsTrue(interactable.isSelected, + "StatefulInteractable did not get selected."); + Assert.IsTrue(interactable.interactorsSelecting.Count == 1, + "StatefulInteractable should only have 1 selecting interactor."); + Assert.IsTrue(isSelected, + "StatefulInteractable did not get selected."); + Assert.IsTrue(selectEntered, + "StatefulInteractable should have had a select enter."); + Assert.IsFalse(selectExited, + "StatefulInteractable should not have had a select exit."); + + // Reset to continue testing + selectEntered = false; + + // Pinch the second hand to select the cube + yield return leftHand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + Assert.IsTrue(interactable.IsRaySelected, + "StatefulInteractable did not stay RaySelected."); + Assert.IsTrue(interactable.isSelected, + "StatefulInteractable did not stay selected."); + Assert.IsTrue(isSelected, + "StatefulInteractable did not stay selected."); + Assert.IsTrue(selectEntered, + "StatefulInteractable should have had a select enter."); + + // Reset to continue testing + selectEntered = false; + + // Both hands are pinching, so we check the select state based on the mode + switch (selectMode) + { + case InteractableSelectMode.Single: + Assert.IsTrue(interactable.interactorsSelecting.Count == 1, + "StatefulInteractable should only have 1 selecting interactor."); + Assert.IsTrue(selectExited, + "StatefulInteractable should have had a select exit."); + // Reset to continue testing + selectExited = false; + break; + case InteractableSelectMode.Multiple: + Assert.IsTrue(interactable.interactorsSelecting.Count == 2, + "StatefulInteractable should have 2 selecting interactors."); + Assert.IsFalse(selectExited, + "StatefulInteractable should not have had a select exit."); + break; + default: + Assert.Fail($"Unhandled {nameof(InteractableSelectMode)}={selectMode}"); + break; + } + + TestHand firstReleasedHand; + TestHand secondReleasedHand; + if (releaseInSelectOrder) + { + firstReleasedHand = rightHand; + secondReleasedHand = leftHand; + } + else + { + firstReleasedHand = leftHand; + secondReleasedHand = rightHand; + } + + // Release a hand to deselect the cube + yield return firstReleasedHand.SetHandshape(HandshapeId.Open); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsFalse(selectEntered, + "StatefulInteractable should not have had a select enter."); + + // The first hand was no longer selecting in Single mode + // If we're releasing in the reverse order we selected, + // releasing the second pinch should release the select fully + if (!releaseInSelectOrder && selectMode == InteractableSelectMode.Single) + { + Assert.IsFalse(interactable.IsRaySelected, + "StatefulInteractable did not get de-RaySelected."); + Assert.IsFalse(interactable.isSelected, + "StatefulInteractable did not get deselected."); + Assert.IsTrue(interactable.interactorsSelecting.Count == 0, + "StatefulInteractable should not have any selecting interactors."); + Assert.IsFalse(isSelected, + "StatefulInteractable should not be selected."); + Assert.IsTrue(selectExited, + "StatefulInteractable should have had a select exit."); + // Reset to continue testing + selectExited = false; + } + // The first hand was no longer selecting in Single mode + // If we're releasing in the same order we selected, + // we should still be selected regardless of the mode + else + { + Assert.IsTrue(interactable.IsRaySelected, + "StatefulInteractable did not stay RaySelected."); + Assert.IsTrue(interactable.isSelected, + "StatefulInteractable did not stay selected."); + Assert.IsTrue(interactable.interactorsSelecting.Count == 1, + "StatefulInteractable should only have 1 selecting interactor."); + Assert.IsTrue(isSelected, + "StatefulInteractable should be selected."); + + if (selectMode == InteractableSelectMode.Multiple) + { + Assert.IsTrue(selectExited, + "StatefulInteractable should have had a select exit."); + // Reset to continue testing + selectExited = false; + } + else + { + // This select exit happened when the second hand pinched, releasing the first + Assert.IsFalse(selectExited, + "StatefulInteractable should not have had a select exit."); + } + } + + // Release the last hand to deselect the cube + yield return secondReleasedHand.SetHandshape(HandshapeId.Open); + yield return RuntimeTestUtilities.WaitForUpdates(); + Assert.IsFalse(interactable.IsRaySelected, + "StatefulInteractable did not get de-RaySelected."); + Assert.IsFalse(interactable.isSelected, + "StatefulInteractable did not get deselected."); + Assert.IsTrue(interactable.interactorsSelecting.Count == 0, + "StatefulInteractable should not have any selecting interactors."); + Assert.IsFalse(isSelected, + "StatefulInteractable should not be selected."); + Assert.IsFalse(selectEntered, + "StatefulInteractable should not have had a select enter."); + + if (!releaseInSelectOrder && selectMode == InteractableSelectMode.Single) + { + Assert.IsFalse(selectExited, + "StatefulInteractable should not have had a select exit."); + } + else + { + Assert.IsTrue(selectExited, + "StatefulInteractable should have had a select exit."); + // Reset to continue testing + selectExited = false; + } + + yield return RuntimeTestUtilities.WaitForUpdates(); + } + /// /// A dummy interactor used to test basic selection/toggle logic. /// From fc1c8bba77e7f507f95c3ef58a2cfcc0bd012d1f Mon Sep 17 00:00:00 2001 From: Kurtis Date: Thu, 16 Oct 2025 15:00:27 -0700 Subject: [PATCH 2/2] Add triggerOnRelease check and track click count --- .../Tests/Runtime/BasicInputTests.cs | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/org.mixedrealitytoolkit.input/Tests/Runtime/BasicInputTests.cs b/org.mixedrealitytoolkit.input/Tests/Runtime/BasicInputTests.cs index 3004d7ac2..1496648e7 100644 --- a/org.mixedrealitytoolkit.input/Tests/Runtime/BasicInputTests.cs +++ b/org.mixedrealitytoolkit.input/Tests/Runtime/BasicInputTests.cs @@ -313,6 +313,7 @@ public IEnumerator GazePinchSmokeTest() [UnityTest] public IEnumerator TestStatefulInteractableSelectMode( [Values(InteractableSelectMode.Single, InteractableSelectMode.Multiple)] InteractableSelectMode selectMode, + [Values(true, false)] bool triggerOnRelease, [Values(true, false)] bool releaseInSelectOrder) { GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); @@ -323,17 +324,28 @@ public IEnumerator TestStatefulInteractableSelectMode( bool isSelected = false; bool selectEntered = false; bool selectExited = false; + int clickCount = 0; + + void ResetState() + { + selectEntered = false; + selectExited = false; + clickCount = 0; + } // For this test, we won't use poke or grab selection interactable.DisableInteractorType(typeof(PokeInteractor)); interactable.DisableInteractorType(typeof(GrabInteractor)); interactable.selectMode = selectMode; + interactable.TriggerOnRelease = triggerOnRelease; - interactable.firstSelectEntered.AddListener((SelectEnterEventArgs) => { isSelected = true; }); - interactable.lastSelectExited.AddListener((SelectEnterEventArgs) => { isSelected = false; }); + interactable.firstSelectEntered.AddListener((_) => isSelected = true); + interactable.lastSelectExited.AddListener((_) => isSelected = false); - interactable.selectEntered.AddListener((SelectEnterEventArgs) => { selectEntered = true; }); - interactable.selectExited.AddListener((SelectEnterEventArgs) => { selectExited = true; }); + interactable.selectEntered.AddListener((_) => selectEntered = true); + interactable.selectExited.AddListener((_) => selectExited = true); + + interactable.OnClicked.AddListener(() => clickCount++); // Introduce the first hand var rightHand = new TestHand(Handedness.Right); @@ -380,9 +392,10 @@ public IEnumerator TestStatefulInteractableSelectMode( "StatefulInteractable should have had a select enter."); Assert.IsFalse(selectExited, "StatefulInteractable should not have had a select exit."); + Assert.AreEqual(triggerOnRelease ? 0 : 1, clickCount); // Reset to continue testing - selectEntered = false; + ResetState(); // Release the first hand to deselect the cube yield return rightHand.SetHandshape(HandshapeId.Open); @@ -399,9 +412,10 @@ public IEnumerator TestStatefulInteractableSelectMode( "StatefulInteractable should not have had a select enter."); Assert.IsTrue(selectExited, "StatefulInteractable should have had a select exit."); + Assert.AreEqual(triggerOnRelease ? 1 : 0, clickCount); // Reset to continue testing - selectExited = false; + ResetState(); // Introduce the second hand var leftHand = new TestHand(Handedness.Left); @@ -443,9 +457,10 @@ public IEnumerator TestStatefulInteractableSelectMode( "StatefulInteractable should have had a select enter."); Assert.IsFalse(selectExited, "StatefulInteractable should not have had a select exit."); + Assert.AreEqual(triggerOnRelease ? 0 : 1, clickCount); // Reset to continue testing - selectEntered = false; + ResetState(); // Pinch the second hand to select the cube yield return leftHand.SetHandshape(HandshapeId.Pinch); @@ -459,9 +474,6 @@ public IEnumerator TestStatefulInteractableSelectMode( Assert.IsTrue(selectEntered, "StatefulInteractable should have had a select enter."); - // Reset to continue testing - selectEntered = false; - // Both hands are pinching, so we check the select state based on the mode switch (selectMode) { @@ -470,20 +482,23 @@ public IEnumerator TestStatefulInteractableSelectMode( "StatefulInteractable should only have 1 selecting interactor."); Assert.IsTrue(selectExited, "StatefulInteractable should have had a select exit."); - // Reset to continue testing - selectExited = false; + Assert.AreEqual(1, clickCount); break; case InteractableSelectMode.Multiple: Assert.IsTrue(interactable.interactorsSelecting.Count == 2, "StatefulInteractable should have 2 selecting interactors."); Assert.IsFalse(selectExited, "StatefulInteractable should not have had a select exit."); + Assert.AreEqual(0, clickCount); break; default: Assert.Fail($"Unhandled {nameof(InteractableSelectMode)}={selectMode}"); break; } + // Reset to continue testing + ResetState(); + TestHand firstReleasedHand; TestHand secondReleasedHand; if (releaseInSelectOrder) @@ -519,8 +534,7 @@ public IEnumerator TestStatefulInteractableSelectMode( "StatefulInteractable should not be selected."); Assert.IsTrue(selectExited, "StatefulInteractable should have had a select exit."); - // Reset to continue testing - selectExited = false; + Assert.AreEqual(triggerOnRelease ? 1 : 0, clickCount); } // The first hand was no longer selecting in Single mode // If we're releasing in the same order we selected, @@ -535,13 +549,12 @@ public IEnumerator TestStatefulInteractableSelectMode( "StatefulInteractable should only have 1 selecting interactor."); Assert.IsTrue(isSelected, "StatefulInteractable should be selected."); + Assert.AreEqual(0, clickCount); if (selectMode == InteractableSelectMode.Multiple) { Assert.IsTrue(selectExited, "StatefulInteractable should have had a select exit."); - // Reset to continue testing - selectExited = false; } else { @@ -551,6 +564,9 @@ public IEnumerator TestStatefulInteractableSelectMode( } } + // Reset to continue testing + ResetState(); + // Release the last hand to deselect the cube yield return secondReleasedHand.SetHandshape(HandshapeId.Open); yield return RuntimeTestUtilities.WaitForUpdates(); @@ -574,10 +590,12 @@ public IEnumerator TestStatefulInteractableSelectMode( { Assert.IsTrue(selectExited, "StatefulInteractable should have had a select exit."); - // Reset to continue testing - selectExited = false; + Assert.AreEqual(triggerOnRelease ? 1 : 0, clickCount); } + // Reset to continue testing + ResetState(); + yield return RuntimeTestUtilities.WaitForUpdates(); }