@@ -310,6 +310,277 @@ public IEnumerator GazePinchSmokeTest()
310310 Assert . IsTrue ( interactable . IsGazePinchHovered ) ;
311311 }
312312
313+ [ UnityTest ]
314+ public IEnumerator TestStatefulInteractableSelectMode (
315+ [ Values ( InteractableSelectMode . Single , InteractableSelectMode . Multiple ) ] InteractableSelectMode selectMode ,
316+ [ Values ( true , false ) ] bool releaseInSelectOrder )
317+ {
318+ GameObject cube = GameObject . CreatePrimitive ( PrimitiveType . Cube ) ;
319+ StatefulInteractable interactable = cube . AddComponent < StatefulInteractable > ( ) ;
320+ cube . transform . position = InputTestUtilities . InFrontOfUser ( new Vector3 ( 0.2f , 0.2f , 0.5f ) ) ;
321+ cube . transform . localScale = Vector3 . one * 0.1f ;
322+
323+ bool isSelected = false ;
324+ bool selectEntered = false ;
325+ bool selectExited = false ;
326+
327+ // For this test, we won't use poke or grab selection
328+ interactable . DisableInteractorType ( typeof ( PokeInteractor ) ) ;
329+ interactable . DisableInteractorType ( typeof ( GrabInteractor ) ) ;
330+ interactable . selectMode = selectMode ;
331+
332+ interactable . firstSelectEntered . AddListener ( ( SelectEnterEventArgs ) => { isSelected = true ; } ) ;
333+ interactable . lastSelectExited . AddListener ( ( SelectEnterEventArgs ) => { isSelected = false ; } ) ;
334+
335+ interactable . selectEntered . AddListener ( ( SelectEnterEventArgs ) => { selectEntered = true ; } ) ;
336+ interactable . selectExited . AddListener ( ( SelectEnterEventArgs ) => { selectExited = true ; } ) ;
337+
338+ // Introduce the first hand
339+ var rightHand = new TestHand ( Handedness . Right ) ;
340+ yield return rightHand . Show ( InputTestUtilities . InFrontOfUser ( 0.4f ) ) ;
341+ yield return RuntimeTestUtilities . WaitForUpdates ( ) ;
342+
343+ Assert . IsFalse ( interactable . IsRayHovered ,
344+ "StatefulInteractable was already RayHovered." ) ;
345+ Assert . IsFalse ( interactable . isHovered ,
346+ "StatefulInteractable was already hovered." ) ;
347+
348+ // Aim the first hand to hover the cube
349+ yield return rightHand . AimAt ( cube . transform . position ) ;
350+ yield return RuntimeTestUtilities . WaitForUpdates ( ) ;
351+ Assert . IsTrue ( interactable . IsRayHovered ,
352+ "StatefulInteractable did not get RayHovered." ) ;
353+ Assert . IsTrue ( interactable . isHovered ,
354+ "StatefulInteractable did not get hovered." ) ;
355+
356+ Assert . IsTrue ( interactable . HoveringRayInteractors . Count == 1 ,
357+ "StatefulInteractable should only have 1 hovering RayInteractor." ) ;
358+ Assert . IsTrue ( interactable . interactorsHovering . Count == 1 ,
359+ "StatefulInteractable should only have 1 hovering interactor." ) ;
360+
361+ Assert . IsFalse ( isSelected ,
362+ "StatefulInteractable should not be selected." ) ;
363+ Assert . IsFalse ( selectEntered ,
364+ "StatefulInteractable should not have had a select enter." ) ;
365+ Assert . IsFalse ( selectExited ,
366+ "StatefulInteractable should not have had a select exit." ) ;
367+
368+ // Pinch the first hand to select the cube
369+ yield return rightHand . SetHandshape ( HandshapeId . Pinch ) ;
370+ yield return RuntimeTestUtilities . WaitForUpdates ( ) ;
371+ Assert . IsTrue ( interactable . IsRaySelected ,
372+ "StatefulInteractable did not get RaySelected." ) ;
373+ Assert . IsTrue ( interactable . isSelected ,
374+ "StatefulInteractable did not get selected." ) ;
375+ Assert . IsTrue ( interactable . interactorsSelecting . Count == 1 ,
376+ "StatefulInteractable should only have 1 selecting interactor." ) ;
377+ Assert . IsTrue ( isSelected ,
378+ "StatefulInteractable did not get selected." ) ;
379+ Assert . IsTrue ( selectEntered ,
380+ "StatefulInteractable should have had a select enter." ) ;
381+ Assert . IsFalse ( selectExited ,
382+ "StatefulInteractable should not have had a select exit." ) ;
383+
384+ // Reset to continue testing
385+ selectEntered = false ;
386+
387+ // Release the first hand to deselect the cube
388+ yield return rightHand . SetHandshape ( HandshapeId . Open ) ;
389+ yield return RuntimeTestUtilities . WaitForUpdates ( ) ;
390+ Assert . IsFalse ( interactable . IsRaySelected ,
391+ "StatefulInteractable did not get de-RaySelected." ) ;
392+ Assert . IsFalse ( interactable . isSelected ,
393+ "StatefulInteractable did not get deselected." ) ;
394+ Assert . IsTrue ( interactable . interactorsSelecting . Count == 0 ,
395+ "StatefulInteractable should not have any selecting interactors." ) ;
396+ Assert . IsFalse ( isSelected ,
397+ "StatefulInteractable should not be selected." ) ;
398+ Assert . IsFalse ( selectEntered ,
399+ "StatefulInteractable should not have had a select enter." ) ;
400+ Assert . IsTrue ( selectExited ,
401+ "StatefulInteractable should have had a select exit." ) ;
402+
403+ // Reset to continue testing
404+ selectExited = false ;
405+
406+ // Introduce the second hand
407+ var leftHand = new TestHand ( Handedness . Left ) ;
408+ yield return leftHand . Show ( InputTestUtilities . InFrontOfUser ( 0.4f ) ) ;
409+ yield return RuntimeTestUtilities . WaitForUpdates ( ) ;
410+
411+ // Aim the second hand to hover the cube
412+ yield return leftHand . AimAt ( cube . transform . position ) ;
413+ yield return RuntimeTestUtilities . WaitForUpdates ( ) ;
414+ Assert . IsTrue ( interactable . IsRayHovered ,
415+ "StatefulInteractable did not stay RayHovered." ) ;
416+ Assert . IsTrue ( interactable . isHovered ,
417+ "StatefulInteractable did not stay hovered." ) ;
418+
419+ Assert . IsTrue ( interactable . HoveringRayInteractors . Count == 2 ,
420+ "StatefulInteractable should have 2 hovering RayInteractors." ) ;
421+ Assert . IsTrue ( interactable . interactorsHovering . Count == 2 ,
422+ "StatefulInteractable should have 2 hovering interactors." ) ;
423+
424+ Assert . IsFalse ( isSelected ,
425+ "StatefulInteractable should not be selected." ) ;
426+ Assert . IsFalse ( selectEntered ,
427+ "StatefulInteractable should not have had a select enter." ) ;
428+ Assert . IsFalse ( selectExited ,
429+ "StatefulInteractable should not have had a select exit." ) ;
430+
431+ // Pinch the first hand to select the cube
432+ yield return rightHand . SetHandshape ( HandshapeId . Pinch ) ;
433+ yield return RuntimeTestUtilities . WaitForUpdates ( ) ;
434+ Assert . IsTrue ( interactable . IsRaySelected ,
435+ "StatefulInteractable did not get RaySelected." ) ;
436+ Assert . IsTrue ( interactable . isSelected ,
437+ "StatefulInteractable did not get selected." ) ;
438+ Assert . IsTrue ( interactable . interactorsSelecting . Count == 1 ,
439+ "StatefulInteractable should only have 1 selecting interactor." ) ;
440+ Assert . IsTrue ( isSelected ,
441+ "StatefulInteractable did not get selected." ) ;
442+ Assert . IsTrue ( selectEntered ,
443+ "StatefulInteractable should have had a select enter." ) ;
444+ Assert . IsFalse ( selectExited ,
445+ "StatefulInteractable should not have had a select exit." ) ;
446+
447+ // Reset to continue testing
448+ selectEntered = false ;
449+
450+ // Pinch the second hand to select the cube
451+ yield return leftHand . SetHandshape ( HandshapeId . Pinch ) ;
452+ yield return RuntimeTestUtilities . WaitForUpdates ( ) ;
453+ Assert . IsTrue ( interactable . IsRaySelected ,
454+ "StatefulInteractable did not stay RaySelected." ) ;
455+ Assert . IsTrue ( interactable . isSelected ,
456+ "StatefulInteractable did not stay selected." ) ;
457+ Assert . IsTrue ( isSelected ,
458+ "StatefulInteractable did not stay selected." ) ;
459+ Assert . IsTrue ( selectEntered ,
460+ "StatefulInteractable should have had a select enter." ) ;
461+
462+ // Reset to continue testing
463+ selectEntered = false ;
464+
465+ // Both hands are pinching, so we check the select state based on the mode
466+ switch ( selectMode )
467+ {
468+ case InteractableSelectMode . Single :
469+ Assert . IsTrue ( interactable . interactorsSelecting . Count == 1 ,
470+ "StatefulInteractable should only have 1 selecting interactor." ) ;
471+ Assert . IsTrue ( selectExited ,
472+ "StatefulInteractable should have had a select exit." ) ;
473+ // Reset to continue testing
474+ selectExited = false ;
475+ break ;
476+ case InteractableSelectMode . Multiple :
477+ Assert . IsTrue ( interactable . interactorsSelecting . Count == 2 ,
478+ "StatefulInteractable should have 2 selecting interactors." ) ;
479+ Assert . IsFalse ( selectExited ,
480+ "StatefulInteractable should not have had a select exit." ) ;
481+ break ;
482+ default :
483+ Assert . Fail ( $ "Unhandled { nameof ( InteractableSelectMode ) } ={ selectMode } ") ;
484+ break ;
485+ }
486+
487+ TestHand firstReleasedHand ;
488+ TestHand secondReleasedHand ;
489+ if ( releaseInSelectOrder )
490+ {
491+ firstReleasedHand = rightHand ;
492+ secondReleasedHand = leftHand ;
493+ }
494+ else
495+ {
496+ firstReleasedHand = leftHand ;
497+ secondReleasedHand = rightHand ;
498+ }
499+
500+ // Release a hand to deselect the cube
501+ yield return firstReleasedHand . SetHandshape ( HandshapeId . Open ) ;
502+ yield return RuntimeTestUtilities . WaitForUpdates ( ) ;
503+
504+ Assert . IsFalse ( selectEntered ,
505+ "StatefulInteractable should not have had a select enter." ) ;
506+
507+ // The first hand was no longer selecting in Single mode
508+ // If we're releasing in the reverse order we selected,
509+ // releasing the second pinch should release the select fully
510+ if ( ! releaseInSelectOrder && selectMode == InteractableSelectMode . Single )
511+ {
512+ Assert . IsFalse ( interactable . IsRaySelected ,
513+ "StatefulInteractable did not get de-RaySelected." ) ;
514+ Assert . IsFalse ( interactable . isSelected ,
515+ "StatefulInteractable did not get deselected." ) ;
516+ Assert . IsTrue ( interactable . interactorsSelecting . Count == 0 ,
517+ "StatefulInteractable should not have any selecting interactors." ) ;
518+ Assert . IsFalse ( isSelected ,
519+ "StatefulInteractable should not be selected." ) ;
520+ Assert . IsTrue ( selectExited ,
521+ "StatefulInteractable should have had a select exit." ) ;
522+ // Reset to continue testing
523+ selectExited = false ;
524+ }
525+ // The first hand was no longer selecting in Single mode
526+ // If we're releasing in the same order we selected,
527+ // we should still be selected regardless of the mode
528+ else
529+ {
530+ Assert . IsTrue ( interactable . IsRaySelected ,
531+ "StatefulInteractable did not stay RaySelected." ) ;
532+ Assert . IsTrue ( interactable . isSelected ,
533+ "StatefulInteractable did not stay selected." ) ;
534+ Assert . IsTrue ( interactable . interactorsSelecting . Count == 1 ,
535+ "StatefulInteractable should only have 1 selecting interactor." ) ;
536+ Assert . IsTrue ( isSelected ,
537+ "StatefulInteractable should be selected." ) ;
538+
539+ if ( selectMode == InteractableSelectMode . Multiple )
540+ {
541+ Assert . IsTrue ( selectExited ,
542+ "StatefulInteractable should have had a select exit." ) ;
543+ // Reset to continue testing
544+ selectExited = false ;
545+ }
546+ else
547+ {
548+ // This select exit happened when the second hand pinched, releasing the first
549+ Assert . IsFalse ( selectExited ,
550+ "StatefulInteractable should not have had a select exit." ) ;
551+ }
552+ }
553+
554+ // Release the last hand to deselect the cube
555+ yield return secondReleasedHand . SetHandshape ( HandshapeId . Open ) ;
556+ yield return RuntimeTestUtilities . WaitForUpdates ( ) ;
557+ Assert . IsFalse ( interactable . IsRaySelected ,
558+ "StatefulInteractable did not get de-RaySelected." ) ;
559+ Assert . IsFalse ( interactable . isSelected ,
560+ "StatefulInteractable did not get deselected." ) ;
561+ Assert . IsTrue ( interactable . interactorsSelecting . Count == 0 ,
562+ "StatefulInteractable should not have any selecting interactors." ) ;
563+ Assert . IsFalse ( isSelected ,
564+ "StatefulInteractable should not be selected." ) ;
565+ Assert . IsFalse ( selectEntered ,
566+ "StatefulInteractable should not have had a select enter." ) ;
567+
568+ if ( ! releaseInSelectOrder && selectMode == InteractableSelectMode . Single )
569+ {
570+ Assert . IsFalse ( selectExited ,
571+ "StatefulInteractable should not have had a select exit." ) ;
572+ }
573+ else
574+ {
575+ Assert . IsTrue ( selectExited ,
576+ "StatefulInteractable should have had a select exit." ) ;
577+ // Reset to continue testing
578+ selectExited = false ;
579+ }
580+
581+ yield return RuntimeTestUtilities . WaitForUpdates ( ) ;
582+ }
583+
313584 /// <summary>
314585 /// A dummy interactor used to test basic selection/toggle logic.
315586 /// </summary>
0 commit comments