#include "Features/Project/ProjectBrowserModel.h" #include #include #include #include #include namespace XCEngine::UI::Editor::App { namespace { class TemporaryRepo final { public: TemporaryRepo() { const auto uniqueSuffix = std::chrono::steady_clock::now().time_since_epoch().count(); m_root = std::filesystem::temp_directory_path() / ("xcengine_project_browser_model_" + std::to_string(uniqueSuffix)); } ~TemporaryRepo() { std::error_code errorCode = {}; std::filesystem::remove_all(m_root, errorCode); } const std::filesystem::path& Root() const { return m_root; } bool CreateDirectory(const std::filesystem::path& relativePath) const { std::error_code errorCode = {}; std::filesystem::create_directories(m_root / relativePath, errorCode); return !errorCode; } bool WriteFile( const std::filesystem::path& relativePath, std::string_view contents = "test") const { const std::filesystem::path absolutePath = m_root / relativePath; std::error_code errorCode = {}; std::filesystem::create_directories(absolutePath.parent_path(), errorCode); if (errorCode) { return false; } std::ofstream stream(absolutePath, std::ios::binary); if (!stream.is_open()) { return false; } stream << contents; return stream.good(); } private: std::filesystem::path m_root = {}; }; TEST(ProjectBrowserModelTests, ReparentFolderMovesFolderMetaAndRemapsCurrentFolder) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/A/Child")); ASSERT_TRUE(repo.CreateDirectory("project/Assets/B")); ASSERT_TRUE(repo.WriteFile("project/Assets/A.meta")); ASSERT_TRUE(repo.WriteFile("project/Assets/A/Child.meta")); ASSERT_TRUE(repo.WriteFile("project/Assets/B.meta")); ProjectBrowserModel model = {}; model.Initialize(repo.Root()); ASSERT_TRUE(model.NavigateToFolder("Assets/A/Child")); std::string movedFolderId = {}; ASSERT_TRUE(model.ReparentFolder("Assets/A", "Assets/B", &movedFolderId)); EXPECT_EQ(movedFolderId, "Assets/B/A"); EXPECT_EQ(model.GetCurrentFolderId(), "Assets/B/A/Child"); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/B/A")); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/B/A.meta")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/A")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/A.meta")); } TEST(ProjectBrowserModelTests, MoveFolderToRootMovesFolderMetaAndRemapsCurrentFolder) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/Parent/Nested")); ASSERT_TRUE(repo.WriteFile("project/Assets/Parent.meta")); ASSERT_TRUE(repo.WriteFile("project/Assets/Parent/Nested.meta")); ProjectBrowserModel model = {}; model.Initialize(repo.Root()); ASSERT_TRUE(model.NavigateToFolder("Assets/Parent/Nested")); std::string movedFolderId = {}; ASSERT_TRUE(model.MoveFolderToRoot("Assets/Parent/Nested", &movedFolderId)); EXPECT_EQ(movedFolderId, "Assets/Nested"); EXPECT_EQ(model.GetCurrentFolderId(), "Assets/Nested"); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Nested")); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Nested.meta")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent/Nested")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent/Nested.meta")); } TEST(ProjectBrowserModelTests, CreateFolderCreatesUniqueDirectoryUnderTargetFolder) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes")); ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes/New Folder")); ProjectBrowserModel model = {}; model.Initialize(repo.Root()); std::string createdFolderId = {}; ASSERT_TRUE(model.CreateFolder("Assets/Scenes", "New Folder", &createdFolderId)); EXPECT_EQ(createdFolderId, "Assets/Scenes/New Folder 1"); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/New Folder 1")); } TEST(ProjectBrowserModelTests, CreateMaterialCreatesUniqueMaterialFileAndExposesRelativePath) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/Materials")); ASSERT_TRUE(repo.WriteFile("project/Assets/Materials/New Material.mat")); ProjectBrowserModel model = {}; model.Initialize(repo.Root()); std::string createdItemId = {}; ASSERT_TRUE(model.CreateMaterial("Assets/Materials", "New Material", &createdItemId)); EXPECT_EQ(createdItemId, "Assets/Materials/New Material 1.mat"); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Materials/New Material 1.mat")); EXPECT_EQ( model.BuildProjectRelativePath(createdItemId), "Assets/Materials/New Material 1.mat"); ASSERT_TRUE(model.NavigateToFolder("Assets/Materials")); const ProjectBrowserModel::AssetEntry* createdEntry = model.FindAssetEntry(createdItemId); ASSERT_NE(createdEntry, nullptr); EXPECT_EQ(createdEntry->kind, ProjectBrowserModel::ItemKind::Material); EXPECT_EQ(createdEntry->displayName, "New Material 1"); EXPECT_EQ(createdEntry->nameWithExtension, "New Material 1.mat"); } TEST(ProjectBrowserModelTests, CanMoveItemToFolderRejectsDescendantFolderTargets) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/FolderA/Nested")); ASSERT_TRUE(repo.CreateDirectory("project/Assets/FolderB")); ProjectBrowserModel model = {}; model.Initialize(repo.Root()); EXPECT_FALSE(model.CanMoveItemToFolder("Assets/FolderA", "Assets/FolderA/Nested")); EXPECT_TRUE(model.CanMoveItemToFolder("Assets/FolderA", "Assets/FolderB")); } TEST(ProjectBrowserModelTests, MoveItemToFolderMovesFileMetaAndRefreshesCurrentListing) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts")); ASSERT_TRUE(repo.CreateDirectory("project/Assets/Archive")); ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs")); ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs.meta")); ProjectBrowserModel model = {}; model.Initialize(repo.Root()); ASSERT_TRUE(model.NavigateToFolder("Assets/Scripts")); std::string movedItemId = {}; ASSERT_TRUE(model.MoveItemToFolder( "Assets/Scripts/Player.cs", "Assets/Archive", &movedItemId)); EXPECT_EQ(movedItemId, "Assets/Archive/Player.cs"); EXPECT_EQ(model.GetCurrentFolderId(), "Assets/Scripts"); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scripts/Player.cs")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scripts/Player.cs.meta")); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Archive/Player.cs")); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Archive/Player.cs.meta")); EXPECT_EQ(model.FindAssetEntry("Assets/Scripts/Player.cs"), nullptr); ASSERT_TRUE(model.NavigateToFolder("Assets/Archive")); EXPECT_NE(model.FindAssetEntry("Assets/Archive/Player.cs"), nullptr); } TEST(ProjectBrowserModelTests, RenameFilePreservesExtensionAndUpdatesCurrentListing) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc")); ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc.meta")); ProjectBrowserModel model = {}; model.Initialize(repo.Root()); ASSERT_TRUE(model.NavigateToFolder("Assets/Scenes")); std::string renamedItemId = {}; ASSERT_TRUE(model.RenameItem("Assets/Scenes/Main.xc", "Gameplay", &renamedItemId)); EXPECT_EQ(renamedItemId, "Assets/Scenes/Gameplay.xc"); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Gameplay.xc")); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Gameplay.xc.meta")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Main.xc")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Main.xc.meta")); const ProjectBrowserModel::AssetEntry* renamedEntry = model.FindAssetEntry("Assets/Scenes/Gameplay.xc"); ASSERT_NE(renamedEntry, nullptr); EXPECT_EQ(renamedEntry->displayName, "Gameplay"); EXPECT_EQ(renamedEntry->nameWithExtension, "Gameplay.xc"); } TEST(ProjectBrowserModelTests, DeleteFolderRemovesMetaAndFallsBackCurrentFolder) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/Parent/Nested")); ASSERT_TRUE(repo.WriteFile("project/Assets/Parent.meta")); ASSERT_TRUE(repo.WriteFile("project/Assets/Parent/Nested.meta")); ProjectBrowserModel model = {}; model.Initialize(repo.Root()); ASSERT_TRUE(model.NavigateToFolder("Assets/Parent/Nested")); ASSERT_TRUE(model.DeleteItem("Assets/Parent")); EXPECT_EQ(model.GetCurrentFolderId(), "Assets"); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent.meta")); } TEST(ProjectBrowserModelTests, AssetEntriesExposeKindAndOpenCapability) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes")); ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc")); ASSERT_TRUE(repo.WriteFile("project/Assets/mesh.fbx")); ASSERT_TRUE(repo.WriteFile("project/Assets/readme.txt")); ProjectBrowserModel model = {}; model.Initialize(repo.Root()); const ProjectBrowserModel::AssetEntry* sceneEntry = model.FindAssetEntry("Assets/mesh.fbx"); ASSERT_NE(sceneEntry, nullptr); EXPECT_EQ(sceneEntry->kind, ProjectBrowserModel::ItemKind::Model); EXPECT_FALSE(sceneEntry->canOpen); const ProjectBrowserModel::AssetEntry* fileEntry = model.FindAssetEntry("Assets/readme.txt"); ASSERT_NE(fileEntry, nullptr); EXPECT_EQ(fileEntry->kind, ProjectBrowserModel::ItemKind::File); EXPECT_FALSE(fileEntry->canOpen); ASSERT_TRUE(model.NavigateToFolder("Assets/Scenes")); const ProjectBrowserModel::AssetEntry* openedSceneEntry = model.FindAssetEntry("Assets/Scenes/Main.xc"); ASSERT_NE(openedSceneEntry, nullptr); EXPECT_EQ(openedSceneEntry->kind, ProjectBrowserModel::ItemKind::Scene); EXPECT_TRUE(openedSceneEntry->canOpen); } } // namespace } // namespace XCEngine::UI::Editor::App