using System; using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace SceneTests.Components; /// /// Tests for Component.Serialize.cs - what a component writes into JSON, which /// flags suppress serialization entirely, and how deserialization applies (or /// preserves) values. /// [TestClass] [DoNotParallelize] public class ComponentSerializeTest : SceneTest { /// /// Serialize writes the type/id/enabled/flags header and every [Property] /// member - including public fields and private setters - while skipping /// [JsonIgnore] members and plain properties without [Property]. /// [TestMethod] public void SerializeWritesHeaderAndProperties() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); comp.Number = 5; comp.Text = "hello"; comp.Position = new Vector3( 1, 2, 3 ); comp.Counter = 9; comp.SetSecret( 42 ); comp.Ignored = 4; comp.NotAProperty = 4; var json = (JsonObject)comp.Serialize(); StringAssert.Contains( (string)json["__type"], "SerializeDataComponent" ); Assert.AreEqual( comp.Id, (Guid)json["__guid"] ); Assert.IsTrue( (bool)json["__enabled"] ); Assert.AreEqual( 5, (int)json["Number"] ); Assert.AreEqual( "hello", (string)json["Text"] ); Assert.AreEqual( 9, (int)json["Counter"] ); Assert.AreEqual( 42, (int)json["Secret"] ); Assert.IsFalse( json.ContainsKey( "Ignored" ) ); Assert.IsFalse( json.ContainsKey( "NotAProperty" ) ); } /// /// Components flagged NotSaved or NotCloned serialize to null - they are /// omitted entirely from saved scenes and clones. /// [TestMethod] public void SerializeReturnsNullForSuppressedFlags() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var notSaved = go.Components.Create(); notSaved.Flags |= ComponentFlags.NotSaved; Assert.IsNull( notSaved.Serialize() ); var notCloned = go.Components.Create(); notCloned.Flags |= ComponentFlags.NotCloned; Assert.IsNull( notCloned.Serialize() ); } /// /// With SkipNulls the serializer omits null reference properties instead of /// writing explicit nulls; without it the key is present. /// [TestMethod] public void SerializeSkipNullsOmitsNullProperties() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); comp.Text = null; comp.Number = 5; var plain = (JsonObject)comp.Serialize(); Assert.IsTrue( plain.ContainsKey( "Text" ) ); var skipped = (JsonObject)comp.Serialize( new GameObject.SerializeOptions { SkipNulls = true } ); Assert.IsFalse( skipped.ContainsKey( "Text" ) ); Assert.AreEqual( 5, (int)skipped["Number"] ); } /// /// A serialized component can be deserialized into a fresh instance, /// restoring its id, enabled state and every [Property] member - including /// public fields and properties with private setters. /// [TestMethod] public void RoundTripRestoresProperties() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); comp.Number = 5; comp.Text = "hello"; comp.Position = new Vector3( 1, 2, 3 ); comp.Counter = 9; comp.SetSecret( 42 ); var originalId = comp.Id; var json = (JsonObject)comp.Serialize(); // free the id so the deserialized copy can claim it comp.Destroy(); var go2 = scene.CreateObject(); var copy = go2.Components.Create( false ); copy.DeserializeImmediately( json ); Assert.AreEqual( originalId, copy.Id ); Assert.IsTrue( copy.Enabled ); Assert.AreEqual( 5, copy.Number ); Assert.AreEqual( "hello", copy.Text ); Assert.AreEqual( new Vector3( 1, 2, 3 ), copy.Position ); Assert.AreEqual( 9, copy.Counter ); Assert.AreEqual( 42, copy.Secret ); } /// /// Properties absent from the JSON are left at their current value - older /// save files must not clobber newer code-defined defaults. /// [TestMethod] public void DeserializePreservesValuesForAbsentKeys() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); comp.Text = "from json"; var json = (JsonObject)comp.Serialize(); json.Remove( "Number" ); comp.Destroy(); var go2 = scene.CreateObject(); var copy = go2.Components.Create( false ); copy.Number = 42; copy.DeserializeImmediately( json ); Assert.AreEqual( 42, copy.Number ); Assert.AreEqual( "from json", copy.Text ); } /// /// A disabled component round-trips its enabled state - the deserialized /// copy comes back disabled. /// [TestMethod] public void DeserializeAppliesEnabledState() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create( false ); var json = (JsonObject)comp.Serialize(); Assert.IsFalse( (bool)json["__enabled"] ); comp.Destroy(); var go2 = scene.CreateObject(); var copy = go2.Components.Create( false ); copy.DeserializeImmediately( json ); Assert.IsFalse( copy.Enabled ); Assert.IsFalse( copy.Active ); } /// /// One property setter throwing during deserialization must not abort the /// whole component - the remaining properties are still applied. /// [TestMethod] public void DeserializeSurvivesThrowingSetter() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); var json = (JsonObject)comp.Serialize(); json["Bad"] = 5; json["Good"] = 7; comp.Destroy(); var go2 = scene.CreateObject(); var copy = go2.Components.Create( false ); copy.DeserializeImmediately( json ); Assert.AreEqual( 7, copy.Good ); } /// /// Only whitelisted flags survive a round trip: ShowAdvancedProperties is /// restored from JSON, transient flags like Hidden are not. /// [TestMethod] public void FlagsRoundTripOnlySavedFlags() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); comp.Flags |= ComponentFlags.ShowAdvancedProperties | ComponentFlags.Hidden; var json = (JsonObject)comp.Serialize(); comp.Destroy(); var go2 = scene.CreateObject(); var copy = go2.Components.Create( false ); copy.DeserializeImmediately( json ); Assert.IsTrue( copy.Flags.HasFlag( ComponentFlags.ShowAdvancedProperties ) ); Assert.IsFalse( copy.Flags.HasFlag( ComponentFlags.Hidden ) ); } } /// /// Component covering the serializable member shapes: plain properties, a /// public field, a private setter, a [JsonIgnore] property and a property /// without [Property]. /// public class SerializeDataComponent : Component { [Property] public int Number { get; set; } [Property] public string Text { get; set; } [Property] public Vector3 Position { get; set; } [Property] public int Counter; [Property] public int Secret { get; private set; } [Property, JsonIgnore] public int Ignored { get; set; } public int NotAProperty { get; set; } /// /// Sets the private-setter property from test code. /// public void SetSecret( int value ) => Secret = value; } /// /// Component whose Bad property throws from its setter, to prove /// deserialization isolates per-property failures. /// public class ThrowingSetterComponent : Component { [Property] public int Bad { get => 0; set => throw new InvalidOperationException( "setter exploded" ); } [Property] public int Good { get; set; } }