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