#include "Features/Project/ProjectPanel.h" #include "Rendering/Assets/BuiltInIcons.h" #include #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_panel_" + 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 = {}; }; UIEditorPanelContentHostFrame MakeProjectHostFrame() { UIEditorPanelContentHostFrame frame = {}; UIEditorPanelContentHostPanelState panelState = {}; panelState.panelId = std::string(kProjectPanelId); panelState.kind = UIEditorPanelPresentationKind::HostedContent; panelState.mounted = true; panelState.bounds = ::XCEngine::UI::UIRect(0.0f, 0.0f, 640.0f, 360.0f); frame.panelStates.push_back(std::move(panelState)); return frame; } ::XCEngine::UI::UIInputEvent MakePointerButtonDown( float x, float y, ::XCEngine::UI::UIPointerButton button) { ::XCEngine::UI::UIInputEvent event = {}; event.type = ::XCEngine::UI::UIInputEventType::PointerButtonDown; event.position = ::XCEngine::UI::UIPoint(x, y); event.pointerButton = button; return event; } TEST(ProjectPanelTests, CreateFolderCommandCreatesDirectoryAndQueuesRename) { TemporaryRepo repo = {}; ProjectPanel panel = {}; panel.Initialize(repo.Root()); const UIEditorHostCommandEvaluationResult evaluation = panel.EvaluateAssetCommand("assets.create_folder"); EXPECT_TRUE(evaluation.executable); const UIEditorHostCommandDispatchResult dispatch = panel.DispatchAssetCommand("assets.create_folder"); EXPECT_TRUE(dispatch.commandExecuted); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/New Folder")); const auto& events = panel.GetFrameEvents(); ASSERT_EQ(events.size(), 2u); EXPECT_EQ(events[0].kind, ProjectPanel::EventKind::AssetSelected); EXPECT_EQ(events[0].source, ProjectPanel::EventSource::Command); EXPECT_EQ(events[0].itemId, "Assets/New Folder"); EXPECT_EQ(events[1].kind, ProjectPanel::EventKind::RenameRequested); EXPECT_EQ(events[1].source, ProjectPanel::EventSource::Command); EXPECT_EQ(events[1].itemId, "Assets/New Folder"); } TEST(ProjectPanelTests, CreateMaterialCommandCreatesFileAndQueuesRename) { TemporaryRepo repo = {}; ProjectPanel panel = {}; panel.Initialize(repo.Root()); const UIEditorHostCommandEvaluationResult evaluation = panel.EvaluateAssetCommand("assets.create_material"); EXPECT_TRUE(evaluation.executable); const UIEditorHostCommandDispatchResult dispatch = panel.DispatchAssetCommand("assets.create_material"); EXPECT_TRUE(dispatch.commandExecuted); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/New Material.mat")); const auto& events = panel.GetFrameEvents(); ASSERT_EQ(events.size(), 2u); EXPECT_EQ(events[0].kind, ProjectPanel::EventKind::AssetSelected); EXPECT_EQ(events[0].itemId, "Assets/New Material.mat"); EXPECT_EQ(events[1].kind, ProjectPanel::EventKind::RenameRequested); EXPECT_EQ(events[1].itemId, "Assets/New Material.mat"); } TEST(ProjectPanelTests, BackgroundContextMenuCreateFolderUsesCurrentFolder) { TemporaryRepo repo = {}; ProjectPanel panel = {}; panel.Initialize(repo.Root()); const UIEditorPanelContentHostFrame hostFrame = MakeProjectHostFrame(); panel.Update( hostFrame, { MakePointerButtonDown(520.0f, 180.0f, ::XCEngine::UI::UIPointerButton::Right) }, true, true); panel.Update( hostFrame, { MakePointerButtonDown(520.0f, 194.0f, ::XCEngine::UI::UIPointerButton::Left) }, true, true); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/New Folder")); const UIEditorHostCommandEvaluationResult renameEvaluation = panel.EvaluateEditCommand("edit.rename"); EXPECT_TRUE(renameEvaluation.executable); EXPECT_EQ(renameEvaluation.message, "Rename project item 'New Folder'."); const auto& events = panel.GetFrameEvents(); ASSERT_FALSE(events.empty()); EXPECT_EQ(events.back().kind, ProjectPanel::EventKind::RenameRequested); EXPECT_EQ(events.back().itemId, "Assets/New Folder"); } TEST(ProjectPanelTests, FolderContextMenuCreateFolderUsesFolderTarget) { TemporaryRepo repo = {}; ASSERT_TRUE(std::filesystem::create_directories(repo.Root() / "project/Assets/FolderA")); ProjectPanel panel = {}; panel.Initialize(repo.Root()); const UIEditorPanelContentHostFrame hostFrame = MakeProjectHostFrame(); panel.Update( hostFrame, { MakePointerButtonDown(300.0f, 80.0f, ::XCEngine::UI::UIPointerButton::Right) }, true, true); panel.Update( hostFrame, { MakePointerButtonDown(320.0f, 120.0f, ::XCEngine::UI::UIPointerButton::Left) }, true, true); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/FolderA/New Folder")); } TEST(ProjectPanelTests, InjectedRuntimeSelectionDrivesRenameWithoutPanelSelectionSync) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts")); ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs")); ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs.meta")); EditorProjectRuntime runtime = {}; ASSERT_TRUE(runtime.Initialize(repo.Root())); ProjectPanel panel = {}; panel.SetProjectRuntime(&runtime); ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts")); ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs")); const UIEditorHostCommandDispatchResult dispatch = panel.DispatchEditCommand("edit.rename"); EXPECT_TRUE(dispatch.commandExecuted); const auto& events = panel.GetFrameEvents(); ASSERT_EQ(events.size(), 1u); EXPECT_EQ(events[0].kind, ProjectPanel::EventKind::RenameRequested); EXPECT_EQ(events[0].source, ProjectPanel::EventSource::GridPrimary); EXPECT_EQ(events[0].itemId, "Assets/Scripts/Player.cs"); } TEST(ProjectPanelTests, InjectedRuntimeCurrentFolderDrivesRenameFallbackWithoutTreeSync) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/FolderA")); EditorProjectRuntime runtime = {}; ASSERT_TRUE(runtime.Initialize(repo.Root())); ProjectPanel panel = {}; panel.SetProjectRuntime(&runtime); ASSERT_TRUE(runtime.NavigateToFolder("Assets/FolderA")); const UIEditorHostCommandEvaluationResult evaluation = panel.EvaluateEditCommand("edit.rename"); EXPECT_TRUE(evaluation.executable); EXPECT_EQ(evaluation.message, "Rename project item 'FolderA'."); } TEST(ProjectPanelTests, BuiltInIconsCanBeConfiguredBeforeRuntimeInitialization) { TemporaryRepo repo = {}; ProjectPanel panel = {}; BuiltInIcons icons = {}; panel.SetBuiltInIcons(&icons); panel.Initialize(repo.Root()); const UIEditorHostCommandEvaluationResult evaluation = panel.EvaluateAssetCommand("assets.create_folder"); EXPECT_TRUE(evaluation.executable); } } // namespace } // namespace XCEngine::UI::Editor::App