using Sandbox.Utility;
using System;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace SceneTests.GameObjects;
///
/// Plain logic component used as prefab content by .
/// Carries two serialized properties so tests can override one and leave the other untouched.
///
public class PrefabInstanceStatComponent : Component
{
///
/// A serialized integer property used to test prefab instance property overrides.
///
[Property]
public int Number { get; set; }
///
/// A serialized string property used to test prefab instance property overrides.
///
[Property]
public string Text { get; set; }
}
///
/// A second plain component type, used when a test needs to add a brand new component
/// to a prefab instance without confusing it with the prefab-defined components.
///
public class PrefabInstanceExtraComponent : Component
{
///
/// A serialized string property so the added component carries data through round trips.
///
[Property]
public string Label { get; set; }
}
///
/// Tests for the prefab instance system: instance creation from a prefab scene, override/patch
/// tracking (), patch-style serialization round trips,
/// revert/apply-to-prefab flows, BreakFromPrefab and nested prefab instances.
///
[TestClass]
[DoNotParallelize]
public class PrefabInstanceTest : SceneTest
{
private const string BasicPrefabPath = "test_prefab_instance_basic.prefab";
private const string InnerPrefabPath = "test_prefab_instance_inner.prefab";
private const string OuterPrefabPath = "test_prefab_instance_outer.prefab";
private const string ContainerPrefabPath = "test_prefab_instance_container.prefab";
private const string GroupedOuterPrefabPath = "test_prefab_instance_grouped_outer.prefab";
// GUIDs used inside the basic test prefab
private const string RootGuid = "9e6a3e6e-0001-4a01-8001-000000000001";
private const string RootCompGuid = "9e6a3e6e-0001-4a01-8001-000000000002";
private const string ChildAGuid = "9e6a3e6e-0001-4a01-8001-000000000003";
private const string ChildACompGuid = "9e6a3e6e-0001-4a01-8001-000000000004";
private const string ChildBGuid = "9e6a3e6e-0001-4a01-8001-000000000005";
// GUIDs used inside the standalone container test prefab
private const string ContainerRootGuid = "9e6a3e6e-0004-4a04-8004-000000000001";
private const string ContainerCompGuid = "9e6a3e6e-0004-4a04-8004-000000000002";
private const string ContainerSlotGuid = "9e6a3e6e-0004-4a04-8004-000000000003";
// GUIDs used inside the grouped outer test prefab (nested instance under a group child)
private const string GroupedOuterRootGuid = "9e6a3e6e-0005-4a05-8005-000000000001";
private const string GroupedOuterGroupGuid = "9e6a3e6e-0005-4a05-8005-000000000002";
private const string GroupedNestedInstanceGuid = "9e6a3e6e-0005-4a05-8005-000000000003";
private const string GroupedNestedCompGuid = "9e6a3e6e-0005-4a05-8005-000000000004";
// GUIDs used inside the inner/outer test prefabs (nested prefab instance tests)
private const string InnerRootGuid = "9e6a3e6e-0002-4a02-8002-000000000001";
private const string InnerCompGuid = "9e6a3e6e-0002-4a02-8002-000000000002";
private const string OuterRootGuid = "9e6a3e6e-0003-4a03-8003-000000000001";
private const string NestedInstanceGuid = "9e6a3e6e-0003-4a03-8003-000000000002";
private const string NestedInstanceCompGuid = "9e6a3e6e-0003-4a03-8003-000000000003";
///
/// Root object JSON for a basic prefab: a root with a stat component and two children,
/// one of which has its own stat component.
///
private static readonly string BasicPrefabJson = $$"""
{
"__guid": "{{RootGuid}}",
"__version": 2,
"Flags": 0,
"Name": "BasicRoot",
"Enabled": true,
"Components": [
{
"__type": "PrefabInstanceStatComponent",
"__guid": "{{RootCompGuid}}",
"__enabled": true,
"Number": 5,
"Text": "prefab"
}
],
"Children": [
{
"__guid": "{{ChildAGuid}}",
"__version": 2,
"Flags": 0,
"Name": "ChildA",
"Enabled": true,
"Components": [
{
"__type": "PrefabInstanceStatComponent",
"__guid": "{{ChildACompGuid}}",
"__enabled": true,
"Number": 10,
"Text": "childA"
}
],
"Children": []
},
{
"__guid": "{{ChildBGuid}}",
"__version": 2,
"Flags": 0,
"Name": "ChildB",
"Enabled": true,
"Components": [],
"Children": []
}
]
}
""";
///
/// Root object JSON for a standalone container prefab: a root with a stat component and an
/// empty child. Used as the drop target when adding a prefab instance to another instance.
///
private static readonly string ContainerPrefabJson = $$"""
{
"__guid": "{{ContainerRootGuid}}",
"__version": 2,
"Flags": 0,
"Name": "ContainerRoot",
"Enabled": true,
"Components": [
{
"__type": "PrefabInstanceStatComponent",
"__guid": "{{ContainerCompGuid}}",
"__enabled": true,
"Number": 20,
"Text": "container"
}
],
"Children": [
{
"__guid": "{{ContainerSlotGuid}}",
"__version": 2,
"Flags": 0,
"Name": "Slot",
"Enabled": true,
"Components": [],
"Children": []
}
]
}
""";
///
/// Root object JSON for an outer prefab whose nested instance sits under a plain group
/// child instead of directly under the root, like props grouped inside a structure prefab.
///
private static readonly string GroupedOuterPrefabJson = $$"""
{
"__guid": "{{GroupedOuterRootGuid}}",
"__version": 2,
"Flags": 0,
"Name": "GroupedOuterRoot",
"Enabled": true,
"Components": [],
"Children": [
{
"__guid": "{{GroupedOuterGroupGuid}}",
"__version": 2,
"Flags": 0,
"Name": "Props",
"Enabled": true,
"Components": [],
"Children": [
{
"__guid": "{{GroupedNestedInstanceGuid}}",
"__version": 2,
"__Prefab": "{{InnerPrefabPath}}",
"__PrefabInstancePatch": {
"AddedObjects": [],
"RemovedObjects": [],
"PropertyOverrides": [],
"MovedObjects": []
},
"__PrefabIdToInstanceId": {
"{{InnerRootGuid}}": "{{GroupedNestedInstanceGuid}}",
"{{InnerCompGuid}}": "{{GroupedNestedCompGuid}}"
}
}
]
}
]
}
""";
///
/// Root object JSON for the inner prefab used by the nested prefab instance tests.
///
private static readonly string InnerPrefabJson = $$"""
{
"__guid": "{{InnerRootGuid}}",
"__version": 2,
"Flags": 0,
"Name": "InnerRoot",
"Enabled": true,
"Components": [
{
"__type": "PrefabInstanceStatComponent",
"__guid": "{{InnerCompGuid}}",
"__enabled": true,
"Number": 7,
"Text": "inner"
}
],
"Children": []
}
""";
///
/// Root object JSON for the outer prefab, containing a prefab instance of the inner prefab
/// stored in the compact patch form, the same way prefab files store them on disk.
///
private static readonly string OuterPrefabJson = $$"""
{
"__guid": "{{OuterRootGuid}}",
"__version": 2,
"Flags": 0,
"Name": "OuterRoot",
"Enabled": true,
"Components": [],
"Children": [
{
"__guid": "{{NestedInstanceGuid}}",
"__version": 2,
"__Prefab": "{{InnerPrefabPath}}",
"__PrefabInstancePatch": {
"AddedObjects": [],
"RemovedObjects": [],
"PropertyOverrides": [],
"MovedObjects": []
},
"__PrefabIdToInstanceId": {
"{{InnerRootGuid}}": "{{NestedInstanceGuid}}",
"{{InnerCompGuid}}": "{{NestedInstanceCompGuid}}"
}
}
]
}
""";
///
/// Registers the basic test prefab in the resource system and fetches its cached prefab scene.
/// Dispose the returned registration to unregister the prefab file again.
///
private static IDisposable RegisterBasicPrefab( out PrefabScene prefabScene )
{
var registration = Helpers.RegisterPrefabFromJson( BasicPrefabPath, BasicPrefabJson );
prefabScene = SceneUtility.GetPrefabScene( ResourceLibrary.Get( BasicPrefabPath ) );
return registration;
}
///
/// Registers the inner and outer test prefabs and fetches the outer cached prefab scene.
/// Dispose the returned registration to unregister both prefab files again.
///
private static IDisposable RegisterNestedPrefabs( out PrefabScene outerScene )
{
var innerRegistration = Helpers.RegisterPrefabFromJson( InnerPrefabPath, InnerPrefabJson );
var outerRegistration = Helpers.RegisterPrefabFromJson( OuterPrefabPath, OuterPrefabJson );
outerScene = SceneUtility.GetPrefabScene( ResourceLibrary.Get( OuterPrefabPath ) );
return new DisposeAction( () =>
{
outerRegistration.Dispose();
innerRegistration.Dispose();
} );
}
///
/// Deserializes the given GameObject JSON into a brand new scene, simulating a scene file
/// being loaded from disk, and returns the restored GameObject.
///
private static GameObject RestoreIntoFreshScene( JsonObject serialized )
{
var restoreScene = new Scene();
using var restoreScope = restoreScene.Push();
var restored = restoreScene.CreateObject();
restored.Deserialize( serialized );
return restored;
}
///
/// Finds a direct child by name, asserting it exists.
///
private static GameObject GetChild( GameObject go, string name )
{
var child = go.Children.FirstOrDefault( c => c.Name == name );
Assert.IsNotNull( child, $"Expected '{go.Name}' to have a child named '{name}'" );
return child;
}
///
/// Cloning a prefab scene must produce a prefab instance: root flagged as the outermost
/// instance root, children part of the instance, all prefab content present, and the
/// GUID lookups translating between prefab ids and fresh instance ids in both directions.
///
[TestMethod]
public void ClonePrefabCreatesPrefabInstance()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
Assert.IsTrue( instance.IsPrefabInstance );
Assert.IsTrue( instance.IsPrefabInstanceRoot );
Assert.IsTrue( instance.IsOutermostPrefabInstanceRoot );
Assert.IsFalse( instance.PrefabInstance.IsNested );
Assert.AreEqual( BasicPrefabPath, instance.PrefabInstanceSource );
Assert.AreEqual( "BasicRoot", instance.Name );
Assert.AreEqual( 2, instance.Children.Count );
var childA = GetChild( instance, "ChildA" );
var childB = GetChild( instance, "ChildB" );
Assert.IsTrue( childA.IsPrefabInstance );
Assert.IsFalse( childA.IsPrefabInstanceRoot );
Assert.AreEqual( instance, childA.OutermostPrefabInstanceRoot );
var rootComp = instance.Components.Get();
Assert.IsNotNull( rootComp );
Assert.AreEqual( 5, rootComp.Number );
Assert.AreEqual( "prefab", rootComp.Text );
Assert.AreEqual( 10, childA.Components.Get().Number );
// The instance must use fresh ids, not the prefab's ids
Assert.AreNotEqual( Guid.Parse( RootGuid ), instance.Id );
Assert.AreNotEqual( Guid.Parse( ChildAGuid ), childA.Id );
// Lookups must map every prefab object to its instance object, in both directions
var toInstance = instance.PrefabInstance.PrefabToInstanceLookup;
var toPrefab = instance.PrefabInstance.InstanceToPrefabLookup;
Assert.AreEqual( instance.Id, toInstance[Guid.Parse( RootGuid )] );
Assert.AreEqual( childA.Id, toInstance[Guid.Parse( ChildAGuid )] );
Assert.AreEqual( childB.Id, toInstance[Guid.Parse( ChildBGuid )] );
Assert.AreEqual( rootComp.Id, toInstance[Guid.Parse( RootCompGuid )] );
Assert.AreEqual( Guid.Parse( RootGuid ), toPrefab[instance.Id] );
Assert.AreEqual( Guid.Parse( ChildAGuid ), toPrefab[childA.Id] );
Assert.AreEqual( Guid.Parse( RootCompGuid ), toPrefab[rootComp.Id] );
}
///
/// An unmodified prefab instance must serialize as a compact patch reference
/// (__Prefab + patch + id lookup) instead of the full hierarchy, must not report itself
/// as modified, and must round trip through serialization preserving ids and content.
///
[TestMethod]
public void UnmodifiedInstanceSerializesCompactAndRoundTrips()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var childA = GetChild( instance, "ChildA" );
var json = instance.Serialize();
Assert.AreEqual( BasicPrefabPath, json["__Prefab"].GetValue() );
Assert.IsTrue( json.ContainsKey( "__PrefabInstancePatch" ) );
Assert.IsTrue( json.ContainsKey( "__PrefabIdToInstanceId" ) );
Assert.AreEqual( instance.Id, json["__guid"].Deserialize() );
// The whole point of the patch format: no expanded hierarchy in the saved scene
Assert.IsFalse( json.ContainsKey( "Children" ) );
Assert.IsFalse( json.ContainsKey( "Components" ) );
Assert.IsFalse( json.ContainsKey( "Name" ) );
Assert.IsFalse( instance.PrefabInstance.IsModified() );
var restored = RestoreIntoFreshScene( json );
Assert.IsTrue( restored.IsPrefabInstanceRoot );
Assert.AreEqual( instance.Id, restored.Id );
Assert.AreEqual( "BasicRoot", restored.Name );
Assert.AreEqual( 2, restored.Children.Count );
Assert.AreEqual( 5, restored.Components.Get().Number );
Assert.AreEqual( "prefab", restored.Components.Get().Text );
// Instance ids survive the round trip via the id lookup
Assert.AreEqual( childA.Id, GetChild( restored, "ChildA" ).Id );
}
///
/// Changing a component property on an instance must be tracked as a property override
/// targeting the prefab's component id, flagged through IsPropertyOverridden,
/// IsComponentModified, IsGameObjectModified and IsModified. Untouched properties must
/// not be reported as overridden.
///
[TestMethod]
public void ComponentPropertyOverrideTracked()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var comp = instance.Components.Get();
comp.Number = 42;
instance.PrefabInstance.RefreshPatch();
Assert.IsTrue( instance.PrefabInstance.IsPropertyOverridden( comp, "Number" ) );
Assert.IsFalse( instance.PrefabInstance.IsPropertyOverridden( comp, "Text" ) );
Assert.IsTrue( instance.PrefabInstance.IsComponentModified( comp ) );
Assert.IsTrue( instance.PrefabInstance.IsModified() );
Assert.IsTrue( instance.PrefabInstance.IsGameObjectModified( instance, ignoreBasicGoOverrides: true ) );
var numberOverride = instance.PrefabInstance.Patch.PropertyOverrides.Single( x => x.Property == "Number" );
Assert.AreEqual( Guid.Parse( RootCompGuid ), Guid.Parse( numberOverride.Target.IdValue ) );
Assert.AreEqual( 42, numberOverride.Value.GetValue() );
}
///
/// Transform and name changes on the instance root are expected on every placed instance,
/// so while they appear in the patch they must not make the instance count as modified,
/// and must be filterable through the ignoreBasicGoOverrides flag.
///
[TestMethod]
public void RootBasicOverridesAreIgnored()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone( new Vector3( 100, 200, 300 ) );
instance.Name = "Renamed Instance";
instance.PrefabInstance.RefreshPatch();
Assert.IsTrue( instance.PrefabInstance.IsPropertyOverridden( instance, "Name" ) );
Assert.IsTrue( instance.PrefabInstance.IsPropertyOverridden( instance, "Position" ) );
Assert.IsFalse( instance.PrefabInstance.IsPropertyOverridden( instance, "Name", ignoreBasicGoOverrides: true ) );
Assert.IsFalse( instance.PrefabInstance.IsPropertyOverridden( instance, "Position", ignoreBasicGoOverrides: true ) );
Assert.IsFalse( instance.PrefabInstance.IsModified() );
Assert.IsFalse( instance.PrefabInstance.IsGameObjectModified( instance, ignoreBasicGoOverrides: true ) );
}
///
/// Unlike on the instance root, name and transform changes on a child inside the instance
/// are real modifications: they must be tracked (including the GameTransform property name
/// remapping LocalPosition -> Position) and make the instance modified.
///
[TestMethod]
public void ChildModificationsTracked()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var childA = GetChild( instance, "ChildA" );
var childB = GetChild( instance, "ChildB" );
childA.Name = "ChildA_Renamed";
childA.LocalPosition = new Vector3( 50, 0, 0 );
instance.PrefabInstance.RefreshPatch();
Assert.IsTrue( instance.PrefabInstance.IsModified() );
Assert.IsTrue( instance.PrefabInstance.IsPropertyOverridden( childA, "Name" ) );
Assert.IsTrue( instance.PrefabInstance.IsPropertyOverridden( childA, "Position" ) );
// Editor passes the GameTransform as the owner, with the C# property name
Assert.IsTrue( instance.PrefabInstance.IsPropertyOverridden( childA.Transform, "LocalPosition" ) );
// Basic overrides are only ignored on the root, not on children
Assert.IsTrue( instance.PrefabInstance.IsPropertyOverridden( childA, "Name", ignoreBasicGoOverrides: true ) );
Assert.IsTrue( instance.PrefabInstance.IsGameObjectModified( childA ) );
Assert.IsFalse( instance.PrefabInstance.IsGameObjectModified( childB ) );
}
///
/// A modified instance must survive a serialize/deserialize round trip: the override
/// values are restored, untouched values still come from the prefab, instance ids are
/// preserved and the restored instance still knows what was overridden.
///
[TestMethod]
public void ModifiedInstanceRoundTrip()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var comp = instance.Components.Get();
var childA = GetChild( instance, "ChildA" );
comp.Number = 42;
childA.Name = "ChildA_Renamed";
// Serialize() refreshes the patch internally for outermost instance roots
var restored = RestoreIntoFreshScene( instance.Serialize() );
var restoredComp = restored.Components.Get();
Assert.AreEqual( 42, restoredComp.Number );
Assert.AreEqual( "prefab", restoredComp.Text );
var restoredChildA = GetChild( restored, "ChildA_Renamed" );
Assert.AreEqual( childA.Id, restoredChildA.Id );
// Data that wasn't overridden still comes from the prefab
Assert.AreEqual( 10, restoredChildA.Components.Get().Number );
Assert.AreEqual( "ChildB", GetChild( restored, "ChildB" ).Name );
Assert.IsTrue( restored.PrefabInstance.IsPropertyOverridden( restoredComp, "Number" ) );
Assert.IsTrue( restored.PrefabInstance.IsPropertyOverridden( restoredChildA, "Name" ) );
}
///
/// GameObjects and components added to a prefab instance must be tracked as additions,
/// survive a round trip exactly once (no duplication through the patch), and keep their
/// position at the end of the child list.
///
[TestMethod]
public void AddedObjectsTrackedAndRoundTrip()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var childA = GetChild( instance, "ChildA" );
var added = scene.CreateObject();
added.Name = "Extra";
added.SetParent( instance );
var addedComp = added.AddComponent();
addedComp.Number = 99;
var extraComp = childA.AddComponent();
extraComp.Label = "added";
instance.PrefabInstance.RefreshPatch();
Assert.IsTrue( instance.PrefabInstance.IsAddedGameObject( added ) );
Assert.IsTrue( instance.PrefabInstance.IsAddedComponent( extraComp ) );
Assert.IsTrue( instance.PrefabInstance.IsModified() );
Assert.IsTrue( instance.PrefabInstance.IsGameObjectModified( instance, ignoreBasicGoOverrides: true ) );
// Added objects have no prefab counterpart, so they can't have property overrides
Assert.IsFalse( instance.PrefabInstance.IsPropertyOverridden( added, "Name" ) );
var restored = RestoreIntoFreshScene( instance.Serialize() );
Assert.AreEqual( 3, restored.Children.Count );
Assert.AreEqual( "Extra", restored.Children[2].Name );
var restoredExtra = GetChild( restored, "Extra" );
Assert.AreEqual( added.Id, restoredExtra.Id );
Assert.AreEqual( 99, restoredExtra.Components.Get().Number );
// The added object and its component must not be duplicated by patch application
Assert.AreEqual( 1, restoredExtra.Components.GetAll( FindMode.EverythingInSelf ).Count() );
var restoredChildA = GetChild( restored, "ChildA" );
Assert.AreEqual( "added", restoredChildA.Components.Get().Label );
Assert.AreEqual( 1, restoredChildA.Components.GetAll( FindMode.EverythingInSelf ).Count() );
Assert.IsTrue( restored.PrefabInstance.IsAddedGameObject( restoredExtra ) );
}
///
/// Removing a prefab-defined GameObject or component from an instance must be tracked as
/// a removal targeting the prefab ids, and the removal must survive a round trip without
/// the prefab content leaking back in.
///
[TestMethod]
public void RemovedObjectsTrackedAndRoundTrip()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var childA = GetChild( instance, "ChildA" );
var childB = GetChild( instance, "ChildB" );
childB.DestroyImmediate();
childA.Components.Get().Destroy();
instance.PrefabInstance.RefreshPatch();
var removed = instance.PrefabInstance.Patch.RemovedObjects;
Assert.AreEqual( 2, removed.Count );
Assert.IsTrue( removed.Any( x => Guid.Parse( x.Id.IdValue ) == Guid.Parse( ChildBGuid ) ) );
Assert.IsTrue( removed.Any( x => Guid.Parse( x.Id.IdValue ) == Guid.Parse( ChildACompGuid ) ) );
Assert.IsTrue( instance.PrefabInstance.IsModified() );
Assert.IsTrue( instance.PrefabInstance.IsGameObjectModified( instance, ignoreBasicGoOverrides: true ) );
Assert.IsTrue( instance.PrefabInstance.IsGameObjectModified( childA, ignoreBasicGoOverrides: true ) );
var restored = RestoreIntoFreshScene( instance.Serialize() );
Assert.AreEqual( 1, restored.Children.Count );
Assert.AreEqual( "ChildA", restored.Children[0].Name );
Assert.IsNull( restored.Children[0].Components.Get() );
}
///
/// Reparenting a prefab-defined child within the instance must be tracked as a moved
/// object and the new hierarchy must survive a round trip.
///
[TestMethod]
public void ReparentedChildTrackedAndRoundTrip()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var childA = GetChild( instance, "ChildA" );
var childB = GetChild( instance, "ChildB" );
childB.SetParent( childA );
instance.PrefabInstance.RefreshPatch();
Assert.AreEqual( 1, instance.PrefabInstance.Patch.MovedObjects.Count );
Assert.AreEqual( Guid.Parse( ChildBGuid ), Guid.Parse( instance.PrefabInstance.Patch.MovedObjects[0].Id.IdValue ) );
Assert.IsTrue( instance.PrefabInstance.IsModified() );
var restored = RestoreIntoFreshScene( instance.Serialize() );
Assert.AreEqual( 1, restored.Children.Count );
var restoredChildA = GetChild( restored, "ChildA" );
Assert.AreEqual( 1, restoredChildA.Children.Count );
Assert.AreEqual( "ChildB", restoredChildA.Children[0].Name );
Assert.AreEqual( childB.Id, restoredChildA.Children[0].Id );
}
///
/// RevertPropertyChange must restore the prefab's value for a single overridden property,
/// remove its override from the patch and leave the instance unmodified.
///
[TestMethod]
public void RevertPropertyChangeRestoresPrefabValue()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var comp = instance.Components.Get();
comp.Number = 42;
instance.PrefabInstance.RefreshPatch();
Assert.IsTrue( instance.PrefabInstance.IsModified() );
instance.PrefabInstance.RevertPropertyChange( comp, "Number" );
Assert.AreEqual( 5, comp.Number );
Assert.IsFalse( instance.PrefabInstance.IsPropertyOverridden( comp, "Number" ) );
Assert.IsFalse( instance.PrefabInstance.IsModified() );
}
///
/// RevertComponentChanges must restore all of a component's overridden properties to
/// the prefab values at once.
///
[TestMethod]
public void RevertComponentChangesRestoresAllProperties()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var comp = instance.Components.Get();
comp.Number = 42;
comp.Text = "changed";
instance.PrefabInstance.RefreshPatch();
Assert.IsTrue( instance.PrefabInstance.IsComponentModified( comp ) );
instance.PrefabInstance.RevertComponentChanges( comp );
Assert.AreEqual( 5, comp.Number );
Assert.AreEqual( "prefab", comp.Text );
Assert.IsFalse( instance.PrefabInstance.IsComponentModified( comp ) );
}
///
/// RevertGameObjectChanges on the instance root must throw away every local change:
/// property overrides revert to prefab values, added objects are removed, and the
/// existing C# object references stay valid because the revert refreshes in place.
///
[TestMethod]
public void RevertGameObjectChangesRevertsEverything()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var comp = instance.Components.Get();
var childA = GetChild( instance, "ChildA" );
comp.Number = 42;
childA.Name = "ChildA_Renamed";
var added = scene.CreateObject();
added.Name = "Extra";
added.SetParent( instance );
instance.PrefabInstance.RefreshPatch();
Assert.IsTrue( instance.PrefabInstance.IsModified() );
instance.PrefabInstance.RevertGameObjectChanges( instance );
scene.ProcessDeletes();
Assert.AreEqual( 5, comp.Number );
Assert.AreEqual( "ChildA", childA.Name );
Assert.AreEqual( 2, instance.Children.Count );
Assert.IsNull( instance.Children.FirstOrDefault( c => c.Name == "Extra" ) );
Assert.IsFalse( instance.PrefabInstance.IsModified() );
}
///
/// UpdateFromPrefab re-applies prefab data plus the cached patch. Overrides captured in
/// the patch must survive the refresh, while changes that were never captured by a patch
/// refresh must be discarded — the patch is the source of truth for instance state.
///
[TestMethod]
public void UpdateFromPrefabKeepsRefreshedOverridesDiscardsRest()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var comp = instance.Components.Get();
comp.Number = 42;
instance.PrefabInstance.RefreshPatch();
// This change never makes it into the patch
comp.Text = "scratch";
instance.UpdateFromPrefab();
Assert.AreEqual( 42, comp.Number );
Assert.AreEqual( "prefab", comp.Text );
Assert.IsTrue( instance.IsPrefabInstanceRoot );
}
///
/// ClearPatch must drop all recorded overrides so the instance reports itself unmodified,
/// and a following UpdateFromPrefab restores the original prefab state.
///
[TestMethod]
public void ClearPatchDiscardsOverrides()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var comp = instance.Components.Get();
var childA = GetChild( instance, "ChildA" );
comp.Number = 42;
childA.Name = "ChildA_Renamed";
instance.PrefabInstance.RefreshPatch();
Assert.IsTrue( instance.PrefabInstance.IsModified() );
instance.PrefabInstance.ClearPatch( keepBasicGoOverridesOnRoot: false );
Assert.IsFalse( instance.PrefabInstance.IsModified() );
Assert.AreEqual( 0, instance.PrefabInstance.Patch.PropertyOverrides.Count );
instance.UpdateFromPrefab();
Assert.AreEqual( 5, comp.Number );
Assert.AreEqual( "ChildA", childA.Name );
}
///
/// BreakFromPrefab must detach the instance from its prefab: no more prefab instance
/// flags or source, all content kept as plain GameObjects, and serialization switches
/// from the compact patch form to the full hierarchy.
///
[TestMethod]
public void BreakFromPrefabDetachesInstance()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var comp = instance.Components.Get();
comp.Number = 42;
instance.BreakFromPrefab();
Assert.IsFalse( instance.IsPrefabInstance );
Assert.IsFalse( instance.IsPrefabInstanceRoot );
Assert.IsNull( instance.PrefabInstanceSource );
Assert.IsFalse( GetChild( instance, "ChildA" ).IsPrefabInstance );
// Content survives the break
Assert.AreEqual( 2, instance.Children.Count );
Assert.AreEqual( 42, comp.Number );
Assert.AreEqual( 10, GetChild( instance, "ChildA" ).Components.Get().Number );
var json = instance.Serialize();
Assert.IsFalse( json.ContainsKey( "__Prefab" ) );
Assert.IsTrue( json.ContainsKey( "Children" ) );
Assert.IsTrue( json.ContainsKey( "Components" ) );
}
///
/// ApplyComponentChangesToPrefab must write the instance's component state into the
/// prefab itself: the cached prefab scene and prefab file update, the source instance
/// stops being modified, new clones get the value, and existing instances pick it up
/// when they call UpdateFromPrefab.
///
[TestMethod]
public void ApplyComponentChangesToPrefabPropagates()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance1 = prefabScene.Clone();
var instance2 = prefabScene.Clone();
var comp1 = instance1.Components.Get();
comp1.Number = 42;
instance1.PrefabInstance.ApplyComponentChangesToPrefab( comp1 );
// The prefab itself now carries the value
Assert.AreEqual( 42, prefabScene.Components.Get().Number );
// The source instance keeps the value but is no longer modified
Assert.AreEqual( 42, comp1.Number );
Assert.IsFalse( instance1.PrefabInstance.IsModified() );
// A fresh clone gets the new prefab value
var instance3 = prefabScene.Clone();
Assert.AreEqual( 42, instance3.Components.Get().Number );
// Existing unmodified instances update when refreshed from the prefab
var comp2 = instance2.Components.Get();
Assert.AreEqual( 5, comp2.Number );
instance2.UpdateFromPrefab();
Assert.AreEqual( 42, comp2.Number );
}
///
/// ApplyPropertyChangeToPrefab must push exactly one overridden property into the prefab,
/// leaving other overrides on the instance intact.
///
[TestMethod]
public void ApplyPropertyChangeToPrefabUpdatesSingleProperty()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var comp = instance.Components.Get();
comp.Number = 42;
comp.Text = "local";
instance.PrefabInstance.RefreshPatch();
instance.PrefabInstance.ApplyPropertyChangeToPrefab( comp, "Number" );
// Only Number was pushed to the prefab
var fresh = prefabScene.Clone();
var freshComp = fresh.Components.Get();
Assert.AreEqual( 42, freshComp.Number );
Assert.AreEqual( "prefab", freshComp.Text );
// The instance no longer overrides Number, but still overrides Text
Assert.IsFalse( instance.PrefabInstance.IsPropertyOverridden( comp, "Number" ) );
Assert.IsTrue( instance.PrefabInstance.IsPropertyOverridden( comp, "Text" ) );
Assert.AreEqual( "local", comp.Text );
}
///
/// AddGameObjectToPrefab must move an added GameObject from the instance patch into the
/// prefab definition: new clones contain it, and the source instance stops reporting it
/// as an addition.
///
[TestMethod]
public void AddGameObjectToPrefabAddsToPrefab()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var added = scene.CreateObject();
added.Name = "Extra";
added.SetParent( instance );
added.AddComponent().Number = 99;
instance.PrefabInstance.RefreshPatch();
Assert.IsTrue( instance.PrefabInstance.IsAddedGameObject( added ) );
instance.PrefabInstance.AddGameObjectToPrefab( added );
// New clones now contain the object as prefab content
var fresh = prefabScene.Clone();
var freshExtra = GetChild( fresh, "Extra" );
Assert.AreEqual( 99, freshExtra.Components.Get().Number );
// The source instance no longer treats it as an addition
Assert.IsFalse( instance.PrefabInstance.IsAddedGameObject( added ) );
Assert.IsFalse( instance.PrefabInstance.IsModified() );
}
///
/// OverridePrefabWithInstance must make the prefab definition match the instance exactly:
/// overrides and additions become prefab content, the instance becomes unmodified, and it
/// still serializes as a compact prefab instance afterwards.
///
[TestMethod]
public void OverridePrefabWithInstanceMakesPrefabMatch()
{
using var registration = RegisterBasicPrefab( out var prefabScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone();
var comp = instance.Components.Get();
comp.Number = 77;
var added = scene.CreateObject();
added.Name = "Extra";
added.SetParent( instance );
instance.PrefabInstance.RefreshPatch();
Assert.IsTrue( instance.PrefabInstance.IsModified() );
instance.PrefabInstance.OverridePrefabWithInstance();
Assert.IsFalse( instance.PrefabInstance.IsModified() );
Assert.IsTrue( instance.IsPrefabInstanceRoot );
// The prefab now matches the instance
var fresh = prefabScene.Clone();
Assert.AreEqual( 77, fresh.Components.Get().Number );
Assert.IsNotNull( fresh.Children.FirstOrDefault( c => c.Name == "Extra" ) );
// The instance still serializes as a compact prefab instance reference
var json = instance.Serialize();
Assert.IsTrue( json.ContainsKey( "__Prefab" ) );
Assert.IsFalse( json.ContainsKey( "Children" ) );
}
///
/// A prefab containing an instance of another prefab must produce nested prefab instances
/// when cloned: the inner root is flagged as nested with the outer root as its outermost
/// root, BreakFromPrefab is a no-op on it, and modifications to nested content serialize
/// through the outer instance's patch and survive a round trip, rebuilding the nested
/// instance's GUID mappings back to the inner prefab.
///
[TestMethod]
public void NestedPrefabInstanceFlagsAndRoundTrip()
{
using var registration = RegisterNestedPrefabs( out var outerScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var outer = outerScene.Clone();
Assert.IsTrue( outer.IsOutermostPrefabInstanceRoot );
Assert.AreEqual( OuterPrefabPath, outer.PrefabInstanceSource );
Assert.AreEqual( 1, outer.Children.Count );
var nested = outer.Children[0];
Assert.AreEqual( "InnerRoot", nested.Name );
Assert.IsTrue( nested.IsPrefabInstanceRoot );
Assert.IsTrue( nested.IsNestedPrefabInstanceRoot );
Assert.IsTrue( nested.PrefabInstance.IsNested );
Assert.IsFalse( nested.IsOutermostPrefabInstanceRoot );
Assert.AreEqual( outer, nested.OutermostPrefabInstanceRoot );
Assert.AreEqual( InnerPrefabPath, nested.PrefabInstanceSource );
// The nested instance maps the inner prefab's ids onto its own objects
var nestedComp = nested.Components.Get();
Assert.AreEqual( 7, nestedComp.Number );
Assert.AreEqual( nested.Id, nested.PrefabInstance.PrefabToInstanceLookup[Guid.Parse( InnerRootGuid )] );
Assert.AreEqual( nestedComp.Id, nested.PrefabInstance.PrefabToInstanceLookup[Guid.Parse( InnerCompGuid )] );
// Breaking from prefab only works on the outermost root, nested roots are a no-op
nested.BreakFromPrefab();
Assert.IsTrue( nested.IsPrefabInstanceRoot );
// Modify nested content, then round trip the whole outer instance
nestedComp.Number = 1234;
var restored = RestoreIntoFreshScene( outer.Serialize() );
Assert.AreEqual( 1, restored.Children.Count );
var restoredNested = restored.Children[0];
Assert.IsTrue( restoredNested.IsNestedPrefabInstanceRoot );
Assert.AreEqual( InnerPrefabPath, restoredNested.PrefabInstanceSource );
Assert.AreEqual( nested.Id, restoredNested.Id );
var restoredNestedComp = restoredNested.Components.Get();
Assert.AreEqual( 1234, restoredNestedComp.Number );
Assert.AreEqual( nestedComp.Id, restoredNestedComp.Id );
// Nested instances don't store their lookups, they are rebuilt on load and
// must cover every object of the inner prefab.
// KNOWN ISSUE: after a load the lookup VALUES point at the outer prefab's
// internal expansion objects rather than the live instance objects (a fresh
// clone maps to live objects) - so we only pin key coverage here, not identity.
Assert.IsTrue( restoredNested.PrefabInstance.PrefabToInstanceLookup.ContainsKey( Guid.Parse( InnerRootGuid ) ) );
Assert.IsTrue( restoredNested.PrefabInstance.PrefabToInstanceLookup.ContainsKey( Guid.Parse( InnerCompGuid ) ) );
}
///
/// Dragging an item prefab instance into another prefab instance and applying changes on
/// the outer root (editor "Apply to Prefab" = OverridePrefabWithInstance) must keep all
/// scene objects intact: names, components, values and children must survive, the item
/// must become a nested instance, and fresh clones must contain the item.
///
[TestMethod]
public void OverridePrefabWithInstanceKeepsAddedPrefabInstanceIntact()
{
using var containerRegistration = Helpers.RegisterPrefabFromJson( ContainerPrefabPath, ContainerPrefabJson );
var containerScene = SceneUtility.GetPrefabScene( ResourceLibrary.Get( ContainerPrefabPath ) );
using var itemRegistration = RegisterBasicPrefab( out var itemScene );
var scene = new Scene();
using var sceneScope = scene.Push();
// Drag outer prefab into scene
var container = containerScene.Clone();
// Drag item prefab into scene, then into the outer instance in the hierarchy
var item = itemScene.Clone();
item.SetParent( container );
container.PrefabInstance.RefreshPatch();
Assert.IsTrue( container.PrefabInstance.IsAddedGameObject( item ) );
// "Apply to Prefab" on the outer root
container.PrefabInstance.OverridePrefabWithInstance();
// The container keeps its own content
Assert.AreEqual( "ContainerRoot", container.Name );
var containerComp = container.Components.Get();
Assert.IsNotNull( containerComp );
Assert.AreEqual( 20, containerComp.Number );
Assert.AreEqual( "container", containerComp.Text );
Assert.IsNotNull( GetChild( container, "Slot" ) );
// The container stays a regular outermost instance, applying must not flag it nested
Assert.IsTrue( container.IsOutermostPrefabInstanceRoot );
Assert.IsFalse( container.PrefabInstance.IsNested );
// The item keeps its content and becomes a nested instance
Assert.AreEqual( "BasicRoot", item.Name );
Assert.IsTrue( item.IsPrefabInstanceRoot );
Assert.IsTrue( item.IsNestedPrefabInstanceRoot );
Assert.AreEqual( BasicPrefabPath, item.PrefabInstanceSource );
var itemComp = item.Components.Get();
Assert.IsNotNull( itemComp );
Assert.AreEqual( 5, itemComp.Number );
Assert.AreEqual( "prefab", itemComp.Text );
var itemChildA = GetChild( item, "ChildA" );
Assert.IsNotNull( GetChild( item, "ChildB" ) );
Assert.AreEqual( 10, itemChildA.Components.Get().Number );
Assert.IsFalse( container.PrefabInstance.IsModified() );
// A fresh clone of the container prefab contains the item as nested instance
var fresh = containerScene.Clone();
var freshItem = GetChild( fresh, "BasicRoot" );
Assert.IsTrue( freshItem.IsNestedPrefabInstanceRoot );
Assert.AreEqual( 5, freshItem.Components.Get().Number );
Assert.AreEqual( 10, GetChild( freshItem, "ChildA" ).Components.Get().Number );
}
///
/// Regression: OverridePrefabWithInstance used to flag the applied root itself as nested.
/// If that instance was later dragged into another prefab instance, the reparent logic
/// treated it as belonging to the new outer instance without any GUID mappings, corrupting
/// both prefabs on the next apply. This chains: apply on the item, drag it into another
/// instance (onto a child), apply on the outer.
///
[TestMethod]
public void ApplyToPrefabAfterAddedInstanceWasPreviouslyApplied()
{
using var containerRegistration = Helpers.RegisterPrefabFromJson( ContainerPrefabPath, ContainerPrefabJson );
var containerScene = SceneUtility.GetPrefabScene( ResourceLibrary.Get( ContainerPrefabPath ) );
using var itemRegistration = RegisterBasicPrefab( out _ );
var itemScene = SceneUtility.GetPrefabScene( ResourceLibrary.Get( BasicPrefabPath ) );
var scene = new Scene();
using var sceneScope = scene.Push();
var container = containerScene.Clone();
var item = itemScene.Clone();
// Tweak the item and apply changes on it first
item.Components.Get().Number = 42;
item.PrefabInstance.RefreshPatch();
item.PrefabInstance.OverridePrefabWithInstance();
Assert.IsTrue( item.IsOutermostPrefabInstanceRoot );
Assert.IsFalse( item.PrefabInstance.IsNested );
// Drag the item onto a child inside the container instance
var slot = GetChild( container, "Slot" );
item.SetParent( slot );
// The item must still be its own instance, added to the container
Assert.IsTrue( item.IsOutermostPrefabInstanceRoot );
container.PrefabInstance.RefreshPatch();
Assert.IsTrue( container.PrefabInstance.IsAddedGameObject( item ) );
// "Apply to Prefab" on the outer root
container.PrefabInstance.OverridePrefabWithInstance();
container.PrefabInstance.RefreshPatch();
// The prefab file is saved, hot reload round-trips the JSON and rebuilds the cache scene
var containerFile = ResourceLibrary.Get( ContainerPrefabPath );
containerFile.RootObject = JsonNode.Parse( containerFile.RootObject.ToJsonString() ).AsObject();
((PrefabCacheScene)(GameObject)containerScene).Refresh( containerFile );
// The editor then refreshes all instances of the changed prefab in open scenes
container.UpdateFromPrefab();
// Everything must still be intact
Assert.AreEqual( 20, container.Components.Get().Number );
var refreshedSlot = GetChild( container, "Slot" );
var refreshedItem = GetChild( refreshedSlot, "BasicRoot" );
Assert.IsTrue( refreshedItem.IsNestedPrefabInstanceRoot );
var itemComp = refreshedItem.Components.Get();
Assert.IsNotNull( itemComp );
Assert.AreEqual( 42, itemComp.Number );
Assert.AreEqual( "prefab", itemComp.Text );
Assert.AreEqual( 10, GetChild( refreshedItem, "ChildA" ).Components.Get().Number );
// Fresh clones contain the chain with correct values
var fresh = containerScene.Clone();
var freshItem = GetChild( GetChild( fresh, "Slot" ), "BasicRoot" );
Assert.AreEqual( 42, freshItem.Components.Get().Number );
}
///
/// Regression: applying an instance whose prefab contains a nested instance under a plain
/// group child (not directly under the root) wiped that nested instance's data in the
/// prefab cache scene. The update serialization only marked direct children of the root
/// with EditorSkipPrefabBreakOnRefresh, deeper nested roots got EditorPrefabInstanceNestedSource,
/// which cannot rebuild mappings inside a PrefabCacheScene. Everything then lost its
/// identity, values and components on refresh.
///
[TestMethod]
public void ApplyToPrefabKeepsDeepNestedInstanceIntact()
{
using var innerRegistration = Helpers.RegisterPrefabFromJson( InnerPrefabPath, InnerPrefabJson );
using var groupedRegistration = Helpers.RegisterPrefabFromJson( GroupedOuterPrefabPath, GroupedOuterPrefabJson );
var groupedScene = SceneUtility.GetPrefabScene( ResourceLibrary.Get( GroupedOuterPrefabPath ) );
using var itemRegistration = RegisterBasicPrefab( out _ );
var itemScene = SceneUtility.GetPrefabScene( ResourceLibrary.Get( BasicPrefabPath ) );
var scene = new Scene();
using var sceneScope = scene.Push();
var outer = groupedScene.Clone();
var group = GetChild( outer, "Props" );
var nested = GetChild( group, "InnerRoot" );
Assert.IsTrue( nested.IsNestedPrefabInstanceRoot );
var nestedId = nested.Id;
var nestedComp = nested.Components.Get();
var nestedCompId = nestedComp.Id;
Assert.AreEqual( 7, nestedComp.Number );
// Drag the item into the outer instance and apply
var item = itemScene.Clone();
item.SetParent( outer );
outer.PrefabInstance.RefreshPatch();
outer.PrefabInstance.OverridePrefabWithInstance();
outer.PrefabInstance.RefreshPatch();
// The prefab file keeps the nested instance with its original stable ids
var outerFile = ResourceLibrary.Get( GroupedOuterPrefabPath );
var fileGroup = outerFile.RootObject["Children"].AsArray().First( c => c["Name"]?.GetValue() == "Props" );
var fileNested = fileGroup["Children"].AsArray().First().AsObject();
Assert.AreEqual( InnerPrefabPath, fileNested["__Prefab"]?.GetValue() );
var fileMapping = fileNested["__PrefabIdToInstanceId"].AsObject();
Assert.AreEqual( GroupedNestedInstanceGuid, fileMapping[InnerRootGuid]?.GetValue() );
Assert.AreEqual( GroupedNestedCompGuid, fileMapping[InnerCompGuid]?.GetValue() );
// Hot reload round-trips the JSON and rebuilds the cache scene, then instances refresh
outerFile.RootObject = JsonNode.Parse( outerFile.RootObject.ToJsonString() ).AsObject();
((PrefabCacheScene)(GameObject)groupedScene).Refresh( outerFile );
outer.UpdateFromPrefab();
// The nested instance keeps its identity, content and mappings in the scene
var refreshedNested = GetChild( GetChild( outer, "Props" ), "InnerRoot" );
Assert.AreEqual( nestedId, refreshedNested.Id );
Assert.IsTrue( refreshedNested.IsNestedPrefabInstanceRoot );
var refreshedComp = refreshedNested.Components.Get();
Assert.IsNotNull( refreshedComp );
Assert.AreEqual( nestedCompId, refreshedComp.Id );
Assert.AreEqual( 7, refreshedComp.Number );
Assert.AreEqual( "inner", refreshedComp.Text );
Assert.AreEqual( refreshedNested.Id, refreshedNested.PrefabInstance.PrefabToInstanceLookup[Guid.Parse( InnerRootGuid )] );
Assert.AreEqual( refreshedComp.Id, refreshedNested.PrefabInstance.PrefabToInstanceLookup[Guid.Parse( InnerCompGuid )] );
// The added item is intact too
var refreshedItem = GetChild( outer, "BasicRoot" );
Assert.AreEqual( 5, refreshedItem.Components.Get().Number );
}
///
/// Breaking the outer instance from its prefab must promote nested prefab instances to
/// full outermost prefab instances, so the link to the inner prefab is preserved even
/// though the outer link is gone.
///
[TestMethod]
public void BreakFromPrefabConvertsNestedInstancesToFull()
{
using var registration = RegisterNestedPrefabs( out var outerScene );
var scene = new Scene();
using var sceneScope = scene.Push();
var outer = outerScene.Clone();
var nested = outer.Children[0];
Assert.IsTrue( nested.IsNestedPrefabInstanceRoot );
outer.BreakFromPrefab();
Assert.IsFalse( outer.IsPrefabInstance );
Assert.IsTrue( nested.IsPrefabInstanceRoot );
Assert.IsFalse( nested.PrefabInstance.IsNested );
Assert.IsTrue( nested.IsOutermostPrefabInstanceRoot );
Assert.AreEqual( InnerPrefabPath, nested.PrefabInstanceSource );
Assert.IsFalse( nested.PrefabInstance.IsModified() );
// The promoted instance serializes as a compact prefab instance of the inner prefab
var nestedJson = nested.Serialize();
Assert.AreEqual( InnerPrefabPath, nestedJson["__Prefab"].GetValue() );
// The broken outer object serializes as a plain hierarchy containing it
var outerJson = outer.Serialize();
Assert.IsFalse( outerJson.ContainsKey( "__Prefab" ) );
Assert.IsTrue( outerJson.ContainsKey( "Children" ) );
}
}