using System;
namespace SceneTests.GameObjects;
///
/// Pins ComponentList behaviour not covered by ComponentLists.cs: creating by
/// TypeDescription, the FirstOrDefault helper, the ChildrenOrSelf/ParentOrSelf
/// search helpers, the IComponentLister default methods, ForEach robustness and
/// queries against destroyed objects.
///
[TestClass]
[DoNotParallelize]
public class ComponentListCoverageTest : SceneTest
{
///
/// Create(TypeDescription) instantiates the component type, honouring the
/// startEnabled flag.
///
[TestMethod]
public void CreateByTypeDescription()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var type = Game.TypeLibrary.GetType( typeof( ListCoverageProbe ) );
Assert.IsNotNull( type, "the test assembly must be registered in the global TypeLibrary" );
var enabled = go.Components.Create( type );
Assert.IsInstanceOfType( enabled, typeof( ListCoverageProbe ) );
Assert.IsTrue( enabled.Enabled );
var disabled = go.Components.Create( type, false );
Assert.IsFalse( disabled.Enabled );
Assert.AreEqual( 2, go.Components.Count );
}
///
/// Create(TypeDescription) refuses non-component types by returning null
/// without adding anything to the list.
///
[TestMethod]
public void CreateByTypeDescriptionRejectsNonComponents()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var type = Game.TypeLibrary.GetType( typeof( GameObject ) );
Assert.IsNotNull( type );
var created = go.Components.Create( type );
Assert.IsNull( created );
Assert.AreEqual( 0, go.Components.Count );
}
///
/// FirstOrDefault runs the predicate over the raw component list, returning
/// null on an object that never had components.
///
[TestMethod]
public void FirstOrDefaultPredicate()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var empty = scene.CreateObject();
Assert.IsNull( empty.Components.FirstOrDefault( x => true ) );
var go = scene.CreateObject();
var a = go.Components.Create();
var b = go.Components.Create();
Assert.AreEqual( b, go.Components.FirstOrDefault( x => x is ListCoverageOther ) );
Assert.IsNull( go.Components.FirstOrDefault( x => false ) );
}
///
/// GetInChildrenOrSelf prefers a component on the object itself and falls
/// back to immediate children - but never grandchildren.
///
[TestMethod]
public void GetInChildrenOrSelf()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
var child = new GameObject( go );
var grandchild = new GameObject( child );
var gcComp = grandchild.Components.Create();
Assert.IsNull( go.Components.GetInChildrenOrSelf(), "grandchildren are out of range for InChildren" );
var childComp = child.Components.Create();
Assert.AreEqual( childComp, go.Components.GetInChildrenOrSelf() );
var selfComp = go.Components.Create();
Assert.AreEqual( selfComp, go.Components.GetInChildrenOrSelf() );
}
///
/// GetInParentOrSelf prefers a component on the object itself and falls
/// back to the direct parent - but never grandparents.
///
[TestMethod]
public void GetInParentOrSelf()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var grandparent = scene.CreateObject();
var parent = new GameObject( grandparent );
var go = new GameObject( parent );
var gpComp = grandparent.Components.Create();
Assert.IsNull( go.Components.GetInParentOrSelf(), "grandparents are out of range for InParent" );
var parentComp = parent.Components.Create();
Assert.AreEqual( parentComp, go.Components.GetInParentOrSelf() );
var selfComp = go.Components.Create();
Assert.AreEqual( selfComp, go.Components.GetInParentOrSelf() );
}
///
/// GameObject implements IComponentLister, whose default interface methods
/// (Create, Get, TryGet, GetAll, GetOrCreate) all route to the component list.
///
[TestMethod]
public void ComponentListerDefaultMethods()
{
var scene = new Scene();
using var sceneScope = scene.Push();
IComponentLister lister = scene.CreateObject();
var created = lister.Create();
Assert.IsNotNull( created );
Assert.AreEqual( created, lister.Get() );
Assert.IsTrue( lister.TryGet( out var found ) );
Assert.AreEqual( created, found );
Assert.IsFalse( lister.TryGet( out var missing ) );
Assert.IsNull( missing );
Assert.AreEqual( 1, lister.GetAll().Count() );
Assert.AreEqual( created, lister.GetOrCreate() );
Assert.AreEqual( 1, lister.Components.Count, "GetOrCreate must not duplicate an existing component" );
}
///
/// ForEach swallows exceptions thrown by the action per-component and keeps
/// going, so one broken component can't starve the rest of a callback.
///
[TestMethod]
public void ForEachSwallowsExceptions()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
go.Components.Create();
go.Components.Create();
int calls = 0;
go.Components.ForEach( "Test", true, c =>
{
calls++;
throw new InvalidOperationException( "intentional test failure" );
} );
Assert.AreEqual( 2, calls, "every component must still be visited" );
}
///
/// The generic ForEach only invokes the action for components assignable to
/// the type argument.
///
[TestMethod]
public void ForEachGenericFiltersByType()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
go.Components.Create();
go.Components.Create();
int calls = 0;
go.Components.ForEach( "Test", true, c => calls++ );
Assert.AreEqual( 1, calls );
}
///
/// RunEvent with the default find mode takes the enabled-in-self-and-descendants
/// fast path: it reaches the object and enabled descendants, skipping
/// components under disabled children.
///
[TestMethod]
public void RunEventDefaultReachesEnabledDescendants()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var root = scene.CreateObject();
var rootReceiver = root.Components.Create();
var child = new GameObject( root );
var childReceiver = child.Components.Create();
var disabledChild = new GameObject( root, false );
var disabledReceiver = disabledChild.Components.Create();
root.RunEvent( x => x.Poke() );
Assert.AreEqual( 1, rootReceiver.Pokes );
Assert.AreEqual( 1, childReceiver.Pokes );
Assert.AreEqual( 0, disabledReceiver.Pokes );
}
///
/// RunEvent with EverythingInDescendants reaches disabled descendants too,
/// but not the object itself (InSelf is not part of the mode).
///
[TestMethod]
public void RunEventEverythingInDescendantsIncludesDisabled()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var root = scene.CreateObject();
var rootReceiver = root.Components.Create();
var disabledChild = new GameObject( root, false );
var disabledReceiver = disabledChild.Components.Create();
root.RunEvent( x => x.Poke(), FindMode.EverythingInDescendants );
Assert.AreEqual( 0, rootReceiver.Pokes, "self is not part of EverythingInDescendants" );
Assert.AreEqual( 1, disabledReceiver.Pokes );
}
///
/// Once an object is marked destroyed - even before the deferred delete is
/// flushed - component searches return nothing.
///
[TestMethod]
public void QueriesOnDestroyedObjectAreEmpty()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
go.Components.Create();
go.Destroy();
Assert.IsTrue( go.IsDestroyed );
Assert.AreEqual( 0, go.Components.GetAll( FindMode.EverythingInSelf ).Count() );
Assert.IsNull( go.Components.Get( FindMode.EverythingInSelf ) );
scene.ProcessDeletes();
Assert.AreEqual( 0, go.Components.GetAll( FindMode.EverythingInSelf ).Count() );
}
///
/// Event interface used by the RunEvent tests.
///
public interface IPokeEvent
{
///
/// Receives the test event.
///
void Poke();
}
///
/// Component that counts how many times it received the poke event.
///
public class PokeReceiverComponent : Component, IPokeEvent
{
public int Pokes;
///
/// Counts the received event.
///
public void Poke() => Pokes++;
}
///
/// Bare component used for list queries.
///
public class ListCoverageOther : Component
{
}
}
///
/// Bare component used for TypeDescription creation - top level so the global
/// TypeLibrary can resolve it cleanly.
///
public class ListCoverageProbe : Component
{
}