Files
XCEngine/tests/UI/Editor/unit/test_project_panel.cpp

327 lines
11 KiB
C++

#include "Features/Project/ProjectPanel.h"
#include "Ports/SystemInteractionPort.h"
#include "Rendering/Assets/BuiltInIcons.h"
#include "Composition/EditorPanelIds.h"
#include <gtest/gtest.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <string>
namespace XCEngine::UI::Editor::App {
namespace {
class FakeSystemInteractionHost final : public Ports::SystemInteractionPort {
public:
bool CopyTextToClipboard(std::string_view text) override {
lastClipboardText = std::string(text);
++clipboardCallCount;
return clipboardResult;
}
bool RevealPathInFileBrowser(
const std::filesystem::path& path,
bool selectTarget) override {
lastRevealPath = path;
lastRevealSelectTarget = selectTarget;
++revealCallCount;
return revealResult;
}
bool clipboardResult = true;
bool revealResult = true;
int clipboardCallCount = 0;
int revealCallCount = 0;
bool lastRevealSelectTarget = false;
std::string lastClipboardText = {};
std::filesystem::path lastRevealPath = {};
};
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;
}
PanelInputContext MakeFocusedPanelInputContext() {
PanelInputContext inputContext = {};
inputContext.allowInteraction = true;
inputContext.hasInputFocus = true;
return inputContext;
}
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) },
MakeFocusedPanelInputContext());
panel.Update(
hostFrame,
{ MakePointerButtonDown(520.0f, 194.0f, ::XCEngine::UI::UIPointerButton::Left) },
MakeFocusedPanelInputContext());
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) },
MakeFocusedPanelInputContext());
panel.Update(
hostFrame,
{ MakePointerButtonDown(320.0f, 120.0f, ::XCEngine::UI::UIPointerButton::Left) },
MakeFocusedPanelInputContext());
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);
}
TEST(ProjectPanelTests, CopyPathCommandUsesInjectedSystemHost) {
TemporaryRepo repo = {};
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts"));
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs"));
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root()));
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts"));
ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs"));
ProjectPanel panel = {};
FakeSystemInteractionHost systemHost = {};
panel.SetProjectRuntime(&runtime);
panel.SetSystemInteractionHost(&systemHost);
const UIEditorHostCommandEvaluationResult evaluation =
panel.EvaluateAssetCommand("assets.copy_path");
EXPECT_TRUE(evaluation.executable);
const UIEditorHostCommandDispatchResult dispatch =
panel.DispatchAssetCommand("assets.copy_path");
EXPECT_TRUE(dispatch.commandExecuted);
EXPECT_EQ(systemHost.clipboardCallCount, 1);
EXPECT_EQ(systemHost.lastClipboardText, "Assets/Scripts/Player.cs");
}
TEST(ProjectPanelTests, ShowInExplorerCommandUsesInjectedSystemHost) {
TemporaryRepo repo = {};
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts"));
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs"));
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root()));
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts"));
ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs"));
ProjectPanel panel = {};
FakeSystemInteractionHost systemHost = {};
panel.SetProjectRuntime(&runtime);
panel.SetSystemInteractionHost(&systemHost);
const UIEditorHostCommandDispatchResult dispatch =
panel.DispatchAssetCommand("assets.show_in_explorer");
EXPECT_TRUE(dispatch.commandExecuted);
EXPECT_EQ(systemHost.revealCallCount, 1);
EXPECT_EQ(
systemHost.lastRevealPath.lexically_normal(),
(repo.Root() / "project/Assets/Scripts/Player.cs").lexically_normal());
EXPECT_TRUE(systemHost.lastRevealSelectTarget);
}
} // namespace
} // namespace XCEngine::UI::Editor::App