namespace SceneTests.GameObjects; /// /// Pins the ComponentList contract: enumeration order, reordering via Move, and the /// FindMode search matrix across the hierarchy. /// [TestClass] [DoNotParallelize] public class ComponentListTest : SceneTest { /// /// GetAll must enumerate components in creation order, stably. /// [TestMethod] public void GetAllPreservesCreationOrder() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var a = go.Components.Create(); var b = go.Components.Create(); var c = go.Components.Create(); CollectionAssert.AreEqual( new Component[] { a, b, c }, go.Components.GetAll().ToArray() ); } /// /// Move shifts a component up or down the list by the given delta, and is a no-op /// for components that aren't in the list. /// [TestMethod] public void MoveReorders() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var a = go.Components.Create(); var b = go.Components.Create(); var c = go.Components.Create(); go.Components.Move( c, -2 ); CollectionAssert.AreEqual( new Component[] { c, a, b }, go.Components.GetAll().ToArray() ); go.Components.Move( c, 1 ); CollectionAssert.AreEqual( new Component[] { a, c, b }, go.Components.GetAll().ToArray() ); // Moving a foreign component is ignored var other = scene.CreateObject().Components.Create(); go.Components.Move( other, 1 ); Assert.AreEqual( 3, go.Components.Count ); } /// /// The default Get skips disabled components; FindMode.Disabled finds only them, /// and EverythingInSelf finds both. /// [TestMethod] public void EnabledDisabledFilters() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var enabled = go.Components.Create(); var disabled = go.Components.Create(); disabled.Enabled = false; Assert.AreEqual( enabled, go.Components.Get() ); Assert.IsNull( go.Components.Get() ); Assert.AreEqual( disabled, go.Components.Get( includeDisabled: true ) ); Assert.AreEqual( disabled, go.Components.Get( FindMode.DisabledInSelf ) ); Assert.AreEqual( 2, go.Components.GetAll( FindMode.EverythingInSelf ).Count() ); } /// /// The hierarchy search directions must look exactly where they say: parent vs /// ancestors, children vs descendants, with and without self. /// [TestMethod] public void HierarchySearchDirections() { var scene = new Scene(); using var sceneScope = scene.Push(); var grandparent = scene.CreateObject(); var gpComp = grandparent.Components.Create(); var parent = new GameObject( grandparent ); var parentComp = parent.Components.Create(); var child = new GameObject( parent ); var childComp = child.Components.Create(); var grandchild = new GameObject( child ); var gcComp = grandchild.Components.Create(); // InParent looks one level up only Assert.AreEqual( parentComp, child.Components.GetInParent() ); // InAncestors finds the nearest ancestor, not self Assert.AreEqual( childComp, grandchild.Components.GetInAncestors() ); // InChildren looks one level down only Assert.AreEqual( childComp, parent.Components.GetInChildren() ); // InDescendants searches the whole subtree, not self Assert.AreEqual( parentComp, grandparent.Components.GetInDescendants() ); // OrSelf variants prefer self Assert.AreEqual( childComp, child.Components.GetInAncestorsOrSelf() ); Assert.AreEqual( childComp, child.Components.GetInDescendantsOrSelf() ); // GetAll over descendants returns every component in the subtree var all = grandparent.Components.GetAll( FindMode.EnabledInSelfAndDescendants ).ToArray(); Assert.AreEqual( 4, all.Length ); } /// /// Hierarchy searches must respect enabled state: a disabled object's components /// aren't found unless disabled finds are requested. /// [TestMethod] public void HierarchySearchRespectsEnabledState() { var scene = new Scene(); using var sceneScope = scene.Push(); var parent = scene.CreateObject(); var child = new GameObject( parent ); var childComp = child.Components.Create(); child.Enabled = false; Assert.IsNull( parent.Components.GetInDescendants() ); Assert.AreEqual( childComp, parent.Components.GetInDescendants( includeDisabled: true ) ); } /// /// TryGet returns whether the component was found and outputs it. /// [TestMethod] public void TryGet() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var a = go.Components.Create(); Assert.IsTrue( go.Components.TryGet( out var found ) ); Assert.AreEqual( a, found ); Assert.IsFalse( go.Components.TryGet( out var missing ) ); Assert.IsNull( missing ); } /// /// GetOrCreate returns the existing component when present and creates one when not. /// [TestMethod] public void GetOrCreate() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var created = go.Components.GetOrCreate(); Assert.IsNotNull( created ); Assert.AreEqual( 1, go.Components.Count ); var found = go.Components.GetOrCreate(); Assert.AreEqual( created, found ); Assert.AreEqual( 1, go.Components.Count ); } /// /// Components are also addressable by their Guid. /// [TestMethod] public void GetById() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var a = go.Components.Create(); Assert.AreEqual( a, go.Components.Get( a.Id ) ); Assert.IsNull( go.Components.Get( System.Guid.NewGuid() ) ); } /// /// Move clamps at the list boundaries instead of wrapping or throwing. /// [TestMethod] public void MoveClampsAtBounds() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var a = go.Components.Create(); var b = go.Components.Create(); var c = go.Components.Create(); go.Components.Move( a, -10 ); CollectionAssert.AreEqual( new Component[] { a, b, c }, go.Components.GetAll().ToArray() ); go.Components.Move( a, 10 ); CollectionAssert.AreEqual( new Component[] { b, c, a }, go.Components.GetAll().ToArray() ); } /// /// MoveToIndex swaps the component with whatever occupies the target index - it /// does not shift the elements between. Pinned as the editor reorder relies on it. /// [TestMethod] public void MoveToIndexSwaps() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var a = go.Components.Create(); var b = go.Components.Create(); var c = go.Components.Create(); go.Components.MoveToIndex( a, 2 ); CollectionAssert.AreEqual( new Component[] { c, b, a }, go.Components.GetAll().ToArray() ); } /// /// Event execution through non-default FindModes must reach the right relatives: /// the parent for InParent, the whole chain for InAncestors, and disabled /// components only when asked. /// [TestMethod] public void RunEventThroughHierarchyModes() { var scene = new Scene(); using var sceneScope = scene.Push(); var grandparent = scene.CreateObject(); var gpReceiver = grandparent.Components.Create(); var parent = new GameObject( grandparent ); var parentReceiver = parent.Components.Create(); var child = new GameObject( parent ); var childReceiver = child.Components.Create(); // InParent reaches the direct parent only child.RunEvent( x => x.Bump(), FindMode.Enabled | FindMode.InParent ); Assert.AreEqual( 1, parentReceiver.Bumps ); Assert.AreEqual( 0, gpReceiver.Bumps ); Assert.AreEqual( 0, childReceiver.Bumps ); // InAncestors reaches the whole chain upwards child.RunEvent( x => x.Bump(), FindMode.Enabled | FindMode.InAncestors ); Assert.AreEqual( 2, parentReceiver.Bumps ); Assert.AreEqual( 1, gpReceiver.Bumps ); Assert.AreEqual( 0, childReceiver.Bumps ); // Disabled components are reached only when the find includes them childReceiver.Enabled = false; child.RunEvent( x => x.Bump(), FindMode.Enabled | FindMode.InSelf ); Assert.AreEqual( 0, childReceiver.Bumps ); child.RunEvent( x => x.Bump(), FindMode.Enabled | FindMode.Disabled | FindMode.InSelf ); Assert.AreEqual( 1, childReceiver.Bumps ); } public interface IBumpEvent { void Bump(); } public class BumpReceiver : Component, IBumpEvent { public int Bumps; public void Bump() => Bumps++; } public class OrderedA : Component { } public class OrderedB : Component { } public class OrderedC : Component { } }