EntityOutlinerTests.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <AzCore/Serialization/SerializeContext.h>
  9. #include <AzTest/AzTest.h>
  10. #include <AzFramework/Entity/EntityContextBus.h>
  11. #include <AzToolsFramework/API/ToolsApplicationAPI.h>
  12. #include <AzToolsFramework/Entity/EditorEntityContextComponent.h>
  13. #include <AzToolsFramework/Entity/EditorEntityHelpers.h>
  14. #include <AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h>
  15. #include <AzToolsFramework/ToolsComponents/TransformComponent.h>
  16. #include <AzToolsFramework/UI/Outliner/EntityOutlinerListModel.hxx>
  17. #include <AzToolsFramework/UI/Prefab/PrefabIntegrationManager.h>
  18. #include <AzToolsFramework/Undo/UndoSystem.h>
  19. #include <Prefab/PrefabTestFixture.h>
  20. #include <QAbstractItemModelTester>
  21. namespace UnitTest
  22. {
  23. // Test fixture for the entity outliner model that uses a QAbstractItemModelTester to validate the state of the model
  24. // when QAbstractItemModel signals fire. Tests will exit with a fatal error if an invalid state is detected.
  25. class EntityOutlinerTest : public PrefabTestFixture
  26. {
  27. protected:
  28. void SetUpEditorFixtureImpl() override
  29. {
  30. PrefabTestFixture::SetUpEditorFixtureImpl();
  31. GetApplication()->RegisterComponentDescriptor(AzToolsFramework::EditorEntityContextComponent::CreateDescriptor());
  32. m_model = AZStd::make_unique<AzToolsFramework::EntityOutlinerListModel>();
  33. m_model->Initialize();
  34. m_modelTester =
  35. AZStd::make_unique<QAbstractItemModelTester>(m_model.get(), QAbstractItemModelTester::FailureReportingMode::Fatal);
  36. }
  37. void TearDownEditorFixtureImpl() override
  38. {
  39. m_undoStack = nullptr;
  40. m_modelTester.reset();
  41. m_model.reset();
  42. PrefabTestFixture::TearDownEditorFixtureImpl();
  43. }
  44. // Creates an entity with a given name as one undoable operation
  45. // Parents to parentId, or the root prefab container entity if parentId is invalid
  46. AZ::EntityId CreateNamedEntity(AZStd::string name, AZ::EntityId parentId = AZ::EntityId())
  47. {
  48. // Normally, in invalid parent ID should automatically parent us to the root prefab, but currently in the unit test
  49. // environment entities aren't created with a default transform component, so CreateEntity won't correctly parent.
  50. // We get the actual target parent ID here, then create our missing transform component.
  51. if (!parentId.IsValid())
  52. {
  53. auto prefabEditorEntityOwnershipInterface = AZ::Interface<AzToolsFramework::PrefabEditorEntityOwnershipInterface>::Get();
  54. parentId = prefabEditorEntityOwnershipInterface->GetRootPrefabInstance()->get().GetContainerEntityId();
  55. }
  56. auto createResult = m_prefabPublicInterface->CreateEntity(parentId, AZ::Vector3());
  57. AZ_Assert(createResult.IsSuccess(), "Failed to create entity: %s", createResult.GetError().c_str());
  58. AZ::EntityId entityId = createResult.GetValue();
  59. AZ::Entity* entity = nullptr;
  60. AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationRequests::FindEntity, entityId);
  61. entity->SetName(name);
  62. // Update our undo cache entry to include the rename / reparent as one atomic operation.
  63. m_prefabPublicInterface->GenerateUndoNodesForEntityChangeAndUpdateCache(entityId, m_undoStack->GetTop());
  64. ProcessDeferredUpdates();
  65. return entityId;
  66. }
  67. // Helper to visualize debug state
  68. void PrintModel()
  69. {
  70. AZStd::deque<AZStd::pair<QModelIndex, int>> indices;
  71. indices.push_back({ m_model->index(0, 0), 0 });
  72. while (!indices.empty())
  73. {
  74. auto [index, depth] = indices.front();
  75. indices.pop_front();
  76. QString indentString;
  77. for (int i = 0; i < depth; ++i)
  78. {
  79. indentString += " ";
  80. }
  81. qDebug() << (indentString + index.data(Qt::DisplayRole).toString()) << index.internalId();
  82. for (int i = 0; i < m_model->rowCount(index); ++i)
  83. {
  84. indices.emplace_back(m_model->index(i, 0, index), depth + 1);
  85. }
  86. }
  87. };
  88. // Gets the index of the root prefab, i.e. the "New Level" container entity
  89. QModelIndex GetRootIndex() const
  90. {
  91. return m_model->index(0, 0);
  92. }
  93. // Kicks off any updates scheduled for the next tick
  94. void ProcessDeferredUpdates() override
  95. {
  96. // Force a prefab propagation for updates that are deferred to the next tick.
  97. PropagateAllTemplateChanges();
  98. // Ensure the model process its entity update queue
  99. m_model->ProcessEntityUpdates();
  100. }
  101. AZStd::unique_ptr<AzToolsFramework::EntityOutlinerListModel> m_model;
  102. AZStd::unique_ptr<QAbstractItemModelTester> m_modelTester;
  103. };
  104. TEST_F(EntityOutlinerTest, TestCreateFlatHierarchyUndoAndRedoWorks)
  105. {
  106. constexpr size_t entityCount = 10;
  107. for (size_t i = 0; i < entityCount; ++i)
  108. {
  109. CreateNamedEntity(AZStd::string::format("Entity%zu", i));
  110. EXPECT_EQ(m_model->rowCount(GetRootIndex()), i + 1);
  111. }
  112. for (int i = entityCount; i > 0; --i)
  113. {
  114. Undo();
  115. EXPECT_EQ(m_model->rowCount(GetRootIndex()), i - 1);
  116. }
  117. for (size_t i = 0; i < entityCount; ++i)
  118. {
  119. Redo();
  120. EXPECT_EQ(m_model->rowCount(GetRootIndex()), i + 1);
  121. }
  122. }
  123. TEST_F(EntityOutlinerTest, TestCreateNestedHierarchyUndoAndRedoWorks)
  124. {
  125. constexpr size_t depth = 5;
  126. auto modelDepth = [this]() -> int
  127. {
  128. int depth = 0;
  129. QModelIndex index = GetRootIndex();
  130. while (m_model->rowCount(index) > 0)
  131. {
  132. ++depth;
  133. index = m_model->index(0, 0, index);
  134. }
  135. return depth;
  136. };
  137. AZ::EntityId parentId;
  138. for (int i = 0; i < depth; i++)
  139. {
  140. parentId = CreateNamedEntity(AZStd::string::format("EntityDepth%i", i), parentId);
  141. EXPECT_EQ(modelDepth(), i + 1);
  142. }
  143. for (int i = depth - 1; i >= 0; --i)
  144. {
  145. Undo();
  146. EXPECT_EQ(modelDepth(), i);
  147. }
  148. for (int i = 0; i < depth; ++i)
  149. {
  150. Redo();
  151. EXPECT_EQ(modelDepth(), i + 1);
  152. }
  153. }
  154. TEST_F(EntityOutlinerTest, TestReparentEntitiesSucceeds)
  155. {
  156. // Level (prefab) <-- focused
  157. // | Seat
  158. // | Driver_1
  159. // | Driver_2
  160. const AZStd::string seatEntityName = "Seat";
  161. const AZStd::string driverOneEntityName = "Driver_1";
  162. const AZStd::string driverTwoEntityName = "Driver_2";
  163. // Create the Seat and Driver entities.
  164. AZ::EntityId seatEntityId = CreateEditorEntityUnderRoot(seatEntityName);
  165. AZ::EntityId driverOneEntityId = CreateEditorEntityUnderRoot(driverOneEntityName);
  166. AZ::EntityId driverTwoEntityId = CreateEditorEntityUnderRoot(driverTwoEntityName);
  167. // Reparent the Driver_1 and Driver_2 entities under the Seat entity.
  168. auto appendForInvalid = AzToolsFramework::EntityOutlinerListModel::AppendEnd;
  169. bool isReparented = m_model->ReparentEntities(
  170. seatEntityId, { driverOneEntityId, driverTwoEntityId }, GetRootContainerEntityId(), appendForInvalid);
  171. EXPECT_TRUE(isReparented);
  172. // Validate that the parent entity of the Driver_1 and Driver_2 entities is the Seat entity.
  173. AZ::EntityId parentEntityIdForDriverOne;
  174. AZ::TransformBus::EventResult(parentEntityIdForDriverOne, driverOneEntityId, &AZ::TransformInterface::GetParentId);
  175. EXPECT_EQ(parentEntityIdForDriverOne, seatEntityId);
  176. AZ::EntityId parentEntityIdForDriverTwo;
  177. AZ::TransformBus::EventResult(parentEntityIdForDriverTwo, driverTwoEntityId, &AZ::TransformInterface::GetParentId);
  178. EXPECT_EQ(parentEntityIdForDriverTwo, seatEntityId);
  179. // Validate that the child entity order of the Seat entity is [Driver_1, Driver_2].
  180. AzToolsFramework::EntityOrderArray entityOrderArray = AzToolsFramework::GetEntityChildOrder(seatEntityId);
  181. EXPECT_EQ(entityOrderArray.size(), 2);
  182. AZStd::string childEntityName;
  183. AZ::ComponentApplicationBus::BroadcastResult(
  184. childEntityName, &AZ::ComponentApplicationRequests::GetEntityName, entityOrderArray[0]);
  185. EXPECT_EQ(childEntityName, driverOneEntityName);
  186. AZ::ComponentApplicationBus::BroadcastResult(
  187. childEntityName, &AZ::ComponentApplicationRequests::GetEntityName, entityOrderArray[1]);
  188. EXPECT_EQ(childEntityName, driverTwoEntityName);
  189. }
  190. TEST_F(EntityOutlinerTest, TestReparentPrefabsSucceeds)
  191. {
  192. // Level (prefab) <-- focused
  193. // | Garage
  194. // | Car (prefab)
  195. // | CarTire
  196. // | Bike (prefab)
  197. // | BikeTire
  198. const AZStd::string carPrefabName = "CarPrefab";
  199. const AZStd::string bikePrefabName = "BikePrefab";
  200. const AZStd::string garageEntityName = "Garage";
  201. const AZStd::string carTireEntityName = "CarTire";
  202. const AZStd::string bikeTireEntityName = "BikeTire";
  203. AZ::IO::Path engineRootPath;
  204. m_settingsRegistryInterface->Get(engineRootPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
  205. AZ::IO::Path carPrefabFilepath = engineRootPath / carPrefabName;
  206. AZ::IO::Path bikePrefabFilepath = engineRootPath / bikePrefabName;
  207. // Create the Garage, CarTire and BikeTire entities.
  208. AZ::EntityId garageEntityId = CreateEditorEntityUnderRoot(garageEntityName);
  209. AZ::EntityId carTireEntityId = CreateEditorEntityUnderRoot(carTireEntityName);
  210. AZ::EntityId bikeTireEntityId = CreateEditorEntityUnderRoot(bikeTireEntityName);
  211. // Create the Car and Bike prefabs.
  212. AZ::EntityId carContainerId = CreateEditorPrefab(carPrefabFilepath, { carTireEntityId });
  213. AZ::EntityId bikeContainerId = CreateEditorPrefab(bikePrefabFilepath, { bikeTireEntityId });
  214. // Reparent the Car prefab under the Garage entity.
  215. auto appendForInvalid = AzToolsFramework::EntityOutlinerListModel::AppendBeginning; // test the opposite way of appending
  216. bool isCarReparented =
  217. m_model->ReparentEntities(garageEntityId, { carContainerId }, GetRootContainerEntityId(), appendForInvalid);
  218. EXPECT_TRUE(isCarReparented);
  219. // Reparent the Bike prefab under the Garage entity.
  220. bool isBikeReparented =
  221. m_model->ReparentEntities(garageEntityId, { bikeContainerId }, GetRootContainerEntityId(), appendForInvalid);
  222. EXPECT_TRUE(isBikeReparented);
  223. // Validate that the parent entity of the Car and Bike prefabs is the Garage entity.
  224. AZ::EntityId parentEntityIdForCar;
  225. AZ::TransformBus::EventResult(parentEntityIdForCar, carContainerId, &AZ::TransformInterface::GetParentId);
  226. EXPECT_EQ(parentEntityIdForCar, garageEntityId);
  227. AZ::EntityId parentEntityIdForBike;
  228. AZ::TransformBus::EventResult(parentEntityIdForBike, bikeContainerId, &AZ::TransformInterface::GetParentId);
  229. EXPECT_EQ(parentEntityIdForBike, garageEntityId);
  230. // Validate that the child entity order of the Garage entity is [Bike, Car], which is reversed due to the AppendBeginning flag.
  231. AzToolsFramework::EntityOrderArray entityOrderArray = AzToolsFramework::GetEntityChildOrder(garageEntityId);
  232. EXPECT_EQ(entityOrderArray.size(), 2);
  233. AZStd::string childEntityName;
  234. AZ::ComponentApplicationBus::BroadcastResult(
  235. childEntityName, &AZ::ComponentApplicationRequests::GetEntityName, entityOrderArray[0]);
  236. EXPECT_EQ(childEntityName, bikePrefabName);
  237. AZ::ComponentApplicationBus::BroadcastResult(
  238. childEntityName, &AZ::ComponentApplicationRequests::GetEntityName, entityOrderArray[1]);
  239. EXPECT_EQ(childEntityName, carPrefabName);
  240. }
  241. TEST_F(EntityOutlinerTest, TestReparentEntitiesThatDoNotBelongToSamePrefabFails)
  242. {
  243. // Level (prefab) <-- focused
  244. // | Car (prefab)
  245. // | Tire
  246. // | Driver
  247. const AZStd::string carPrefabName = "CarPrefab";
  248. const AZStd::string bikePrefabName = "BikePrefab";
  249. const AZStd::string tireEntityName = "Tire";
  250. const AZStd::string pedalEntityName = "Pedal";
  251. const AZStd::string driverEntityName = "Driver";
  252. AZ::IO::Path engineRootPath;
  253. m_settingsRegistryInterface->Get(engineRootPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
  254. AZ::IO::Path carPrefabFilepath = engineRootPath / carPrefabName;
  255. AZ::IO::Path bikePrefabFilepath = engineRootPath / bikePrefabName;
  256. // Create the Car prefab.
  257. AZ::EntityId tireEntityId = CreateEditorEntityUnderRoot(tireEntityName);
  258. AZ::EntityId carContainerId = CreateEditorPrefab(carPrefabFilepath, { tireEntityId });
  259. // Create the Driver entity.
  260. AZ::EntityId driverEntityId = CreateEditorEntityUnderRoot(driverEntityName);
  261. // Retrieve the Tire entity id.
  262. InstanceOptionalReference carInstance = m_instanceEntityMapperInterface->FindOwningInstance(carContainerId);
  263. EXPECT_TRUE(carInstance.has_value());
  264. EntityAlias tireEntityAlias = FindEntityAliasInInstance(carContainerId, tireEntityName);
  265. EXPECT_FALSE(tireEntityAlias.empty());
  266. tireEntityId = carInstance->get().GetEntityId(tireEntityAlias);
  267. // Validate that the Tire and Driver entities cannot be reparented to Level.
  268. bool isReparented = m_model->ReparentEntities(GetRootContainerEntityId(), { tireEntityId, driverEntityId });
  269. EXPECT_FALSE(isReparented);
  270. }
  271. TEST_F(EntityOutlinerTest, TestReparentEntityToAnotherPrefabFails)
  272. {
  273. // Level (prefab) <-- focused
  274. // | Car (prefab)
  275. // | Tire
  276. // | Bike (prefab)
  277. // | Pedal
  278. // | Driver
  279. const AZStd::string carPrefabName = "CarPrefab";
  280. const AZStd::string bikePrefabName = "BikePrefab";
  281. const AZStd::string tireEntityName = "Tire";
  282. const AZStd::string pedalEntityName = "Pedal";
  283. const AZStd::string driverEntityName = "Driver";
  284. AZ::IO::Path engineRootPath;
  285. m_settingsRegistryInterface->Get(engineRootPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
  286. AZ::IO::Path carPrefabFilepath = engineRootPath / carPrefabName;
  287. AZ::IO::Path bikePrefabFilepath = engineRootPath / bikePrefabName;
  288. // Create the Car prefab.
  289. AZ::EntityId tireEntityId = CreateEditorEntityUnderRoot(tireEntityName);
  290. AZ::EntityId carContainerId = CreateEditorPrefab(carPrefabFilepath, { tireEntityId });
  291. // Create the Bike prefab.
  292. AZ::EntityId pedalEntityId = CreateEditorEntityUnderRoot(pedalEntityName);
  293. AZ::EntityId bikeContainerId = CreateEditorPrefab(bikePrefabFilepath, { pedalEntityId });
  294. // Create the Driver entity.
  295. AZ::EntityId driverEntityId = CreateEditorEntityUnderRoot(driverEntityName);
  296. // Retrieve the Tire entity id.
  297. InstanceOptionalReference carInstance = m_instanceEntityMapperInterface->FindOwningInstance(carContainerId);
  298. EXPECT_TRUE(carInstance.has_value());
  299. EntityAlias tireEntityAlias = FindEntityAliasInInstance(carContainerId, tireEntityName);
  300. EXPECT_FALSE(tireEntityAlias.empty());
  301. tireEntityId = carInstance->get().GetEntityId(tireEntityAlias);
  302. // Retrieve the Pedal entity id.
  303. InstanceOptionalReference bikeInstance = m_instanceEntityMapperInterface->FindOwningInstance(bikeContainerId);
  304. EXPECT_TRUE(bikeInstance.has_value());
  305. EntityAlias pedalEntityAlias = FindEntityAliasInInstance(bikeContainerId, pedalEntityName);
  306. EXPECT_FALSE(pedalEntityAlias.empty());
  307. pedalEntityId = bikeInstance->get().GetEntityId(pedalEntityAlias);
  308. // Validate that the Driver entity cannot be reparented from the focused Level prefab to the unfocused Car prefab.
  309. bool isReparented = m_model->ReparentEntities(tireEntityId, { driverEntityId });
  310. EXPECT_FALSE(isReparented);
  311. // Validate that the Pedal entity cannot be reparented from the unfocused Bike prefab to the unfocused Car prefab.
  312. isReparented = m_model->ReparentEntities(tireEntityId, { pedalEntityId });
  313. EXPECT_FALSE(isReparented);
  314. // Validate that the Tire entity cannot be reparented from the unfocused Car prefab to the focused Level prefab.
  315. isReparented = m_model->ReparentEntities(driverEntityId, { tireEntityId });
  316. EXPECT_FALSE(isReparented);
  317. }
  318. TEST_F(EntityOutlinerTest, TestReparentPrefabToAnotherPrefabFails)
  319. {
  320. // Level (prefab) <-- focused
  321. // | Car (prefab)
  322. // | Wheel (prefab)
  323. // | Tire
  324. // | Trunk
  325. // | Bike (prefab)
  326. // | Pedal
  327. const AZStd::string carPrefabName = "CarPrefab";
  328. const AZStd::string wheelPrefabName = "WheelPrefab";
  329. const AZStd::string bikePrefabName = "BikePrefab";
  330. const AZStd::string tireEntityName = "Tire";
  331. const AZStd::string trunkEntityName = "Trunk";
  332. const AZStd::string pedalEntityName = "Pedal";
  333. AZ::IO::Path engineRootPath;
  334. m_settingsRegistryInterface->Get(engineRootPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
  335. AZ::IO::Path carPrefabFilepath = engineRootPath / carPrefabName;
  336. AZ::IO::Path wheelPrefabFilepath = engineRootPath / wheelPrefabName;
  337. AZ::IO::Path bikePrefabFilepath = engineRootPath / bikePrefabName;
  338. // Create the Wheel prefab.
  339. AZ::EntityId tireEntityId = CreateEditorEntityUnderRoot(tireEntityName);
  340. AZ::EntityId wheelContainerId = CreateEditorPrefab(wheelPrefabFilepath, { tireEntityId });
  341. AZ::EntityId trunkEntityId = CreateEditorEntityUnderRoot(trunkEntityName);
  342. // Create the Car prefab.
  343. AZ::EntityId carContainerId = CreateEditorPrefab(carPrefabFilepath, { wheelContainerId, trunkEntityId });
  344. // Create the Bike prefab.
  345. AZ::EntityId pedalEntityId = CreateEditorEntityUnderRoot(pedalEntityName);
  346. AZ::EntityId bikeContainerId = CreateEditorPrefab(bikePrefabFilepath, { pedalEntityId });
  347. // Retrieve Trunk entity id.
  348. InstanceOptionalReference carInstance = m_instanceEntityMapperInterface->FindOwningInstance(carContainerId);
  349. EXPECT_TRUE(carInstance.has_value());
  350. EntityAlias trunkEntityAlias = FindEntityAliasInInstance(carContainerId, trunkEntityName);
  351. EXPECT_FALSE(trunkEntityAlias.empty());
  352. trunkEntityId = carInstance->get().GetEntityId(trunkEntityAlias);
  353. // Retrieve the Wheel container entity id.
  354. EntityOptionalReference wheelContainerEntity;
  355. carInstance->get().GetNestedInstances(
  356. [&wheelContainerEntity](AZStd::unique_ptr<Instance>& nestedInstance)
  357. {
  358. wheelContainerEntity = nestedInstance->GetContainerEntity();
  359. });
  360. EXPECT_TRUE(wheelContainerEntity.has_value());
  361. wheelContainerId = wheelContainerEntity->get().GetId();
  362. // Retrieve the Pedal entity id.
  363. InstanceOptionalReference bikeInstance = m_instanceEntityMapperInterface->FindOwningInstance(bikeContainerId);
  364. EXPECT_TRUE(bikeInstance.has_value());
  365. EntityAlias pedalEntityAlias = FindEntityAliasInInstance(bikeContainerId, pedalEntityName);
  366. EXPECT_FALSE(pedalEntityAlias.empty());
  367. pedalEntityId = bikeInstance->get().GetEntityId(pedalEntityAlias);
  368. // Validate that the Bike prefab cannot be reparented from the focused Level prefab to the unfocused Car prefab.
  369. bool isReparented = m_model->ReparentEntities(trunkEntityId, { bikeContainerId });
  370. EXPECT_FALSE(isReparented);
  371. // Validate that the Wheel prefab cannot be reparented from the unfocused Car prefab to the unfocused Bike prefab.
  372. isReparented = m_model->ReparentEntities(bikeContainerId, { wheelContainerId });
  373. EXPECT_FALSE(isReparented);
  374. // Validate that the Wheel prefab cannot be reparented from the unfocused Car prefab to the focused Level prefab.
  375. isReparented = m_model->ReparentEntities(GetRootContainerEntityId(), { wheelContainerId });
  376. EXPECT_FALSE(isReparented);
  377. }
  378. } // namespace UnitTest