namespace SceneTests.GameObjects; /// /// Pins destroy edge cases not covered by Destroy.cs: idempotent destroys, the /// IsDestroyed/IsValid timeline around the deferred delete queue, component /// teardown, and Clear's handling of DontDestroyOnLoad children during scene /// transitions. /// [TestClass] [DoNotParallelize] public class DestroyCoverageTest : SceneTest { /// /// Calling Destroy twice queues only one delete - the object disappears /// exactly once and the second call is a harmless no-op. /// [TestMethod] public void DestroyIsIdempotent() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); go.Destroy(); go.Destroy(); scene.ProcessDeletes(); Assert.IsFalse( go.IsValid ); Assert.AreEqual( 0, scene.Children.Count ); Assert.AreEqual( 0, scene.Directory.GameObjectCount ); } /// /// After Destroy the object reports IsDestroyed immediately, but stays /// valid and in the scene until the delete queue is flushed by /// ProcessDeletes (no full GameTick required). /// [TestMethod] public void IsDestroyedImmediateValidUntilProcessDeletes() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); go.Destroy(); Assert.IsTrue( go.IsDestroyed ); Assert.IsTrue( go.IsValid, "the object must stay valid until deletes are processed" ); Assert.IsNotNull( go.Scene ); scene.ProcessDeletes(); Assert.IsTrue( go.IsDestroyed ); Assert.IsFalse( go.IsValid ); Assert.IsNull( go.Scene ); } /// /// DestroyImmediate tears down the object's components synchronously - they /// are invalid and gone from the list afterwards. /// [TestMethod] public void DestroyImmediateDestroysComponents() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); go.DestroyImmediate(); Assert.IsFalse( comp.IsValid ); Assert.AreEqual( 0, go.Components.Count ); Assert.AreEqual( 0, scene.Directory.ComponentCount ); } /// /// Clear destroys all components and children synchronously but leaves the /// object itself alive and in the scene. /// [TestMethod] public void ClearDestroysComponentsAndChildren() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); var child = new GameObject( go ); go.Clear(); Assert.IsTrue( go.IsValid ); Assert.IsFalse( comp.IsValid ); Assert.IsFalse( child.IsValid ); Assert.AreEqual( 0, go.Components.Count ); Assert.AreEqual( 0, go.Children.Count ); } /// /// The scene-transition Clear (includeSaved=false) spares children flagged /// DontDestroyOnLoad, while a full Clear removes them too. /// [TestMethod] public void ClearSparesDontDestroyOnLoadChildren() { var scene = new Scene(); using var sceneScope = scene.Push(); var parent = scene.CreateObject(); var keeper = new GameObject( parent, true, "Keeper" ); keeper.Flags |= GameObjectFlags.DontDestroyOnLoad; var doomed = new GameObject( parent, true, "Doomed" ); parent.Clear( includeSaved: false ); Assert.IsTrue( keeper.IsValid, "DontDestroyOnLoad children must survive a scene-transition clear" ); Assert.IsFalse( doomed.IsValid ); Assert.AreEqual( 1, parent.Children.Count ); parent.Clear( includeSaved: true ); Assert.IsFalse( keeper.IsValid, "a full clear removes even DontDestroyOnLoad children" ); Assert.AreEqual( 0, parent.Children.Count ); } } /// /// Bare component used to observe teardown in the destroy tests. /// public class DestroyCoverageProbe : Component { }