Enforce single source of truth for editor project folder state

This commit is contained in:
2026-05-01 00:55:11 +08:00
parent a4da05008f
commit cd309736f9
8 changed files with 355 additions and 227 deletions

View File

@@ -58,7 +58,7 @@ private:
std::filesystem::path m_root = {};
};
TEST(ProjectBrowserModelTests, ReparentFolderMovesFolderMetaAndRemapsCurrentFolder) {
TEST(ProjectBrowserModelTests, ReparentFolderMovesFolderMetaAndRemapsExplicitCurrentFolder) {
TemporaryRepo repo = {};
ASSERT_TRUE(repo.CreateDirectory("project/Assets/A/Child"));
ASSERT_TRUE(repo.CreateDirectory("project/Assets/B"));
@@ -69,20 +69,19 @@ TEST(ProjectBrowserModelTests, ReparentFolderMovesFolderMetaAndRemapsCurrentFold
ProjectBrowserModel model = {};
model.Initialize(repo.Root() / "project");
ASSERT_TRUE(model.NavigateToFolder("Assets/A/Child"));
std::string currentFolderId = "Assets/A/Child";
std::string movedFolderId = {};
ASSERT_TRUE(model.ReparentFolder("Assets/A", "Assets/B", &movedFolderId));
ASSERT_TRUE(model.ReparentFolder("Assets/A", "Assets/B", &movedFolderId, &currentFolderId));
EXPECT_EQ(movedFolderId, "Assets/B/A");
EXPECT_EQ(model.GetCurrentFolderId(), "Assets/B/A/Child");
EXPECT_EQ(currentFolderId, "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) {
TEST(ProjectBrowserModelTests, MoveFolderToRootMovesFolderMetaAndRemapsExplicitCurrentFolder) {
TemporaryRepo repo = {};
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Parent/Nested"));
ASSERT_TRUE(repo.WriteFile("project/Assets/Parent.meta"));
@@ -91,13 +90,12 @@ TEST(ProjectBrowserModelTests, MoveFolderToRootMovesFolderMetaAndRemapsCurrentFo
ProjectBrowserModel model = {};
model.Initialize(repo.Root() / "project");
ASSERT_TRUE(model.NavigateToFolder("Assets/Parent/Nested"));
std::string currentFolderId = "Assets/Parent/Nested";
std::string movedFolderId = {};
ASSERT_TRUE(model.MoveFolderToRoot("Assets/Parent/Nested", &movedFolderId));
ASSERT_TRUE(model.MoveFolderToRoot("Assets/Parent/Nested", &movedFolderId, &currentFolderId));
EXPECT_EQ(movedFolderId, "Assets/Nested");
EXPECT_EQ(model.GetCurrentFolderId(), "Assets/Nested");
EXPECT_EQ(currentFolderId, "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"));
@@ -136,10 +134,12 @@ TEST(ProjectBrowserModelTests, CreateMaterialCreatesUniqueMaterialFileAndExposes
model.BuildProjectRelativePath(createdItemId),
"Assets/Materials/New Material 1.mat");
ASSERT_TRUE(model.NavigateToFolder("Assets/Materials"));
const ProjectBrowserModel::FolderViewState viewState =
model.Refresh("Assets/Materials");
const ProjectBrowserModel::AssetEntry* createdEntry =
model.FindAssetEntry(createdItemId);
ASSERT_NE(createdEntry, nullptr);
EXPECT_EQ(viewState.currentFolderId, "Assets/Materials");
EXPECT_EQ(createdEntry->kind, ProjectBrowserModel::ItemKind::Material);
EXPECT_EQ(createdEntry->displayName, "New Material 1");
EXPECT_EQ(createdEntry->nameWithExtension, "New Material 1.mat");
@@ -157,7 +157,7 @@ TEST(ProjectBrowserModelTests, CanMoveItemToFolderRejectsDescendantFolderTargets
EXPECT_TRUE(model.CanMoveItemToFolder("Assets/FolderA", "Assets/FolderB"));
}
TEST(ProjectBrowserModelTests, MoveItemToFolderMovesFileMetaAndRefreshesCurrentListing) {
TEST(ProjectBrowserModelTests, MoveItemToFolderMovesFileMetaAndPreservesExplicitCurrentFolder) {
TemporaryRepo repo = {};
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts"));
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Archive"));
@@ -166,44 +166,53 @@ TEST(ProjectBrowserModelTests, MoveItemToFolderMovesFileMetaAndRefreshesCurrentL
ProjectBrowserModel model = {};
model.Initialize(repo.Root() / "project");
ASSERT_TRUE(model.NavigateToFolder("Assets/Scripts"));
std::string currentFolderId = "Assets/Scripts";
std::string movedItemId = {};
ASSERT_TRUE(model.MoveItemToFolder(
"Assets/Scripts/Player.cs",
"Assets/Archive",
&movedItemId));
&movedItemId,
&currentFolderId));
EXPECT_EQ(movedItemId, "Assets/Archive/Player.cs");
EXPECT_EQ(model.GetCurrentFolderId(), "Assets/Scripts");
EXPECT_EQ(currentFolderId, "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"));
model.Refresh("Assets/Scripts");
EXPECT_EQ(model.FindAssetEntry("Assets/Scripts/Player.cs"), nullptr);
ASSERT_TRUE(model.NavigateToFolder("Assets/Archive"));
model.Refresh("Assets/Archive");
EXPECT_NE(model.FindAssetEntry("Assets/Archive/Player.cs"), nullptr);
}
TEST(ProjectBrowserModelTests, RenameFilePreservesExtensionAndUpdatesCurrentListing) {
TEST(ProjectBrowserModelTests, RenameFilePreservesExtensionAndUpdatesExplicitCurrentFolderListing) {
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() / "project");
ASSERT_TRUE(model.NavigateToFolder("Assets/Scenes"));
std::string currentFolderId = "Assets/Scenes";
std::string renamedItemId = {};
ASSERT_TRUE(model.RenameItem("Assets/Scenes/Main.xc", "Gameplay", &renamedItemId));
ASSERT_TRUE(model.RenameItem(
"Assets/Scenes/Main.xc",
"Gameplay",
&renamedItemId,
&currentFolderId));
EXPECT_EQ(renamedItemId, "Assets/Scenes/Gameplay.xc");
EXPECT_EQ(currentFolderId, "Assets/Scenes");
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"));
model.Refresh(currentFolderId);
const ProjectBrowserModel::AssetEntry* renamedEntry =
model.FindAssetEntry("Assets/Scenes/Gameplay.xc");
ASSERT_NE(renamedEntry, nullptr);
@@ -211,7 +220,7 @@ TEST(ProjectBrowserModelTests, RenameFilePreservesExtensionAndUpdatesCurrentList
EXPECT_EQ(renamedEntry->nameWithExtension, "Gameplay.xc");
}
TEST(ProjectBrowserModelTests, DeleteFolderRemovesMetaAndFallsBackCurrentFolder) {
TEST(ProjectBrowserModelTests, DeleteFolderRemapsExplicitCurrentFolderToParent) {
TemporaryRepo repo = {};
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Parent/Nested"));
ASSERT_TRUE(repo.WriteFile("project/Assets/Parent.meta"));
@@ -219,16 +228,16 @@ TEST(ProjectBrowserModelTests, DeleteFolderRemovesMetaAndFallsBackCurrentFolder)
ProjectBrowserModel model = {};
model.Initialize(repo.Root() / "project");
ASSERT_TRUE(model.NavigateToFolder("Assets/Parent/Nested"));
ASSERT_TRUE(model.DeleteItem("Assets/Parent"));
std::string currentFolderId = "Assets/Parent/Nested";
ASSERT_TRUE(model.DeleteItem("Assets/Parent", &currentFolderId));
EXPECT_EQ(model.GetCurrentFolderId(), "Assets");
EXPECT_EQ(currentFolderId, "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) {
TEST(ProjectBrowserModelTests, AssetEntriesExposeKindAndOpenCapabilityForExplicitView) {
TemporaryRepo repo = {};
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes"));
ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc"));
@@ -238,11 +247,14 @@ TEST(ProjectBrowserModelTests, AssetEntriesExposeKindAndOpenCapability) {
ProjectBrowserModel model = {};
model.Initialize(repo.Root() / "project");
const ProjectBrowserModel::AssetEntry* sceneEntry =
const ProjectBrowserModel::FolderViewState rootView = model.Refresh("Assets");
ASSERT_EQ(rootView.currentFolderId, "Assets");
const ProjectBrowserModel::AssetEntry* modelEntry =
model.FindAssetEntry("Assets/mesh.fbx");
ASSERT_NE(sceneEntry, nullptr);
EXPECT_EQ(sceneEntry->kind, ProjectBrowserModel::ItemKind::Model);
EXPECT_FALSE(sceneEntry->canOpen);
ASSERT_NE(modelEntry, nullptr);
EXPECT_EQ(modelEntry->kind, ProjectBrowserModel::ItemKind::Model);
EXPECT_FALSE(modelEntry->canOpen);
const ProjectBrowserModel::AssetEntry* fileEntry =
model.FindAssetEntry("Assets/readme.txt");
@@ -250,7 +262,9 @@ TEST(ProjectBrowserModelTests, AssetEntriesExposeKindAndOpenCapability) {
EXPECT_EQ(fileEntry->kind, ProjectBrowserModel::ItemKind::File);
EXPECT_FALSE(fileEntry->canOpen);
ASSERT_TRUE(model.NavigateToFolder("Assets/Scenes"));
const ProjectBrowserModel::FolderViewState sceneView =
model.Refresh("Assets/Scenes");
ASSERT_EQ(sceneView.currentFolderId, "Assets/Scenes");
const ProjectBrowserModel::AssetEntry* openedSceneEntry =
model.FindAssetEntry("Assets/Scenes/Main.xc");
ASSERT_NE(openedSceneEntry, nullptr);